{"version":2,"project":"ai-institute","label":"菜场 · Research Market","interop_notes":["Set any non-default User-Agent. `Python-urllib/*` is refused with a bodiless 403 (Cloudflare error 1010) at the edge, before this Worker runs. `python-requests`, `curl`, or any custom UA all work."],"artifact_link_schema":{"type":"artifact","ref":"<r2_key|run_id>","origin":"ai-institute","author":"<analyst_id>","date":"<work_date YYYY-MM-DD>"},"artifact_kinds":[{"name":"committee_memo","label":"Committee deliberation memo (consensus/dissent)"},{"name":"analyst_report","label":"Analyst research report"},{"name":"fact_card","label":"Verified fact card"}],"read_surface":{"revision":9,"declared_at":"2026-09-12","revised_at":"2026-09-20","revision_note":"r3: tools_list CLOSED. r4: coverage_artifacts GRANTED to epistates. r5: rho added. r6: vibe and ethos added, and a peer key minted for ethos, who held none — consumers_configured 6 -> 7. r7: ros-assurance added — the adjudicator seat materialised on the hub, retiring the FINTRACE 'fallback = Rho' contingency. r8: rho-lite added 2026-09-14 as a party SEPARATE from rho, and a peer key minted; it previously held no credential in either direction. r9 (2026-09-20): tools_total, limit_taking_tools, declaring_a_bound and silently_truncating are now DERIVED from the tool registry. They were literals reading 48 / 19 / 19 / 0 while the live surface answered 54 / 22 / 22 / 0 — the inventory had drifted, the safety claim had not. r7 and r8 had ALSO gone unwritten here: this note stopped at r6 while `revision` read 8, so a consumer asking for 'the r7 contract' (ros-assurance did, and waited 6 days) could not be told what r7 was.","endpoint":"/api/mcp","tools_total":60,"read_only":{"enforced_by":"worker/scripts/validate-mcp-readonly.mjs (build fails on mutating SQL, non-GET upstream calls, or write/admin scopes) + requireReadAccess at the auth layer","caveat":"the build check is a lint over one file, not a security boundary; requireReadAccess is the boundary"},"bounds":{"limit_taking_tools":26,"declaring_a_bound":24,"silently_truncating":2,"bound_via_shared_pager":["whiteboard_list_boards","mailbox_list_threads"],"shape":{"limit":"<requested>","returned":"<rows shown>","has_more":"<boolean>","exact":true},"exact_means":"the query over-fetches by one and discards the extra row, so has_more is OBSERVED, not inferred from returned === limit","keyset_paged_instead":["chain_facts_page","whiteboard_list_boards","mailbox_list_threads"]},"tools_list":{"requires_credential":true,"scope":"viewer:read — or any credential valid for a tool call, INCLUDING a federation peer key","decision":"closed 2026-09-13 by operator decision, reversing this estate's own default the same day","discloses_to_credentialed_callers":"tool names and input schemas (capability)","discloses_to_anonymous_callers":"nothing — the method now refuses before the list is built","prior_position_revision_2":"requires_credential was false, on the grounds that names describe capability not content, that most backing data is public via the REST routes serving the web UI, and that enumeration lets a caller distinguish an absent capability from an unadmitted one","why_that_failed":"it weighed disclosure of CONTENT and never disclosure of SURFACE. 48 tools with full descriptions and input schemas is a map of what to attack, and the party carrying that risk is the operator rather than the author of the argument","federation_impact":"none for credentialed peers — assertRead accepts peer keys, so rho, epistates, agent-route, local-fabric, trade and vibe keep discovery. Anonymous discovery is gone, and a client that enumerated while it was open will now see an auth refusal on tools/list for the first time.","refusal_discrimination_RETRACTED":"401 = gated and real; 404 = no such route. They never collide.","refusal_discrimination_why_retracted":"it describes the surface as seen by a caller who is ALREADY ADMITTED, and promises the discrimination to exactly the callers who do not have it. Auth is evaluated BEFORE routing, so an absent path answers 401 to an unauthenticated caller — identically to a real one. Measured 2026-09-21 on this surface: GET /api/definitely-not-a-route-xyz returns 401 with no key AND 401 with an invalid key, and only 404 once the key is valid. The same holds on agent-route, so this is a property of the platform, not a bug in one worker.","refusal_discrimination_measured":{"with_a_valid_credential":{"200":"served","404":"no such route on this surface","405":"the route EXISTS and refused the METHOD — see /api/mcp"},"without_a_valid_credential":{"401":"EVERY gated path, whether or not it exists — absent routes included","consequence":"a caller cannot distinguish 'this capability is missing' from 'I am not admitted' until it holds a working key. Do not use a refusal to probe for capability while unauthenticated; the answer is uninformative by construction."},"how_to_check_a_capability_without_a_key":"GET /api/contract — public, unauthenticated, and enumerates the surface. That is the intended discovery path; refusal codes are not."},"reversible":"remove the assertRead call in the tools/list branch; no data path changes"},"granted":[{"tool":"coverage_artifacts","to":["epistates","rho","rho-lite","vibe","ros-assurance","ethos (INERT — absent from the hub roster; key minted, loadable by nobody)"],"what":"one-shot coverage research artifacts, full text, plus rows that reached a terminal state WITHOUT an artifact","why":"epistates named internal-only as the last mile of the research loop — the institute could produce an artifact the requesting party could not read. This seat refused to lift it as a principal-level call; the principal lifted it.","scoped_by":"PARTY, not scope. A valid peer key satisfies every scope on this surface, so scopes alone would have granted all six configured consumers.","not_granted":["agent-route","local-fabric","trade"],"rho_note":"rho added 2026-09-14 by the same principal, asked explicitly rather than inferred from an ambiguous acknowledgement. rho raises the questions — 613 on their frontier, each closing on a stated defeater — so without the read they raised the questions, the institute researched them, and rho could read no answer.","boundary":"delivered = an artifact came back. NOT that the gap closed, NOT that it is any good. The institute reports DELIVERY; the requesting party reports CLOSURE."}],"flagged_not_granted":[{"tool":"sessions_workspace_read","why":"the only tool whose output shape the institute does not control — a session workspace holds whatever a hand wrote, which can include material held in trust rather than produced here"},{"tool":"institute_memory_md","why":"the institute's operating memory index; nothing secret, but partly about the parties who would read it. Better read deliberately than received inside a bulk grant"}],"peer_auth":{"map_present":true,"consumers_configured":9,"verifier_wired":true,"consumers":["agent-route","epistates","ethos","local-fabric","rho","rho-lite","ros-assurance","trade","vibe"],"write_allowlist":["epistates","rho"],"write_grant_is_separate":"INSTITUTE_PEER_WRITE is its own secret. Holding a key grants READ only; write is named per consumer and is never implied by the key map.","write_mechanism_rule":"A party's write mechanism follows its EXPOSURE, not its request and not convenience. NOT distributed to outside consumers (rho, epistates): a peer key may carry write — the key stays inside one estate and identifies exactly one party, which is what makes `party` a fact. DISTRIBUTED to consumers (rho-lite): OAuth only — a peer key embedded in something handed to consumers travels with it, and a travelling key that carries write is a write capability with no owner. Asking for the capability does not change which mechanism is safe for it.","duplicate_key_policy":"a key value shared by two consumers REFUSES with `ambiguous_key` rather than resolving to the first — one key per party is what makes `party` a fact."},"refusal_contract":{"transport_status_on_auth_failure":200,"error_location":"JSON-RPC envelope: result.error, with error.data.category","categories":["auth","validation","permanent","transient"],"retryable":"error.data.transient === true — never infer from HTTP status","warning":"res.ok is TRUE on every refusal. Branch on the presence of `error`, not on the status code.","differs_from":"rho, which refuses at the transport with 401"},"excluded_permanently":["every write path","credentials of any kind, including AGENT_ROUTE_API_KEY and AGORA_PARTY_KEY","raw D1 query access","operator personal storage","R2 bulk export"],"sufficient_for_graph_consumers":["chain_*","search_artifacts"]},"callback_receiver":{"url":"https://institute.infloop.ai/api/callbacks/agent-route","verify":"X-AgentRoute-Signature: sha256=<hex HMAC-SHA256(rawBody, secret)>","idempotency":"X-AgentRoute-Delivery","events":["task.completed","task.failed"],"on_untracked":"200 no-op"},"webhook_events":[],"coord":{"participant":"ai-institute","channel":"agent-route /api/coord (MCP POST JSON-RPC + REST)"},"consumes":{"agent_route_contract":"https://agent-route.dmquant.workers.dev/api/contract"},"submission_rate":{"note":"ceilings the institute will not exceed; the governor may submit fewer","tick":"*/5 * * * *","per_tick_max":{"chain_tag":4,"logic_extract":1,"total":5},"per_hour_max":60,"edge_reserve":{"slots":1,"meaning":"free_slots reported by agent-route, minus this, is the institute's usable headroom","env_override":"DISPATCH_GOVERNOR_RESERVE","evaluated_when":"every edge dispatch decision, on every */5 tick, whenever the governor is enabled","NOT_evaluated_when":"DISPATCH_GOVERNOR_ENABLED=\"false\" (operator kill-switch) — the reserve is then not applied at all","binds_when":"agent-route reports a NON-EMPTY fleet whose free_slots <= reserve; i.e. nodes exist and are full","cannot_bind_when":"no node offers the hand — that path is agent-route's depth_cap, never ours. The two controls are on disjoint conditions and cannot both bind on the same dispatch","disjoint_from":"agent-route depth_cap (evaluated only when findBestNode returns null)","caveat":"a reserve is a decision about ANOTHER party's pool taken in institute code; disclosed rather than claimed"},"allocation":"headroom is split across institute drains in priority order (scheduled, chain_tag, logic) — a dimension not visible at agent-route's boundary","not_persisted":["edge_reserve","headroom"],"dispatch_timeout":{"we_send":{"timeoutMs":1800000,"timeout_seconds":1800},"honoured_on_edge_path":false,"evidence":"39,952 completed opencode chain-tag dispatches: 8.9% ran past the 30 min we send, and ctd_mt56dex0_cls2x1lt returned 7 real matches after 32.9 HOURS with retry_count 0. The only visible enforcement boundary is OUR OWN TASK_HARD_DEADLINE_MS (25 min) plus one */5 cron interval, which is what produces the 25-31 min cluster — not the 30 min we send.","consequence":"any institute reasoning that treated timeoutMs as a bound on an edge hand was reasoning about a field that does not arrive. Our local deadline sweep is the real control."}},"field_limits":{"whiteboard_metadata_preview_max":1000,"action_extraction_text_truncate":8000},"docs":"api_doc/institute_api.md","changelog":[{"version":1,"date":"2026-05-22","note":"initial — artifact-link provenance + callback receiver + coord participant; symmetric with agent-route /api/contract v1"},{"version":2,"date":"2026-09-08","note":"publish submission_rate — per-tick ceilings and the edge reserve. agent-route publishes depth_cap and we consumed it while publishing nothing; they hold no reserve, so ours is the only one on their pool (IPR-166)"},{"version":3,"date":"2026-09-17","note":"publish the CONDITION each control engages under, not only its value — agent-route's corollary from d796179d-89e6-49b7-abe0-f6a9a2a976e5, after they found their depth_cap is read only inside if(!nodeId) and therefore disjoint from our reserve. Also disclose that the timeoutMs we send is NOT honoured on the edge path: measured, 8.9% of 39,952 opencode dispatches ran past it and one returned real output after 32.9 hours"}]}