Skip to content

Give commands an authenticated API client, per-workspace auth sessions, telemetry, and real prompts — the foundations for porting the platform CLI onto the engine - #130

Merged
wmadden merged 68 commits into
mainfrom
s2a-foundations
Aug 11, 2026
Merged

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

You can now be signed in to several workspaces at once, and say which one you are working in without signing in again.

$ prisma auth workspace list
ℹ Listing your workspace sessions on this machine.
name  id  status
Acme Inc  wksp_acme  current
Globex  wksp_globex

$ prisma auth workspace use Globex
ℹ Switching the current workspace session.
previous: Acme Inc
workspace: Globex
✔ Current workspace session updated.

$ prisma auth workspace use wksp_nope
✖ [AUTH.NO_SESSION_FOR_WORKSPACE] You have no session for workspace 'wksp_nope'.
→ Sign in and pick 'wksp_nope' in the browser: prisma auth login

Today's CLI stores the same per-workspace tokens but only really models one "active" workspace: it re-fetches workspace names on every read, ends a single workspace through auth logout --workspace, and can leave entries behind that nothing cleans up. The runs above are the new shell against a real credential file — the second command changed which session is current without opening a browser, and the third failed at exit 2 with an error that tells you the only thing that can fix it.

The decision

We are replacing this repo's commander-based shell with @prisma/cli-engine (merged in #129). Every platform command will be re-mounted on the engine and the old shell will be deleted. Before that port can start, the engine has to be production-ready and the auth family has to sit on something the rest of the port can build on. This pull request builds those foundations and proves them by porting the first command group, auth. The port itself follows in sibling pull requests for resources, services, and init plus shell removal.

The foundations

An API client on the command context. Nearly every platform command calls the management API. Rather than each command constructing its own SDK client, the engine builds ctx.api once per run, lazily, for the session that process is acting as. A token refreshed mid-run is picked up on the next request, and an expired session surfaces as the standard sign-in error instead of a crash. The test harness accepts a fake client, which is the single mock seam every ported command will use.

Real interactive prompts. On a terminal, prompts render through @clack/prompts — the same library today's CLI uses, so the upcoming init wizard keeps its current feel. In tests, pipes, and CI a plain line-based renderer runs instead, so no test depends on terminal rendering. Clack is internal to the engine and invisible in its public API. Its spinners are deliberately unused, because they install process-global signal handlers.

Telemetry, identical to the ORM CLI's. The detached sender process, the consent configuration with a shared installation id, and the value-free command snapshots move into this repo and report through one engine hook that fires once per command with its id, exit code, and duration. telemetry status|enable|disable are ported. Flag values, arguments, and paths never reach the wire, which hostile-input tests hold down.

One version number and prisma/prisma's release machinery. This repo adopts prisma/prisma's versioning model unchanged: a single lockstep version, now 8.0.0-rc.1, committed in every manifest and advanced only by a maintainer running pnpm bump-version and merging the resulting pull request. prisma --version reports it. Merging a bump publishes that version to latest; ordinary merges publish -dev.N builds. The old publish-time arithmetic against npm dist-tags is deleted. @prisma/compute, an app-runtime library slated for extraction to another repo, keeps its own line. Merge order: merge #131 first. It pre-sets the root version on main, so merging this pull request is a version no-op to the publish workflow and ships only a development build. The first real latest release then happens through a deliberate release bump.

The auth rework: sessions, and the thing that is not one

The auth family no longer keeps a pile of credentials with a pointer at one of them. It separates three things that were previously one.

A session is a stored logged-in-ness for one workspace — at most one per workspace. It is the only thing called a session: what auth workspace list lists, what use selects, what logout ends. The selection is a separate scalar of stored state: which session is used where a session is needed. And the active credential is what a given process authenticates as, which is either the selected session's or the one PRISMA_SERVICE_TOKEN supplies.

That third thing is why this went through two revisions. The first modelled the environment token as a session, and it produced four defects that were all the same defect: a source field whose job was to say "this one is not really a session", a hardcoded current: true that gave the word two meanings, an empty-string workspace id because a non-session was forced to carry a session's key, and a guard rejecting the non-session from APIs that only take sessions. Separating the three made all four stop existing rather than each need a fix.

A new component, the credential manager, owns the state. It is the only thing that touches the credential file, and it is also what the platform SDK writes through, so a token refreshed on a 401 lands under the same rules as a fresh login. The engine owns the API client; the manager never hands a token to a command, and never talks to the user or opens a browser.

Four decisions are worth knowing:

  • A process pins its decision once. Which credential a command acts as is settled at its first read and does not move for the life of that process, though the material behind it is re-read every time — so another shell switching workspaces mid-run cannot redirect a running command, while a token another process rotates is still picked up.
  • Refresh follows the credential, not where it came from. A credential refreshes if it has a refresh token, full stop. There is one API client over whatever storage the manager hands out: file-backed for a stored session, memory-backed for an environment credential, which is how a 401 that could never be renewed reports that the token was rejected instead of telling a CI job to retry a permanent failure.
  • Concurrent refreshes are left to the auth service. Refresh tokens are single-use with a ten-second reuse grace, and rotation does not invalidate a pair that was already issued, so two CLI processes refreshing the same session both succeed and the file ends up holding a working pair. There is no client-side coordination beyond deduplicating within one process. A cross-process test drives two real refreshes through a scripted token endpoint that reproduces the grace.
  • The state file is one file, at the same path as today's. Writes are atomic — temp file, fsync, rename, mode 0600 — and a short advisory lock covers read-modify-write so two mutations cannot lose each other. No network call ever runs while that lock is held. Reads never write and take no lock.
  • The old store migrates by being read, not rewritten. Existing credentials are adopted as sessions on read; nothing is written until your first mutation, at which point the file is rewritten in the new shape. That is a one-way door: from then on a still-installed older CLI reads as signed out. We chose that over two auth worlds diverging silently, because prisma auth login fixes it and nothing else has to.

The commands keep their existing names, which the session model makes honest: auth login, auth logout, auth whoami, and auth workspace list|use|logout. auth workspace use selects among the sessions you already hold and never creates one, because the consent screen cannot be told which workspace to grant — you pick it in the browser.

The engine grew three things the auth commands needed, all of which the rest of the port will use. A repeatable global --confirm <value> flag carries consent for destructive work: interactively you type the value to confirm, non-interactively you pass it exactly, and --yes deliberately cannot grant it. ctx.openUrl opens a browser and degrades to printing the URL. prompt.browserWait waits for a browser round trip on a terminal and returns a structured "this needs interaction" error at exit 2 everywhere else.

What changes for users

The published binary still runs the old shell; the engine-based shell is an unpublished development binary until the port finishes. For the commands ported here, every behavioural difference is written down in the divergence record rather than left to be discovered. The ones most likely to affect someone:

  • auth logout --workspace <ref> is gone. auth workspace logout <ref> is the one way to end a single session, and auth logout now ends every session and reports how many, reaping orphaned entries the old code could leave behind.
  • Workspace switching works while PRISMA_SERVICE_TOKEN is set, where the old CLI refused it. The variable supplies the credential this process uses; it does not occupy a slot, so changing stored state is coherent and every mutation succeeds, each saying the environment credential stays in force until you unset it.
  • Ending a session is idempotent. A workspace you never had is still an error, raised when the reference fails to resolve. But if another prisma process removes the session between your command reading it and writing, you now get exit 0 rather than an exit 2 telling you something that is no longer true.
  • Error codes are dotted (AUTH.NO_SESSION_FOR_WORKSPACE), and several failures that exited 1 now exit 2, which means "could not complete" rather than "crashed".
  • auth whoami returns the active credential rather than an auth-state snapshot: workspace, user, source, and expiry. Identity comes from the credential's own claims, enriched from /v1/me when that answers within a short deadline. A service token whose subject names a workspace reports no user, where a naive reading would have put workspace:<id> in the user field.
  • Workspace names are no longer refreshed on every read. A name is fetched once when the session is created, so a workspace renamed in the console keeps its local name until you next log in to it. Reads are entirely offline.
  • Scripted consent uses --confirm <value>. The mock-only login flags --provider, --user, and --workspace do not port.

