Skip to main content
Version: v5.7.0

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:

ToolPlatform laneGovernance applied server-side
opschain_readGET /api/... over the agent request laneauthorisation 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_proposePOST .../recommendationsthe ADVISE lane: a human accepts, the change runs under the human's authority
opschain_request_changePOST /api/changestrust-tier resolution: SUPERVISED gates in the approval funnel; ADVISE/ungranted/over-cap refuse by name
opschain_docs_search / opschain_docs_pageGET /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:

  1. Brokered (primary) — the agent's manifest references a ModelProvider catalog record by code (model_provider, set from OPSCHAIN_MODEL_PROVIDER on 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 via opschain 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 a model_provider reference 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 anthropic records 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 as model_provider_code with the entry's model_provider_kind and live model_provider_state (ok / disabled / missing) — opschain admin agent show renders it as the MODEL PROVIDER row, opschain admin agents list as the PROVIDER column, and the GUI agent sheet links it to the catalog page. Do not read the disclosure model_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-approved on 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.

  2. Agent-deployment custody (BYO-key fallback) — ANTHROPIC_API_KEY reaches 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.

  3. 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):

  1. the adapter redacts locally before anything leaves the agent (secret-shaped key/values, bearer/basic credentials, model-key shapes, email addresses), and
  2. the server redacts again at AgentTranscriptService/ AppendAdapterTranscript before 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:asker grant. 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: ai hard-depends on packs:agent-governance and packs:ai-agents, checked at the enrolment seam with every missing pack named in one refusal. An estate holding packs:ai-agents alone 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:agents is the platform floor — agent registration and session establishment of any type, with the agents limit bounding concurrently connected instances. No AI agent exists without it.
  • packs:agent-governance layers 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 ai type 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, with owned_by naming 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 shadow lane beside production (same dispatch, every write forced to advise, its recommendations tagged lane: shadow and linked to the stable lane's proposal for the same incident — one card, both provenances), then roll the stable lane with opschain admin agent rollover (canary first, continue or abort, rollback, finish), and retire the old release. opschain docs agent-releases is 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 the info:agent:invoked root 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_type agent) and in the run's opschain.correlation_id provenance;
  • 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 rationale attribute; 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 opschain provenance 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.