Deliver named Pi tokens and /token selection runtime #51
No reviewers
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/archetypes!51
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/named-tokens-runtime"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A managed Pi credential can now hold several named bearer tokens, and Pi can switch between them with a new /token command while a request is running or queued. On a running machine with today's empty public registries nothing changes except cleanup: the next activation retires the old per-credential runtime symlink layer that only the framework itself ever pointed at, and removes any managed auth.json entries the old layout recorded. Everything an operator put in auth.json or models.json by hand stays untouched, and no bearer ever leaves the volatile Age files except through Pi's own credential pipe. What proves it is the rewritten pi-provider-lifecycle witness, which drives the real generated extension through a real Pi process, kills the reconciler at every publication boundary, and greps every artifact and scenario home for leaked bearers; the whole flake check suite is green. If this is wrong, revert the commit — the retired link layer is regenerated by the previous revision's reconciler on the next rebuild, and no persistent state outside the managed files is touched.
Two contracts in the plan were corrected against measured Pi 0.84.2 behavior rather than followed as written, and the plan doc may deserve a matching amendment. First, the plan places the startup selector command in auth.json; measured provider composition resolves a stored auth.json credential in preference to any provider or extension API key, which would have made the /token extension's supported registerProvider re-registration permanently inert. The startup command therefore lives in the managed models.json provider record (an equally supported command-backed key surface), auth.json carries no managed entries, and the witness pins the working end-to-end switch. Second, the plan has the interactive picker run at session start; awaiting a UI dialog inside session_start drains Pi's event loop in RPC mode and exits the process before any response could arrive, so the picker fires as a detached task with identical user-visible semantics, and the witness drives it through Pi's RPC extension-UI surface.
Risk
R3, per the plan's archetypes slice row: startup ordering, queued-work pinning, concurrent state writes, owned removal, and the bearer leak boundary. The residual risk after the witness is mainly interactive-TUI behavior of the picker and status output, which the sandbox cannot render; the RPC extension-UI surface it does exercise shares the same code path.
Validation
nix build path:.#checks.x86_64-linux.pi-provider-lifecyclegreen: selector precedence matrix; both adapters authenticating per-token against a local server through realpi; process-lifetime cache pinning; RPC-driven /token idle switch, deferral with latest-wins, cancel, interactive picker, non-interactive refusal, credential independence, concurrent preference-file merge, and per-process pinning; legacy link retirement (idempotent, absent-as-retired, foreign-occupant refusal preserved byte-for-byte, unrecorded operator symlink survival); SIGKILL injection after the auth rename, models rename, recorded unlink, and manifest rename with convergent retries; the pass-12 bounded-convergence window with its refuse-by-name repair; extension installer collisions; comparator sabotage; and leak scans over every scenario home, log, and generated artifact.nix flake check path:.exits 0 (all 15 checks).Refs allod/strategy#36
Review pass (required post-open review): read-only diff review by gpt-5.6-sol at reasoning xhigh. Both implementation deviations from the plan were adjudicated CONFIRMED as necessary against Pi 0.84.2 source (stored auth.json credentials win over provider keys; session_start cannot await the RPC picker), but the review found 5 blockers and 6 gaps in how they and the surrounding contract were implemented. Findings below; fixes will land as follow-up commits on this branch, and the plan will be amended afterward to record the measured Pi behavior (models.json selector placement, request-time command resolution).
[BLOCKER] Deviation (b): CONFIRMED as necessary for RPC startup, but the detached implementation violates the first-request guarantee.
modules/pi-token-extension.ts:117-149starts the picker without awaiting or otherwise gating requests. Pi RPC cannot await the picker insession_start: extension binding finishes before the RPC stdin reader is installed, whilectx.ui.select()waits for an RPC response. However, once detached, RPC can accept a prompt before the selection response, and TUI initial prompts can run immediately after initialization. That request reaches authentication with no selected credential and fails. Multiple ambiguous credentials can also start concurrent TUI pickers, with the later picker replacing the first UI and potentially leaving its promise unresolved. The claimed RPC behavior is real, but detachment needed a request gate until startup selection completes.[BLOCKER] Deviation (a): CONFIRMED as necessary, but the new ownership model can delete user-created
auth.jsoncredentials.modules/pi-provider-reconcile.sh:364-366removes every provider listed in the previous manifest fromauth.json, whilemodules/pi-provider-reconcile.sh:383-388continues recording providers because it owns theirmodels.jsonrecords. Pi does prefer stored credentials over provider/extension API keys:pi-ai/dist/auth/resolve.js:40-54anddist/core/provider-composer.js:226-239confirm that anauth.jsoncredential wins, making re-registration inert. Moving the startup command tomodels.jsonis therefore necessary, keeps models discoverable, and does not itself widen bearer exposure. But after activating this version, an operator can add or/loginanauth.jsoncredential under a managed provider ID; the next activation deletes it even though this version never created that entry. This contradicts exact per-file ownership and the PR claim that hand-writtenauth.jsoncontent remains untouched. The collision test atchecks/pi-provider-lifecycle.nix:999-1009runs after the manifest has already been removed and misses this case.[BLOCKER] Missing-secret reconciliation is not a whole-operation no-op because the extension is still published.
modules/pi-provider-reconcile.sh:85-95returns success without changing managed JSON or the manifest when any desired Age file is missing.modules/pi-provider-lifecycle.nix:180-189then unconditionally runs the extension installer. For example, deploying a catalogue that changes the default token while its new secret is absent leaves old models/manifest state but installs the new extension; Pi can select according to the new catalogue against stale provider configuration. The witness atchecks/pi-provider-lifecycle.nix:947-954compares models only and does not install the extension in that scenario.[BLOCKER] An absent recorded legacy link is omitted from both rollback and the publication recheck.
modules/pi-provider-reconcile.sh:343-353writeslinks.rollbackonly when the recorded path currently exists, despite the laterexisted=0branches atmodules/pi-provider-reconcile.sh:126-148and401-409. If an interrupted prior retirement left the recorded path absent and another writer creates a file or wrong symlink after the initial check, this activation neither refuses it nor preserves evidence: it publishes the final manifest withcredentialLinks:{}. The required absent-as-retired behavior therefore has a concurrency window that can silently convert a foreign occupant into an unrecorded path.[BLOCKER] Interactive
/tokencan bypass active/queued deferral because idleness is sampled before awaiting the picker.modules/pi-token-extension.ts:185capturesbusy = !ctx.isIdle()beforectx.ui.select()is awaited atmodules/pi-token-extension.ts:221-224. In RPC mode, a client can invoke/token, enqueue a prompt while the picker is open, then answer the picker. The stalebusy=falsecauses immediate re-registration and persistence atmodules/pi-token-extension.ts:195-202, so already-active or queued work may use the new credential instead of being deferred untilagent_settled.[GAP] The process-lifetime command-result cache claim and its witness exercise the wrong Pi API.
README.md:133-136andmodules/pi-token-extension.ts:27-30say Pi caches the resolved bearer for the process lifetime. Actual provider composition callsresolveConfigValueOrThrow, which uses the uncached path atdist/core/resolve-config-value.js:199-207; Pi’s models documentation also says commands are resolved at request time. The witness instead imports and directly invokes cachedresolveConfigValueatchecks/pi-provider-lifecycle.nix:256-267and492-507. Rotating the selected token’s volatile file during a Pi process therefore changes the bearer on the next request, contrary to the documented and claimed behavior.[GAP] Malformed preference state is silently ignored and later overwritten.
modules/pi-token-extension.ts:35-47converts every preference read or parse error into{}. ConsequentlyresolveStartup()reports only well-formed stale token names, not malformed JSON, andpersistSelection()atmodules/pi-token-extension.ts:68-75can replace the corrupt file with a single merged-from-empty choice. A truncated preference file can therefore cause silent fallback and later loss of remembered selections for unrelated credentials.[GAP] A crash can permanently strand the preference lock.
modules/pi-token-extension.ts:55-66implements locking with a directory and a fixed retry loop, but has no owner metadata or stale-lock recovery. A SIGKILL aftermkdirSync(lock)leaves the lock directory indefinitely; subsequent selections activate in memory but fail persistence after five seconds until an operator manually removes it.[GAP] Non-interactive ambiguity does not surface the selector’s actionable diagnostic through real Pi. The selector emits the required explanation at
modules/pi-provider-token-select.sh:112-114, but Pi runs command-backed keys with child stderr ignored atdist/core/resolve-config-value.js:161-169, then reports only a generic configuration-resolution failure.checks/pi-provider-lifecycle.nix:628-641suppresses Pi output and checks only that no request/state change occurred. A headless ambiguous startup therefore does not tell the operator to use/tokenor configure a default.[GAP] The agenix assertion does not pin root-owned
0755intermediate directories. The generated configuration appears correct today, butchecks/pi-provider-lifecycle.nix:273-287asserts the leaf path, owner, group, mode, and the presence ofmkdir -p; it does not assert the activation script’sumask 0022, root ownership, or resulting0755modes for nested directories. An upstream activation-umask change to0077would make the secret unreachable by the configured user while this assertion still passes.[GAP] The claimed bearer leak coverage omits argv/environment, many scenario homes, and store closures.
checks/pi-provider-lifecycle.nix:1054-1069scans selected homes, logs, observations, and top-level generated artifacts. It does not inspect live/proc/*/cmdlineorenviron, does not include several legacy/kill/sabotage scenario homes, and does not traverse Nix store closures. A regression placing a synthetic bearer in a child process environment or an omitted scenario directory would pass this scan.Validation: I could not rerun
nix build path:.#checks.x86_64-linux.pi-provider-lifecycle; the sandbox cannot connect to/nix/var/nix/daemon-socket/socket(Operation not permitted). Static review confirmed the intended auth/models/link/manifest publication order, pass-12 refuse-by-name convergence logic, real Pi RPC extension execution, and non-vacuous comparator sabotage. The worktree was not modified.All eleven findings are addressed in
9ba5883(authored by gpt-5.6-sol; the closureInfo repair replacing an in-sandbox nix-store query was made by the managing agent). Both confirmed deviations are now implemented safely: the detached startup picker is serialized and gated so no request authenticates an unselected credential, and auth.json ownership is bounded to the legacy transition with a later operator credential under a managed provider ID refusing by name, preserved. The false process-lifetime cache claim is replaced by the measured request-time resolution property with agent-settled registration. The plan was amended accordingly at allod/strategy 130ebcc. Validation by the managing agent from this worktree: nix build path:.#checks.x86_64-linux.pi-provider-lifecycle green, and all fourteen remaining checks built green sequentially (a parallel nix flake check was OOM-killed in this VM; per-check builds all pass).