Verification

840 CLI tests, 258 engine tests, 97 telemetry tests, typecheck, and lint all pass on Linux, macOS and Windows. Beyond the usual coverage, the credential manager is tested across real processes on a real filesystem: two processes mutating at once both land, a crashed process's lock is taken over, a running process keeps its pinned session when another switches the marker, and two processes really refresh the same session through a scripted token endpoint. A filesystem spy asserts that no read path writes, including migration adoption, and a leak scan seeds known secrets and asserts they never appear in output, debug logs, error metadata, envelopes, or a worker process's stderr.

Alternatives considered

  • Tracking identity in the stored state, so the CLI could tell you a session belongs to a different account: rejected. The credential file has always been identity-blind and nothing today depends on it being otherwise. Identity stays a read-time decode in whoami.
  • Letting auth workspace use create a session by opening the browser: rejected, and it is not actually possible — the authorize request carries no workspace parameter, so the CLI cannot ask for a particular workspace. Creating a session belongs to auth login alone.
  • Coordinating refreshes across processes with an epoch or a longer-held lock: rejected once the auth service's reuse grace was confirmed. The server absorbs the race, so the machinery would only add ways to fail.
  • A separate auth package instead of a module: rejected — the boundary matters, the packaging does not, and there is no second consumer.
  • A per-family context-extension mechanism instead of putting the API client on the context: rejected. This engine serves Prisma specifically, and indirection to avoid naming our own API bought nothing.
  • Arktype for the telemetry payload guard, which the ORM uses: a hand-rolled six-field guard, proven equivalent field by field, avoids adding a published dependency.
  • Sending telemetry before the command runs, which is the ORM's timing: replaced by reporting at completion, so exit codes and durations are accurate. The privacy notice still prints first. The trade-off is that a crashed process reports nothing.

The remaining open design questions are collected in s2-overview.md.

wmadden-electric and others added 17 commits August 10, 2026 01:11
…atch plan

S2 ships as four area PRs (>=1k LOC floor). S2a: engine publish
metadata + production dependency, ctx.api on the command context,
auth module extraction, the auth family port, update check,
cli-telemetry package move with the RunHooks.onSettled amendment,
and the clack prompt renderer from the spike. Contracts pin every
design decision; dispatches stop rather than improvise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ction dependency

Give @prisma/cli-engine everything npm publish needs: version 0.1.0, the
one-line description, Apache-2.0 license file, files list (dist, README,
LICENSE), repository/homepage/bugs pointing at prisma/prisma-cli,
publishConfig access public, engines.node >=22.12.0, and a prepack build
script, all shaped like packages/cli's package.json. Drop private: true
so the package can actually be published, and add a terse README covering
the package and its three entry points. Move @prisma/cli-engine from the
cli's devDependencies to dependencies (still workspace:*) and settle the
lockfile. npm pack --dry-run ships exactly dist + README.md + LICENSE +
package.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Move token-storage, auth-ops (now operations), and the auth client into
src/auth/, extract the real-mode workspace helpers out of the auth
controller into src/auth/workspaces.ts, and relocate makeGetCredentials
from the v8 runtime into src/auth/credentials.ts. src/auth/index.ts is
the module's only public face; controllers, the legacy shell, and the
v8 code import through it. No behavior change: both suites pass with
only import and mock path updates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…2 question ledger

The 60-command inventory grounds the S2b (resources), S2c (services),
and S2d (init + shell retirement) contracts. The overview gains the
operator question ledger (Q1 auto-login, Q2 service-run passthrough,
Q3 rm alias, Q4 config evaluation for the shipped bin, Q5 exit-code
unification) with the defaults the contracts build to. S2a contract
errata from D2/D6 grounding: auth logout --workspace, the auth index
export list, the white-box test exception.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Contract s2a-foundations §2 (dispatch D3).

- @prisma/management-api-sdk pinned exact (1.55.0) in the engine and
  the cli
- Runtime gains managementApi.baseUrl; the v8 bin derives it via
  getApiBaseUrl(env)
- CommandContext.api: lazy, once per run, constructed on first access
  by copying the shell's createManagementApiSdk call site with the
  token source backed by ctx.getCredentials, so refresh is picked up
  per request; the client is proxied to rethrow structured errors the
  SDK's onError middleware would otherwise wrap in FetchError
- Unauthenticated use throws CLI.CREDENTIALS_REQUIRED via the single
  constructor now exported from execution/needs.ts
- Harness spec gains managementApi { baseUrl?, client? }; an injected
  client IS ctx.api; baseUrl defaults to https://test.invalid
- Draft amendments: §4 CommandContext, §10 Runtime, §11 harness spec

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Prompt rendering is now two-tier. Real TTYs — isTty.stdin AND
stdin.setRawMode present, no scripted answers — render through
@clack/prompts (exact-pinned 1.5.0, loaded by dynamic import only on
that path); scripted answers, piped stdin, and the test harness stay
on the plain line renderer. --yes resolution and structural failures
are decided before the tier branch, so both tiers share identical
semantics.

clack-renderer.ts adapts Runtime streams for clack: Readable.from over
Runtime.stdin presenting isTTY with setRawMode forwarded, a Writable
over the stderr OutputStream, and { input, output } injected per
prompt, so all prompt UI stays on stderr. Clack's cancel symbol
(including the \x03 byte path) maps to the existing
CLI.PROMPT_CANCELLED exit-3 settlement. consent maps to clack confirm
starting on No: Enter-through returns false, only explicit Yes grants.
Clack's spinner/log helpers are never used (process-global handlers);
progress remains engine events.

Draft notes: §4a two-tier rendering, select's Enter-picks-highlighted
behavior, and the accepted quirk that clack reads
process.stdout.columns for wrap width.

Tests: a fake raw-mode stdin fixture drives confirm/consent/select/
text through the clack tier, asserting resolved values, stderr-only
UI bytes, setRawMode forwarding, and \x03 -> exit 3; a module-load
spy (with a canary validating the spy) proves the scripted and
non-TTY paths never load @clack/prompts. Dist .d.ts stays clack-free.

Reimplements spike/clack-prompts (903b25a) on the current module
layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…elemetry

Ports packages/1-framework/3-tooling/cli-telemetry from prisma/prisma
into this repo as a private workspace package, per the S2a contract
(§6). Preserved unchanged: the shared prisma-next user-config path and
format (one installation id with the ORM CLI), gating resolution with
the exact env var names and precedence (PRISMA_NEXT_DISABLE_TELEMETRY
truthy, DO_NOT_TRACK=1, stored consent, opt-out default-on), the
detached fork/IPC/unref sender mechanism with its silence and exit-0
contract, the production endpoint and wire protocol, and the
sanitizer's value-free discipline.

Adapted for this repo: the Commander snapshot is replaced by the
engine shape EngineCommandSnapshot (command path, flag names with
value source, positional count — no values, ever) and the sanitizer
projects it to the same wire fields; the arktype payload schema is
spelled out as a hand-rolled guard with identical semantics (no
arktype dependency here); the ORM's @internal/config validator is
replaced by a structural extraction of target.targetId and
extensions[].id with the same empty-on-invalid outcome; pathe and
@internal/utils are replaced by node:path and an inline spread.

The DB-backed integration harness is replaced by a local mock HTTP
backend driven through the same endpoint override the reference
suite used; the production endpoint is never contacted from tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…nt commands

