Repository navigation
Conversation
…cess environment, injection, SigV4 re-signing, minting block
…development build identity
…Cursor, Codex, OpenCode and Devin
… control API, events and relay bootstrap
… proxy daemon and phase relay start
Deploying phase-cli-install-script with
|
| Latest commit: |
89841bb
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2740958f.phase-cli-install-script.pages.dev |
| Branch Preview URL: | https://feat--agents.phase-cli-install-script.pages.dev |
…permission assertions there
… and guard ECR Public The minting block resolved one operation per request, so a harmless X-Amz-Target or query Action could mask the form-body Action a query-protocol service actually executes. Every selector the request carries is now checked and a match on any of them refuses the call. ECR Public GetAuthorizationToken joins the rule table.
… the relay A request refused for a missing capability is recorded with the method, host and path the client chose. A credential-shaped value there failed redaction validation and took the proxy down, which anyone able to reach the listener could trigger. Those fields are now replaced with fixed placeholders; proxy-written fields still fail closed.
…ycle errors visible Requests read off a decrypted CONNECT tunnel carried a background context, so a stalled upstream round trip held shutdown until it answered. They now follow the engine lifecycle. When the final audit flush fails, the flush error is joined to the lifecycle error rather than replacing it, so an expired or revoked session is still recognised by the recycle paths.
…meter statuses The adapter dropped the client's startup parameters (options, application_name, client_encoding, DateStyle) and answered with a made-up server_version and encoding. It now forwards the client's settings to the upstream, apart from the managed identity and replication, replays the server's own ParameterStatus messages, refuses replication connections explicitly and keeps the backend key hidden.
The spool may re-send a durable event on the next flush tick before its acknowledgement lands, which the slower Windows runner exposed.
…inting selectors Request-derived audit fields now follow Console's own limits (method as a short identifier, host bounded and control-free, path bounded), so a denial for an unauthenticated request can never be refused by Console and block later batches until the spool fills. The minting check refuses a request with a repeated X-Amz-Target instead of ignoring it, and reads any non-JSON body on a guarded service as a form whatever its Content-Type.
…credential expiry in discover checks A forwarded client setting the upstream refuses (unknown parameter, invalid value, privileged option) is now relayed with its SQLSTATE and a bounded message instead of a generic rejection. The discover-versus- runtime comparison no longer includes the credential expiry, which rotates on every refresh for assumed roles. The joined shutdown error is limited to session lifecycle errors.
…test The spool may re-send a durable event before its acknowledgement lands, which the macOS and Windows runners exposed.
The hook daemons cover harnesses on this machine and phase relay start covers remote sandboxes, both with a session lifecycle that recycles in place. serve was the last caller of the lifecycle that could not, and its printed environment handoff duplicated what phase ai run already injects into its child. The clipboard helpers stay for the relay handoff.
Nothing ever pointed a JVM at the generated PKCS12 file: JAVA_TOOL_OPTIONS is protected and left to the user, and the relay bootstrap already tells Java users to import the CA themselves. The store files, their plumbing through CAPaths and the prepared bundle, and the only third-party module the branch added go with it.
…brary crypto/pbkdf2 has been in the standard library since Go 1.24.
…ead of failing enable Stock installs of every common Linux distribution ship without NSS certutil, and the trust prompt defaults to yes, so phase ai enable created the CA and then aborted before installing anything. enable now checks for the tool first: without it the offer is skipped with the distribution's package name, an explicit --trust-ca fails with that hint, and the rest of the setup completes. Command-line tools and SDKs never depended on the user trust store; it matters for browsers and for Go programs on macOS and Windows. CI gains an end-to-end enable job on the three runner OSes and inside stock Fedora, Debian, Ubuntu, Alpine and Arch containers, covering the skip, the explicit-flag failure, the interactive prompt and the NSS trust path once certutil is installed.
Docker's seccomp profile refuses the syscall filter pacman installs for its download user, so the package install failed before the test ran.
…ust attempts grep -q under pipefail closed the pipe before the CLI finished writing, which made the idempotency check fail on the Ubuntu runner. The macOS and Windows trust attempts always blocked on the consent dialog until their timeout, so they are gone; the unit tests cover those paths.
…s and report the refresh The hook daemon recycles its backend session within one refresh interval of a Connection or credential edit in the Console, but it built a fresh proxy engine per cycle, so the proxy capability and the memory of earlier decoy generations were lost: a shell or process started before the edit failed with an invalid-credential error afterwards even once the edit was correct. The daemon now serves every cycle on one engine, as the relay does, and wrapped harness commands recognise a new cycle by its start time. The hook's status line and phase ai status now say when the session was refreshed and why, so an agent retries the command that failed around the change instead of giving up or switching credential sources.
phase ai enable failed on Windows whenever an older Phase AI CA was still
trusted in the current-user Root store:
inspect and rotate Phase AI CA in Windows current-user Root store:
exit status 1: Remove-Item : The operation is on user root store and
UI is not allowed.
The rotation script deleted obsolete roots with Remove-Item on the Cert:
drive, but trustWindows runs PowerShell with -NonInteractive, and Windows
PowerShell's certificate provider refuses every removal from
CurrentUser\Root when -NonInteractive is on its command line.
The PowerShell step is now read-only: it prints PHASE_CA_OBSOLETE with
the SHA-1 thumbprint of each older Phase-owned root, and trustWindows
removes each one with certutil -user -delstore before adding the new CA
with -addstore, as before. Thumbprints must be 40 hex characters, since
-delstore also matches store indexes and common names. The Windows trust
notice now mentions the confirmation shown for each removed root.
A session got its lease only when the bounded kick saw the daemon ready. When startup outlasted both SessionStart's and the first prompt's waits, the daemon published with no session lease and its lease monitor stopped it after the idle grace, while that prompt was still running. The kick now also registers the lease while a live daemon is starting, bound to the generation in its ownership lock, which is the generation the daemon's lease monitor counts.
Enable stopped the hook daemon before installing adapters but saved the new Workflow only afterwards. A prompt in an open session during that window restarted the daemon on the previous configuration, and heartbeats then kept it alive because they never compare scope fingerprints. Enable now holds an OS file lock from before it stops the daemon until it has saved the configuration, and claiming the runtime lock refuses while that lock is held. A session's next prompt after enable starts a daemon on the new configuration. This also keeps a daemon from running the shared hook executable while enable replaces it.
The OpenCode plugin exports the hook daemon's environment, including PHASE_AI_ENABLED=1, into its shells. A Claude started from one took that for an environment from phase ai run: SessionStart installed no loader and registered no lease, and the heartbeat returned early. Once the daemon was replaced, Claude kept the old proxy capability, and with the parent's lease gone the daemon could idle out under it. The environment the daemon publishes now carries PHASE_AI_HOOK_RUNTIME, a digest of its proxy URL. The Claude hooks treat an inherited environment as a launcher's only when that marker is absent or does not match the inherited proxy, as when phase ai run replaced it; otherwise they install the loader and lease as usual, and the loader sources the daemon's file over the inherited values.
The refresh lookup checked only the three records before a recycled session_ready. With three or more backend retries in between, the environment_changed record fell out of reach, and the hook notice and phase ai status described a Console change as a scheduled renewal. The lookup now walks back past retries (and rejected daemon starts) to the record that began the recycle.
Windows cannot replace a file while a reader has it open, so readers polling without pause could keep every save's rename waiting. Four readers a millisecond apart still catch a truncating write every run.
A session whose hooks ran while an established daemon was recycling saw no runtime state and a lock older than the startup window, so it was reported neither ready nor starting and registered no lease. With no other leases, the daemon could idle out after republishing, during that session's work. Health inspection now treats a recent RecyclingAt in the lock like a recent startup, so the kick registers the lease against the lock's generation.
The refresh test counted "guard" across the whole config, so a temporary directory containing that word made it fail although the user handler was kept and exactly one guard was installed. It now parses the hooks and counts handlers that run the rendered guard command.
The plugin's location follows OPENCODE_CONFIG_DIR and XDG_CONFIG_HOME, and enable, refresh and disable each recomputed it from the current environment. Run without OpenCode's environment, disable left the plugin in place, and a later runtime for another harness exported the shared Phase environment into OpenCode's shells through it. Enabling another harness likewise installed a second plugin at the default root and left the real one stale. ai.json now records each OpenCode plugin path. Enable refreshes the recorded plugins and adds the current root only when OpenCode itself is enabled or nothing is recorded yet; disable removes the recorded plugins, falling back to the current root for configs written by earlier builds. The default path is made absolute so a relative override is recorded independently of the working directory.
phase ai exec --connection kept the inherited value of every variable the selected plan left unchanged from the runtime plan. After a shell sourced an .env file, ambient PGHOST and PGPASSWORD therefore survived an explicit selection and psql went around the selected Phase endpoint. Only CA and trust-store paths keep their inherited form now; credentials, configuration, adapter endpoints and routing always come from the selection.
…ed rule Path-scoped host rules matched the request path with a trailing slash cleaned off, so a rule for /allowed also bound /allowed/ to the Connection. S3 treats those as different object keys. The rule now matches the path as sent, trailing slash included; paths that cleaning would otherwise change are still refused.
The credential-change guard read every dollar-quoted string as SQL, so a statement that only selects or stores text such as $$CREATE ROLE example$$ was refused as a credential change. A dollar-quoted string is now read as code only where it runs: the body of a DO block or of CREATE FUNCTION or CREATE PROCEDURE. Other strings are data.
The credential-change guard still let statically written password changes through. A -- comment ended only at LF, so "-- note\rALTER ROLE ... PASSWORD" reached the server, which also ends comments at CR. DO blocks and function bodies written as '...' strings were skipped unread. With standard_conforming_strings off, a backslash before a quote ends an ordinary string earlier than the guard assumed, hiding the statement that followed. The guard now reads SQL a token at a time as the server's lexer does: comments end at CR or LF, and the value of a code body is read in any form (dollar-quoted, '...', E'...' and U&'...' with UESCAPE, including strings continued across a newline) before it is checked. SQL that contains a backslash is checked a second time with backslash escapes in ordinary strings, since the proxy cannot see that setting. Code nested more than four DO blocks or function bodies deep is refused instead of passing unread.
A config written before plugin paths were recorded still made disable and refresh look only at the current environment's OpenCode root. Run without the OPENCODE_CONFIG_DIR used at install time, disable left the plugin active and refresh wrote a second one at the default root. With nothing recorded, both now look for Phase-owned plugins in the current root and in the root of each recorded skill, falling back to the current root's path when none is found. Foreign files are never matched.
Enable kept new OpenCode plugin paths in memory and saved them only after every adapter had been installed. When a later adapter failed, the plugin stayed installed but unrecorded, and disable left it active. Enable now adds each plugin path to the saved ai.json before writing the plugin, and disable removes recorded plugins even when OpenCode is not among the saved harnesses.
…enCode plugin Redaction fails closed on an ai.json this build cannot read, and the file may hold a newer CLI's settings, so recording a plugin path now refuses instead of overwriting it.
Enable recorded a plugin path before InstallOpenCodePlugin refused a foreign file there. Disable then treated that file as a Phase plugin and failed on every run while the record stayed in ai.json. The ownership check is now its own function, and enable runs it before recording the path; the record still precedes any write.
…eSQL token The credential-change guard split U&"plpgsql" into three tokens, so in DO LANGUAGE U&"plpgsql" '...' the body no longer followed the language name and was not read as code. A UESCAPE clause whose operand was an E'' or dollar-quoted string was split off the same way. U&"..." identifiers are now read as one token like U&'...' strings, and a UESCAPE clause with any string-constant operand is read with either.
The classifier rebuilt an eval or sh -c script from words that had lost their quoting. An expansion the outer shell runs first, such as $'\x3b ...', "$VAR", a glob or ~, could add a separator to the script, so ordinary work hidden in it ran as an automatically approved Phase call. A nested script containing $, a backtick, %, a glob character or ~ is now treated as opaque and stays ambient, as a script starting with $ already was.
One phase.invalid URL made the whole call Phase-managed, so further URLs in it, including transfers after curl --next, ran with the Phase environment and automatic approval. curl -O also hid the URL after it, because short options were compared without case. A curl or wget call is now managed only when every operand is a Phase target and it reads no config or URL file (curl -K/--config, wget -i/--input-file and -e/--execute). Short curl options are compared case-sensitively, and the documented request options (-X, --json, --proxy, --max-time) keep their values out of the target check. Every argument is now inspected, so a word that does not parse as a host, such as ^, is reported as no Phase target instead of panicking.
Treating the & after > or < as descriptor duplication also covered cmd's ^> and ^<, which are literal characters, so the & after them still separates commands. A command such as curl http://phase.invalid/v1/status -o out^>& calc ran calc as part of an automatically approved Phase call. The & is now read as part of a redirection only when the > or < before it is not caret-escaped.
The scope tokenizer read a backslash-escaped quote as POSIX does and a cmd /c script's single quotes as protective, but cmd and PowerShell parse neither that way. A command relying on either could smuggle an ordinary command past the separator scan and be auto-run with the Phase environment. Flag a backslash-escaped " inside double quotes as ambiguous so the command cannot be proved plain, and leave a cmd /c script carrying a single quote ambient. Both now stay unmanaged.
curl and wget read implicit config (~/.curlrc, ~/.wgetrc) before their arguments, and an inline HOME, CURL_HOME, XDG_CONFIG_HOME, WGETRC or SYSTEM_WGETRC assignment can point them at a file that adds another transfer. The argv-only scan had stripped those leading assignments, so such a call was still promoted and run with the Phase environment. Detect a config-redirecting assignment ahead of a curl or wget invocation, including through env and sudo, and keep the call unmanaged.
The curl and wget scan looked at each argument in isolation, so an option value consumed by an earlier option (curl -H -o file://...) was skipped as if it were still a value, hiding a second transfer; -- was not honoured; wget abbreviations such as --inp and --config slipped past the input/config checks; and a letter inside an attached value (-oKeyData.json, -HX-Api-Key) was read as a -K/-i config switch, leaving valid Phase calls ambient. Walk the arguments in order against a small allow-list, consuming each option's value the way curl and wget do and honouring --. A call is managed only when every operand and --url is phase.invalid and every option is recognised; anything else, including -K, -T and unknown options, stays unmanaged.
Harnesses hand the hook the command as the agent typed it, and the skill tells agents to call curl directly, so the managed path never needs to look inside a wrapper, interpreter, substitution or quoted script. Trying to made the scan keep pace with cmd, PowerShell, curl and wget syntax kept leaving gaps: an empty quoted argument let a later option swallow a real target, a nested command substitution classified before the Windows-quote check, and a config-redirecting assignment survived wrapper unwrapping. Reduce shellCommandRequiresPhase to a single simple command: the client must be curl, curl.exe or wget as the first word (no leading assignment, no wrapper), with no separators, pipes, subshells, command substitutions, heredocs or interpreters, no empty or $/backtick-bearing word, only allow-listed options, every target phase.invalid, and only the redirections already accepted. The tokenizer now preserves empty quoted words so they cannot be dropped. The codex, cursor and devin guards check the mixed-command denial before this narrower recognition, matching the OpenCode guard, so mixed command lines are still denied. Closes the residual HRN-2 (empty quoted argument), HRN-15 (nested marker ahead of the quote check) and HRN-16 (config env through option-aware wrappers): each is now simply not a single plain call and stays ambient.
…iptor The scope tokenizer dropped any all-digit word adjoining > or <, even when it was quoted or escaped, so an option lost its real value and swallowed the next operand instead, letting a non-Phase transfer pass as a Phase call. Mark a word as quoted when a quote or backslash contributes to it, and keep a quoted word as an argument before a redirection.
PowerShell accepts typographic single and double quotes as string delimiters, but the scope tokenizer read them as ordinary characters. A command could close a string with one and run ordinary work that the tokenizer still saw as part of a quoted header value. Leave any command carrying one of those quotes ambient.
The plain-call check rejected $ and backtick expansions but not pathname expansion, so an unquoted *, ? or [ in an option value could expand into several arguments after classification and add a non-Phase operand. Record an unquoted, unescaped glob character on each token and leave any call carrying one ambient. Quoted or escaped glob characters stay managed.
Windows refuses to open ai.json or config.json for the instant a save renames its replacement over the file, so a concurrent read briefly returned nothing. Reads now retry for up to about 100ms on Windows; a missing file is not retried.
feat(ai): accept key-bearing v1 Agent tokens
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.
Summary
Phase AI lets coding agents use third-party services (HTTP APIs, AWS, PostgreSQL) through workflow-scoped Connections approved in the Console, without ever receiving a live credential. The agent's process gets decoys; a local proxy swaps them for the real credential on the way out, enforces the approved Connections, redacts responses, and blocks calls that would mint new credentials.
What's in here
phase ai enable | disable | status,phase ai run | exec | serve, and a hook-managed proxy daemon (internal/aiproxy,internal/airuntime).phase ai exec; VS Code Copilot gets the skill.phase-cli-ai-relay(proxy, Connections, requests) andphase-cli-app-secrets(app secrets; the previousphase-cliskill is migrated in place).phase relay start: a TLS relay for sandboxes that cannot run the CLI. Onecurlline bootstraps a sandbox, and the relay survives Console session refreshes.pkg/proxy: a per-installation CA (90-day validity, renewed automatically, trusted per user with a single OS dialog), the process environment compiler, credential injection, SigV4 re-signing, and the credential-minting block.internal/agentsdk; the CLI stays on golang-sdk v2.4.1.phase auth --mode tokenaccepts Agent tokens, which are scoped tophase aiandphase relay.Environment variables
Read by the CLI:
PHASE_AI_ENABLED,CLAUDE_ENV_FILE,PHASE_CURSOR_SESSION_ID;PHASE_SERVICE_TOKENnow also accepts Agent tokens.Set for managed processes:
PHASE_AI_ENABLED,PHASE_AI_CA_BUNDLE,HTTP_PROXY/HTTPS_PROXY,NO_PROXY,NODE_EXTRA_CA_CERTS,AWS_CA_BUNDLE,PGSSLROOTCERT, the replacement-root variables when a complete bundle exists, plus each Connection's decoys and configuration.Ports and files
Proxy on
127.0.0.1:9543, PostgreSQL adapter on127.0.0.1:9544(a relay binds0.0.0.0with PostgreSQL on the next port). Runtime state lives under~/.phase/ai/, configuration in~/.phase/ai.json. The proxy's control API is documented insrc/internal/aiproxy/CONTROL_API.md.Testing
go test ./...is green and every commit builds on its own. Verified end to end: Claude Code, Cursor, Codex and OpenCode locally; S3 uploads and special-character object keys; the relay from a container with only curl, psql and the AWS CLI, including a forced Console session recycle.Docs site updates follow in a separate PR.