Skip to content

feat!: a deploy reads its configuration once, and Composer redacts every secret the graph names - #419

Merged
wmadden merged 37 commits into
mainfrom
claude/module-input-config-once
Nov 6, 2026
Merged

wmadden merged 37 commits into
mainfrom
claude/module-input-config-once

Conversation

@wmadden-electric

Copy link
Copy Markdown
Contributor
// prisma.config.ts
composer({
  extensions: [prismaCloud(), nodeBuild()],
  state: prismaState(),
  config: [dotenv(), environmentVariables()],
  secrets: [dotenv(), environmentVariables()],
});
$ prisma deploy --environment staging
✘ [CONFIG.SECRET_AS_PLAIN_VALUE] Root input field input.DATABASE_URL holds the value of the secret "DB_PASSWORD", which an envSecret() in the graph reads.
  Fix: Read the secret with envSecret('DB_PASSWORD') where it is used, or give this plain value text that does not contain the value of "DB_PASSWORD".

prisma deploy runs in two processes. The first is the Prisma CLI: it reads the app's configuration, checks it, and runs preflight. The second runs Alchemy, which creates the resources. Before this PR, the second process ran the config adapters again and had to get the same answer. After it, the configuration is read once, the second process receives what the first one read in a file, and a secret's value never goes into that file.

The decision

  • The configuration is read once, in the first process. The config and secrets adapters run there and nowhere else. The second process receives exactly what Load read, in a file only the user can read, and that file is deleted when the command ends.
  • The file holds no secrets. Composer refuses to write it when any value in it contains a known secret. The error above is that refusal.
  • Composer redacts every known secret in what it prints. A known secret is the value under a name an envSecret() in the graph reads.

Background: two processes, two reads

A Composer app lists its configuration sources in prisma.config.ts. config: lists the adapters for plain values; secrets: lists the adapters for secrets. dotenv() reads .env files and environmentVariables() reads the shell. What the config adapters return, merged, is the app configuration. What the secrets adapters return is the app secrets. These names replace "config pool" and "secrets pool" everywhere.

Before this PR, both processes ran the config adapters. To make the second run return what the first did, environmentVariables() passed the first run's values to the second process in an environment variable, PRISMA_COMPOSER_PARENT_VALUES. Anything else, such as a .env.staging edited during the deploy, could still make the two runs differ. The deploy would then create resources from values nobody had checked.

How the second process gets its configuration now

  1. The first process runs the secrets adapters, then the config adapters.

  2. It Loads the graph. The root module's input schema checks the app configuration. Load records what it read: each root input field the root's body passes on, with its checked value, and the text of each name an envParam() reads.

  3. It writes that record and the graph's node addresses to .prisma-composer/app-configuration-<pid>-<uuid>.json, readable only by the user:

    {"appConfigurationRead":{"rootInput":[{"path":["EMAIL_FROM"],"value":"shop@example.com"}],"configurationKeyText":{}},"nodeAddresses":["shop","shop.email"]}
  4. It starts the second process with PRISMA_COMPOSER_APP_CONFIGURATION_FILE naming the file. The second process runs no adapter. It Loads the graph from the record without running the root's schema again.

  5. The second process refuses to deploy a graph whose node addresses differ from the file's. That happens when a module body reads process.env, a file or the clock, and builds a different graph the second time.

  6. The first process deletes the file when the command ends, whether it succeeds or fails, and when the process exits. A later run deletes a file left by a process that was killed.

prisma dev works the same way. Its second process also needs the secrets the local services use; it receives them in memory, in PRISMA_COMPOSER_NAMED_SECRETS, never in a file. destroy reads only the config list.

Which values are secrets

Load knows the name each envSecret() reads. The value under each of those names, in the app configuration or the app secrets, is a known secret. No other value is. With secrets: [dotenv(), environmentVariables()], a plain .env value or the shell's SHLVL=1 is not a known secret, so it is neither redacted nor refused.

Two checks keep secrets out of the file:

  • Load refuses a name read by both envParam() and envSecret(), and a root input field passed on whose name an envSecret() reads. These fail with COMPOSE.GRAPH_INVALID.
  • The CLI refuses to write the file when a value in it contains a known secret, with CONFIG.SECRET_AS_PLAIN_VALUE. A secret of 8 characters or more is refused anywhere inside a value, such as a password inside a database URL. A shorter one is refused only when it is the whole value, so a secret dev does not refuse dev@example.com.