Engine amendment (draft §10): RunHooks gains onSettled(summary) with
RunSummary { commandId, exitCode, durationMs, snapshot } and the
value-free EngineCommandSnapshot { commandPath, flags (name + source),
positionalCount }. The snapshot is captured at parse time when a
command mounts: flags explicitly present on argv are source 'cli'
(long, =-form, kebab or camel spelling, --no- negation, and aliases);
the engine reads no flags from the environment today, so everything
else is 'default'. The hook fires exactly once per run after
settlement, never for --help/--version or pre-mount usage errors,
with durationMs from the injectable clock; a throwing hook is
swallowed. RunHooks stays internal — the public growth is the minimal
hooks parameter on Cli.run (CliRunHooks, onSettled only), and the
test harness gains a matching onSettled tap.

Bin wiring (v8 main): the CI/env/consent decision resolves before the
run; when enabled an onSettled hook fires runTelemetry (detached
fork + IPC send + disconnect + unref, reference spawn semantics),
performing the first-run stderr disclosure + shared-id mint when no
installation id is stored; the telemetry command family is exempt;
when disabled no hook is attached. CI detection ports the ORM CLI's
ci-info wrapper.

Commands telemetry status|enable|disable port the ORM consent surface
as engine result commands, mounted shell-owned under the new
telemetry group, with the reference copy, cards per the S1 whoami
pattern, and json serializers. status is a pure read.

The cli bundles @repo/cli-telemetry (workspace devDependency, never a
published dep): a second tsdown config emits dist/v8/cli.js with the
telemetry source inlined plus the forkable dist/v8/sender.js; the
sender's third-party deps (c12, @vercel/detect-agent) move onto the
cli's own dependencies. Smoke-verified against a local mock backend:
a force-enabled run POSTs exactly one wire-shape event with the
stored installation id; CI=1 spawns nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Mounts all six auth commands (login, logout [--workspace], whoami,
workspace list/use/logout) as engine result commands in the platform
command family, backed by the extracted auth module.

- login runs the real OAuth flow via performLogin, emitting
  step-started/finished events around it and an 'endpoint' event for
  the verification URL (new optional onVerificationUrl hook on
  performLogin; legacy callers unaffected), then reads the auth state
  and appends the ported agent-setup tip (CI-suppressed; tip line is
  human-mode only). Fixture-only --provider/--user/--workspace flags
  do not port.
- logout --workspace calls the shared workspace-logout operation
  directly instead of the legacy argv re-dispatch.
- workspace use resolves by id/case-insensitive name, auto-selects a
  single workspace, prompts via ctx.prompt.select otherwise, and lets
  the engine's structural prompt failure cover non-interactive runs.
- Legacy flat error codes map mechanically to dotted AUTH.* codes
  (fix prose -> one user-choice nextAction, meta preserved), matching
  the S1 whoami precedent.
- The workspace operations' context parameter narrows to a structural
  type both shells satisfy; whoami now shares the state-card and
  config-invalid helpers.
- Semantic tests stub the auth module at the src/auth/index.ts seam;
  fixture-mode auth.test.ts cases covering ported commands are
  deleted (real-mode and shell-presentation cases stay until S2d).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
packages/cli/src/shell/update-check.ts moves to
packages/cli/src/update-check.ts; its CliRuntime parameter narrows to
the structural UpdateCheckRuntime (env/argv/stderr) both shells
satisfy. The legacy shell keeps its call sites; the v8 bin copies
their sequencing exactly: main() awaits the cached notification (and
detached refresh spawn) before dispatch, and the v8 bin entry gains
the PRISMA_CLI_RUN_UPDATE_CHECK_WORKER branch. Suppression rules are
copied as-is, including silence when argv contains --json/--quiet/-q,
recorded in the S2 divergence list.

Tests cover the v8 wiring: cached-newer notify, interval silence,
json-mode silence, non-TTY silence, and the detached refresh spawn's
arguments and worker env contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…oxes

Creates the S2 cumulative parity divergence list (seeded with a
pointer to the S1 whoami-scoped record) covering the auth family's
AUTH.* error-code mapping, login's fixture-flag removal and event
surface, the logout --workspace commandId change, prompt-path
behavior for workspace use, and the update-check json-mode finding.
Marks the whoami record as whoami-scoped and updates its stale
update-notification note. Checks every S2a acceptance box except the
operator publish and the operator's divergence-list review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… load

Review-round fixes for slice s2a-foundations (B1, C4, A3, A7, C7):

- Attach a swallowing 'error' listener on the forked sender child so an
  async fork failure can neither crash the parent CLI nor flip its exit
  code (B1), with a regression test driving a fake child's error event.
- Drop the detached child's prisma-next.config.* c12 load entirely: the
  config does not exist in this product and the load evaluated arbitrary
  user TS in a detached process. databaseTarget now ships null (payload
  override kept for wire compatibility) and extensions ships []; the c12
  and magicast dependencies are removed (C4). Recorded as an S2a parity
  divergence.
- resolveGating now takes { env, config, inCI } and returns a total
  reason union (ci | env-opt-out | stored-opt-out | stored-opt-in |
  default-on); CI is part of the resolution itself (A3).
- Public-surface tightening: the duplicated EngineCommandSnapshot type
  is no longer re-exported (the structural declaration stays internal)
  and SanitisedCommand is normalized to SanitizedCommand (A7).
- Pin @clack/prompts to exact 1.5.0 in packages/cli (C7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…odule

Review-round fixes for slice s2a-foundations (C1, A10, A11, A8):

- restoreStructuredThrows maps the SDK's AuthError (the 401 /
  refresh-unavailable path) to the shared CLI.CREDENTIALS_REQUIRED
  structured error, matched structurally by name so a duplicate module
  instance cannot defeat it (C1).
- The cause-chain walk tracks visited errors with a depth cap so a
  cyclic cause chain terminates (C1), with a settlement test.
- The @prisma/management-api-sdk module now loads via dynamic import on
  the first actual request: ctx.api is a Proxy whose async method
  wrappers await the lazy construction, mirroring the clack renderer's
  lazy-import pattern; the structured-throw restoration merged into the
  same wrapper (A10).
- The engine's OAuth client id / redirect URI are inert empty-string
  placeholders — no refresh token is ever supplied, so the SDK's OAuth
  flow is unreachable (A11).
