Repository navigation
Conversation
ACP agents, the `t3 acp-mcp-bridge` stdio server they spawn, ACP client terminals and Pi received the thread's MCP bearer in an environment variable. Everything those processes spawn inherits it, and on Linux systemd-coredump writes the environment of a crashing process to the journal as COREDUMP_ENVIRON. ACP and Pi now write the Authorization header to an owner-only file in a private temporary directory and pass only its path (T3_ACP_MCP_AUTHORIZATION_FILE, T3_MCP_AUTHORIZATION_FILE). The bridge, `t3 acp-mcp-call` and the Pi extension read the file. ACP keeps one path per thread for the life of the provider session. A rotated header is written beside the file and renamed over it. Once the credential is cleared, the next read deletes the file, and a reissued credential comes back at the same path. A failed write leaves no temporary file. Pi writes its file once when the session opens. Closing the session removes the files. If the file cannot be written, ACP runs the session without T3's MCP server and Pi fails to open the session, as it does when it cannot write its extension. The raw-credential variables older builds exported (T3_ACP_MCP_AUTHORIZATION, T3_MCP_BEARER_TOKEN, T3_CODE_MCP_AUTHORIZATION) are dropped where every ACP flavor spawns its agent, where ACP client terminals start, and from Pi's launch environment, whether or not the session has a credential. The MCP server doc now describes how Codex receives its header (JSON-RPC thread config, not an environment variable). Refs pingdotgg#12031 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR changes the default authentication-credential transport for ACP and Pi processes, adding session-owned files, rotation and cleanup logic, and broad child-environment sanitization. Because it affects security-sensitive production behavior across multiple provider paths, human review is required. You can add or adjust custom eligibility rules. Learn more. |
knip flagged the export; it is only used inside mcpSession.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
ACP and Pi still hand each provider session's MCP bearer token to their children in environment variables, so it lands in every copy of those environments, crash-dump journal entries included. This PR moves both to an owner-only file per thread and puts only the file's path in the environment.
Problem
Each provider session gets a bearer token for the
t3-codeMCP server. The/mcpendpoint sits outside the environment auth stack, so this token is the only thing guarding the session's orchestrator tools (#12031 triage).Codex and Claude no longer put the token in a child environment on main. Codex receives
http_headers.Authorizationover its app-server JSON-RPC connection. Claude moved from argv to an env var in #17408, then to the Agent SDK control channel in #17898. ACP joined the env channel with the V2 orchestrator (#2829), after the triage was written. ACP and Pi still pass the token itself through environment variables:T3_ACP_MCP_AUTHORIZATIONin their own environment, and each of their client terminals gets it in the terminal's environment, through the adapter'sprocessEnvironmentoverlay (packages/provider-acp/src/server/adapter.ts:714on main).session/newto spawn thet3 acp-mcp-bridgestdio server withT3_ACP_MCP_AUTHORIZATIONin that server's environment (mcpServers,adapter.ts:705). Grok and Antigravity ignoreprocessEnvironment, so this is the one place their sessions put the token.T3_MCP_BEARER_TOKENin its environment (packages/provider-pi/src/server/mcpInjection.ts:313).An environment variable gets copied far beyond the process it was meant for:
core_patternpipes crashes to systemd-coredump, a crash of any of these processes writes its whole environment to the journal asCOREDUMP_ENVIRON(MCP bearer token passed via environment is captured in systemd-coredump journal entries on provider crashes #12031). That entry outlives the process, and journal-reading admin groups and anyone holding a copy of the journal can read it. The core size limit does not reliably prevent it. For a piped dump the kernel ignoresRLIMIT_COREexcept for the value 1, which aborts the dump. At 0, systemd-coredump declines to store the core file but still sends the journal entry, environment included. A crashing subprocess does not end the provider session, so the token in that entry stays valid until T3 releases, rotates or revokes the session.The #12031 triage already asked for a non-env channel: item 1 proposes a
0600file for Codex, and item 5 says that Claude, leaving argv, should not stop at a child env var on Linux coredump hosts. The same reasoning applies to ACP and Pi.Change
ACP and Pi now get the
Authorizationheader in an owner-only file. Only the file's path goes into the environment:T3_ACP_MCP_AUTHORIZATION_FILEfor the ACP agent, its bridge and its client terminals, andT3_MCP_AUTHORIZATION_FILEfor Pi. Both files hold the full header value (Bearer ...), hence the names. The old variables are gone as channels.The files.
makeMcpCredentialFilesinpackages/provider-core/src/server/mcpSession.tsowns the files for one adapter session.mkdtempdirectory (0700) under the OS temp dir, created with Effect'smakeTempFileScoped.<path>.tmpwith mode0600and renamed over the file, so the path never goes missing and a reader never sees half a header. If the write or the rename fails, the.tmpis deleted..tmp, also after a failed first write. A later write puts it back at the same path.ACP syncs the file each time the adapter reads the thread's credential: on a turn, a resume, a snapshot read, a fork or a rollback. A rotated header is written over the file on that read, and the next read after a clear deletes it. An idle session keeps the file until that read or until the session closes; the token in it was already revoked by the clear. The adapter's credential reads for one session never interleave either. Every read after open holds the session's runtime transition lock, so an older header cannot overwrite a newer one.
The path has to stay put. The agent's environment, the bridge it registered and the terminal environments the adapter remembers are all fixed when the native session starts. When the credential rotates and the next turn runs on the same native session, the adapter rewrites the file those processes already name. Pi reads the registry once per
openSession, so it uses the same helper with one key.The readers.
t3 acp-mcp-call, the fallback an agent runs from a client terminal, reads the file on every call, so it follows rotation.t3 acp-mcp-bridgeand the Pi extension read it at start, as they read the env var before. "What this does not fix" covers what that means for the bridge after a rotation.acp-mcp-bridge requires T3_ACP_MCP_ENDPOINT and a readable T3_ACP_MCP_AUTHORIZATION_FILE.It no longer reads the old variable.Old variables. A T3 server started from the terminal of an older build's provider child can carry a raw credential in its own environment. On main that copy reached ACP agents and their terminals even in sessions with no credential.
LEGACY_RAW_MCP_CREDENTIAL_ENVin provider-core lists the three names older builds used (T3_ACP_MCP_AUTHORIZATION,T3_MCP_BEARER_TOKENand Claude'sT3_CODE_MCP_AUTHORIZATION), andwithoutRawMcpCredentialsdrops all three in three places:AcpSessionRuntime.make, where every ACP flavor spawns its agent (registry, Grok, Antigravity, and the registry probe and auth runtimes).makeAcpClientTerminals.T3_MCP_URLandT3_MCP_AUTHORIZATION_FILEbefore it adds the current session's values.This applies whether or not the session has a credential and whether or not the file write worked. When no environment is configured the helper starts from
process.env, which Node would pass on anyway, so a terminal keeps the server's environment minus those names.When the file cannot be written.
Could not write the T3 Code MCP credential file.and opens the session without the t3-code MCP server and without the file variable. T3's tools are an addition to the session, and two ACP call sites have error channels that cannot take a new error.openSessionthrough its existingprovideCacheFsmapping, the same way it fails when it cannot write its extension file.Where the file lives. The file is in a fresh private
mkdtempdirectory under the OS temp dir rather thansecretsDir. Files left behind by a crashed server go when the OS cleans its temp dir instead of piling up in the T3 home. One directory per file needs no shared directory and no cleanup across sessions. Where the temp dir is tmpfs, the file is never written to a disk file system. The adapters own the files, notMcpSessionRegistry: writing them from the registry would put every provider session's bearer on disk, including Codex, Claude, Cursor, OpenCode and Muse, which never read it.Docs.
docs/orchestration-v2/orchestrator-mcp-server.mdshowsT3_MCP_AUTHORIZATION_FILEin Pi's environment block and says commands Pi runs do not inherit the token, so it is not in theirCOREDUMP_ENVIRON. Its Codex section still describedbearer_token_env_varandT3_MCP_BEARER_TOKEN, which main no longer uses. It now says Codex getshttp_headers.Authorizationin theconfigofthread/start,thread/resumeandthread/forkover JSON-RPC.Why a file and not a protocol channel
COREDUMP_ENVIRON/proc/<pid>/environmcp_set_servers)mcpServersis a process spec (command, args, env). It has no header field, so the bridge can only receive the header through its environment, its argv or a file.--mode rpchas no command that hands an extension a value. The extension could request one with anextension_ui_requestthat the adapter answers over stdin, but that is new protocol on both sides, and the token would then travel in the RPC stream that native protocol logging records.What this does not fix
acp-mcp-calldepends on that. The change takes the token out of every copy of the environment and off disk once the session closes, not out of the user's reach. The triage's0600file has the same property.t3 acp-mcp-bridgestarted before a rotation keeps the old header in memory, as on main (ProviderSessionManager.ts:445: rotating afterwards cannot repair an already-configured process). Its MCP HTTP session was opened under that header.acp-mcp-callfollows the rotation.T3_ACP_MCP_ENDPOINTandT3_ACP_MCP_AUTHORIZATION_FILEinherited by the server pass through when a session has no credential. At the spawn point the instance environment and the session overlay are already merged, so an inherited path looks like the current one. They hold an endpoint and a path, not a token. The server's own environment is not touched either, so children of other providers still pass on any legacy variable the server inherited, as on main.acp-mcp-calland the Pi extension, so a full core image can contain it if the core file itself is stored.COREDUMP_ENVIRONno longer contains it.McpSessionRegistrykeeps credentials in memory only. The directories are0700.ProviderSessionManager.ts:1978).Scope and approval
accepted. The maintainer triage confirmed the coredump exposure and asked for a non-env channel.477282263a, 20 files, +798 / -107, most of it tests.Part of #12031.
Verification
Tests, red/green, mutation checks, typecheck, lint and format
Focused tests, run from the repo root:
New and changed tests:
keeps an ACP thread's MCP credential file at one path through rotation and clear: after a rotation and a turn on the same native session there is still one runtime, the same path holds the new header, and a terminal started afterwards reads it through its remembered environment. The next read after a clear removes the file, a reissue restores it at the same path, and closing the session removes it.opens an ACP session without T3's MCP server when its credential file cannot be written: no t3-code server and no file variable, and the agent does not see a stale raw variable.drops a raw MCP credential the server inherited from an older build: a raw value seeded in the parentprocess.envdoes not reach the spawned agent.clientTerminals.environment; terminals of a session without a credential and of a session with one both print it empty.reads the credential from the file the environment namesfor the bridge andacp-mcp-call.hands Pi the MCP credential as a session-owned file, not in its environment: the file is0600and gone after close. The Pi launch test inmcpInjection.test.tsseeds all three legacy names plus a stale URL and file variable and asserts each is gone.mcpSession.test.ts, 6 new tests. FormakeMcpCredentialFileson the real filesystem: an unchanged header is a no-op (same inode); rotation keeps the path and mode0600and replaces the inode; remove then write puts the file back at the same path; an injected rename failure leaves no.tmp,removeclears a key with no recorded header, and a failed rotation keeps the old header; closing the scope removes every key's directory. ForwithoutRawMcpCredentials: all three names masked and the rest kept; an unset environment starts fromprocess.env.Related suites, every test that builds
AcpSessionRuntimesince its spawn environment changed:The one failure is
AcpSessionRuntime.processTree.test.ts"contains a packaged-runtime command without requiring cgroup delegation" (expected 127 to be 125). This PR does not touch that test or the cgroup wrapper it exercises. Run alone ona8bdfdb5on a host whose/bin/shis bash, the file fails the same way (1 failed, 22 passed). The cause is the host's/bin/sh: bash skips the wrapper'sEXITtrap after a failedexec, so a missing target exits 127 instead of 125. With dash bound over/bin/shand nothing else changed, the same file passes 23 of 23 ona8bdfdb5. Upstream CI ran it green ona8bdfdb5on Ubuntu, where/bin/shis dash. #17949 fixes it separately, carrying forward #15344.Red/green: with the new strip tests on the source from before the strip existed (file channel only), all four fail: the parent-seed test (the agent sees the raw variable), the rotation test (the agent sees the stale raw value, the Grok case), the write-failure test (same), and the fork test (the terminal of a session without a credential prints
Bearer stale-dummy-credential|). I did not run the new tests againsta8bdfdb5, where the*_AUTHORIZATION_FILEassertions would fail too.Mutation checks, one per new behavior, each source file restored and checksum-verified afterwards. The first five ran before the strip helper moved to provider-core, when it was
withoutRawMcpCredentialin provider-acp at the same two call sites. The last five ran on the final code; the two that touch Pi's launch environment were rerun after rebasing onto477282263a.expected 'Bearer stale-dummy-credential|' to equal '|'NotFound: FileSystem.readFileon the path the agent and terminals were givenremoveleaves the file on clearexpected true to be falseonexists(file)after clearProviderAdapterOpenSessionError ... PermissionDenied: FileSystem.makeTempFileScoped.tmpexpected true to be falseonexists(<path>.tmp)removereturns early when no header is recordedremoveT3_MCP_BEARER_TOKENexpected 'Bearer stale-token' to equal undefinedT3_CODE_MCP_AUTHORIZATIONwritedrops the unchanged-header checkThe rename step and the semaphore have no mutation check, because no test races a reader or a second writer against a write.
tsc --noEmitthrough each package'stypecheckscript inprovider-core,provider-acp,provider-piandapps/server: exit 0, 0 errors.vp lint --report-unused-disable-directiveson the changed.tsfiles: 0 errors. 4 warnings, all on lines this PR does not change.vp fmt --checkon the changed files:All matched files use the correct format.Manual checks
Child environments, crash dumps, the credential at work, and a live T3 thread
Main (
a8bdfdb5) and this change on that base (36da67d1, before a conflict-only rebase onto477282263a; the ACP and core code is byte-identical, and Pi's launch code differs only by upstream's new extension-path lines), each in a throwaway clone, underenv -iwith a privateHOMEandTMPDIR. A temporary test file that is not part of this PR drove the real code:makeAcpRegistryAdapterV2spawning the repo'sacp-mock-agent.ts,makeAcpAdapterV2withmcpServerspointing at the clone's realt3 acp-mcp-bridge, andbuildPiRpcLaunch. Each child's/proc/<pid>/environwas read and filtered to T3 MCP variables. The credential was a dummy,Bearer dummy-e1-00112233445566778899aabbccddeeff, and the MCP endpoint was a local dummy HTTP server that logs each request'sAuthorizationheader. Both clones were clean afterwards.Child environments. ACP agent process (the
T3_ACP_MCP_NODEandT3_ACP_MCP_ENTRYPOINTlines omitted):The bridge, the client terminal and the Pi child show the same change (
T3_MCP_BEARER_TOKENbecomesT3_MCP_AUTHORIZATION_FILEfor Pi). Two more runs put stale raw copies in the environment of the process standing in for the T3 server and opened a session with no credential:Crash dumps. For each captured environment, a dummy
/usr/bin/sleepran as a transient user unit with exactly those T3 MCP variables, was crashed with SIGSEGV, andCOREDUMP_ENVIRONwas filtered to T3 MCP lines. The dummy core files were deleted afterwards.All four channels (agent, bridge, terminal, Pi) held the dummy bearer before and only the path after.
The credential still works. On this branch, the bridge started from the exact
mcpServersspec,acp-mcp-callfrom a client terminal, and the Pi extension (loaded with Pi's environment, realfsand realfetch) each sent the issued header. After a rotation the file at the same path held the new header andacp-mcp-callsent it:The file was
0600in a0700directory, and both were gone once the owning scope closed. Not3-mcp-*directory was left inTMPDIR. The bridge given only the old raw variable exits with code 2.A live T3 thread. A dev server from this branch, loopback only, with an isolated T3 home and an ACP registry instance running
codex-acp. One prompt asked the agent to callorchestrator_capabilities. The call went throught3 acp-mcp-bridge, completed, and the agent reported 10 providers. While the agent was alive I listed only the variable names in its environment and the bridge's. Thecodex-acpprocess hadT3_ACP_MCP_AUTHORIZATION_FILE,T3_ACP_MCP_ENDPOINT,T3_ACP_MCP_ENTRYPOINTandT3_ACP_MCP_NODE. The bridge hadT3_ACP_MCP_AUTHORIZATION_FILEandT3_ACP_MCP_ENDPOINT. Neither hadT3_ACP_MCP_AUTHORIZATION.Not checked:
os.tmpdir()is per user on both andrenamereplaces an existing file on both, but I ran neither. On Windows, mode0600is not an owner-only ACL.pibinary. The Pi environment came frombuildPiRpcLaunchwithout starting Pi, and the extension ran in avmcontext, as the repo's testkit does.codex-acpthrough the registry driver.sleepcarrying each captured environment.Made with Claude Opus 5.5 in T3 Code (Claude Code harness).
🤖 Generated with Claude Code