AI agents — the Claude Agent SDK lane
packs:ai-agents
packs:ai-agents is the capability that lets an AI team member work on
your estate: an LLM-driven agent enrolled, scoped, audited and stoppable
by exactly the machinery that governs every other agent — plus the
controls a regulator asks about only when the operator is software. The
governance fabric itself is opschain docs agent-plane; this page is the
LLM-specific lane.
The carve-out, stated plainly: LLM-native capability lives in
packs:ai-agents and only there. An estate that does not licence the
pack has no embedded LLM in the product at all; one that licences it but
enrols no AI-type agent carries no exposure. An AI-type enrolment (a
manifest declaring type: ai) hard-depends on packs:agent-governance +
packs:ai-agents — the refusal names every missing pack, so ungoverned
AI cannot even enrol.
The adapter — where the model meets the platform
The Claude Agent SDK adapter (sdk/python/opschain_claude_adapter.py)
lives in the agent process, as a layer over the Python agent SDK.
The platform neither hosts nor proxies model inference:
it supplies governed context reads and receives governed action
requests. Concretely, the model loop's only estate-facing tools are:
| Tool | Platform lane | Governance applied server-side |
|---|---|---|
opschain_read | GET /api/... over the agent request lane | authorisation under the agent's service account — or, with asker_reads=True, under the asking user's own authority (the asker:<exchange-id> run-as mode: read-only, live-exchange-scoped, requires an act_as:asker grant); audited either way |
opschain_propose | POST .../recommendations | the ADVISE lane: a human accepts, the change runs under the human's authority |
opschain_request_change | POST /api/changes | trust-tier resolution: SUPERVISED gates in the approval funnel; ADVISE/ungranted/over-cap refuse by name |
opschain_docs_search / opschain_docs_page | GET /api/docs/... | the entitlement-gated docs index: the served corpus is the knowledge boundary, and a page outside the licence answers with the pack-upgrade guidance, never content |
The adapter's tools= selection picks which of these a given agent's
loop carries.
A hallucinated action is therefore refused server-side like any other ungranted request, the refusal is audited, and the adapter hands the refusal text back to the model as the tool result — a refusal is an answer, never a wall to climb around.
Run an AI agent with agent_type="ai" on the SDK constructor
(opschain agent dev --type ai for the local loop), the adapter bound to
chat, and the Claude Agent SDK installed in the agent's environment
(pip install claude-agent-sdk). The example entrypoint is
sdk/python/examples/claude_sme_agent.py; the hardened reference image
is examples/ai-agent-image/ (build/run instructions in its README).
Manifest disclosures — the attested "what is this thing"
An ai-type registration must disclose what it is: the model identification (model_provider/model_name, optional
model_version), the adapter identification (adapter, optional
adapter_version), the tool_surface it wields (the table above:
request_lane, docs, asker_reads, read), and the custody_mode
(brokered, agent-host or vault-to-host). Any type
may add a purpose label. The adapter discloses itself: bind_chat()
attaches ClaudeAdapter.disclosures() (its model, adapter identity, the
actual tools= selection plus asker_reads, and its custody_mode=
option) when the agent declares none and ships without a signed release.
Enrolment attests the type: an ai-type manifest without these
disclosures refuses by name, a non-ai manifest declaring them refuses
as a type contradiction, and adapter-transit transcripts from a non-ai
agent refuse with the audited warn:agent:adapter_transit_refused flag.
A signed AgentRelease freezes the disclosures in its manifest document
("disclosures": {...}): the frozen set is what the fleet detail and
trust review render (disclosures_from_release), and a contradicting
live declaration refuses — re-release to change them. Detection of an
LLM agent that misdeclares its type and never transits a platform seam
stays an audit/licence-compliance matter, stated honestly.
Model credentials — brokered custody, or the agent host's
The custody posture ("Rather than share API keys with each Agent, they reference a provider who does the heavy lifting"): the manifest never carries a credential and the platform never pushes secrets to agents. Three custody modes:
-
Brokered (primary) — the agent's manifest references a ModelProvider catalog record by code (
model_provider, set fromOPSCHAIN_MODEL_PROVIDERon the agent host) and carries no model key. The adapter points the model SDK at the platform's model gateway (/api/model/{provider}/…), authenticating with the agent's own session credential; the gateway resolves the record, injects the stored key server-side (encrypted at rest, write-only, rotated in one place viaopschain admin model-providers update) and streams the provider's answer through untouched. Every call is checked (registered, non-revoked, non-halted agent whose manifest references the addressed provider) and audited (info:agent:model_call/warn:agent:model_call_refused); a registration naming a provider the catalog does not hold refuses by name (warn:agent:model_provider_refused). Registration of amodel_providerreference requires the record to exist; calls additionally require it enabled.Which provider is used: the one catalog entry whose code the manifest names — exactly one, addressed by code, never resolved by kind. Several entries of one kind coexist on purpose (two
anthropicrecords for different keys, regions or data-handling approvals), and an agent uses only the entry it names; registering a second provider of the same kind changes nothing for agents bound to the first. The binding is visible on every fleet read asmodel_provider_codewith the entry'smodel_provider_kindand livemodel_provider_state(ok/disabled/missing) —opschain admin agent showrenders it as the MODEL PROVIDER row,opschain admin agents listas the PROVIDER column, and the GUI agent sheet links it to the catalog page. Do not read the disclosuremodel_provider(the MODEL row) as the binding: that is the model vendor the agent attests to (anthropic), not a catalog code (demo-anthropic).Each catalog entry also carries a data-handling-approved marker (
data_handling_approved, set with--data-handling-approvedon the create/update verbs): the record of the estate's one-time review of that provider's data-processing terms (training use, retention, residency, DPA). Lanes that send customer-submitted content to a model — such as a support-desk agent's AI lane — refuse to be configured against an unapproved provider, so a new or custom provider added to the catalog inherits the same check: review its terms, mark the entry, and only then can such a lane use it. The marker does not gate ordinary brokered calls over the estate's own content. -
Agent-deployment custody (BYO-key fallback) —
ANTHROPIC_API_KEYreaches the agent process from the host's own secret handling (container secret, systemd credential, orchestration secret mount). A key in the pane keeps this mode exactly as it was. -
Vault-to-host at deploy time — the customer's vault serves the key to the agent's host when the agent deploys; the platform stays out of the path.
Hosted vs local models are both adapter configurations: hosted means
estate context read by the agent transits to the model provider — the
redaction rule bounds what can leave and the transcripts make it
reviewable — while an air-gapped estate points ANTHROPIC_BASE_URL at a
local endpoint with identical governance.
Two model adapters ship with the SDK, selected in the bundled example
agents by OPSCHAIN_MODEL_ADAPTER (claude, the default, or openai):
the Claude adapter drives the Claude Agent SDK, and the OpenAI adapter
speaks the chat-completions wire — for OpenAI-hosted models or an Azure
OpenAI deployment (provider kind azure-openai; the model name is the
deployment name, from OPSCHAIN_AGENT_MODEL in the examples). Claude
deployed in Azure AI Foundry keeps the Claude adapter unchanged: register
the provider with kind azure-foundry-anthropic and its Foundry endpoint.
Both adapters share the same custody modes, governed tool surface,
redaction and transcript lanes; the OpenAI adapter's agent-host fallback is
OPENAI_API_KEY / OPENAI_BASE_URL.
Adapter-transit transcripts — captured, redacted, readable
Every model interaction transiting the adapter — prompts, assistant turns, tool calls and their results — is captured platform-side , in two passes under the standing redaction rule (all resolved secrets and PII redacted, debug lanes included; masked before persistence, never at render):
- the adapter redacts locally before anything leaves the agent (secret-shaped key/values, bearer/basic credentials, model-key shapes, email addresses), and
- the server redacts again at
AgentTranscriptService/ AppendAdapterTranscriptbefore the lines are persisted.
Captured exchanges are readable at
GET /api/projects/{project}/agents/{agent}/adapter_transcripts/{exchange}
(node-read authorised, 404-on-deny, the log read API's paging), beside
the chat transcripts the platform already captures at the chat proxy.
Capture has no off switch on either lane.
Volume, honestly: an agent loop emits tens of model interactions per
task — far chattier than human chat. Both transcript kinds ride the
step-log sealed-segment machinery (spool, masking, blob store, the log
read API) but carry their own exchange-keyed segment index
(transcript_segments) and their own retention family: the
horizon retention.chat_transcripts_days (default 365 days when omitted,
0 means never) purges expired exchanges of both kinds with
archive-before-purge, and an exchange cited as transcript evidence on a
legal-held ledger entry is exempt until the hold is lifted (see
opschain docs retention). Size storage for adapter cadence.
Budget, rate and token caps — bounding the loop
Trust grants carry three cap fields and one condition. The condition is
release_stage (opschain admin trust grant … --release-stage <rung>): the grant admits only agent instances whose enrolled release
sits at or above that rung of the estate's stage ladder, evaluated in the
compiled trust snapshot like every other selector — so a grant over a
production scope can require a production-stage build explicitly, and an
instance enrolled without a release never satisfies it. The stage is a
principal attribute the platform refreshes on promotion; the environment
floor that refuses below-stage work before any grant is consulted, and
the ladder itself, are on opschain docs agent-releases ("Where a stage
may act").
The caps: rate and budget are enforced at the governed request lane beside tier resolution; the token caps are enforced at the model gateway. All are O(1) counter reads on shared, replica-safe rows:
rate— governed work requests per minute under the grant;budget— work invocations per UTC day under the grant;- the six token-cap windows — model tokens (input + output) spent
through the model gateway under the grant per sliding minute (the
burst cap,
--tokens-per-minute), per local day (--tokens-per-day, alias--tokens), week (Monday 00:00), month, fiscal quarter and fiscal year — the calendar windows on the estate's reporting calendar (usage.timezone,usage.fiscal_year_start, with a project override), never in UTC. A grant carries any subset; every window set must pass.
A request over any cap refuses loudly with a named error telling the
agent (and its humans) which grant, which cap — for tokens, which
window failed first (minute, then day, week, month, quarter, year) —
and the local boundary it resets at, and lands the audited
warn:trust:cap_refused event — a runaway loop is a bounded, audited
event, not an incident. Unused budget never rolls over; an exhausted
month refuses for the rest of the month, and the remedy is a governed
grant change (an emergency top-up rides the same approval funnel with
its own evidence) — no silent degrade, no automatic roll-forward.
warn:trust:cap_approaching fires once per window per period when
spend crosses model_usage.cap_warning_fraction (default 0.8) of a cap.
Both events carry scope: grant; the model provider record can carry a
ceiling of its own on the same windows, checked after the grant windows
and reported with scope: provider (model-providers).
Caps are written with the grant (opschain admin trust grant --rate N --budget N --tokens-per-day N --tokens-per-month N …), granted and
raised through the same approval funnel as every other grant change,
and shown on the trust review.
The token caps meter governed agent identities, not API keys: in
brokered custody the gateway reads token figures and the served model
off the provider's own responses — Anthropic streaming usage blocks and
chat-completions usage alike, cache figures included — while the
response streams through untouched, checks every window before
forwarding each call, lands the call in its UTC minute bucket keyed on
the model the response reported, and stamps the observed tokens onto
the info:agent:model_call audit event. The trust review shows
tokens_by_provider and, per token-capped grant, token_caps —
observed against cap for every window with its local reset, and the
minute distribution (p95, peak) behind it — with the agent's cost
attribution read from the effective core.cost_centre metadata at its
project, so AI spend lands beside every other governed cost. Measure,
then set the budget: opschain agents usage --recommend-caps --grant <id> proposes every window's cap from the observed distribution (p95 ×
1.5) and --propose raises the replacement grant through the funnel.
The telemetry itself — minute buckets, calendar rollups with peaks, the
opschain agents usage verb, the dashboard's Model tokens panel, the
CSV export — is opschain docs provider-usage. Monetary conversion stays
outside the platform: token counts are the platform-honest measure, and
the CSV is what a finance model prices. The same gauges — caps and spend
per window, over-cap — sit on every agent's row of the inventory,
opschain docs agent-posture (Governance → Agents → Posture,
opschain agents posture): one row per agent enrolled in this estate
with its tier, owner, scope, release, last action and staleness.
The bundled OpsChain SME — the pack's flagship content
packs:ai-agents ships with a working agent, not just a lane: the OpsChain SME, the product's own Q&A subject-matter expert —
manifest identity opschain-sme, type ai, owned_by chosen at seed
time (the installing admin's group is the recommended default). Its
reference deployment is examples/opschain-sme/ (seed script + README),
its entrypoint sdk/python/examples/opschain_sme_agent.py, and it runs
from the same hardened reference image; a customer starts it with one
model-key paste (I3: the credential is customer custody, always).
Two knowledge sources, and deliberately nothing else:
- the embedded, entitlement-gated docs corpus, version-matched to the running server: questions about unlicensed surfaces get the pack-upgrade shape, never content;
- estate context at the asking user's own authority — the asker
run-as mode above, seeded by the one
act_as:askergrant. Ask it "what changes are pending?" and it can only ever see what you can: two users with different grants get different answers from the same SME, and a refusal is relayed as the answer.
The SME is read-tier by construction: its loop carries the docs tools
and opschain_read only — no propose, no request-change — and its seed
grants no action tiers. The authoring copilot and support-triage
assistants are deliberately later releases.
The pack and its composition
packs:ai-agents is the pack entitlement over everything on this page.
It is minted onto the licence like every packs:<name> grant and
enforced through the standard gate mechanics — the typed refusal naming
the pack, the throttled customer-visible warn:licence:feature_refused
event, the polite-lapse posture (see entitlements).
The enforcement sites, exactly:
- AI-type enrolment — a manifest declaring
type: aihard-depends onpacks:agent-governanceandpacks:ai-agents, checked at the enrolment seam with every missing pack named in one refusal. An estate holdingpacks:ai-agentsalone cannot enrol an AI agent either — the governance pack is the hard dependency directly beneath, so ungoverned AI cannot even enrol, by construction. - Model-provider brokerage — registering a ModelProvider catalog
record, and enabling a disabled one, gate on
packs:ai-agents. Everything else on stored records stays open on lapse: reads, credential rotation, disabling and deletion — the polite-lapse posture. Gateway calls themselves check the agent (registered, non-revoked, non-halted, manifest naming the addressed provider), never the licence per call: already-enrolled agents against already-enabled providers keep working through a lapse, and the gate bites again at the next enrolment or enabling write.
The pack sits in a stack:
packs:agentsis the platform floor — agent registration and session establishment of any type, with theagentslimit bounding concurrently connected instances. No AI agent exists without it.packs:agent-governancelayers the governance funnel over the floor — the trust-grant lifecycle, recommendations and the trust-review surfaces this page leans on.packs:ai-agents— this pack: AI-type enrolment and model-provider brokerage, and with them the whole LLM lane this page describes, plus the bundled OpsChain SME as its flagship content.packs:ai-agents-<domain>— the separately-licensed specialised SME-domain packs layered on top, each requiring this pack: see the domain-pack family page.
Base platform and never behind any layer of the stack: the kill switches and halts, tier enforcement on the request lane, and the audit trail — the controls stay ungated; the capability is the pack.
The rest of the governance story
Unchanged from the agent plane, and worth restating for the AI type:
- Tier ceiling — the
aitype ships ceilinged at SUPERVISED: unattended AI mutation is an explicit, evidenced, step-up-floored raise, never a default. - Identity — the AI operator is a first-class identity badged
agent, withowned_bynaming its accountable human or team. - Kill switch — drain for routine, per-agent/group HALT for emergencies, platform-wide halt for lockdown; all base controls, never behind this pack.
- Changing the agent — an AI agent is one logical identity with
many instances: try the next release in the
shadowlane beside production (same dispatch, every write forced to advise, its recommendations taggedlane: shadowand linked to the stable lane's proposal for the same incident — one card, both provenances), then roll the stable lane withopschain admin agent rollover(canary first,continueorabort,rollback,finish), and retire the old release.opschain docs agent-releasesis the model; the SDK handles the platform-instructed restart (restart_from_release) itself. - Evidence — every request, applied tier, refusal, transcript and halt is platform-emitted (R16); the agent cannot omit its own audit.
The operator-facing walk-through is opschain learn 12.2.
One chain per unit of work — SDK-managed correlation scopes
The agent's steps and the actions taken to resolve an issue group under ONE correlation id, so the lineage can be established — and the SDK does it, never the agent developer.
When the runtime dispatches a unit of work to the agent's handler — a capability invocation such as a BAU sweep, a chat turn, a recommendation cycle — it opens a correlation scope automatically:
- it inherits the chain the platform stamped on the inbound exchange
(
X-Opschain-Correlation-Id): the capability proxy carries the caller's id through when the caller supplied one — an incident relay, a trigger, an operator passing a chain on — so the chain starts at the incident or operator request, not at the agent; with no caller-supplied id the proxy mints one for the invocation and lands theinfo:agent:invokedroot hop; - every call the handler makes inside the scope through the SDK —
governed reads and run creations over
call_api/CallAPI, recommendations, the adapter transcript (record_adapter_transcript/RecordAdapterTranscript), a brokered model call through the gateway — carries that id on the wire, and the server records it on the R16 event, on the created run (correlation_id,initiated_by_typeagent) and in the run'sopschain.correlation_idprovenance; - the developer writes nothing. Explicit or nested scopes exist for
advanced use (
with agent.correlation(id):in Python,agent.WithCorrelation(ctx, id)in Go) and are never required.
opschain chains show <correlation-id> then renders the whole lineage —
invocation → each read → recommendation → transcript → the created change
→ the approval decision — and the approvals inbox reads the same chain
back as the approval context.
What an approver sees
A SUPERVISED agent's change enters the same approval funnel as a human's
(D1), and the approver sees WHY it was raised, not only who raised it
(opschain docs resources/approval, ApprovalContext):
- the requester — "felix (agent, SUPERVISED, owner release_managers)", linking the agent and its trust review;
- the rationale — the agent's stated reason, verbatim: an
agent-originated change or workflow run MUST carry one (the create
request's
rationaleattribute; the server refuses an agent create without it by name) — what the incident said, what the agent observed under its tiered reads, why this action on this target; - the chain — the correlation chain's recent hops in order, with the deep link to the chain page: the invocation, each governed read, the recommendation, the redacted adapter transcript, the change;
- the recommendation the change implements, when it does;
- the subject's metadata — the operator keys and the server-owned
opschainprovenance namespace.
The context is served to the checklist's approvers (any line, groups
resolved), its requester and agent-governance reviewers — an approver
sees the context of what they decide, without the /governance/agents
reviewer role (the approver-scoped subject rule). The CLI prints it with
opschain approvals show <run-id> (and approvals list carries a WHY
column); the GUI inbox opens it per row and shows the rationale above
the approve/reject controls.
A trust raise held in the inbox carries the same context, naming the
agent whose trust is being raised (its fleet id, code, project and
owner), the granter as the requester and the grant's note as the
rationale — and the approver may READ that agent: the approver-scoped
subject read admits a named approver on the open checklist (or its
requester) to that agent's detail and trust review (GetAgent,
GetAgentTrustReview — identity, disclosures, release, and the trust
review listing the pending grant with its tier, scope, actions and
caps) without the /governance/agents reviewer role, and to nothing
else: not the fleet, not another agent, not the agent's history. In the
GUI the inbox row's subject, and the context panel's "agent" and "trust
review" links, open the agent sheet in place, with a quiet line saying
why the approver can see it; the admission lapses when the checklist
closes. The same rule admits a pending promotion's release and a
pending transport grant to their approvers (opschain docs change-governance, "What an approver may read").
What the agent sees when the platform acts on it
An AI agent built on the SDK logs the platform-driven states that happen
to it — in its own log file and on its logs surface — once per
transition, with the server's reason, so the operator who halted it or
changed the licence sees the effect from the agent's side:
platform: agent halted — compromised key (operator); channel closed, retrying every ≤60 s until resumed
platform: agent plane halted — incident drill (posture 01JD…); channel closed, retrying every ≤60 s until resumed
platform: entitlement packs:ai-agents withdrawn — AI-type agents require the "packs:agent-governance" and "packs:ai-agents" entitlements — …; channel closed, retrying every ≤60 s until re-entitled
platform: channel restored after 2m14s (long-poll)
Each of these is a wait: the agent keeps its identity and its state
directory, retries under its bounded backoff, and comes back by itself
when the halt is resumed or the pack is re-applied — the channel is
closed with the typed reason the moment the state is imposed, so a
connected agent reports it at once rather than at its next handshake,
and a model call through the gateway during that window is refused with
the same reason in the error JSON (error.reason). The only state that
ends the process is revocation (platform: agent revoked — …; exiting).
readyz and info carry the current state as a platform object. The
full reason catalogue and the complete line set are in
opschain docs agent-plane ("What the agent sees"); the SDK-side
contract (PlatformRefusal, classify_platform_refusal,
agent.platform_state(), logging through a logging.Logger) is in the
SDK's README.