- RunSummary.commandId documents its derivation from
  snapshot.commandPath (always equals commandPath.join('.')); draft §10
  carries the same note (A8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…e homes

Review-round fixes for slice s2a-foundations (A1, A2, A5, A6, C8, C6):

- src/lib/auth/{login,guard,recipient}.ts move into src/auth/; the
  index gains requireComputeAuth and the recipient exports, and every
  controller imports via src/auth/index.ts only (A1). src/auth is now a
  leaf cluster: its one remaining legacy import is the CliError base
  class (named as an S2d survivor).
- The auth-specific error constructors move from shell/errors into
  src/auth/errors.ts (shell re-exports for legacy imports), and
  resolveStateDir moves to src/state-dir.ts with shell/runtime
  re-exporting (A2).
- WorkspaceOperationContext flattens to { env, signal }; the v8 adapter
  shape is gone and legacy controller call sites build the flat context
  (A5).
- listRealAuthWorkspaces/useRealAuthWorkspace/logoutRealAuthWorkspace
  rename to listAuthWorkspaces/useAuthWorkspace/logoutAuthWorkspace —
  there is no fixture-mode counterpart to distinguish from (A6).
- The onVerificationUrl observer hook is invoked inside try/catch so an
  observer bug cannot break the login flow (C8).
- New src/cli-name.ts owns the user-facing binary name and docs URL;
  getCliName and the update-check fallback URL consume it (C6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…g semantics

Review-round fixes for slice s2a-foundations (A4, A9, C2, C3, C5, C6, C9, C10, A13):

- v8/auth/workspace-commands.ts splits into workspace-list.ts,
  workspace-use.ts, workspace-logout.ts with a workspace-shared.ts
  helper module; the shared runWorkspaceLogout operation gets its own
  module. v8/telemetry/commands.ts splits into status.ts (merged with
  the status resolution), enable.ts, disable.ts, and a shared consent
  presentation (A4).
- v8/telemetry/wiring.ts renames to reporting.ts; options type renamed
  to TelemetryReportingOptions (A9).
- auth workspace list now maps an empty PRISMA_SERVICE_TOKEN to
  AUTH.CONFIG_INVALID (exit 2), matching whoami/login/logout, with a
  divergence row (C2).
- The first-run telemetry disclosure prints at gating time — pre-run,
  before the command's output — while the event still fires at
  settlement; the installation-id mint stays at first settlement so
  'telemetry status' keeps reporting 'not stored' (C5). Status projects
  the gating resolver's total reason union directly (A3).
- The disclosure and the telemetry group help now use the real docs
  page (the update-check fallback URL) and the CLI_NAME constant in
  every user-facing command string (C6).
- New packages/cli/vitest.config.ts sets PRISMA_NEXT_DISABLE_TELEMETRY=1
  suite-wide, mirroring the cli-telemetry package's own guard, so no
  test reaches the developer's real user config or the endpoint (C3);
  the bin/update-check test processes opt out explicitly.
- Tests: AUTH.CONFIG_INVALID on login and logout, the json login tip
  envelope (agentSetupTip + tip nextAction), and the tip-suppression
  branch when Prisma skills are already installed (C9); update-check
  notify-before-dispatch ordering via a marker-writing stub CLI and the
  '--format json is NOT suppressed' literal-argv quirk (C10).
- v8-auth.test.ts byte assertions convert to semantic assertions
  (envelope / presented / events / exit code); the sanctioned golden
  suite v8-golden-rendering.test.ts pins one representative card, table,
  and error rendering byte-exactly; the S1 whoami byte pins remain
  (A13).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Operator-process-sanctioned S2 doc amendments from the review round:

- parity-divergences.md: workspace list joins the AUTH_CONFIG_INVALID
  mapping row; new sections for the empty-service-token structuring and
  the two telemetry divergences (config enrichment dropped; onSettled
  emission timing with pre-run disclosure — crashed/killed/process.exit
  runs emit nothing).
- s2a-foundations.md §3: the workspace operation exports are the
  *AuthWorkspaces names, with a one-line erratum; §6: CI is part of the
  gating resolution, and the wording separates spawn semantics (copied)
  from timing (onSettled by design, pre-run disclosure).
- s2d-init-and-retirement.md R-S2d-4: known survivors named —
  src/state-dir.ts and the CliError base class residue behind
  src/auth/errors.ts and the v8 error mapping.
- s2b-resources.md D1: build-time test requirement that the family maps
  and the mount map cover the same command set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Telemetry docs URL, config-enrichment drop, disclosure timing —
each built to a stated default awaiting ratification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 186 files, which is 86 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 364797b7-7f33-4965-91a7-b375a508dc64

📥 Commits

Reviewing files that changed from the base of the PR and between 58a1585 and 9bc47d0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (186)
  • .drive/projects/prisma-cli-v8/assets/briefs/credential-manager-handover.md
  • .drive/projects/prisma-cli-v8/assets/briefs/s2c-handover.md
  • .drive/projects/prisma-cli-v8/assets/engine/credential-manager-design.md
  • .drive/projects/prisma-cli-v8/assets/engine/engine-interface-draft.ts
  • .drive/projects/prisma-cli-v8/assets/engine/whoami-parity-divergences.md
  • .drive/projects/prisma-cli-v8/assets/rollout-plan.md
  • .drive/projects/prisma-cli-v8/assets/s2/command-inventory.md
  • .drive/projects/prisma-cli-v8/assets/s2/parity-divergences.md
  • .drive/projects/prisma-cli-v8/plan.md
  • .drive/projects/prisma-cli-v8/plans/s2a-foundations.md
  • .drive/projects/prisma-cli-v8/plans/s2b-resources.md
  • .drive/projects/prisma-cli-v8/plans/s2c-services.md
  • .drive/projects/prisma-cli-v8/plans/s2d-init-and-retirement.md
  • .drive/projects/prisma-cli-v8/specs/s2-overview.md
  • .drive/projects/prisma-cli-v8/specs/s2a-foundations.md
  • .drive/projects/prisma-cli-v8/specs/s2b-resources.md
  • .drive/projects/prisma-cli-v8/specs/s2c-services.md
  • .drive/projects/prisma-cli-v8/specs/s2d-init-and-retirement.md
  • .github/workflows/pr-quality.yml
  • .github/workflows/preview-cli-package.yml
  • .github/workflows/publish-cli.yml
  • .github/workflows/publish.yml
  • CONTRIBUTING.md
  • README.md
  • biome.jsonc
  • docs/README.md
  • docs/architecture/adrs/0001-preview-package-and-publishing.md
  • docs/oss/versioning.md
  • package.json
  • packages/cli-engine/LICENSE
  • packages/cli-engine/README.md
  • packages/cli-engine/package.json
  • packages/cli-engine/src/cli.ts
  • packages/cli-engine/src/commands.ts
  • packages/cli-engine/src/context.ts
  • packages/cli-engine/src/credential-errors.ts
  • packages/cli-engine/src/credential-manager.ts
  • packages/cli-engine/src/execution/api-client.ts
  • packages/cli-engine/src/execution/clack-renderer.ts
  • packages/cli-engine/src/execution/command-context.ts
  • packages/cli-engine/src/execution/command-snapshot.ts
  • packages/cli-engine/src/execution/debug.ts
  • packages/cli-engine/src/execution/engine.ts
  • packages/cli-engine/src/execution/needs.ts
  • packages/cli-engine/src/execution/open-url.ts
  • packages/cli-engine/src/execution/prompts.ts
  • packages/cli-engine/src/execution/rendering.ts
  • packages/cli-engine/src/execution/shared-flags.ts
  • packages/cli-engine/src/exports/index.ts
  • packages/cli-engine/src/exports/testing.ts
  • packages/cli-engine/src/in-memory-credential-manager.ts
  • packages/cli-engine/src/management-api.ts
  • packages/cli-engine/src/protocol.ts
  • packages/cli-engine/src/run-summary.ts
  • packages/cli-engine/src/runtime.ts
  • packages/cli-engine/src/testing.ts
  • packages/cli-engine/src/token-claims.ts
  • packages/cli-engine/tests/clack-isolation.test.ts
  • packages/cli-engine/tests/clack-prompts.test.ts
  • packages/cli-engine/tests/config.test.ts
  • packages/cli-engine/tests/credential-manager.test.ts
  • packages/cli-engine/tests/engine.test.ts
  • packages/cli-engine/tests/engine.type-test.ts
  • packages/cli-engine/tests/events.test.ts
  • packages/cli-engine/tests/execution.test.ts
  • packages/cli-engine/tests/interaction-affordances.test.ts
  • packages/cli-engine/tests/lifetimes.test.ts
  • packages/cli-engine/tests/management-api.test.ts
  • packages/cli-engine/tests/prompts.test.ts
  • packages/cli-engine/tests/protocol.test.ts
  • packages/cli-engine/tests/run-hooks.test.ts
  • packages/cli-telemetry/package.json
  • packages/cli-telemetry/src/endpoint.ts
  • packages/cli-telemetry/src/enrich.ts
  • packages/cli-telemetry/src/exports/index.ts
  • packages/cli-telemetry/src/gating.ts
  • packages/cli-telemetry/src/payload.ts
  • packages/cli-telemetry/src/sanitize.ts
  • packages/cli-telemetry/src/sender.ts
  • packages/cli-telemetry/src/spawn.ts
  • packages/cli-telemetry/src/user-config.ts
  • packages/cli-telemetry/tests/endpoint.test.ts
  • packages/cli-telemetry/tests/enrich.test.ts
  • packages/cli-telemetry/tests/gating.test.ts
  • packages/cli-telemetry/tests/no-spawn-in-tests.test.ts
  • packages/cli-telemetry/tests/payload.test.ts
  • packages/cli-telemetry/tests/sanitize.test.ts
  • packages/cli-telemetry/tests/sender-integration.test.ts
  • packages/cli-telemetry/tests/spawn-fork-error.test.ts
  • packages/cli-telemetry/tests/spawn.test.ts
  • packages/cli-telemetry/tests/user-config.test.ts
  • packages/cli-telemetry/tsconfig.json
  • packages/cli-telemetry/tsdown.config.ts
  • packages/cli-telemetry/vitest.config.ts
  • packages/cli/package.json
  • packages/cli/src/auth/client.ts
  • packages/cli/src/auth/credential-manager.ts
  • packages/cli/src/auth/errors.ts
  • packages/cli/src/auth/guard.ts
  • packages/cli/src/auth/legacy-state.ts
  • packages/cli/src/auth/login.ts
  • packages/cli/src/auth/operations.ts
  • packages/cli/src/auth/recipient.ts
  • packages/cli/src/auth/service-token.ts
  • packages/cli/src/auth/state-file.ts
  • packages/cli/src/auth/token-storage.ts
  • packages/cli/src/auth/workspace-name.ts
  • packages/cli/src/auth/workspaces.ts
  • packages/cli/src/bin.ts
  • packages/cli/src/cli-name.ts
  • packages/cli/src/cli.ts
  • packages/cli/src/controllers/app-env.ts
  • packages/cli/src/controllers/app.ts
  • packages/cli/src/controllers/auth.ts
  • packages/cli/src/controllers/branch.ts
  • packages/cli/src/controllers/bucket.ts
  • packages/cli/src/controllers/build.ts
  • packages/cli/src/controllers/database.ts
  • packages/cli/src/controllers/project.ts
  • packages/cli/src/lib/version.ts
  • packages/cli/src/shell/command-runner.ts
  • packages/cli/src/shell/errors.ts
  • packages/cli/src/shell/runtime.ts
  • packages/cli/src/state-dir.ts
  • packages/cli/src/update-check.ts
  • packages/cli/src/v8/auth/agent-setup-tip.ts
  • packages/cli/src/v8/auth/credential-card.ts
  • packages/cli/src/v8/auth/login.ts
  • packages/cli/src/v8/auth/logout.ts
  • packages/cli/src/v8/auth/session-ref.ts
  • packages/cli/src/v8/auth/whoami.ts
  • packages/cli/src/v8/auth/workspace-list.ts
  • packages/cli/src/v8/auth/workspace-logout.ts
  • packages/cli/src/v8/auth/workspace-use.ts
  • packages/cli/src/v8/bin.ts
  • packages/cli/src/v8/cli.ts
  • packages/cli/src/v8/main.ts
  • packages/cli/src/v8/runtime.ts
  • packages/cli/src/v8/telemetry/consent.ts
  • packages/cli/src/v8/telemetry/disable.ts
  • packages/cli/src/v8/telemetry/enable.ts
  • packages/cli/src/v8/telemetry/is-ci.ts
  • packages/cli/src/v8/telemetry/reporting.ts
  • packages/cli/src/v8/telemetry/sender.ts
  • packages/cli/src/v8/telemetry/status.ts
  • packages/cli/tests/app-branch-database.test.ts
  • packages/cli/tests/app-controller.test.ts
  • packages/cli/tests/app-env-vars.test.ts
  • packages/cli/tests/app-env.test.ts
  • packages/cli/tests/auth-login.test.ts
  • packages/cli/tests/auth-ops.test.ts
  • packages/cli/tests/auth-real-mode.test.ts
  • packages/cli/tests/auth.test.ts
  • packages/cli/tests/branch-controller.test.ts
  • packages/cli/tests/credential-manager-login.test.ts
  • packages/cli/tests/credential-manager-migration.test.ts
  • packages/cli/tests/credential-manager-processes.test.ts
  • packages/cli/tests/credential-manager.test.ts
  • packages/cli/tests/helpers/credential-manager-worker.ts
  • packages/cli/tests/project-controller.test.ts
  • packages/cli/tests/project-real-mode.test.ts
  • packages/cli/tests/resolve-package-version.test.ts
  • packages/cli/tests/token-storage.test.ts
  • packages/cli/tests/update-check.test.ts
  • packages/cli/tests/v8-auth.test.ts
  • packages/cli/tests/v8-bin.test.ts
  • packages/cli/tests/v8-golden-rendering.test.ts
  • packages/cli/tests/v8-telemetry-reporting.test.ts
  • packages/cli/tests/v8-telemetry.test.ts
  • packages/cli/tests/v8-update-check.test.ts
  • packages/cli/tests/v8-whoami.test.ts
  • packages/cli/tsdown.config.ts
  • packages/cli/vitest.config.ts
  • packages/compute/package.json
  • packages/tsconfig/package.json
  • scripts/bump-version.ts
  • scripts/determine-version-utils.test.ts
  • scripts/determine-version-utils.ts
  • scripts/determine-version.ts
  • scripts/resolve-package-version.d.mts
  • scripts/resolve-package-version.mjs
  • scripts/resolve-package-version.test.mjs
  • scripts/set-version-utils.test.ts
  • scripts/set-version-utils.ts
  • scripts/set-version.ts
  • skills-contrib/publish-npm-version/SKILL.md

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@wmadden-electric wmadden-electric changed the title S2a foundations: ctx.api, the auth module and family, telemetry, clack prompts — everything the platform port builds on Give commands an authenticated API client, one auth module, telemetry, and real prompts — the foundations for porting the platform CLI onto the engine Aug 10, 2026
wmadden-electric and others added 7 commits August 10, 2026 10:02
… line

Operator ruling 2026-08-10: this repo takes over prisma@8.0.0-rcX, so it
adopts prisma/prisma's versioning scripts AND its version number. Ports
determine-version / set-version / bump-version (+ pure-helper tests, run
via the new root test:scripts) from wip/repos/prisma, adapted to
node:path and this repo's package set. Stamps 8.0.0-rc.1 in lockstep
across the root, cli, cli-engine, cli-telemetry, and tsconfig manifests
with workspace:8.0.0-rc.1 internal pins (the reference's convention).

@prisma/compute is hard-excluded from the lockstep (second ruling: it
versions independently pending extraction); its manifest moves to its
honest npm state 0.1.0-beta.0 and resolve-package-version.mjs survives
trimmed to the dev/next-beta commands publish-compute.yml still uses,
with its test moved into the script test suite (the cli-package copy is
deleted).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ion workflow

Ports publish.yml from the reference verbatim per the operator ruling
(2026-08-10): a push to main with the root version unchanged publishes
<base>-dev.N under the dev dist-tag; a push that changes the root
version (a merged bump PR) publishes the committed base under latest
for BOTH @prisma/cli-engine and @prisma/cli (engine first — the cli
depends on it) plus a GitHub Release marked pre-release on the rc line.
The operator's words: merging a version bump PR counts as a deliberate
explicit action to alter latest. workflow_dispatch keeps the chosen
dist-tag + dry-run escape hatch. publish-cli.yml is deleted whole —
both its jobs are superseded.

The PR preview drops version stamping and copies the reference's
pkg.pr.new model (the committed rc base ships as-is at per-commit
URLs); the engine is published alongside the cli so its workspace pin
resolves to the preview build. pr-quality's test job now runs the
ported script tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Adapts prisma/prisma's docs/oss/versioning.md to this repo (package
names, the verbatim latest model, the @prisma/compute exclusion pending
extraction — operator rulings 2026-08-10). README/CONTRIBUTING publish
sections and ADR 0001's status now point at the committed-version
model; the S2a contract carries a one-line erratum (engine version
0.1.0 -> 8.0.0-rc.1); the rollout plan's step 2 and its latest
invariant are reworded per the ruling — latest moves only through a
deliberately merged version-bump PR (or manual dispatch), superseding
the earlier next-tag interim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
The v8 telemetry suites pointed only XDG_CONFIG_HOME at their temp dir,
but on win32 userConfigPath() resolves from APPDATA first, so every read
and write on the Windows runner hit the real %APPDATA%\prisma-next\config.json.
That file is never cleaned between tests, so earlier cases leaked stored
ids and consent choices into later ones — the six Windows-only failures.
Point APPDATA at the same temp dir alongside XDG_CONFIG_HOME and restore
both afterwards; expected paths already derive from userConfigPath().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Operator ruling 2026-08-10: a version change alone must not move
latest — merging PR #130 jumps the lockstep to 8.0.0-rc.1 but only a
deliberately merged 'chore(release): ...' bump PR publishes a release.
Any other version-changing merge publishes a dev build, loudly.
pkg.pr.new previews are unaffected and already cover both packages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
The release procedure's step 1 is now the ported skill, not a bare
script invocation: fresh worktree off origin/main, pnpm bump-version,
lockfile refresh, diff sanity check, and the release PR. Adapted for
this repo: the PR title must carry the chore(release) marker because
squash merges make the PR title the commit subject the publish
workflow checks; compute exclusion and generated release notes noted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… prisma/prisma

Operator ruling: the publish machinery must match prisma/prisma
exactly so it never needs remembering. The transient problem the gate
solved (the lockstep-adoption merge changing the root version without
being a release) is handled by sequencing instead: a root-version
pre-PR lands on main under the old workflows first, so this branch's
merge shows no version change to the new workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
wmadden-electric and others added 3 commits August 10, 2026 10:26
The use- prefix read as a React hook. The operation switches the
active workspace session — its own presenter already says so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…iClient

The name was wrong twice: 'Compute' is branding residue that now
collides with the unrelated @prisma/compute package, and 'require'
implied a throw where the function returns null. It resolves an
authenticated management API client (service token, else stored OAuth
with refresh) or null. Legacy-shell-only; dies with it in S2d.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
The request-failure mapping checks for a CLI structured error before it
checks whether the failure came from the refresh path, and that order is
what lets a manager-raised error survive. Nothing held the order down:
swapping the two blocks left every suite green, while in production a
session ended by another process mid-rotation would have surfaced as the
transient auth-service error — "try again" — instead of telling the user
their session had ended. There is now a test that fails when the order is
swapped. The test harness made that case unreachable, because its
TokenStorage threw a plain error where the real manager throws a
structured one; it now throws the same error, with a test pinning the
agreement.

The engine built the environment-session bearer from the raw variable
while the manager composed the session from the trimmed value, so
PRISMA_SERVICE_TOKEN=" tok " would report a valid session and then send
a padded bearer on the wire. The legacy path trimmed. The engine now
trims too.

The debug valve echoed the token endpoint's error_description verbatim,
which is free text the auth service chooses, immediately below a branch
that deliberately logs only an error's type for exactly that reason. It
now logs the endpoint's verdict without the description: the OAuth error
field for a 4xx, and the SDK's own message — which carries the HTTP
status — otherwise. That is what the design asks for.

The reads-never-write probe also now spies the synchronous rename, rm
and open calls, and asserts a positive control on `open` as well as
`rename`, so its breadth is real rather than nominal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric changed the title Give commands an authenticated API client, one auth module, telemetry, and real prompts — the foundations for porting the platform CLI onto the engine Give commands an authenticated API client, per-workspace auth sessions, telemetry, and real prompts — the foundations for porting the platform CLI onto the engine Aug 10, 2026
wmadden-electric and others added 19 commits August 10, 2026 19:17
Clearing a crashed holder's lock was not atomic. Two waiting processes
could both see the same stale lock, and the second one's unlink removed
the FIRST one's freshly created lock — both then ran their
read-modify-write at once and one update was silently lost, which is the
one thing the lock exists to prevent, in the one situation the takeover
path exists for. Clearing is now a rename, which only one process can
win; the loser waits. A test holds both waiters until each has seen the
lock as stale and asserts exactly one of them reports a takeover. It
fails against the unlink version on every run.

A takeover that could not remove the lock still reported success, and
that success made the acquisition loop skip both the timeout check and
the sleep. With a stale lock in a directory the process cannot write to,
the loop ran flat out forever: no timeout, one core pinned, the command
never returning. The timeout is now checked on every pass and the sleep
is skipped only after a takeover that really happened. The test for this
finishes in under a second against the fix and hangs until the test
timeout against the defect.

A write that failed after opening its temp file left that file on disk
holding the whole state, tokens included, under a name nothing looks for
again — so someone running `auth logout` to revoke local access could be
left with a working refresh token. Every path out of the write now
removes the temp file, and `endAllSessions` reaps any that an earlier
crash left behind.

Also removes a duplicated file comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…it is

`TestCredentialManager` was named for who uses it rather than what it
is. It is not a stub: it implements the same session rules as the
file-backed manager — pinning, upsert by workspace, the
environment-override refusals, and all four TokenStorage write slices —
and adds a seed and a state read-back. Tests are simply where it is
most useful.

`TestCredentialManager` becomes `InMemoryCredentialManager`, its seed
and state types follow, and the module becomes
`in-memory-credential-manager.ts`. `TestSessionRecord` becomes
`SessionRecord` for the same reason: it is the manager's record, not a
test's. `createTestCli` and `mintTestJwt` keep their names, because a
test CLI and a test JWT minter are exactly what those are.

This renames exports on the ./testing subpath. The engine is at
8.0.0-rc.1 and unpublished at that version, so nothing outside this
repo consumes the old names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…t action

Both are what S2b needs to finish `git connect`, and both are additive.

`BrowserWaitRequest` gains an optional `interval`. Legacy `git connect`
polls on PRISMA_CLI_GITHUB_INSTALL_POLL_INTERVAL_MS, and until now that
value had nowhere to go: the engine polled on a private constant. The
constant stays as the default, so every existing caller is unchanged.
The test harness gains an optional `delay` for the same reason `now` is
already there — a poll loop's cadence is only assertable if the test can
see it.

`NextAction` gains an `open-url` kind and a `url` field. Two legacy repo
errors carry a GitHub app install URL in their follow-up steps, and
every follow-up string currently becomes a `run-command`, so the
envelope was telling consumers to execute a URL as a shell command. A
URL is not a command. `renderNextAction` falls back to the url, so an
open-url action prints its address the way a run-command prints its
command; nothing in the engine switches on the kind, so machine
consumers get the new one for free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…hang

Reported from S2b as a flake under full-suite load. It is not flakiness,
it is a hang the winner usually outruns.

The barrier releases both waiters once both have seen the stale lock,
then resets to empty. The waiter that loses the takeover race sleeps and
comes back for another look, and if the winner is still holding the lock
at that moment it stats again — entering a fresh barrier cycle that
needs two arrivals and will only ever get one. It blocks until vitest
times the test out at five seconds. It normally passes because the
winner finishes its whole mutation inside the loser's ten-millisecond
sleep, so the loser's next attempt to create the lock succeeds and it
never stats a second time. Load is what removes that margin, which is
why adding test files makes it appear.

The barrier now latches open, which is what its comment always claimed.
Five runs in isolation and three full-suite runs pass, and it still
fails on every run against the unlink version of the takeover, so it
holds the behaviour down exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…nt credential is not a session

Rev 5 modelled the PRISMA_SERVICE_TOKEN credential as a Session. It is
not one, and forcing it into that shape produced four defects that are
all the same defect: a `source` field to say "this is not really a
session", a hardcoded `current: true` that gave the word two meanings,
a `workspaceId: ""` because a non-session was made to carry a session's
key, and a guard rejecting the non-session from APIs that only take
sessions. Rev 6 separates the three things — a stored session, the
selection, and the credential this process authenticates as — and all
four stop existing.

Refresh follows from the credential having a refresh token, not from
where it came from, so the engine builds one client over storage the
manager hands it: file-backed with no cache in front, or memory-backed
touching no file at all. The custody rule is restated as what it always
meant — credentials never reach commands; the engine may hold them.
Removal is idempotent. The mutation refusals under an environment token
go, since the rule they enforced was the last piece of the session
confusion.

Written with the architect and principal-engineer passes folded in. The
principal engineer caught that a 401 on a credential that never had a
refresh token would lose today's service-token message and tell CI to
retry a permanent failure; the design now discriminates by state rather
than by message, which also repairs a rev-5 defect for migrated entries
with no refresh token. The architect caught that the delta introduced
"selected" without retiring "current", leaving three words for one
idea; §11.1 rules the vocabulary and names what deliberately keeps the
old word.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…atch

The legacy app group fused building, repo wiring, and deploying into
single commands. Composer takes the building and deploying; what the CLI
should own is managing the remote resource, and today it cannot — there
is no way to list or create a service, none to start or stop a
deployment, and a service can only be born as a side effect of deploying
to it. S2c ported the survivors under their legacy names so the
commander shell could die in S2d. That is continuity, not endorsement.

The slice is recorded as blocked on design work, and the design work is
blocked on a fact we do not have. Composer deploys through Alchemy, not
the management API, so a Composer-deployed service does not appear under
/v1/apps at all and the resource model those endpoints describe is the
product being replaced. Until we know what a service is after Composer,
there is nothing to design primitives over. An earlier sizing of this as
"small, mostly a rename" assumed Composer would create deployments
through the platform API; it does not, so the sizing is withdrawn and
the slice is unsized.

Ordering is after S3, because Composer's contract is the input, and
before S7, because S7 mounts the full grammar tree behind a completeness
check that this slice changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… API resources

The previous entry claimed a Composer-deployed service would not appear
under /v1/apps because Composer deploys through Alchemy. That does not
follow: Alchemy's providers call the management API to do the work, so
Composer's services and deployments are ordinary resources under the
same endpoints. Only the orchestration differs.

So the seam is the one the API already draws — Composer produces
deployments, the CLI manages them — and the slice is mostly a rename
into a `service deployment` subgroup plus the five operations that have
no command at all. The sizing withdrawn in the previous commit is
restored.

What the slice genuinely waits on is narrower and sharper: whether
Alchemy holds desired state, in which case an imperative promote or
rollback from the CLI either fights Composer or is reverted on its next
run; what Composer's app and deployment records actually contain; and
whether log reading belongs to `composer log` or to the service
subgroup, since a subgroup is owned by exactly one command family.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…eleted

The environment credential stops being modelled as a session. Session
loses `source` and `current`; the selection is read directly instead of
scanned off a flag on each element; what the process authenticates as
becomes ActiveCredential, carrying no token material. ctx.session()
becomes ctx.activeCredential().

There is now one API client. ClientBinding, the static-token path and
the resolved-401 inspection are gone: the engine builds the SDK's
refreshing client over whatever storage the manager hands it, which is
file-backed for a stored session and memory-backed for a credential with
no home. A 401 on a credential that could never be renewed no longer
falls through to the session-ended or transient mapping — the engine
asks the storage for its tokens, and a set with no refresh token yields
the credential-rejected error, whose wording follows the origin.

getCredentials goes with it: the context accessor, Runtime's member, the
Credentials type, the needs-check fallback and the harness's legacy seed
were the bridge that let commands reach a raw token while the manager
landed, and rev 6 finishes that swap.

packages/cli does not compile against this yet; its cascade is the next
commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
`label` is required and `command` is optional, so a mapper building an
action out of a legacy error's follow-up step — a bare command string
with no prose beside it — has nothing to put in the label but the
command. Both fields then hold the same string and the renderer printed
it twice:

  → prisma-cli project list: prisma-cli project list

Fixed in the renderer rather than the callers, because label is
required: every mapper in that position would otherwise have to invent
prose, and the renderer is the only place that sees both fields and can
tell they are the same. `url` takes the same path and is covered too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Composer holds desired state through Alchemy, and changing the platform
directly is overwritten on its next deploy. The operator accepts that,
so the imperative deployment operations stay and their effect on a
Composer-managed service is understood to be transient.

What survives for the design is narrower: whether the CLI should say so
at the point of use. That depends on whether an app or deployment record
carries anything identifying it as Composer-managed, which is already
question 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
The CLI half of the cascade. The file-backed manager implements the
rev-6 interface: the pin holds the decision, not a token, and the
material is read through the file on every call. Two storages, chosen
once — file-backed reads through with no cache in front, so the SDK can
still recover when another process rotated first; memory-backed closes
over a local variable, never receives the file path, and touches no file
on any method, so an environment credential cannot delete the stored
session whose workspace its token happens to name.

`serviceTokenWorkspaceId(token) ?? ""` is gone. It was the only place in
the codebase that manufactured an empty workspace id, and it reached the
user: whoami printed a workspace whose id was the empty string. A
credential whose claims name no workspace now reports none, whoami omits
the row, and its JSON workspace is null.

Ending a session is idempotent — the postcondition is the same either
way — so losing the race to another process exits 0 instead of claiming
you have no such session. Selecting still refuses a workspace with no
session, because there is no state in which it would afterwards be
selected. The mutation refusals under PRISMA_SERVICE_TOKEN go entirely;
all three succeed and say the environment credential stays in force
until the variable is unset.

whoami asks for an identity and renders it, with no branch on where the
credential came from and no token in the command's hands. /v1/me wins
field by field over the claims and the claims are the offline fallback.

Also restores the user's name to whoami's output. CredentialIdentity was
defined with only a user id and an email, which silently dropped a field
the previous shape carried; only an online lookup supplies it, so it is
absent offline rather than removed. The JSON reports it as `user.id`
rather than `user.userId`, which reads badly beside `workspace.id`.

`environmentSessionMutationError` and its AUTH.ENV_SESSION_IN_FORCE code
are deleted from the engine, having lost their last consumer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Renaming the corpse aside instead of unlinking it narrowed the window
but did not close it. Two waiters both see the lock as stale; the first
renames it away and creates its own; the second then renames away THAT
lock, and both believe they hold it. Windows produced the ordering
naturally and macOS did not, which is how it reached CI green.

Rename cannot be made conditional, so the takeover now confirms
afterwards that what it moved aside is the corpse it examined, by
comparing mtime. If it is not, the lock belongs to whoever created it
and goes back — via `link`, which fails when the path is occupied, so
restoring can never overwrite a third process's lock.

The test forces the interleaving rather than leaving it to the
scheduler: one hook holds both waiters until each has seen the lock as
stale, a second holds the loser's removal until the winner's lock
actually exists, signalled off the create rather than polled. Six runs
pass with the fix and six fail without it, where before it passed either
way on macOS. The assertion is "at most one takeover", because a winner
that releases before the loser looks leaves no corpse and zero is also
correct; two is the defect.

Also guards the two 0600 assertions behind a POSIX check. Windows has no
Unix permission bits and reports 0o666 whatever the file was created
with, so those assertions could only ever fail there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
A real service token's subject is `workspace:<id>`, not a person.
`claimedIdentity` read `sub` and called it a user id unconditionally,
twenty lines below a function that existed precisely because the subject
can name a workspace. So `PRISMA_SERVICE_TOKEN=<real token> prisma auth
whoami --json` emitted `"user": {"id": "workspace:ws_abc"}` whenever
/v1/me returned no user, which is the likely answer for a machine
credential. The human card hid it, because it only prints a user row
when there is an email.

The reason no test caught it is the second half of this change. The four
claim helpers were duplicated across the CLI's `claims.ts` and the
engine's in-memory manager, and the copies had drifted: production knew
about the `workspace:` subject and the harness did not, so a service
token that works in production could not be represented in a test at
all, and the harness reported no workspace where production reported
one. There is now one implementation in the engine that both managers
use, and the CLI keeps its existing names by delegating to it.

The engine's public surface grows by the four helpers the CLI consumes.
The raw decoder stays internal, and `decodeClaims` is dropped from the
CLI's barrel — it was re-exported and never used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Rev 6 stopped whoami branching on where the credential came from, so it
now attempts the /v1/me enrichment for an environment credential too,
where rev 5's path was local and instant. Nothing bounded that request:
ctx.signal only fires on Ctrl-C, so a host that accepts the connection
and never answers would hold the command for as long as the runtime's
own timeouts allow — minutes behind a black-holing proxy, which is
exactly the setting a service token gets used in.

The enrichment now carries its own three-second deadline alongside
ctx.signal. A cancellation still propagates, because the catch rethrows
on the original signal; a timeout falls through to the claims, which is
what whoami answers from anyway. The test points it at a server that
accepts and never responds, and it completes in about three seconds
with the claims intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…to share

Two full implementations of one contract, and every command test in this
slice and the next two runs against the in-memory one — so where they
disagree, the tests assert behaviour the product does not have. Three
disagreements left after the claim helpers were unified.

The workspace-mismatch refusal threw a plain harness error where the
file-backed manager throws a structured one. The constructor moves to
the engine and both raise it, so a test of that refusal now sees what
production raises. Same for the rotation-time re-scope check.

withRefreshLock was `fn => fn()`. Section 6 requires the hook to
serialise within a process, and the file-backed manager runs a real
queue, so two concurrent refreshes would pass a harness test and fail in
production. The in-memory manager now runs the same queue.

The blank-PRISMA_SERVICE_TOKEN refusal genuinely cannot live here — this
manager is handed a credential and never reads the variable — so the
header now says so instead of claiming parity it does not have. That
rule is covered end to end through the real environment in the CLI's
tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Revision 6 is titled "the environment credential is not a session", and
the command layer went on calling it one — in the notice every mutation
prints, in the helper that reports it, in two json field names, and in
the module that builds whoami's card. The retired model was what the
user read.

ENVIRONMENT_SESSION_NOTICE becomes ENVIRONMENT_CREDENTIAL_NOTICE and
says "supplies the credential in force". environmentSessionInForce
becomes environmentCredentialInForce, including the json fields on
`auth workspace list`'s context and `auth login`'s result. session-card
becomes credential-card, since it builds rows for an ActiveCredential.

Also the leftover "current" outside the deliberate exceptions:
`wasCurrent` in workspace logout's result becomes `wasSelected`,
whoami's title says "active authenticated identity", and `auth logout`
says it is clearing your stored workspace sessions rather than "the
current CLI session" — it clears every one of them, so the old wording
was inaccurate as well as retired. The on-disk currentWorkspaceId, the
`auth workspace use` command name, and `auth workspace list`'s json
`currentWorkspaceId` and per-item `current` keep the word on purpose:
they are contracts, and the divergence document now says so for the
renamed field too. The legacy shell's own wording is untouched; it goes
in S2d.

serviceTokenRejectedError leaves the public exports. Wording that
differs by origin belongs in credentialRejectedError alone, and an
export was a second door into the environment-specific text.

The divergence document described a whoami shape the code does not
produce — it claimed `user.userId` and that `user.name` had no
successor, where the code emits `user: {id, email, name}` — and said no
json result carried the environment flag when two do. Both corrected,
and `auth workspace logout`'s result is now described at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Four things, none of which changes a green suite on its own.

The stored-state mapping could tell an environment credential that its
session had expired or ended. Both are statements about a stored
session, and an environment credential has none, so neither can be the
answer for it however its refresh fails. It gets the credential-rejected
error for a rejected refresh token and the transient error for a
transient failure — never an explanation whose remedy does nothing while
the variable is set. Unreachable today, because one environment variable
carries one bearer string and so there is no refresh token to fail, but
§11.2 keeps the uniform path deliberately and the discrimination now
travels with it.

whoami's identity merge could report a person who does not exist. The
claims and the /v1/me lookup are read at different moments, so another
process replacing the session in between leaves them describing two
different users; filling a gap in one from the other then produces one
user's id beside another's email. Field-by-field merging is right where
both describe the same person and is now limited to that; otherwise the
lookup is taken whole.

The active credential's storage was memoized, but every mutation moves
the pin afterwards, so a command that mutated and then reached for
ctx.api would have been handed storage for the credential it used to be
acting as. Moving the pin now discards it. Nothing hits this today —
auth login never touches ctx.api — but the next command that mutates
would have.

Also the two comments naming ctx.session and currentSession(), both
deleted in rev 6 and both flagged twice; and the blank-token check whose
body was a discarded getter call with nothing at the call site to say
that reading is what raises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Review comment on packages/cli/src/auth/index.ts: separate exports from
src, and if it is not a package entrypoint, delete it. It is not —
packages/cli ships a binary and its package.json exposes only
./package.json — so the barrel goes and its twenty-two importers now
name the module they want. biome's noBarrelFile was already flagging it;
the rule is exempted for the two src/exports directories, which is the
pattern this file was not following.

The claims module went the same way. It had just become a re-export of
the engine's token-claims, so it was a second barrel by the time it was
flagged; its three importers now use the engine directly, and
`serviceTokenWorkspaceId` loses its alias in favour of the one name the
engine gives it.

Test mocks that stubbed the barrel now name the module holding the
function they replace, which also makes it visible which module each
test is actually faking. Repo-wide barrel count is zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
The telemetry group said "Inspect and change anonymous CLI telemetry".
You do not change telemetry; you change whether the CLI sends it. It now
says so.

Three files built an SDK client with `http://localhost:0/auth/callback`
written out by hand. The SDK's config demands a redirect URI even for a
client that only ever calls the API with tokens it already has, so the
value exists to satisfy a type and no browser is ever sent to it. It is
now one named constant next to the real one, with a comment saying why
port zero is the honest choice.

In the interface draft: the two inline `import('…')` type positions
become a top-level `import type`, matching what the shipped
management-api module already does. And the telemetry snapshot's
description now says what it means — which command ran and which flags
were given, never what any of them was set to — instead of naming
itself "value-free" and leaving the reader to work it out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden
wmadden merged commit 14f9c25 into main Aug 11, 2026
10 checks passed
@wmadden
wmadden deleted the s2a-foundations branch August 11, 2026 05:50
wmadden-electric added a commit that referenced this pull request Aug 11, 2026
S2a landed on main as a squash (#130), so main carries its content
without its commits. This branch already had those commits from the
earlier merge, which is why the overlap resolved cleanly on content.

Eight conflicts, all resolved toward the state this branch already
holds, checked one at a time rather than by taking a side wholesale:

- v8/cli.ts: kept the exported spec constants and all 40 mounted
  commands; main's inline version is the pre-slice shape.
- The three .drive documents: kept this branch's, which are newer and
  carry the removal of the committed workstation paths.
- v8-golden-rendering.test.ts: this branch has main's three entries plus
  the masked-secret one; the only text main had that this branch lacked
  was the file header, which was deliberately rewritten to list four
  surfaces.
- v8-update-check.test.ts: this branch's version spreads the original
  module before overriding, a superset of main's.
- project-controller.test.ts: deleted here, import-path updated there.
  Deletion stands; the change was mechanical.
- project-real-mode.test.ts: main still holds three git cases this
  branch deleted once their v8 equivalents existed. Kept the deletions
  and main's mock-path updates in the five surviving cases.

Verified on the merge: build, cli-engine test, cli test at 62 files and
1006 tests, cli-telemetry test, typecheck and lint all green.
wmadden-electric added a commit that referenced this pull request Aug 11, 2026
PR #130 was squash-merged, so main carries the whole s2a-foundations
lineage as one commit rather than the commits this branch already
merged. The content is identical — a diff between main and the
s2a-foundations tip is empty — so the four conflicts are all the same
shape: main's copy of a shared file predates this slice's additions,
and ours is a superset.

Verified rather than assumed. The mount map was checked
programmatically: every command main mounts is still mounted here, and
this branch adds exactly its seventeen. The plan kept every slice
heading main has, including the S8 the other stream wrote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
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.

2 participants