Both errors name the field and the secret's name, never its value.

What Composer redacts, and what it does not yet

Composer replaces every known secret with [redacted], at any length, in: every error a command returns, thrown defects and their causes, prisma dev's events, the run report, the deploy reporter and the deployment summary. The redaction that shipped before this PR still applies on top: values of 8 characters or more under credential-looking variable names, and token shapes.

Not redacted, recorded as known limits:

  • Alchemy's own output under the CLI. The second process still writes to the terminal directly. Composer redacts its failure cause, not its progress lines. This goes away when Composer runs Alchemy in its own process through Alchemy's programmatic API. A programmatic host that calls spawnAlchemy already gets redacted output.
  • Errors before Load. Until Load has run, Composer does not know which names are secrets. An adapter that throws, or a root schema failure that echoes a value, is redacted only by the credential-looking rule.
  • Step labels and the Deployed <app> line. They carry node addresses and the app name, which are not redacted.
  • destroy. It reads no secrets, so it cannot refuse a value that only the secrets list returns.

S11: dotenv() reads through dotenv-flow

dotenv() now reads its files through dotenv-flow 4.1.0, in the same order as before: .env, .env.local, .env.<environment>, .env.<environment>.local. It never reads .env.defaults. It now skips .env.local for the environment test, as dotenv-flow does.

Breaking changes

// before
Load(root, { configPool: { EMAIL_FROM: 'a@example.com' } });
paramSource(payload, { label: "myParam('X')", poolKey: 'X' });

// after
Load(root, { configuration: { EMAIL_FROM: 'a@example.com' } });
paramSource(payload, { label: "myParam('X')", configurationKey: 'X' });
  • Renames. LoadOptions.configPool is configuration. paramSource()'s poolKey is configurationKey. The types ConfigPool, SecretsPool and ConfigPools are AppConfiguration and AppSecrets. LoadOptions is now a union of FirstProcessLoadOptions and SecondProcessLoadOptions, so interface X extends LoadOptions stops compiling; extend one of the two.

  • Extension authors. A preflight step and a local attachment's serve step receive configuration and secrets in place of pools. localTargetProviders() takes configurationKeyText and namedSecrets. @prisma/composer/internal/pools is gone; it was not public API.

  • import 'dotenv/config'. If prisma.config.ts has it and you adopt dotenv(), remove it. Otherwise .env's values enter the process environment, environmentVariables() returns them last, and they override .env.<environment>.

    // before
    import 'dotenv/config';
    composer({ /* … */ config: [environmentVariables()] });
    
    // after
    composer({ /* … */ config: [dotenv(), environmentVariables()] });
  • dotenv() skips .env.local for the environment test.

  • A module body that builds a different graph in the second process fails the deploy, naming the addresses.

  • A root input field passed on must hold JSON data: text, a finite number, a boolean, null, or arrays and plain objects of these. A Date from a transform fails the Load.

  • A name read by both envParam() and envSecret() fails the Load. So does a root input field passed on whose name an envSecret() reads.

  • The file check, CONFIG.SECRET_AS_PLAIN_VALUE, as above.

  • A non-text envParam() value in a service's input or params fails at Load, before anything is created. It used to fail later.

  • Adapter order. The secrets adapters run before the config adapters, so a broken secrets adapter is reported first.

  • Redaction of the lists. Only values under names an envSecret() reads are known secrets. Before, every value a written secrets list returned was redacted. A known secret is redacted at any length: a one-character secret blanks that character in the output.

  • environmentVariables() returns the whole process environment, PRISMA_COMPOSER_ variables included.

  • Versions. @prisma/composer and @prisma/composer-cli must be the same version: the generated stack file imports @prisma/composer/internal/app-configuration-file.

Testing

  • Core tests for Load in each process: the record it writes, the second process reading it without the root's schema, a root field missing from the record, every new Load error, and the node-address check.
  • CLI tests for the adapters' order, the file's contents, mode and deletion (including a file left by a killed process), the file check at both secret lengths, redaction in every place listed above, and the secrets prisma dev hands its second process.
  • test/integration/test/local-dev.integration.ts and local-dev-store.integration.ts drive prisma dev's second process with the file and the named secrets.
  • Manual QA against Prisma Cloud, six steps, all passing: .drive/projects/module-input/slices/s11-s13-configuration-read-once/qa.md.

