Agent Harness
Run Claude Code and Codex on your own machine under your organisation's policy: governed model traffic, governed tools, one metered and sealed audit trail.
Nothing about the agent moves into the cloud. Claude Code and Codex keep running on the developer's machine, with their own processes, files, built-in tools and agent loop. What Sluis governs is the traffic they send out: the model requests and the tool calls.
Region is not enforced for coding agentsA coding agent talks to your own provider subscription (Anthropic, OpenAI or Cursor), and the provider decides where that request is served. Sluis cannot pin the serving region, so on the agent plane residency is recorded, not enforced: an agent turn is never refused on jurisdiction, and its audit row records the region as unverified rather than claiming one. Everything else still applies: purpose-isolated agent keys, tenant rollout gating, DLP scanning and pseudonymization (including on the Cursor protobuf wire), plugin and tool governance, budget and rate ceilings, and sealed audit.
The local client authenticates to Sluis with an agent key and talks to the ingress its protocol speaks: /agent/claude speaks the Anthropic API for Claude Code, /agent/codex/v1 speaks the OpenAI Responses API for Codex, /agent/cursor speaks Cursor's Connect protocol over protobuf, and /agent/mcp is a stateless Streamable HTTP MCP endpoint that carries the governed tools.
Claude Code and Codex are examples, not an allow-list. Pi, T3 Code and any other client that accepts an Anthropic Messages, OpenAI Responses or MCP base URL and API key use the same Agent Harness.
The whole setup runs in one order, and each step needs the one before it:
- 1. Connect a subscription. Claude or Codex, connected once per provider account.
- 2. Create an agent key. The secret is shown once, at creation.
- 3. Link the key to that account. A key spends only the accounts it is linked to.
- 4. Configure the local client. The key lives in
SLUIS_AGENT_KEY, and the base URL points at/agent/claudefor Claude Code,/agent/codex/v1for Codex or/agent/cursorfor Cursor, which carries the same key in Authorization: Bearer. - 5. Register the tool endpoint. Tools arrive only over
/agent/mcp, registered separately: a base URL adds none.
Connect a subscription
An organisation connects one or more Claude and Codex subscription accounts in the Agent Harness view. Claude is connected with a token from claude setup-token, Codex through OpenAI's device authorization. Sluis proves the credential with the provider before it stores anything.
Claude: setup token
You need Claude Code installed on your machine and signed in to a Claude subscription that may make model requests.
- 1. Run
claude setup-tokenin a terminal. It prints a long-lived token, good for about a year, beginningsk-ant-oat. - 2. Paste it into the Claude field in the Console. The input is masked and autocomplete is off.
- 3. On submit, Sluis verifies the token with Anthropic before it stores anything.
- 4. A verified token is sealed into the gateway vault, encrypted. It is never shown again, never returned to a client and never sent anywhere but Anthropic.
A token Anthropic will not confirm is refused with a 422: no account is created, nothing is stored and nothing is billed. Reconnecting the same subscription later reuses the existing account instead of adding a second billable one.
Codex: device authorization
You need a ChatGPT plan that includes Codex.
- 1. Start the device login in the Console. Sluis asks OpenAI and shows you a verification URL and a user code.
- 2. Open that URL with the button, sign in to OpenAI, enter the code and approve. OpenAI may ask for MFA.
- 3. The Console polls while you approve. The exchange runs in this browser tab: leaving the page, or switching to another tab of the view, cancels the login and stores nothing, exactly as the Cancel action does.
- 4. On approval Sluis stores the access and refresh tokens encrypted in the gateway vault and refreshes them for you; refresh-token rotation is serialized across replicas.
A user code expires. An expired flow reports expired and has to be started again — as does an account whose stored credential OpenAI later rejects, which stops serving until a human reconnects it.
Sluis encrypts those credentials, keeps them in the gateway vault and refreshes them there. A provider token never reaches the local client, the logs, the audit records or the traces: Claude Code and Codex only ever hold their Sluis agent key.
Connecting is self-service, so a member can add their own subscription. Owners and admins see every account in the organisation; a member sees and disconnects only their own.
Create an agent key
Agent keys are minted on that same view, separately from your API keys, because the two purposes are isolated. An agent key authenticates only on /agent/*, an ordinary API key only on /v1/*. Either one on the wrong surface is a 401, and the refusal never says which purpose the key had.
The secret is shown once, at creation, and only its hash is stored. Each key is routed through the provider accounts you pick for it, so connecting a second member's subscription never reroutes a key that already works.
A key also records the organisation's ceiling on the local client's built-in tools, written in Claude Code's tool grammar and validated when the key is minted. The developer's client enforces that list on the machine; Sluis never reads it on a request, so no key holder can widen their own key, and an empty list simply records no ceiling.
The tools Sluis gates itself, on every request, are the MCP tools it exposes: see the plugins below.
Point the clients at Sluis
Claude Code needs one base URL and one credential. Set ANTHROPIC_BASE_URL to this gateway's Claude ingress and ANTHROPIC_API_KEY to the agent key you keep in SLUIS_AGENT_KEY. The Console generates the block below with your gateway's public URL already filled in.
A base URL adds no tools: the Anthropic protocol has no plugin discovery, so tools arrive only through an MCP endpoint you register yourself, with claude mcp add or an .mcp.json entry. Both reference ${SLUIS_AGENT_KEY} rather than the key, so no secret lands in a committed file.
Codex reads its provider block from ~/.codex/config.toml. Point base_url at the Responses ingress /agent/codex/v1, set wire_api = "responses" and env_key = "SLUIS_AGENT_KEY", and add the same MCP endpoint under [mcp_servers.sluis].
# the agent key is shown once, at creation: keep it in the environment export SLUIS_AGENT_KEY='sluis-9f2c…' export ANTHROPIC_BASE_URL='https://api.sluis.ai/agent/claude' export ANTHROPIC_API_KEY="$SLUIS_AGENT_KEY"
# a base URL adds no tools: register the MCP endpoint separately claude mcp add --transport http sluis 'https://api.sluis.ai/agent/mcp' \ --header 'Authorization: Bearer ${SLUIS_AGENT_KEY}'
// .mcp.json: the same registration, kept with the project { "mcpServers": { "sluis": { "type": "http", "url": "https://api.sluis.ai/agent/mcp", "headers": { "Authorization": "Bearer ${SLUIS_AGENT_KEY}" } } } }
# Codex reads the key from SLUIS_AGENT_KEY, never from the config file codex exec -m openai/gpt-5.4 'tighten the retry test'
# ~/.codex/config.toml model = "openai/gpt-5.4" model_provider = "sluis" [model_providers.sluis] name = "Sluis" base_url = "https://api.sluis.ai/agent/codex/v1" env_key = "SLUIS_AGENT_KEY" wire_api = "responses" [mcp_servers.sluis] url = "https://api.sluis.ai/agent/mcp" bearer_token_env_var = "SLUIS_AGENT_KEY" default_tools_approval_mode = "approve"
Cursor is pointed at the same gateway. Set its base URL to this gateway's Cursor ingress, /agent/cursor, with the agent key in the Authorization header. The documented route is POST /agent/cursor/agent.v1.AgentService/Run, carried over Connect with content type application/connect+proto. A client that cannot set a base URL may point at the bare origin instead, because Cursor's protocol pins an absolute path.
Sluis walks those Connect frames without a schema and applies the organisation's data-protection mode to the prompt text inside them: tokenize replaces detected values with reversible placeholders and restores the originals in the streamed reply, mask redacts them outright, block refuses the turn before anything reaches Cursor, and a frame Sluis cannot decode is refused rather than forwarded. Only allow_log forwards such a frame, and the audit trail records that it was not inspectable.
Two limits belong to this transport. The name-detection sidecar and the prompt-injection guard read a JSON request body, which Connect protobuf is not, so neither runs: the audit row records them as not run, and an organisation that made name detection mandatory gets a refusal instead of an uninspected forward. The Connect protocol also reports no token usage, so a Cursor turn is sealed into the audit trail without token counts.
Each surface accepts its credential in exactly one form: x-api-key on the Claude ingress, which is what Anthropic clients send, and Authorization: Bearer on the Codex, Cursor and MCP ingresses. The same key in the other header is refused.
MCP plugins
Governed tools arrive over one stateless endpoint, POST /agent/mcp. It keeps no session: every request re-derives the whole authorization chain, so revoking a key, a plugin or a signing key takes effect on the next call.
There are two classes. Sluis-maintained plugins are compiled Sluis code, offered organisation-wide, and run no tenant code at all. Organisation plugins are your own: a TOML manifest, signed with Ed25519.
A manifest is accepted only when its signature verifies against a signing key your organisation has enrolled, and it is immutable per (organisation, id, version): a changed tool definition is a new version, never a silent edit. Each plugin is pinned to one governed HTTPS MCP server, whose endpoint must match the URL inside the signed manifest exactly.
The operator workflow is three steps. The owner enrols the organisation's signing key. An owner or admin registers the signed manifest against a governed MCP server. Then the plugin is bound to the agent keys that may use it, and a plugin bound to nothing is exposed to nobody.
Server URLs, manifests, signatures and upstream credentials never reach a client. A developer sees tool names and schemas, and nothing else.
Context and skills the gateway injects
Your engineering standards are only policy if a developer cannot forget them. A CLAUDE.md committed to the repository is a suggestion: it can be edited, deleted or simply not read, and nothing records which of those happened. The same text pushed into every governed agent request from the gateway is the organisation's decision — write the failing test before the fix, never add a dependency without an approved ADR, this repository holds personal data so pseudonymize before you paste. This is the half of the harness that governs how an agent works, rather than which model and which tools it may reach.
Two plugin kinds carry it, and neither grants any capability of its own:
- Context. Verbatim text, injected ahead of the developer's own prompt, up to 16 KiB per version.
- Skill. A name, its instructions, and the tool names it expects. Those names are intersected with the tool ceiling recorded on the key, so a skill declares what it needs and can never widen a key: a tool the ceiling does not carry is simply left out of what the model is told.
A version is bound at one of three scopes, and every request resolves all three:
- Organisation. Every agent key in the organisation, including keys minted after the binding was made. This is the scope for a rule nobody should have to remember to attach.
- User. Every agent key one member owns — an onboarding brief, or a stricter posture for one person.
- Key. One exact agent key, which is what a binding already did.
Resolution is deterministic, because injected order changes the prompt. Sluis takes the union of the three scopes, re-verifies each plugin exactly as it does for tools, drops duplicate versions, and orders organisation first, then user, then key, so the general rule is read first and the narrow exception last. Context is injected verbatim, a skill as a named section. On the Claude ingress the blocks are prepended to system, in either shape that API allows; on the Codex ingress to instructions, or as a leading developer instruction when the request carries none.
Everything injected into one request fits inside 32 KiB. Past that the request is refused with a 422 naming the plugins that overflowed the cap. Nothing is silently truncated: half an instruction is worse than none, and an organisation that believes a rule is in force has to hear when it is not.
Injected text is not rewritten by data protection, and the order is one-way: the developer's prompt is scanned and pseudonymized first, then your policy is prepended. These blocks are your own governed content, authored in your own Console, so there is no party they need protecting from — and tokenizing them would destroy what they say. “Escalate to security@example.com before pasting” turned into “escalate to «EMAIL_1» before pasting” is no longer an instruction anyone can follow.
These two kinds are authored in the Console and carry no signature. A signature exists to stop a manifest pointing a governed credential at an endpoint nobody approved; a block of text points at nothing. Everything else that makes a registration trustworthy still holds: a version is immutable, its exact bytes are hashed, and the row records who wrote it and when. A manifest carrying an MCP entry keeps the mandatory signature and the enrolled-signer check on every request. The Plugins tab shows which of the two a version is, so “console-authored” is a stated fact rather than an absent field, and it previews the resolved blocks in the order the gateway injects them for a chosen key or member.
Each injected block leaves one marker on the sealed audit row, context:<plugin_id>@<version>#<digest8>, where the digest is the first eight characters of that version's hash. Because a version is immutable, the marker is enough to reconstruct the exact text the model was handed, so the audit trail alone answers whether an agent was under a given rule at the moment it acted.
Cursor is the honest gap. Its /agent/cursor transport is a proprietary binary schema: Sluis can find and rewrite the text inside those frames, which is how data protection works there, but it cannot tell which field is the system prompt, and writing into a field it guessed at would corrupt the request instead of governing it. So nothing is injected on that ingress, and a Cursor turn carries your context only if the developer's own client sends it. The Console's preview says the same.
Governance that still applies
Agent traffic is ordinary Sluis traffic, with one exception. Pseudonymisation and DLP run before dispatch, including inside Cursor's protobuf frames, the key's rate and budget limits are enforced per request, the MCP tools are gated call by call, and every model call and tool call is audited and metered. The exception is residency: on the agent plane the policy is recorded rather than enforced, for the reason in the note at the top of this page.
The rules are the same ones described under Data protection and Budgets & caching, and they are organisation policy: an agent key cannot loosen them.
Billing
Each active Sluis-managed provider account costs €16.50 list price per calendar month, charged at most once per account per month. It is billed to your top-level billing root and appears as its own invoice line, separate from metered usage. VAT and the payment-method surcharge apply exactly as they do to the rest of the invoice.
Disconnecting an account stops future months. Reconnecting the same upstream subscription reuses the existing account instead of adding a second billable one, so a disconnect and reconnect inside one month still bills once.
Gateway usage itself stays consumption-billed, exactly as for API keys.
Availability
The Agent Harness view appears in the Console once the agent_harness rollout flag is enabled for your organisation. Ask us to switch it on if you do not see it.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| 401 on an /agent/* call | The key is on the wrong surface or in the wrong header. An API key buys nothing on the agent routes and an agent key buys nothing on the model plane; each ingress accepts one header form only. |
| 502 no credential configured for provider | The agent key names a provider it has no linked account for. A key spends only the subscriptions it is linked to, and never falls back to the organisation's other credentials: link an account for that provider, or name a model the linked one serves. |
| Subscription refresh rejected | The provider refused the refresh permanently, so the account stops serving until a human reconnects it. Reconnect it in the Console; the same subscription reuses its existing account and its existing monthly fee. |
| 400 mcp_servers is not supported | A remote MCP declaration in a Responses body is refused before dispatch, because a server reached from the model would bypass the tool gate. Register the server with Sluis and reach it over the MCP ingress instead. |
| No setup snippets in the Console | The deployment has no validated public gateway URL, so ready-made setup cannot be shown. Configure SLUIS_GATEWAY_PUBLIC_URL, then read the endpoints from the Connections tab. |