Agent tools (MCP)
Any MCP-compatible client reaches the vault through the tools below at POST /mcp, presenting a delegation token — the shape the three flows below mint, and the only agent credential this transport accepts. Every call travels the same verified, compartment-bounded, audited path as a human query — this page is generated from the same registry tools/list publishes. An agent running on ANOTHER machine gets that token by client-credentials exchange rather than by signing in as a person — see connecting a remote agent for the administrator's provisioning steps.
Which token this transport takes
One shape: a delegation token from /api/agent/self, /api/agent/exchange or /api/agent/device/poll. The ordinary opaque session that POST /api/agent/login mints is not accepted here — a tool call presenting one is refused with denied and a message saying so, and the refusal is audited. That session is still how an agent uses the PORTAL's own routes; nothing about it is deprecated and nothing about this narrows what an agent may reach.
The reason is what each shape can say. A delegation token carries a signed chain naming who acted and for whom, so the audit row for the request answers that question and the rate meter charges the person the work was done for. An opaque agent session names only the agent, which would make the wider-reaching credential the one the record says least about. An agent acting purely as itself loses nothing by the change: /api/agent/self returns a chain one link long that reaches exactly the compartments that agent was granted.
How an agent gets a token
Three ways, and all three end the same way: the token goes to POST /mcp in an Authorization: Bearer header, the same header an ordinary session travels in. Every credential travels in the POST body, never in a URL, and every refusal is the same 403 with no detail in it — a refusal that named the wrong part would tell a caller which of its guesses had the right shape.
- As itself.
POST /api/agent/selfwith the agent's ownclient_idandclient_secret. The token reaches exactly the compartments the agent was granted, and nobody else's. - For a person.
POST /api/agent/exchangewith the agent's own credentials and the sign-in token of the person it is acting for. The token reaches the INTERSECTION of what that person reaches and what the agent itself reaches — never wider than either of them, so lending an agent a sign-in cannot widen the agent and granting an agent generously cannot widen the person. A person of kind administrator or clientadmin can never be acted for at all: that is refused when the token is minted and again every time it is used. - On a machine with no browser.
POST /api/agent/device/beginreturns a device code the agent keeps and a short user code it shows the person; the person approves that short code from their own signed-in session withPOST /api/agent/device/approve; the agent collects its token withPOST /api/agent/device/poll. The grant is collected ONCE — a second poll on the same device code is refused.
Every one of these tokens lives at most FIVE MINUTES, and a token acting for a person never outlives that person's own authority: the chain is re-checked against the vault on every single call, so a revoked agent client, a disabled account in the chain, or a compartment taken back stops the token inside those five minutes rather than after them. There is no refresh call — an agent that needs longer asks again. Every grant, every refusal and every use is audited, and the audit row for a served call carries the whole chain of who acted for whom. The operator's side of all of this — registering the client, granting its compartments, revoking it — is on connecting a remote agent.
What an agent cannot do
These tools are the vault's DOCUMENT surface, and that is the whole of it. An agent reads, searches, thinks over, uploads, edits, lists and compares documents, sees the ones the vault could not read, withdraws its own held uploads, works the review queue, and reads its own figures — always inside the compartments its token reaches, and never outside them.
It does not administer anything. Creating accounts, making or granting compartments, registering agents, changing vault settings, purging documents, and letting a held document into the vault are the administrator's console and have no tool here. Neither does changing anybody's password, e-mail, second factor or profile, nor reading the audit trail. There is no tool name for any of them: an agent that asks for one is told the tool is unknown, and the listing never offers one.
An administrator's own account reaches none of these tools either, and is told so plainly rather than handed an empty result — an administrator holds no compartment, so an empty answer would read to an agent as an empty vault.
search
Hybrid search over the caller's admitted compartments. Fewer than k results after over-fetch is reported (under_return), never silently truncated. date_from/date_to narrow the results to the DOCUMENT'S own date (what the document claims about itself, not when the vault received it). compartment_ids narrows to named compartments and concept_type to one kind of document. A filter only ever narrows what the caller may already reach: naming a compartment you do not belong to returns no hits and no error, exactly as a compartment holding nothing relevant does.
| Parameter | Type | Description |
|---|---|---|
query required | string | |
k | integer | |
include_all_versions | boolean | |
date_from | string | inclusive lower bound on the document's own date, YYYY-MM-DD |
date_to | string | inclusive upper bound on the document's own date, YYYY-MM-DD |
compartment_ids | array | narrow to these compartments. Only ever a narrowing of what you may already reach: an id you do not belong to simply matches nothing. An empty array names no compartment and so matches nothing at all; omit the argument for no filter. |
concept_type | string | narrow to one kind of document, as the vault classified it at ingest. A label the vault does not publish is refused (invalid-params) naming the labels that are, never guessed at and never answered with an empty result. |
representation | string | Which representation to render. Overrides the Accept header. One of: application/json, text/markdown; profile="okf-bundle". Anything else is refused with HTTP 406 before the tool runs. |
Error codes: denied, invalid-params, model-unavailable, rate-limited
read
The full canonical markdown of one visible document version (superseded versions included — history is readable).
| Parameter | Type | Description |
|---|---|---|
version_id required | string | |
representation | string | Which representation to render. Overrides the Accept header. One of: application/json, text/markdown; profile="okf-concept". Anything else is refused with HTTP 406 before the tool runs. |
Error codes: denied, invalid-params, rate-limited
synthesize
Think mode: a cited answer synthesized ONLY from DAL-admitted passages (ADR-0009). covered=false with a verbatim not-covered statement when the corpus holds no match — no model call is made in that case.
| Parameter | Type | Description |
|---|---|---|
question required | string | |
k | integer |
Error codes: denied, invalid-params, model-unavailable, rate-limited
ingest
Ingest one document through the full gated pipeline (malware gate, parse, content screen, classification bounded by the caller's write reach). Gate-held content answers 'quarantined' — honest, not stored.
| Parameter | Type | Description |
|---|---|---|
filename required | string | Filename, recorded as an unverified hint from the caller. It does NOT choose the parser: the type is decided from the file's own bytes, so a Word document named report.txt is read as a Word document. An unclaimed format quarantines, never silently stores. |
content_text | string | Document content as UTF-8 text (markdown family). |
content_base64 | string | Document content base64-encoded (binary formats). |
compartment_ids | array | Explicit classification request; must sit inside the caller's own write reach or the call is denied. |
folder | string | Where to FILE the document: a relative path of folder names, '/' separated, never the document's own filename. Filing only — a folder confers no access and hides nothing; the compartments decide who can reach it. Omit it, or send an empty string, and the document is unfiled, which is the absence of a folder and not a root folder. An absolute path, a '..' segment, a backslash, a control character or more than the deployment's segment limit is refused as invalid-params and nothing is stored. |
Error codes: denied, invalid-params, quarantined, rate-limited, duplicate, storage-full
checkout
The document's current visible version as an edit base: content plus base_version_id to present at commit.
| Parameter | Type | Description |
|---|---|---|
document_id required | string | |
representation | string | Which representation to render. Overrides the Accept header. One of: application/json, text/markdown; profile="okf-concept". Anything else is refused with HTTP 406 before the tool runs. |
Error codes: denied, invalid-params, rate-limited
commit
Commit an edit as a NEW version superseding the checkout base (versions are never destroyed). A base that is no longer current answers 'stale-base': checkout again and rebase.
| Parameter | Type | Description |
|---|---|---|
document_id required | string | |
base_version_id required | string | |
filename required | string | Filename, recorded as an unverified hint from the caller. It does NOT choose the parser: the type is decided from the file's own bytes, so a Word document named report.txt is read as a Word document. An unclaimed format quarantines, never silently stores. |
content_text | string | Document content as UTF-8 text (markdown family). |
content_base64 | string | Document content base64-encoded (binary formats). |
compartment_ids | array | Explicit classification request; must sit inside the caller's own write reach or the call is denied. |
folder | string | Where to FILE the document: a relative path of folder names, '/' separated, never the document's own filename. Filing only — a folder confers no access and hides nothing; the compartments decide who can reach it. Omit it, or send an empty string, and the document is unfiled, which is the absence of a folder and not a root folder. An absolute path, a '..' segment, a backslash, a control character or more than the deployment's segment limit is refused as invalid-params and nothing is stored. |
Error codes: denied, invalid-params, quarantined, stale-base, rate-limited, duplicate
documents
The document versions the caller may read, newest first. Superseded versions are excluded unless asked for -- history is readable, not implied. The answer states which ORDER actually ran, which is not always the one asked for: an unrecognised order falls back to the vault's own received date rather than refusing.
| Parameter | Type | Description |
|---|---|---|
include_all_versions | boolean | |
order | string | Which date to order by. Anything the vault does not recognise falls back to the received date, and the answer says which order ran. |
Error codes: denied, invalid-params
actionable
The documents this vault could not fully read and would like help with, bounded by the caller's own reach. A document whose later version converted cleanly is not listed. The answer names the rule that actually ran and the rules that exist.
| Parameter | Type | Description |
|---|---|---|
rule | string | Which actionable rule to apply. Empty means the deployment's configured rule; an unrecognised value falls back to it rather than refusing, and the answer says which rule ran. |
Error codes: denied, invalid-params
diff
What changed between two versions of ONE document. Both reads happen under the caller's own predicates in a single transaction. Two versions whose combined text is over the cap answer 'too-large' rather than a truncated comparison -- a diff missing its last hunk claims two versions agree in a place nothing looked.
| Parameter | Type | Description |
|---|---|---|
version_a required | string | |
version_b required | string |
Error codes: denied, invalid-params, too-large
original
The bytes of one version exactly as they were uploaded, base64-encoded. This is the only way to reach a version the vault stored without ever reading it, whose canonical markdown is empty by construction. A version that retained no original answers 'not-retained', which is not a refusal of authority.
| Parameter | Type | Description |
|---|---|---|
version_id required | string |
Error codes: denied, invalid-params, not-retained, rate-limited
quarantine
The caller's OWN uploads that the ingest gates held for review, with the vault's reason for each. No argument names whose queue to read, on any layer. Letting a held document into the vault is a reviewer's act and is not a tool.
| Parameter | Type | Description |
|---|
Error codes: denied, invalid-params
quarantine_withdraw
Withdraw one of the caller's own held uploads. An item that is not the caller's and an item that does not exist are one answer, and the trail carries which.
| Parameter | Type | Description |
|---|---|---|
item_id required | string |
Error codes: denied, invalid-params
workflow
The processing queue for the caller's own compartments: what each document is waiting for and how long it has waited. Finished items are excluded unless asked for.
| Parameter | Type | Description |
|---|---|---|
order | string | Queue ordering. An unrecognised value falls back to the vault's own default. |
include_finished | boolean |
Error codes: denied, invalid-params
workflow_item
One queue item in full, including the summary the vault made of it. A version outside the caller's compartments, an unknown version and one the vault no longer holds are ONE answer.
| Parameter | Type | Description |
|---|---|---|
version_id required | string |
Error codes: denied, invalid-params
workflow_advance
Approve the next processing stage for each named version the caller may write. Answers per id what was done AND what was refused -- a bulk action reporting only its successes would let a selection quietly shrink. An id that is not a uuid refuses the whole call rather than being dropped.
| Parameter | Type | Description |
|---|---|---|
version_ids required | array | The versions to approve. |
Error codes: denied, invalid-params
workflow_abandon
Stop processing the named versions deliberately, with a short reason. This does NOT remove a document from the vault: there is no such transition from an admitted version, so one already serving readers refuses. Answers per id what was done and what was refused.
| Parameter | Type | Description |
|---|---|---|
version_ids required | array | The versions to stop. |
reason | string | Why, in a sentence. It is recorded in the audit trail; empty is allowed and means no reason was given. |
Error codes: denied, invalid-params
workflow_mode
Choose who advances the named versions through processing: 'manual' means a person approves each stage, 'automatic' means the vault decides. Required, with no default -- a call that named no mode would otherwise decide silently which way a control went. A mode outside the vocabulary is refused and the refusal is recorded.
| Parameter | Type | Description |
|---|---|---|
version_ids required | array | The versions to switch. |
mode required | string | 'manual' or 'automatic'. The vault holds the vocabulary and refuses anything else. |
Error codes: denied, invalid-params
workflow_reconvert
Read one stored document again, optionally as another type and with other conversion settings. ONE version, never a list: re-conversion re-reads and re-parses a whole document, and a bulk form would let one call start an unbounded amount of work.
| Parameter | Type | Description |
|---|---|---|
version_id required | string | |
media_type | string | Read it as this type instead. Empty keeps the type the vault decided. The vocabulary is the format registry's, one layer down. |
settings | object | Conversion settings as scalar values. A nested object or array is refused rather than coerced. |
Error codes: denied, invalid-params
workflow_requeue
Send one admitted document back through processing. It builds the NEXT version from the retained original; the version already in the vault keeps answering every reader the whole time, so nothing has to be removed.
| Parameter | Type | Description |
|---|---|---|
version_id required | string |
Error codes: denied, invalid-params
workflow_review
Submit a person's own reading of a document the vault read poorly. It becomes a NEW version through the ordinary pipeline, with the reviewer and the time on it; the version it corrects is untouched. Needs write reach, not read reach.
| Parameter | Type | Description |
|---|---|---|
version_id required | string | |
text required | string | What the document actually says. |
Error codes: denied, invalid-params
stats_documents
Per-document figures for the documents the caller may read. ADR-0067: every count is the CALLER'S, never the vault's.
| Parameter | Type | Description |
|---|
Error codes: denied, invalid-params
stats_compartments
Per-compartment figures, bounded to the caller's own reach.
| Parameter | Type | Description |
|---|
Error codes: denied, invalid-params
stats_compartment_documents
The documents in ONE compartment the caller may read. A compartment outside the caller's reach and a compartment that does not exist are the same answer, so an identifier cannot become an existence oracle.
| Parameter | Type | Description |
|---|---|---|
compartment_id required | integer |
Error codes: denied, invalid-params
stats_compartment_documents_over_time
How one compartment's document count moved, day by day, bounded to the caller's reach exactly as the listing above is. The vault-WIDE series has no compartment dimension to bound a caller to and is not a tool.
| Parameter | Type | Description |
|---|---|---|
compartment_id required | integer |
Error codes: denied, invalid-params
models
Which models this deployment will answer with, and which one answers when the caller names none.
| Parameter | Type | Description |
|---|
Error codes: denied, invalid-params
limits
The caps in force on every text field, so a request can be composed within them rather than refused by them. These are the numbers IN FORCE on this deployment, not shipped defaults.
| Parameter | Type | Description |
|---|
Error codes: denied, invalid-params
status
Whether the vault's database and its model runtime are answering. Three states each, and 'unknown' is a first-class one: it means the check could not run, which is neither of the other two.
| Parameter | Type | Description |
|---|
Error codes: denied, invalid-params
duplicates
The documents in the caller's reach that closely resemble one another, with the distance and the kind (near or whitespace_only) and the decision if one has been made. Listing only: it never removes anything.
| Parameter | Type | Description |
|---|---|---|
include_decided | boolean |
Error codes: denied, invalid-params
decide_duplicate
Record what one detected near-duplicate pair is: dated_version, numbered_version, whitespace_only, or discard. A decision is made once and is audited. 'discard' destroys nothing by itself: it requires discard_version_id naming which of the pair's two versions to throw out, records the verdict, and asks an administrator to destroy that copy. They destroy it or decline with a reason, and the pair reports which. Every other verb refuses discard_version_id.
| Parameter | Type | Description |
|---|---|---|
candidate_id required | string | |
decision required | string | |
discard_version_id | string |
Error codes: denied, invalid-params
cancel
Stop a synthesis this caller submitted, by the job reference the vault handed back. Cancelling twice, or cancelling one that has already finished, is not an error: cancelled=false means this call did not end it, and status says what it actually is. A reference the caller did not submit is refused exactly as an unknown one is.
| Parameter | Type | Description |
|---|---|---|
job required | string |
Error codes: denied, invalid-params