Documentation

  • The decision records for secrets (ADR-0029), env-sourced params (ADR-0032), errors (ADR-0044), the composer config section (ADR-0049), dev secrets (ADR-0058) and environment values (ADR-0062) are each rewritten as one current record.
  • The glossary, deploy-cli.md, config-params.md, local-dev.md, the building-an-app, running-locally and deploying guides, and the core-concepts agent skill match the code.

Alternatives considered

  • Keep running the adapters in the second process. Both runs must then agree, and nothing makes them. A .env file edited mid-deploy, or an adapter that reads a remote store, could deploy values nobody checked.
  • Pass the whole app configuration to the second process. It would carry every variable of the shell, and any secret in the config list, into a file. The record holds only what Load read.
  • Treat every value in the secrets list as a secret. With the recommended secrets: [dotenv(), environmentVariables()], every shell variable would be a secret, including SHLVL=1. Redacting 1 everywhere makes output unreadable, and the file check would refuse ordinary values.
  • Redact Alchemy's output through a new engine option (feat(engine): ctx.spawn can pass a child's output through a transform before printing it prisma-cli#383). Dropped: running Alchemy in Composer's own process removes the second process, and the option with it.

Spec: .drive/projects/module-input/slices/s11-s13-configuration-read-once/spec.md. Design: .drive/projects/module-input/design.md, sections 8 and 9.

Agent: hipparchus-99

🤖 Generated with Claude Code

…e change

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ine transform, not a relay

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
….10.0 API

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…s, names secretsKey, refuses a name read as both, and redacts through the engine's output transform

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…tion and the app secrets

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… the second process Loads with it

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… adapter

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…and the second process's output

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ors, the composer section and dev secrets; the app configuration and the app secrets

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…nown secrets

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…re's environment invariant still holds

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ration file and the app secrets as prisma dev does

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…and the host release step 5 needs

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…iguration file and the app secrets as prisma dev hands them over

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…eld or a plain value

Load refuses a root input field the body passes on when an envSecret() in the graph reads the same name. Before it writes the configuration file, the CLI refuses when any text in it equals a value under a secrets key or a value a written secrets list returned (CONFIG.SECRET_AS_PLAIN_VALUE). Both name the field, never the value.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…'s cause chain, and uses one set for every output

One SecretMatcher, built once per set, finds the longest secret at each position and merges overlapping spans, in whole-text and stream redaction alike. The stream redactor holds back a match a later secret could extend. Its cost no longer grows with the number of secrets times their length squared.

The redaction helpers move from deployment-summary.ts to redaction.ts. KnownSecrets starts with the credential-looking values of the process environment, so errors, events and the second process's output hide the same set. redactThrown also redacts the cause chain and an AggregateError's errors, and executeDev calls it.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
It no longer leaves out PRISMA_COMPOSER_ variables. The second process runs no adapter, so it never reads them back.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…dlers

A host whose engine has no spawnOptionNames runs createDeployCommand and createDevCommand. Each refuses with DEPS.CLI_ENGINE_INCOMPATIBLE before it spawns or calls an operation.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…, which runs prisma deploy

The old hint ran alchemy deploy directly, without Composer's first process.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…le, and names say what each piece holds

The configuration file is now the app configuration file (PRISMA_COMPOSER_APP_CONFIGURATION_FILE, @prisma/composer/internal/app-configuration-file). It holds what Load read and the node addresses of the graph the first process built, so PRISMA_COMPOSER_NODE_ADDRESSES is gone. Under prisma dev the second process also gets the named secrets, in PRISMA_COMPOSER_NAMED_SECRETS.

Names: CheckedConfiguration is AppConfigurationRead, with rootInput (RootInputField) and configurationKeyText; Graph.appConfigurationRead; lower()'s firstProcessNodeAddresses; readAppConfigurationFile and namedSecretsFromEnvironment. The local target gets configurationKeyText and namedSecrets (NamedSecrets), not the whole app configuration and app secrets. LoadOptions is a union, so the compiler refuses configuration with appConfigurationRead, and a source origin carries a configurationKey or a secretsKey by its sourceKind.

One function, prepareSecondProcess, writes and removes the file, builds the environment, chooses the named secrets and refuses a secret in the file. Deploy, destroy, dev and the integration tests call it; it is the one handover export on @prisma/composer-cli/testing. nodeAddresses and secretsKeys move from @prisma/composer to @prisma/composer/deploy.

Each deploy, destroy and dev run starts by deleting app configuration files earlier runs left, when their process is no longer running. The stack-import fixture's adapters throw, so the stack file tests prove the second process runs none. A deploy with the default lists and a reporter shows that a secret known only through an envSecret() name reaches no file, report, reporter, error or output.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ixes in the decision records

The glossary gains "The app configuration file" and defines the secrets key and the named secrets. ADR-0062, ADR-0049, ADR-0058, ADR-0032, the domain docs and the project design describe one app configuration file plus, under prisma dev, one variable for the named secrets; the two new refusals; the start-of-run cleanup; and environmentVariables() returning the whole process environment.

ADR-0029 and ADR-0058 are renamed after their current titles, and every link follows. ADR-0061 names configurationKey and secretsKey. "Read once" now says once per deploy, and at start and at each rebuild under prisma dev. ADR-0044 states its one exception, redaction, in its Decision section. CONFIG.SECRET_AS_PLAIN_VALUE is in the failure table.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…nd two items are deferred

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
LoadOptions is a union, so TypeScript refuses both. A JavaScript caller, or one that casts, still could pass both and silently get appConfigurationRead. Load now refuses that, as it did before the union.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…a plain value, and no app configuration file outlives its run

redactThrown never throws. It sets each property with Object.defineProperty, which works on a DOMException, and replaces an error it cannot change, such as a frozen one, with a new error that carries the redacted message, stack and cause chain. The caller rethrows what it returns.

The check before the app configuration file is written refuses a known secret anywhere in a plain value, not only a value equal to one, such as the password in a URL or "Bearer <secret>" from a schema transform. Numbers and booleans are compared as text. The message names the field, never the value.

writeFile removes the file when the process exits, and removeFile stops that. A later run deletes a leftover file only when its process is not running and the file is older than ten minutes, so a run in a container that shares the folder keeps its file.

The names say which of the two kinds of value each piece holds. SecretRedaction is what one command hides: its known secrets, which Composer knows are secrets, and credential-looking values, which only look like secrets by their variable name. KnownSecrets holds the known secrets, and prepareSecondProcess takes one as knownSecrets. credentialNamedValues replaces secretNamedValues and credentialNamedValuesIn, and redactCredentialLooking replaces redactSecrets.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…d options the code has

ADR-0058 says the adapters run once per deploy, and at start and each rebuild under prisma dev. ADR-0062, ADR-0044, ADR-0061, deploy-cli.md and the guide say the check refuses a known secret anywhere in a value, and that the CLI deletes the app configuration file at exit and deletes a leftover only when it is ten minutes old. ADR-0062 records two known limits: destroy cannot check against values only a secrets list returns, and the stream redactor costs the square of the longest secret. core-model.md shows CommonLoadOptions, FirstProcessLoadOptions and SecondProcessLoadOptions. The glossary gives the configuration key, the secrets key, the named secrets and the known secrets their own entries.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…pass

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…Thrown never throws

A thrown string or plain object now becomes an Error whose message is its redacted text, before the engine prints it or deploy sends it to the reporters. redactThrown survives a message that is not text, getters that throw, and a revoked proxy, and falls back to a new Error with redacted text. The credential-looking heuristic has one name: credentialLookingValues and addCredentialLookingValues.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
redactThrown takes any thrown value and never throws, the heuristic has one name, and the ConfigPools row no longer lists LocalTargetProvidersInput.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
wmadden-electric and others added 6 commits November 6, 2026 09:39
… in the root's input

Settled with Will on 2026-11-05. A value the platform owns is a resource node whose value is a pointer; its parse is a Standard JSON Schema that travels with the subscription. The root's secret() fields are carried secrets read from the app secrets, and only those values are known secrets. The deploy no longer fills platform variables. S15 to S17 fold into S18.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
… dropped, seeding is shelved

Will's rulings of 2026-11-05: the platform validates nothing, Composer validates a platform variable's value against its JSON Schema at boot and in the local target; the first step stops stripping a service's environment on Prisma Cloud; prisma/prisma-cli#383 is dropped because the Alchemy integration moves to the programmatic API; seeding is shelved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…econd process keeps the terminal

A value in the app secrets is a known secret only when an envSecret() in the graph names it. Before, every value a written secrets list returned counted, so the recommended secrets: [dotenv(), environmentVariables()] refused every root input field before the app configuration file was written and blanked every 1 and true in the output (review finding F16).

The engine-based redaction of the second process's output goes with prisma/prisma-cli#383, which Will dropped: Composer will run Alchemy through its programmatic API soon, which removes the second process. The CLI's second process is started as before, with the terminal. DEPS.CLI_ENGINE_INCOMPATIBLE goes with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…value only for secrets of 8 characters or more

Round 4 review fixes (F19 to F25, D24 to D32). A short secret such as dev no longer refuses EMAIL_FROM=dev@example.com. The records state the limits that remain: nothing is redacted by the known secrets before Load has named them, destroy cannot refuse a value only the secrets list returns, and the CLI's second process prints Alchemy's output unredacted while a programmatic host's spawnAlchemy redacts it. The stale statements about the dropped engine change and the old known-secrets rule are gone from the skill, the guides and the records.

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

Six steps run on 2026-11-05: the two-list setup deploys with plain values in the configuration file and none left behind, a five-character secret inside a plain value is not refused, an adapter failure prints the credential-looking value redacted, a graph that differs in the second process is refused, prisma dev keeps the secret out of project files, and destroy runs no secrets adapter. One expectation was wrong: step labels and the Deployed line carry node addresses and are not redacted, now recorded as a known limit.

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

Found in the 2026-11-05 manual QA: the command builds them from node addresses and the app name, which the author wrote, so they do not pass through the redactor.

Co-Authored-By: Claude Fable 5.1 <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 Nov 6, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 2fcb95c2-e747-4e5d-81e1-dcd685a59457
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@prisma-gizmo

prisma-gizmo Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

✅ Gizmo reviewed d412ca8 — posted 1 inline comment(s) this pass.

Open findings: 🟡 1 minor

Change walkthrough

This delta makes the app-configuration-file tests tolerate Windows, where Node cannot observe POSIX permission bits: the 0o600 mode assertions become conditional on process.platform !== 'win32'.

The pull request's second-process handover writes the deploy's configuration record with writeFileSync(..., { mode: 0o600 }) in app-configuration-file.ts:188. On Windows that mode is unrepresentable — Node only maps 0o444 to the read-only attribute — so statSync().mode & 0o777 never reports 0o600 there, and ci.yml runs this package's tests on windows-latest. The delta adapts three assertions to that reality:

  • app-configuration-file.test.ts:103 and line 123 guard the mode expectation with an inline if, so the JSON-content and file-replacement assertions still run on every platform; only the mode check is POSIX-only.
  • operations.test.ts:3980 turns the expected mode in the deploy handover test into process.platform === 'win32' ? expect.any(Number) : 0o600. This is the right shape for that test, since the mode sits inside a toEqual against the object the alchemy hook observed; the record's JSON, the node-address match, and the post-deploy deletion assertions are untouched.

The behavior being tested is unchanged — this is a test-only delta, and no production code moved. The file's owner-only guarantee remains asserted on macOS and Linux, which matches where the guarantee is observable; on Windows the file's protection rests on default ACLs, which the mode API cannot express.

@pkg-pr-new

pkg-pr-new Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@prisma/composer@419
npm i https://pkg.pr.new/@prisma/composer-cli@419
npm i https://pkg.pr.new/@prisma/composer-prisma-cloud@419

commit: d412ca8

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New findings: 🔴 6 critical · trace

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No Gizmo finding is open and the head commit has been reviewed. Approving.

…s have POSIX modes

Windows reports no owner-only mode, so the four 0600 assertions failed there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden
wmadden enabled auto-merge (squash) November 6, 2026 09:25

@prisma-gizmo prisma-gizmo Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New findings: 🟡 1 minor · trace

knownSecrets: new KnownSecrets(),
}).writeFile();

if (process.platform !== 'win32') expect(fs.statSync(file).mode & 0o777).toBe(0o600);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor · consistency — Inline if (process.platform !== 'win32') guard around expect contradicts the package's test.skipIf idiom and silently drops the mode assertion on Windows

Every other platform-conditional test in this package uses test.skipIf(process.platform === 'win32')(...) or describe.skipIf(...) (e.g. control.test.ts:40, operations.test.ts:1795, run-alchemy.test.ts:413), which shows up as a skip in test output. An inline if around expect instead reports the test as passing on Windows with the owner-only-mode guarantee unverified, and both affected test titles ('…readable only by its owner', 'a file already there is replaced, and gets mode 0600') still state the 0600 claim unconditionally. Same guard pattern repeated at line 123 and echoed by the 'mode 0600' title in operations.test.ts:3962.

Recommended fix

Extract the mode check into its own test.skipIf(process.platform === 'win32')('the file is readable only by its owner', ...) matching the package idiom, or qualify the titles (e.g. 'gets mode 0600 (POSIX)') so the skipped coverage is visible in test names.

@wmadden
wmadden merged commit 882ef36 into main Nov 6, 2026
25 checks passed
@wmadden
wmadden deleted the claude/module-input-config-once branch November 6, 2026 09:34
wmadden-electric added a commit that referenced this pull request Nov 6, 2026
…cts checked, and the root is a type of its own

The six open questions about Prisma Cloud are answered against the live API and recorded as facts in design section 3: the platform refuses empty values and non-upper-case keys, resolves a name at two scopes today (project class and branch), has no system-managed rows in a fresh project, sets a preview-wide row with `prisma project env add --role preview`, and reports a deployment whose process exits at boot as running and promotes it.

The plan is written again from `main` after #419 with four numbered slices and plain titles, no letter codes. The slice 1 spec makes the app root a type of its own through `isRoot()` with literal return types, accepts a `module()` root silently, and rewrites ADR-0003, ADR-0014 and the glossary in place as current statements.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
wmadden pushed a commit that referenced this pull request Nov 6, 2026
* docs(drive): platform variables are their own project, and module-input S18 keeps the root's input

Platform variables leave the module-input project. The new project records the model Will settled on 2026-11-06: a platform variable is a named request for a value the operator sets; only the root, written with appRoot(), may provision one; variable() and variable.secret() replace envSecret(); schema replaces parse; the deploy seeds nothing and refuses only a required variable with no row. No platform change is needed. Three reviews of the first draft and their decisions are applied.

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

* docs(drive): platform-variables slices are S1 to S3

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

* docs(drive): platform-variables S1 spec, and module() entries are deprecated rather than refused

S1 ships appRoot() alone. An entry built with module() keeps deploying with a warning, because module() roots exist in create-prisma, prisma-examples, prisma/studio and prisma/web; removing it is deferred until they move.

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

* docs(drive): platform-variables plan rewritten from main, platform facts checked, and the root is a type of its own

The six open questions about Prisma Cloud are answered against the live API and recorded as facts in design section 3: the platform refuses empty values and non-upper-case keys, resolves a name at two scopes today (project class and branch), has no system-managed rows in a fresh project, sets a preview-wide row with `prisma project env add --role preview`, and reports a deployment whose process exits at boot as running and promotes it.

The plan is written again from `main` after #419 with four numbered slices and plain titles, no letter codes. The slice 1 spec makes the app root a type of its own through `isRoot()` with literal return types, accepts a `module()` root silently, and rewrites ADR-0003, ADR-0014 and the glossary in place as current statements.

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

* feat(core): the app's root is written with appRoot(), and an app root is a type of its own

`appRoot(name, { input? }, body)` builds the root. It takes what `module()` takes minus `deps` and `expose`. Its result is an `AppRootNode`, whose `isRoot()` answers `true` where a `ModuleNode`'s answers `false`; the literal return types make the two unassignable, so `provision()` refuses an app root in every overload and through any wrapper typed `ModuleNode`. One internal builder serves both constructors. Load, lowering and the CLI's entry accept either kind of node, and Load refuses an app root that reaches `provision()` through a cast.

The entry's service check moves before Load, so every command gives the same answer. An entry built with `module()` is accepted as it is. Every root in the repository, the guides, the skill and the decision records' samples is written with `appRoot()`. ADR-0003, ADR-0014, ADR-0016, ADR-0044, ADR-0061 and the glossary are rewritten in place as current statements, and the glossary gains an "App root" entry.

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

* fix(core): slice 1 review findings

Load's internal root flag is `isLoadRoot`, so it cannot be read as the node's `isRoot()`. Constructors check the name, then the body, then `deps` and `expose`. The glossary separates the root as a position from the app root as a type, ADR-0014 states the root as an exception to "compose, don't special-case" and why, and the spec's file list matches what moved. One operations test keeps a `module()` entry through the deploy pipeline.

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

* fix(core): review findings from CodeRabbit on slice 1

Load's diagnostics name appRoot() beside service() and module(). The appRoot() options overload refuses deps and expose on a non-literal options object too, with type tests. Design notes: the decoder refuses an unsafe integer by its parsed value, preflight keeps the updatedAt of the row the platform resolves and validates the whole input before blaming a variable, and the release note for slice 2 says to rename a lower-case name that only ever worked in prisma dev.

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

* docs(drive): a service scope also touches prisma dev, and a leading-underscore name may be deployed

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

---------

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