agent_run subject: company Agent code execution with a private run namespace (Team chat) - #20
Merged
Merged
Conversation
Deterministic fixture (fixed Ed25519 seed, clock, jti and UUIDs) over the personal identity consumers and routes. The golden file is generated on the unchanged e9ad47c tree so later identity work must reproduce it exactly.
Adds principal_source 'agent_run' with its own claim grammar (24-hex Agent sub, canonical UUID run_id, required colon-free tenant whatever the strict flag, role AGENT, no borrowed human claims). The verified principal alone selects the private '<ns>:agent-run:<A>:<R>' key; kind=agent must name the signed run, kind=skill uploads must be read_only, kind=user is refused, and outputs always land in the private key. Deletion-only tokens (file_delete) are honoured only for the signed DELETE /files/:sid/:fid?kind=agent&id=R and refused everywhere else in apiKeyAuth before dispatch; personal tokens carrying file_delete are malformed. Private objects carry an owner binding (hash of tenant, Agent, run) signed by the api at upload and by the gateway from the grant for sandbox outputs; the file server stores only a binding signed for that exact object, so after the 24 h session cache expires a deletion token can still remove its own bytes and nobody else's. Every identity consumer branches on the subject with no userId fallback: principal/auth context, getPrincipalOrReject, sessionAuth, output bucket, replay/blocking continuation equality and the replay session-key fallback, per-Agent rate-limit bucket, runtime-session scope, manifest/grant/worker/ lambda identity (agent_id/run_id, no user_id), and logs (kind plus hashed ids). Personal behaviour is unchanged and pinned by the e9ad47c parity fixture.
…-upload kind=user with 403 Security review of 850dbae (P1): agent_run tokens are refused with 401 (reason agent_run_unavailable) unless CODEAPI_HARDENED_SANDBOX_MODE and the internal service token are both configured, so output owner bindings are always written by the gateway from the sealed grant. Personal tokens are unaffected. P3: /upload and /upload/batch refuse agent_run kind=user with 403 even without an id.
…eader maps; typed test assertions Foundation review of 3a0b16b (P1-1): an owner-bound deletion forwarded as a plain DELETE with X-CodeAPI-Owner-Expect would be executed by a file server that predates owner bindings, so a token for another tenant could delete after the session cache expired. Owner-bound deletes now go only to the new POST /sessions/:sid/objects/:fid/owner-delete operation, which refuses a request without an expected owner and an object without a matching stored binding. The api treats every outcome other than an explicit 2xx deleted/absent as a refusal, so an old file server's 404/405 leaves the bytes in place; there is no fallback to the plain delete. Personal and cache-present agent_run deletes keep the plain route, which is back to its e9ad47c form. P1-2: the parity comparison sorts HTTP header maps (the only unordered input) on both sides; the golden is unchanged. P2: agent_run tests assert typed reasons, statuses and side effects instead of error wording.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Engine: agent_run subject for company Agent code execution
Branch
agent-run-subjectoff e9ad47c (Optale-ops/code-interpreter). Head:e5f925fa5054b8943f83145fb5b25015edd2ce4f. Commits:2f5c98atest: personal parity fixture; golden generated on the unchanged e9ad47c tree850dbaefeat: agent_run subject3a0b16bfix: Security review of 850dbae (ENGINE-REVIEW-850dbae.md), P1 and P3; P2 proven livee5f925ffix: Foundation review of 3a0b16b (ci20-3a0b16b.json): owner-checked delete operation (P1-1), parity header maps (P1-2), typed test assertions (P2)Contract: native-chat-backend/agent-code-identity/PROPOSED-CONTRACT-2026-10-04-v2.md (sha256 41d50608…). Security conditions C1-C6: security-agent-code-identity-2026-10-04/CONTRACT-REVIEW-v2.md.
Exact diffs for Security (
/home/thor/optale-artifacts/agent-mcp-clone/agent-code-identity/engine-diff/):engine-agent-run-subject-e9ad47c7..e5f925f.diff(sha256 449546df65879dc7fbd8270c335a6e7ef48c89dc1fd920c30889aa5747caf1d7)engine-agent-run-subject-3a0b16b..e5f925f.interdiff(sha256 6b29393da9458076676b12d66d02facd1e518acde37cef2eb86b1e508eabb6da)engine-agent-run-subject-e9ad47c7..3a0b16b.diff(sha256 bb703cd7ba11789d9adbff390f706da069099527efa7c187f2aa2bbb41f1667c)engine-agent-run-subject-850dbae..3a0b16b.interdiff(sha256 e43fe15a769ef6e1282e80c2f7548ebca60fec6cda6e18641c872fd3892e7885)engine-agent-run-subject-e9ad47c7..850dbae.diff(sha256 f4be4b52…819ad0)Foundation review of 3a0b16b: changes in e5f925f
DELETEplusX-CodeAPI-Owner-Expect; an e9ad47c file_server ignores that header and deletes. Now:POST /sessions/:sid/objects/:fid/owner-delete(file-server.ts:763). It refuses a request without a well-formed expected owner (400) and an object with no stored binding or a different one (403). It answers200 {outcome:'deleted'}or200 {outcome:'absent'}, 500 on storage error. It never deletes on "no expectation".service/router.ts:979). Only an explicit 2xxdeleted(→ 200) orabsent(→ 404) counts; every other answer, including an old file_server's 404/405, is a refusal (403; 500 stays 500) and the bytes stay. No branch falls back to the plain delete.DELETEroute on the file_server is back to its e9ad47c form. Personal deletes and cache-present agent_run deletes keep using it, unchanged.mixed version: against a file server without the owner-checked operation, an expired-cache deletion is refused and deletes nothing(cross-tenant token and owner token both 403 on an old file server, bytes present, no plain DELETE sent; on the new file server the other tenant gets 403 and the owner 200); ARafter the 24 h session cache expires …asserts the call isPOST …/owner-deleteand no plain DELETE; ARan object with no binding ….ks7-engine-svc-production:e9ad47c76): tenant B 403, tenant A 403, statObject shows the bytes still present. Recordside-pair/out/mixed-delete-e5f925f-oldfs.json(sha256 c70e7579c9729c7489ce33f562bd52cd9f40043f06e1bf2e3f96de10cce9ee32).side-pair/out/mixed-delete-e5f925f-newfs.json(sha256 6b232a2120bfe57e2a5b1c3816b03d0b54374e4a5eb898cb38dc458107aa8500).reverse gap: an object stored without a binding (old file server or old gateway) cannot be deleted after expiryand live: the upload through the new api onto the e9ad47c file_server stored the object with no binding (oldfs record). Such objects need cleanup after a full upgrade by other means; they are never deleted for the wrong owner.oven/bun:1.3.14: parity, agent-run and egress-gateway tests pass (81/81).CodeApiJwtAuthError.reason(malformed_claims,tenant_not_allowed) against a baseline that verifies, plus statuses and side effects; no error wording is pinned.Security review of 850dbae: changes in 3a0b16b
apiKeyAuthrefuses every agent_run token with 401 (Invalid bearer token, logged reasonagent_run_unavailable) unlessCODEAPI_HARDENED_SANDBOX_MODE=trueandCODEAPI_INTERNAL_SERVICE_TOKENis configured (middleware/auth.ts:196). All routes are covered because the check runs before dispatch. This keeps output owner bindings coming only from the gateway's sealed grant. Personal tokens are unaffected. Test: ARagent_run availability (hardened sandbox + internal service auth) › agent_run is refused with 401 unless hardened mode and internal service auth are both on; personal is unaffected; PP unchanged./uploadand/upload/batchrefuse agent_runkind=userwith 403 even without an id, before parsing (service/router.ts:494,717). Test: ARC1: upload kind=user is refused for agent_run.agentrun-pair, hardened mode on, own egress gateway and file_server, 3a0b16b service images, e9ad47c runner with the patched rootfs bind-mounted). Record:/home/thor/optale-artifacts/agent-mcp-clone/agent-code-identity/side-pair/out/agent-3a0b16b.json(sha256 9a86fa8ae1dc349f28362e790ccc4ec6fd130ed5bd71113dfe208161cabc9caf).agent-output.txt. Its MinIOcodeapi-ownerequalsownerBindingValuecomputed from the raw tenant, Agent and run ids (outputBindingIsFromRawIds: true), not the masked labels.session:<sid>key was deleted to force expiry. After that, run R2's file_delete token got 403 and the active-run token got 403.NotFound, file_server returned 404, and a repeat delete returned 404.Paths below are relative to
service/srcunless markedapi/. Tests:agent-run.test.ts(AR),personal-parity.test.ts(PP),egress-gateway.test.ts(GW),api/src/execution-manifest-request.test.ts(RM).Claims, grammar, keys (contract §1)
principal_source: 'agent_run'takes its own validator:auth/librechat-jwt.ts:616(branch),:688validateAgentRunClaims. Old engines still refuse it (onlylibrechat_jwt/openid_reusethere; side-pair proof below: 401).agent-run.tsAGENT_ID_PATTERN), run_id = canonical lowercase UUID (RUN_ID_PATTERN), role must beAGENT, tenant_id required regardless ofCODEAPI_TENANT_ISOLATION_STRICTand colon/whitespace-free (librechat-jwt.ts:698), no borrowed human claims (:669HUMAN_ONLY_CLAIMS: org_id, service_id, external_user_id, chc_user_id, plan_id). Values are refused, never normalized.librechat-jwt.ts:819).assertTokenWindow).agent_run claim grammar› all six cases (well-formed; tenant required strict on/off; malformed values refused; feat(auth): bind a JWKS key to the tenants it may sign for #18 bound tenant; personal file_delete refused; Agent-looking personal sub stays personal).Private namespace (contract §2)
<ns>:agent-run:<A>:<R>built only from the verified principal:agent-run.ts:31agentRunSessionKey;session-key.ts:72branch,:144resolveAgentRunSessionKey,:125output bucket.collision: a user token with kind=agent id=<agentId|runId|agent-run:A:R> cannot touch Agent objects;the Agent cannot read a user bucket.C1 upload kinds
session-key.ts:144-170: kind=agent requires id == signed run_id (else 403); kind=skill keeps the shared key; kind=user refused (403).:173resolveUploadSessionKey: agent_run kind=skill requiresread_only=true. Router uses it in/uploadand/upload/batch(service/router.ts,resolveUploadSessionKey(req, sessionKeyInput, readOnly)), 403 surfaced for single and batch.session-key.ts:125);sessionAuthrefuses agent_run DELETE outside kind=agent (middleware/auth.ts:308), so shared Skill objects stay read-only.FileRefAuthorizationError(403,'subject_refused')(service/file-authorization.ts).C1: upload kind=agent id=<signed run> …,C1: upload kind=agent with an id other than the signed run is refused,C1: upload kind=user is refused for agent_run,C1: upload kind=skill needs read_only=true …,exec with a kind=user file reference is refused for agent_run.C3 deletion-only tokens
file_delete = {storage_session_id, file_id}parsed strictly (librechat-jwt.ts:741); a personal token carrying it is malformed (:641).apiKeyAuthbefore any handler:middleware/auth.ts:103fileDeleteRequestMatches(method DELETE, exact path/files/<sid>/<fid>, query exactlykind=agent&id=<run_id>), enforced at:201→ 403.every route other than the signed DELETE target is refused before dispatch(/exec, /exec/programmatic initial and continuation, upload, upload/batch, download, files list, metadata, other target, other run, kind=user/skill, extra query, trailing slash, wrong route; asserts no job and no file-server write),the signed target is deleted while the session cache is present.C4 owner binding
agent_run.<sha256([agent_run, tenant, A, R])>(agent-run.ts:75), stored as MinIO object metadataX-Amz-Meta-Codeapi-Ownerso it lives exactly as long as the bytes.sid/fid+ binding): api at upload (service/router.tsownerBindingHeaders, private kind=agent only), gateway for sandbox outputs from the grant (egress-gateway.tsagentRunOutputOwnerHeaders). The gateway never forwards sandbox headers; client headers on /upload are ignored.file-server.tsPUT route,ownerBindingFromHeader,agent-run.ts:113).sessionAuth(middleware/auth.ts:319) lets only a file_delete token with an absent cache entry proceed withownerBindingExpectation; the file server deletes only if the stored binding equals it, 403 if absent/mismatched, 404 only if the bytes are absent, 500 on storage error (file-server.tsDELETE). Router maps agent_run 404/403 explicitly (service/router.tsDELETE). Present-but-different cache entry stays 403. Ordinary read/exec gets no exception.owner binding (C4) › only a binding signed for this exact object is accepted;C4: a client-supplied owner header on upload is ignored …;C4: a PUT with a forged owner header for another run cannot change the binding; that run cannot delete;after the 24 h session cache expires the binding authorizes deletion; repeat is not-found;an object with no binding is never reported deleted through the binding path. GWagent_run output PUT carries the grant owner binding and drops a sandbox-supplied one,personal output PUT carries no owner binding.C5 identity consumers (no userId fallback)
auth/principal.ts:30AgentRunPrincipal(no userId by type);applyPrincipal(:52) writesagentRun, no userId/planIdauth context and execution identity carry the Agent subject and no user idgetPrincipalOrRejectauth/principal.ts:115execution-identity.tsAgentRunExecutionIdentity;getExecutionIdentitynever builds an Agent from a user idsessionAuthmiddleware/auth.ts:261session-key.ts:125(+ programmatic call sites)exec: outputs go to the private key …service/replay-state.tscontinuationSubjectMatches(used by replay and blocking paths inprogrammatic-router.ts)continuation equality compares kind, Agent, run, tenant and context hash, never a sub stringprogrammatic-router.ts:371fallback →replay-state.tsreplaySessionKeyreplay output key: …middleware/limits.tskeyGenerator<ns>:agent:<A>per Agent, not per runrate-limit bucket is stable per Agent …runtime-session/id.tsruntime session scope separates runs, Agents and users whatever the hintexecution-manifest-claims.ts, grantegress-grant.ts,execution-manifest.ts,api/src/execution-manifest.tsprincipal_source=agent_run, agent_id, run_id, no user_id; ambiguous subjects refused; masked labelsagent:/run:manifest and grant …,a grant naming both …; RMagent_run manifest subjectworkers.ts:185,sandbox-backend/types.ts,lambda-microvm.ts:823,runtime-session/registry.tsagentRuninstead of canonicalUserId; registry record carries principal_source/agent_id/run_idpayload.ts:35,preamble.ts:800,860)planLimits.default; no new capplan_id is not accepted) + identity test (req.planIdundefined)middleware/auth.ts33-41/50-65/243,service/router.ts76-79/110-112/203-206/461/577,programmatic-router.ts,file-server.ts:282,limits.ts,request-error-logger.ts, gateway auditagent-run#<hash>; query strings dropped for agent_run (also for refused tokens declaring agent_run)withoutRawIdentityInLogson upload, read, exec, delete, expired-cache delete, refused tokensC6 personal parity
personal identity consumers, routes and log lines are byte-identical to the pre-change engine: fixed Ed25519 seed, clock, jti/UUIDs; token bytes (sha256), verification result, auth context, execution identity, planId, sessionKeys (user, personal kind=agent with run uuid and public key, skill), output bucket, rate-limit key, runtime-session id, manifest + grant claims, replay state, continuation outcomes; plus personal HTTP routes (upload/batch/skill/metadata/download/files/exec/delete and refusals) with Redis writes, forwarded file-server headers, job data and every log line. Golden generated on e9ad47c in commit 2f5c98a; compared both deep-equal and serialized (key order).Side-pair proof (KS-7, project
agentrun-pair, api 127.0.0.1:27411, own redis/minio/secrets/JWKS)See the lane report for results; records in
/home/ubuntu/agentrun-pair/out/on KS-7 and copied to/home/thor/optale-artifacts/agent-mcp-clone/agent-code-identity/side-pair/out/./sandbox_api/.build/index.js(e735d566…). Deployment must rebuild the runner normally (api/src/execution-manifest.ts changed); an unchanged runner refuses agent_run manifests (fail-closed).Deploy requirements
api/src/execution-manifest.tschanged. An unchanged runner refuses agent_run manifests, which fails closed but breaks every company execution.CODEAPI_HARDENED_SANDBOX_MODE=trueandCODEAPI_INTERNAL_SERVICE_TOKENon the api (P1). The gateway and file_server need the same internal service token, because it keys the owner binding. Without them, every agent_run token gets 401; personal tokens are unaffected.tenantslist (KS-7's lab key has none).Side-pair results (2026-10-04, KS-7)
Invalid bearer token.