Skip to content

feat(keychain): materialize credentials for human sandbox sessions - #631

Open
yzxcj797 wants to merge 1 commit into
yc-software:mainfrom
yzxcj797:feat/keychain-session-materialization-550
Open

feat(keychain): materialize credentials for human sandbox sessions#631
yzxcj797 wants to merge 1 commit into
yc-software:mainfrom
yzxcj797:feat/keychain-session-materialization-550

Conversation

@yzxcj797

@yzxcj797 yzxcj797 commented Aug 20, 2026

Copy link
Copy Markdown

Implements the suggested direction of #550 — the session-materialization half. The in-sandbox link plumbing (ephemeralCredLinkScript, EPHEMERAL_CRED_DIR) is reused exactly as the issue prescribes; what was missing was a non-agent driver, which this adds.

What was unreachable

Every materialization path sat behind an agent capability token (handleKeychain's gate) and ran inside a turn. The Keychain interface already declared materialize/materializeOwn/materializeStanding; every sandbox driver already ran ephemeralCredLinkScript; renderUseScript already produced the shell-sourceable form. A human reaching the same sandbox interactively (SSH, browser IDE) had exactly the two bad options the issue names: a shared credential in the scope's home volume, or nothing.

The driver

Keychain.materializeStandingForUser(ownerId, scopeId)materializeStanding's selection filtered to one person's credentials. The grants table is keyed by audience scope, so the filter is what makes a shared sandbox's sessions per-user: Alice's session materializes Alice's grants, never Bob's, even though both granted toward the same scope. Once-mode and expired grants are excluded identically to the agent path.

POST /v1/keychain/sessions { userId, scopeId, ttlMinutes? } — source-auth (auth: "source"): the caller is the trusted integration holding CORE_SIGNING_SECRET (the SSH/IDE attach helper), naming the human on the session — the same trust boundary the issue's "authenticated internal route" describes. It materializes the user's set, records a credential_usage row per credential exactly as a turn does, audits keychain.session.open, and returns the same renderUseScript output /keychain/use emits — the helper sources it in the user's shell, so the materialization lifetime and isolation semantics are identical to an agent turn's. TTL bounded: default 8h, max 12h.

DELETE /v1/keychain/sessions/:id — idempotent release: released usage rows + audit. GET /v1/keychain/sessions lists live sessions for operators. An expired session sweeps to released on next touch — no re-materialization past its TTL.

Destination.humanSession { userId, sessionId } — the non-agent counterpart of the web destination, so asks and audit can name where a materialization went (the issue's item 1).

What deliberately didn't change

The agent surface: existing routes, their capability gate, and turn-time materialization are untouched — the new routes are additive and source-auth-gated, so an agent token alone cannot reach them (and a source-auth caller was already trusted with the whole keychain surface these routes read).

Tests

  • materializeStandingForUser returns only that person's standing grants — Alice + Bob both grant toward one scope; each session gets exactly one credential (their own), a third user gets nothing. Fails on main (no such method).
  • materializeStandingForUser excludes once-mode and expired grants — pins the mode boundary.

Full keychain.test.ts: 43 passed / 1 pre-existing environment failure (sprites exec ... bad envelope — identical on clean main); keychain-ask suite green; tsc --noEmit clean.

Not included (per the issue's own scoping)

File-credential materialization for sessions (the agent path materializes files only via explicit /keychain/use re-fetch, and the same one-shot semantics deserve their own design), and the dashboard surface — the API + script contract here is the integration seam both would build on.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

A scope's sandbox is reachable interactively — SSH attach, browser IDE —
by the same people the agent serves, but credential materialization was
only drivable from an agent turn: the keychain routes require an agent
capability token, and the only materialization surface was
POST /v1/keychain/use inside one. A human working interactively in the
sandbox got either a shared credential dropped in the scope's home
volume (no expiry, no per-user attribution, no revocation short of
rotating the token) or nothing at all — both defeating the keychain's
per-user/per-scope/expiry/audit model (yc-software#550).

The primitives now have a non-agent driver:

- Keychain.materializeStandingForUser(ownerId, scopeId) — the same
  selection as materializeStanding filtered to ONE person's credentials:
  a session materializes exactly the standing grants that person gave
  toward this scope, never a neighbor's (once-mode and expired grants
  excluded identically).
- POST /v1/keychain/sessions { userId, scopeId, ttlMinutes? } — a
  source-authenticated integration (the SSH/IDE attach helper holding
  CORE_SIGNING_SECRET) opens a session: materializes the user's set,
  records per-credential usage rows exactly as a turn does, audits the
  open, and returns the SAME renderUseScript shape /keychain/use emits
  for the helper to source in the user's shell. TTL bounded (default 8h,
  max 12h).
- DELETE /v1/keychain/sessions/:id — releases (idempotent), records
  released usage rows, audits. GET lists live sessions for operators.
- Destination carries an optional humanSession { userId, sessionId } —
  the non-agent counterpart of the web destination, so asks and audit
  can name where a materialization went.

Agent-surface behavior is unchanged: the routes are additive and
source-auth-gated; an agent capability token alone does not reach them.

Implements the suggested direction of yc-software#550
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant