Outside agents: the gateway, actions API, MCP and SDKs
Last updated October 5, 2026
On this page
- How an outside agent differs from a kernel agent
- Before you start in the demo
- Registering an outside agent
- From the command line
- The agent in the Directory
- Tokens
- Who the agent acts for
- Asking for an action
- What the agent learns of a decision
- Try it in the demo
- Approvals and expiry
- Who can read an action and its result
- Callbacks
- Work sessions
- MCP
- Limits
- Offboarding
- The threat model in brief
- Known problems
- Not possible yet
- For developers
- The clients
- Agent routes
- Admin routes
- Errors
Draft for review
An outside agent is an agent your company built somewhere else: on LangGraph, the OpenAI Agents SDK, Copilot Studio or its own code. It does not run inside OrchKernel. It reaches your company's tools and data only through the gateway: it asks for one action at a time, and each request goes through the same checks (the gate), the same approvals and the same event log as an action by one of OrchKernel's own agents (a kernel agent, see Agents). The kernel then carries the action out with its own connections, so the outside agent never holds a vendor key or password.
This page covers registering an outside agent, giving it a token, what it can
ask for, how approvals, expiry and callbacks work, the MCP endpoint, the
limits, offboarding, and the TypeScript and Python clients. It describes the
gateway as it works today on the golive branch. What is not possible yet is
listed at the end.
How an outside agent differs from a kernel agent#
| Kernel agent | Outside agent | |
|---|---|---|
| Where it runs | Inside OrchKernel, on its playbook and skills | In your own runtime |
| How it acts | Plans steps that the kernel runs | Asks for each action through POST /api/actions or /mcp |
| What it can reach | The tools of its skills, within delegations | Its ceiling: the tools, collections and data classes picked at registration, within delegations |
| Credentials | None; the kernel calls vendors | None; the kernel calls vendors |
| Trust | Up to trusted | Sandboxed (default) or standard, never higher |
| Can be assigned tasks, mentioned in threads, asked from a conversation, put in a module | Yes | No |
| Its page in the Directory | Overview, Playbook, History, Scorecard | Overview, Gateway, Tokens, Scorecard |
A delegation is a person's grant to an agent: who it acts for, what it may
read and use, and how much it may spend (see Delegations).
A data class is a label on data, such as pii.customer for customer
personal data; picking it at registration lets that data leave to the agent's
runtime.
An outside agent cannot be hired, forked, installed with a pack or a module, or created through the change pipeline. Only a human admin registers one.
Before you start in the demo#
The steps on this page were run in the demo company (see
Quickstart): ok demo, then ok demo serve, signing in with
the tokens it prints. The curl examples use http://127.0.0.1:8080, the
default address; use the Workspace address ok demo serve printed if you
started it with --bind.
No model is needed for anything on this page: the outside agent brings its own, and you play the agent with curl.
Two things to know:
-
Callbacks need
OK_SECRETS_KEY.ok demo servedoes not set one, so registering with a callback URL ends with "No callback secret: the server has no OK_SECRETS_KEY. Set it, then issue one from the agent page." To try callbacks, stop the demo and start it again with a key. Every start issues new sign-in tokens and revokes the old ones, agent tokens included, so issue the agent a new token afterwards.export OK_SECRETS_KEY=$(openssl rand -base64 32) ok demo serve -
A plain
httpcallback URL works only to loopback in development mode.ok demo serveruns in development mode, sohttp://127.0.0.1:<port>/...is accepted from the registration sheet. Everywhere else the callback URL must behttps.
Registering an outside agent#
Sign in as ada (the admin).
- Click Directory, then Register an outside agent (top right; only admins see it). A sheet opens: "An agent built elsewhere acts only through the gateway: ..."
- Fill in: Id
support-bot, NameSupport bot, Rolesupport agent, RuntimeLangGraph, Team Support. Under Acts for tick Tom Becker (tom). Under Tools ticksupport-ops.helpdesk.replyandsupport-ops.helpdesk.tickets. Under Reads ticktickets. - Under Data classes the sheet now offers
pii.customer (restricted), the class those tools and that collection carry. Tick it. A red line appears: "Data of class pii.customer is restricted: it will leave to the agent's runtime." That is a warning, not a refusal. - Callback URL
http://127.0.0.1:18931/hooks/kernel(optional; nothing needs to listen there for this walkthrough), First token lives7d, Pin the first token to Tom Becker. - Choose Register. The sheet now reads "Registered support-bot" with
"Copy them now: each is shown once." and the Agent token (
orchk_ag_and 64 hex digits) with a Copy button, then "Token , expires , pinned to tom." In the demo it also says there is no callback secret (see above), and a yellow badge repeats the restricted data warning. Copy the token, then choose I have copied it.
The agent now appears in the Directory with an Outside: LangGraph badge.
| Field | What it does |
|---|---|
| Id, Name, Role | Required. The id is the agent's address; it cannot change later. |
| Runtime | Free text shown on its page and on approval cards ("Outside agent: LangGraph"). |
| Description | Free text. |
| Team, Reports to | At least one is required: "Pick a team or a manager, so a manager approval has a person." The team's lead sees the agent's actions. A manager who is not the team lead gets no inbox items for it and does not see its Gateway tab; the sheet says so when you pick one. |
| Trust | sandboxed (default) or standard. |
| Acts for | People and teams. Each gets a delegation to the agent (a team: each member). The agent can act only for people who delegated to it. |
| Tools | What it may call. Built-in kernel tools are not offered. |
| Reads, Writes | Collections it may query, and write. A collection ticked under Writes is also read. System collections (modules, views) are not offered. |
| Data classes | The data classes that may leave to the agent. Nothing is ticked by default, and the list offers only the classes the chosen tools and collections carry. A tool or collection whose class is not ticked is left out of the ceiling. |
| Identities | Greyed out: identities need an agent identity provider, which is not available yet. |
| Callback URL | Where the kernel posts status changes. Optional; without it the agent polls. |
| Approvals expire after | Default 24h. |
| First token lives, Pin the first token to | The first token's lifetime (empty means the agent's cap, 30 days) and the one person it acts for. Only people ticked under Acts for are offered. |
A problem with the registration comes back as one list, and nothing is created. For example, an unknown tool, a write collection that is not read, a trust above standard and an identity together give: "tool no.such.tool does not exist; write_collections must be a subset of read_collections: tickets; trust must be sandboxed or standard for an outside agent; identities need an agent issuer, which is not available yet".
From the command line#
The same registration as a file, for ok gateway agent add:
id: support-bot
name: Support bot
role: support agent
runtime: LangGraph
team: team:support
tools: [support-ops.helpdesk.tickets, support-ops.helpdesk.reply]
read_collections: [tickets]
data_classes: [pii.customer]
acts_for: [tom]
callback_url: https://agents.example.com/hooks/kernel
limits: { actions_per_min: 60, pending_max: 20 }
approval_ttl: 24h
The CLI works on the state file directly, so stop the demo server first, run
the commands from the directory where you ran ok demo, and name the state
file and the admin:
S="--state orchkernel-demo/state.db --as ada"
ok gateway agent add support-bot.yaml $S # prints warnings, then the agent
ok gateway token issue support-bot --pin tom --ttl 7d $S # prints the token once
ok gateway callback rotate support-bot $S # prints the secret once; needs OK_SECRETS_KEY
ok gateway agent set support-bot '{"approval_ttl":"2h"}' $S
Without --state and --as the CLI looks for ./.orchkernel/state.db and
your login name, and fails with "unknown actor ". The command line
is never in development mode, so it refuses an http callback URL even to
loopback ("the URL scheme must be https (plain http is allowed only to a
loopback IP literal in dev mode)"). Only human admins run these; Maya gets
"only human admins manage outside agents" from agent add and "only human
admins issue agent tokens" from token issue.
Optional settings not on the sheet: budget (a spending cap of the agent's
own; without one it has none, though delegation, team and work session
budgets still apply), limits (see Limits), token_max_ttl_days
(default 30, at most 90) and allowed_cidrs (addresses the agent may call
from; empty means any).
The agent in the Directory#
Click Support bot in the Directory. The details show its facts, Let Support bot act for me, Acts under (Tom's delegation: "registered to act for this person through the gateway") and, for admins, Promote to standard and Offboard. Promoting to trusted is refused ("Support bot runs outside OrchKernel; the trusted tier is not possible").
Open agent page (address /directory/support-bot) has four tabs. The
header shows Outside agent: LangGraph and its state (Active or
Offboarded).
| Tab | What it shows | Who sees it |
|---|---|---|
| Overview | Id, runtime, role, team, reports to, trust, identities, tools, reads, writes, data classes, callback URL and callback health, approval expiry ("after 1 days"), token lifetime cap ("at most 30 days"), allowed addresses ("any"), limits ("60 a minute, 5000 a day, 20 waiting, 30 approvals an hour, 120 polls a minute") and version. Admins also get Issue callback secret (or Rotate callback secret once one exists) and Send a test. | Anyone who opens the page. Callback health is accurate only for admins (see Known problems). |
| Gateway | Every action with a status filter (all, received, executing, pending approval, executed, failed, denied, rejected, expired, withdrawn, unknown): status, action (tool.call(support-ops.helpdesk.reply)), acting for, the decision ("allow", "require approval at policy", "deny at availability"), asked, finished and its run. Pending actions show their approval card underneath. Then Work sessions. |
Admins and the lead of the agent's team. Others see "Only admins and the agent's team lead see its actions." |
| Tokens | Each token's id, source ("admin (ada)"), pinned to, issued and expires, with Revoke. Under the table a lifetime box, a Pin to list and Issue token. | Admins. Everyone else sees "No tokens." |
| Scorecard | Actions by status ("executed 6 of 13 actions") and Approval latency: how many asked for approval, and the median and longest time from asking to a decision. | Admins and the team lead. Others see "Only admins and the agent's team lead see its scorecard." |
To change settings later there is no form yet. Use ok gateway agent set
(a JSON merge patch, above) or PUT /api/gateway/agents/:id with the whole
registration and the version you read (see For
developers). Trust is kept as it is: change it with
Promote to standard. Changing the callback URL clears the callback secret.
Tokens#
The agent sends its token as Authorization: Bearer orchk_ag_....
- Issue from the Tokens tab: type a lifetime such as
1d, optionally pick a person under Pin to, and choose Issue token. A sheet "Token for Support bot" says "Copy it now: it is shown once." (and, when pinned, "Every action with it acts for alice."). Or useok gateway token issue. A token lives at most the agent's cap (30 days by default); asking for more is refused ("ttl must be at most 30 days for support-bot"). - Pin a token to one person when the agent does one person's work. Every
action with it acts for that person; naming anyone else is refused with 403
pinned("this token acts for one person only"). You can pin only to someone who has delegated to the agent. - Revoke: choose Revoke on the token's row, then Revoke in the dialog "Revoke token f69a8ea7? The agent can no longer use it." A note says "Revoked f69a8ea7". The next request with it gets 401 "invalid token".
An agent token works only on the agent's routes: POST /api/actions,
GET /api/actions, GET /api/actions/:id, POST /api/actions/:id/cancel,
/api/gateway/work-sessions..., GET /api/gateway/tools and /mcp.
Anywhere else it gets 403 agent_token_scope ("an agent token works on the
agent-facing gateway routes only"). The other way round, a person's token
cannot ask for an action: POST /api/actions with Tom's token is 403
not_external_agent ("this route serves outside agents' tokens only").
Who the agent acts for#
Every action acts for one person. The kernel picks that person from, in order:
- the token's pin;
- the request's
acting_for(on MCP, thex-ok-acting-forheader sent withinitialize); - the only person who has delegated to the agent, if there is exactly one.
Otherwise the request is refused with 422 acting_for_required ("name the
person this request acts for in acting_for"). The person is never taken from
the action's arguments or the agent's reason.
More people can let the agent act for them after registration: open the agent
in the Directory and choose Let Support bot act for me. A note says
"Support bot now acts for you". That person's delegation then bounds what the
agent may do for them. Try it as alice: afterwards an unpinned token with
no acting_for is refused with acting_for_required, because two people now
delegate to the agent.
Be careful who you let an outside agent act for: today it can read data the person it acts for may not read (see Known problems).
Asking for an action#
The agent posts to POST /api/actions:
{
"idempotency_key": "reply-hd4826-1",
"action": { "kind": "tool", "tool": "support-ops.helpdesk.reply",
"args": { "ticket": "HD-4826", "body": "Your refund is on its way." } },
"reason": "Customer asked about the duplicate charge refund",
"wait": true
}
| Field | What it does |
|---|---|
action.kind |
tool (with tool and args), query or aggregate (with collection and a query), or upsert (with collection, fields and an optional id). Nothing else can be asked. |
idempotency_key |
Required (400 idempotency_key_required without it): 1 to 128 letters, digits and . _ : -. Sending the same request again with the same key answers the first action (200, header idempotent-replayed: true) and runs nothing. The same key with a different request is 409 idempotency_mismatch ("this idempotency key was used with another request"). A key stays in use while its action is open and for 24 hours after. |
acting_for |
The person, when the token is not pinned. |
reason |
Shown to the approver under "The agent says:". It is treated as data, never as instructions. |
wait |
true (default) answers once the action has run or is waiting. false answers as soon as the gate has decided. |
work_session |
Optional; see Work sessions. |
The answer is the action itself: its id, status, version, who it acts
for, the decision with its steps, the approval (approver's name and
expiry) when it waits, the error when it did not run, and the result once
it ran.
| Status | Meaning |
|---|---|
executing |
Allowed and running. |
pending_approval |
Waiting for a person. |
executed |
Done; result holds the answer. |
failed |
The tool or write failed; error says why. |
denied |
The gate or a limit refused it. Nobody was asked. |
rejected |
The approver said no. Their note is passed on. |
expired |
Nobody decided in time. |
withdrawn |
Cancelled by the agent, or the agent was offboarded, or the person's delegation was revoked. |
unknown |
The server restarted while the action was running; the vendor may or may not have done it. |
denied, rejected, expired and withdrawn are normal answers (HTTP 201),
not errors.
What the agent learns of a decision#
The decision's steps are rewritten for the agent: each check, its result and a short line ("acting for Tom Becker under their delegation", "Maya Patel would be asked"). A matched policy rule keeps its id and description. The agent never sees a budget figure, a team, a module, a connection or the rule's conditions.
A tool the agent may not use and a tool that does not exist get the same
answer: denied at stage availability, "tool no.such.tool is not available
to this agent".
A tool's argument rules are checked before anyone is asked. The helpdesk
reply goes only to a ticket read earlier in the same work session (see
Work sessions); a reply to any other ticket is denied at
stage arguments with only "an argument breaks the tool's rules", without
saying which rule.
Try it in the demo#
With the support bot registered and its pinned token in $TOKEN:
-
Read the open tickets:
curl -s http://127.0.0.1:8080/api/actions \ -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ -d '{"idempotency_key":"tickets-open-1","action":{"kind":"tool", "tool":"support-ops.helpdesk.tickets","args":{"status":"open"}}, "reason":"Find open tickets to answer"}'You get
"status": "executed","acting_for": "tom"and aresultwith two tickets, HD-4821 "CSV export fails since Monday" and HD-4826 "Refund for duplicate charge". Add-iand send the same command again: 200 withidempotent-replayed: trueand the same action. -
Ask for something outside the ceiling, such as
support-ops.billing.refund(with a new idempotency key):"status": "denied", "tool support-ops.billing.refund is not available to this agent". -
Reply to HD-4826 with the reply tool (the JSON above). The answer is
"status": "pending_approval"with"approver": "Maya Patel"and an expiry 24 hours away. The decision's reason reads "rule support-ops-customer-replies-need-the-lead: Agents never reply to a customer without the support lead approving the draft".
Approvals and expiry#
An action that needs approval waits and asks the person the policy names; by default that is the person the agent acts for. In the demo the Support operations module's rule names the support lead, so the support bot's reply for Tom goes to Maya, not Tom. See Inbox and approvals.
- Sign in as maya and open Inbox. Under Waiting on you: Reply on a ticket, badges Outside agent: LangGraph and Pending, "Support bot asks to reply on a ticket. acting for Tom Becker", "The arguments, exactly as they will run:", "The agent says: Customer asked about the duplicate charge refund", "Expires in 24 h", the rule's description, See the run, Why am I asked?, Reject and Approve. The same card shows on the agent's Gateway tab.
- Choose Approve. A note says "Approved" and the card leaves the inbox.
- Fetch the action (
GET /api/actions/<id>with the agent's token). It isexecutedwith a result such as{"id": "reply-90c1e2"}.
Only the named approver can decide. Tom trying to approve through the API
gets 403 not_approver. To reject, choose Reject, type a reason under
Reason (optional) and choose Confirm reject; a note says "Reply on a
ticket: rejected · Tom Becker was told". The reason goes back to the agent;
over MCP it reads "Rejected: Say when exactly the refund lands".
Approving checks the policy again first, and two approvers at once, an approval racing the expiry, or a restart mid-call never run the action twice.
- Expiry. Nobody deciding within the agent's approval time (24 hours by
default) ends the action
expiredwith "the approval expired before anyone decided it". With a 5-minute expiry, the action turnedexpireda few seconds after its deadline. - Withdrawal. The agent cancels with
POST /api/actions/:id/cancel(statuswithdrawn, error "withdrawn: cancelled"). Offboarding the agent, or the person revoking their delegation, withdraws their waiting actions too.
Who can read an action and its result#
| Who | Reads the action | Gets the result |
|---|---|---|
| The agent itself | Yes | Yes |
| The person it acted for (Tom) | Yes, with their own token | Yes |
| The approver, the agent's team lead (Maya), admins (Ada) | Yes | No. An admin can ask ?result=true, which is recorded as an "Outside agent read a result" event. |
| Anyone else (Alice, Erin) | 404 | No |
Results are kept 24 hours after the action ends; arguments and the reason 30 days (the operator can change the second, see Limits).
Callbacks#
An agent with a callback URL gets a POST each time one of its actions changes status, so it does not need to poll. Deliveries go out from one worker with retries after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours.
- Secret. Issued at registration when the server has
OK_SECRETS_KEY, or later with Issue callback secret / Rotate callback secret on the Overview tab orok gateway callback rotate. It starts withwhsec_and is shown once ("Copy it now: it is shown once. The secret it replaces no longer works."). Rotating does not ask you to confirm. WithoutOK_SECRETS_KEYthe button answers "a secret cannot be stored: OK_SECRETS_KEY is not set". - Test. Send a test on the Overview tab (or
ok gateway callback test) sends a signed{"type":"test"}and says "Test delivered" or why it failed ("Test failed: Connection Failed: Connect error: Connection refused"). With no secret yet it says "support-bot has no callback secret; issue one first".
A delivery looks like this:
POST /hooks/kernel
x-ok-signature-version: 2
x-ok-timestamp: 1791198929
x-ok-delivery: dlv_90ce8f25ddc644118b45806c74df43dd
x-ok-signature: sha256=7c155e38...
{"action":"act_948f72c61fecfb211f66d872e6b864db","agent":"support-bot",
"at":"2026-10-05T11:15:29.636Z","status":"pending_approval",
"type":"action.updated","version":2,"work_session":"ws_ac948c..."}
The body never carries a result or the decision's text; fetch the action for
those. Every status change is delivered, including executing, denied and
withdrawn.
Verify every delivery. The signature is HMAC-SHA256, keyed with the whole
callback secret (whsec_...), over <timestamp>.<delivery>.<raw body>.
Compare it in constant time, refuse timestamps more than a few minutes old,
and remember delivery ids to refuse replays. The SDK functions below do all
three: version 2 only, within five minutes, and no delivery id seen in the
last ten minutes.
Work sessions#
A work session groups an agent's actions for one person into one kernel run, so the person, the agent's team lead and admins can follow it on the Work page ("Support bot through the gateway"). An action that names no work session goes into the agent's default one for that person (shown as default on the Gateway tab).
- Open one with
POST /api/gateway/work-sessionsand{"acting_for": "tom", "name": "Refund follow-ups", "budget": {"limit": 500}}(the budget in cents, optional); end it withPOST /api/gateway/work-sessions/:id/end. - An agent holds at most 20 open work sessions; opening a 21st closes its longest-idle one, if that has been idle 5 minutes or more.
- A work session closes by itself after 30 idle minutes or 24 hours in all.
- Each MCP session is a work session, named "MCP: ".
- What a work session read decides what some tools may do next: a helpdesk reply in a new MCP session is denied until that session has read the tickets.
MCP#
Any MCP client can use the agent's token against /mcp at the server's root
(not under /api). It speaks MCP over Streamable HTTP (protocol versions
2025-06-18 and 2025-03-26), one JSON answer per request.
url: http://127.0.0.1:8080/mcp
headers: Authorization: Bearer orchk_ag_...
x-ok-acting-for: tom # only when the token is not pinned
What a client sees:
- initialize opens a work session and answers its id in the
Mcp-Session-Idheader, which every later request must carry (missing: 400session_required; unknown or ended: 404session_not_found). With no person to act for it answers error -32602 "acting_for_required: send x-ok-acting-for". The server's instructions say: "Every call is governed. A call that needs a person's approval returns 'waiting for approval' with an action id. Call kernel_action_status with that id later to get the result." - tools/list lists the ceiling for that person, with dots in tool ids
turned into
__(support-ops__helpdesk__reply) and the real id in_meta["orchkernel/tool"], plus the kernel's own tools:kernel_collections,kernel_query,kernel_aggregate,kernel_upsert(only when a collection is writable, so not for the support bot) andkernel_action_status._meta["orchkernel/may_need_approval"]is a hint only.readOnlyHintis true only for tools at riskreadwith no side effect, so the demo's tickets tool (risklow) is not marked read-only. - tools/call is a governed action. An executed call returns the result. A
call that needs approval returns
isError: truewith "Not done yet: waiting for approval from Maya Patel (action act_..., expires 2026-10-06 12:07 UTC). Call kernel_action_status with this action id to get the result." Nothing failed: callkernel_action_statuswith{ "action": "act_...", "wait_s": 20 }(at most 30) to wait for it. A refused call returnsisError: truewith the reason, such as "Not allowed: an argument breaks the tool's rules". - DELETE /mcp ends the session (204).
Not served: resources, prompts, sampling, elicitation, roots, completions,
logging and server-initiated messages. GET /mcp is 405; a request with no
token is 401 with WWW-Authenticate: Bearer.
Limits#
Per agent, under limits at registration (the Overview tab shows them):
| Limit | Default | Bounds | Over it |
|---|---|---|---|
actions_per_min |
60 | 1 to 600 | 429 rate_limited with Retry-After: 60 ("actions_per_min reached; retry after 60 seconds") |
actions_per_day |
5,000 | 1 to 100,000 | The same |
pending_max |
20 | 1 to 200 | denied at stage limits, "pending_max is reached: no more actions may wait for approval now"; nobody is asked |
approvals_per_hour |
30 | 1 to 500 | denied at stage limits |
polls_per_min |
120 | 10 to 1,200 | 429 on reads of actions, the tool list and kernel_action_status |
Opening a work session (or an MCP initialize) counts as one action against
the per-minute and per-day limits. The agent's own budget, the delegation's
per-action cap, the team's and the work session's budgets apply as for kernel
agents.
For the operator, per server:
| Variable | Default | What it sets |
|---|---|---|
OK_GATEWAY_MAX_EXECUTING |
64 | Gateway actions running at once; over it, 429. |
OK_GATEWAY_MAX_WAITS |
1,000 | Long polls and kernel_action_status waits held at once. |
OK_GATEWAY_KEEP_ARGS_DAYS |
30 | Days an action's arguments and reason are kept after it ends (1 to 365). |
OK_GATEWAY_KEEP_DAYS |
400 | Days an action's row and callback records are kept. |
OK_CORS_ORIGINS |
unset | Browser origins /mcp serves; agents send none. |
OK_TRUSTED_PROXIES |
unset | Proxies whose X-Forwarded-For is trusted for allowed_cidrs. |
A client address gets 429 after 60 failed sign-ins in a minute.
Offboarding#
As ada, in the Directory, click the agent, then Offboard, and confirm with Offboard in the dialog "Offboard Support bot? All delegations are revoked and running work cancelled." A note says "Offboarded". Then:
- its tokens stop working (401 "invalid token");
- waiting actions become
withdrawn(error "withdrawn: offboarded"), and the agent gets that callback; - its work sessions close (reason
offboarded); - its callback secret is cleared.
Its page, now marked Offboarded, and its history stay readable.
The threat model in brief#
- The agent's runtime and model are outside the trust boundary. They get filtered decision steps, never a budget figure, a connection or a credential. Their arguments and reason are data, never instructions.
- A stolen agent token reaches only the agent routes, within the ceiling, for
its pinned person, from
allowed_cidrs, until it expires or is revoked. - Nothing runs twice: idempotency keys, a row claimed before any vendor call, and a vendor idempotency key derived from the action id.
- The event log records who asked, for whom, the decision, the approver and the outcome; never arguments, results, the reason, a token or a callback secret. See Events and the audit log.
On the Events page, the Type list's Sign-in and outside agents group holds Token issued, Token narrowed, Token revoked, Outside agent changed, Work session opened, Work session closed, Outside agent asked to act, Outside action decided, Outside action finished, Callback sent, Outside agent read a result and Outside agent rate limited.
Known problems#
Found while writing this page:
- An outside agent can read data the person it acts for cannot. In the
demo, Alice (sales) is refused the
ticketscollection ("alice may not read tickets"), but after she chooses Let Support bot act for me, the support bot acting for her reads all five tickets with aqueryaction. The gateway checks the agent's ceiling and Alice's delegation, not Alice's own access. - The delegation shows an absurd spending limit. In the Directory details, Acts under reads "up to $23,058,430,092,136,940.00" for a delegation with no cap.
- For the team lead (Maya), the Overview tab says "callback health: no delivery yet" even after deliveries. The Tokens tab says "No tokens." to everyone but admins, even when tokens exist, instead of saying only admins see them.
- The Tokens tab's Pin to list offers every person; pinning to someone who has not delegated to the agent is refused ("pin alice must be an active human holding a delegation to support-bot").
- The Overview tab reads "approvals expire after 1 days".
- The decision steps list "budget: the budgets allow this call" twice.
- A withdrawn action's decision on the Gateway tab reads "cancelled: withdrawn: cancelled".
- A reply refused by an argument rule tells the agent only "an argument breaks the tool's rules", not which rule or how to fix it.
- The Directory details do not refresh after Let Support bot act for me; close and reopen them to see the new delegation.
Not possible yet#
- Agent identity providers: no Entra or Okta tokens, no token exchange, no identities on a registration (refused with 422). An admin issues every agent token.
- Editing a registration from the screen (use
ok gateway agent setor the API). - Assigning an outside agent a task, mentioning it in a thread, asking it from a conversation, or putting it in a module or pack.
- Trust above standard.
- Advisory mode (refused: "advisory mode is not available").
- Changing who the agent acts for after registration, other than people delegating to it themselves.
For developers#
The clients#
Both clients live under sdk/ in the source tree: sdk/typescript (Node
18+, Deno, Bun; no dependencies; build with npm install && npm run build)
and sdk/python (Python 3.9+, standard library only; pip install ./sdk/python). Both refuse plain http except to loopback, never follow
redirects, generate an idempotency key when you give none and retry a network
error with the same key, and return denied, rejected, expired and
withdrawn as statuses, not exceptions.
TypeScript: ask, wait for the decision, verify a callback.
import { Client, isFinal, verifyCallback } from 'orchkernel'
const ok = new Client({ baseUrl: 'http://127.0.0.1:8080', token: process.env.OK_AGENT_TOKEN! })
const a = await ok.actions.create({
tool: 'support-ops.helpdesk.reply',
args: { ticket: 'HD-4821', body: 'We fixed the CSV export.' },
reason: 'Customer reported the CSV export bug',
})
// a.status === 'pending_approval', a.approval.approver === 'Maya Patel'
const done = isFinal(a) ? a : await ok.actions.wait(a.id, { timeoutMs: 10 * 60_000 })
console.log(done.status, done.result) // executed { id: 'reply-...' }
// In your callback handler, with the raw body as received:
const event = await verifyCallback(secret, request.headers, rawBody)
Python: the same, plus a callback receiver.
import os
from orchkernel import Client, is_final, verify_callback, CallbackError
secret = os.environ["OK_CALLBACK_SECRET"]
ok = Client("http://127.0.0.1:8080", token=os.environ["OK_AGENT_TOKEN"])
a = ok.actions.create(
tool="support-ops.helpdesk.reply",
args={"ticket": "HD-4826", "body": "Your refund is on its way."},
reason="Customer asked about the duplicate charge refund",
)
done = a if is_final(a) else ok.actions.wait(a["id"], timeout=600)
def handle(headers, raw_body): # in your HTTP server
try:
event = verify_callback(secret, headers, raw_body)
except CallbackError as e: # bad_signature, stale_timestamp, replayed, ...
return 401
if event.get("type") == "action.updated":
action = ok.actions.get(event["action"])
These replies go into the agent's default work session for the person, so they pass the reply's argument rule only after that session read the tickets (step 1 of Try it in the demo).
Also: actions.query, actions.aggregate, actions.upsert, actions.get
(with after_version and wait for a long poll), actions.list,
actions.cancel, work_sessions.open / workSessions.open, get, end,
and tools(). wait gives back the latest state when the timeout passes
first; check it with is_final / isFinal.
Agent routes#
All under /api with the agent token, except /mcp.
| Method and path | Notes |
|---|---|
POST /actions |
Ask for an action (above). 201 new, 200 replayed. |
GET /actions |
The agent's actions, newest first; status, work_session, limit, cursor in x-next-cursor. |
GET /actions/:id?after_version=N&wait=30 |
Holds the request until the version passes N, at most 30 seconds. |
POST /actions/:id/cancel |
Withdraw a waiting action. |
GET /gateway/tools?acting_for= |
The ceiling for that person, with each tool's argument schema, risk and side effect. |
POST /gateway/work-sessions, GET /gateway/work-sessions[/:id], POST /gateway/work-sessions/:id/end |
Work sessions. |
POST, DELETE /mcp |
MCP. |
Admin routes#
With a person's token. Admins, and for reads the agent's team lead.
| Method and path | Notes |
|---|---|
GET, POST /gateway/agents |
List and register. 201 with actor and warnings; 422 invalid with problems. |
GET, PUT /gateway/agents/:id |
Read (actor, version, callback), replace. PUT takes the whole registration plus the version read: without it 428 version_required, stale 409 version_conflict. Fields left out go back to their defaults. |
POST /gateway/agents/:id/tokens |
{ ttl?, pin? }; the token is in the answer once. |
POST /gateway/agents/:id/callback-secret, .../callback-test |
Issue or rotate (503 secrets_key_missing without OK_SECRETS_KEY); send a test (409 no_callback_secret before a secret exists). |
GET /gateway/agents/:id/actions, .../work-sessions |
The agent's actions and work sessions. |
DELETE /tokens/id/:token |
Revoke one token. |
POST /approvals/:id |
{ approved, note? }, by the named approver. |
POST /actors/:id/trust |
{ tier: "standard" }; trusted is 409 external_agent. |
ok gateway actions --agent support-bot and ok gateway work-sessions --agent support-bot list the same from the command line (with --state and --as
as above), without results.
Errors#
| Status and code | When |
|---|---|
400 idempotency_key_required |
No idempotency key. |
401 unauthorized |
Unknown, expired or revoked token. |
403 agent_token_scope |
An agent token outside the agent routes. |
403 not_external_agent |
A person's or API token asking for an action. |
403 pinned |
A pinned token naming another person. |
403 not_approver |
Someone other than the named approver deciding. |
409 idempotency_mismatch |
A key reused with a different request. |
409 now_denied |
Approving an action the policy now refuses. |
422 acting_for_required |
No person could be chosen. |
422 invalid |
A bad registration, token request or idempotency key, with problems. |
429 rate_limited |
Over a per-minute or per-day limit, with Retry-After. |
See also The gate, Delegations, Policy and Inbox and approvals.