flowviant 0.91.2 → 0.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.mjs CHANGED
@@ -60,7 +60,8 @@
60
60
  * instance, login, mcp-cli; fleet (the roster loop) and work (session turns);
61
61
  * claude + runtimes + prompts + stream (spawning a CLI and reading its events);
62
62
  * git + worktreeDiff + patch; localSessions, listeners, preview + authproxy;
63
- * env + env-cli + vault, resources, deploy, shot.
63
+ * env (the box keypair, the uplink scrubber, the env-comparison scan) + vault
64
+ * (the knowledge wiki, not secrets), resources, deploy, shot.
64
65
  */
65
66
  import { FLEET_TOKEN, CREDENTIAL, adoptStoredCredential } from './lib/config.mjs';
66
67
  import { runFleetDaemon } from './lib/fleet.mjs';
@@ -311,9 +312,14 @@ if (process.argv[2] === 'machines') {
311
312
  process.exit(0);
312
313
  }
313
314
 
314
- // `flowviant env <import|set|show>` — the CLI half of team env sync. Values
315
- // are sealed to the project pubkey ON THIS MACHINE (same write-only crypto as
316
- // the browser); `show` decrypts locally — it only works on an ENROLLED machine.
315
+ // `flowviant env <import|set|show>` IS DELETED (2026-09-21), with the
316
+ // end-to-end-encrypted secrets vault it was the terminal half of. The owner:
317
+ // "no i dont want it. unless its needed where i want to show the env of each
318
+ // of the machines (for comparison)." So the terminal surface is back to the
319
+ // three commands it is allowed: `npx flowviant`, `npx flowviant login`, and
320
+ // the view-only `npx flowviant machines`. The comparison readout that ruling
321
+ // carves out is a daemon REPORT, not a command — see `scanEnvFiles` in env.mjs.
322
+ //
317
323
  // `flowviant mcp` — connect YOUR Claude to Flowviant so you can file work from
318
324
  // the terminal. Mints a `cli` credential: a separate principal from the
319
325
  // per-session tokens, with only the management tools and no way to work or ship
@@ -324,12 +330,6 @@ if (process.argv[2] === 'mcp') {
324
330
  process.exit(0);
325
331
  }
326
332
 
327
- if (process.argv[2] === 'env') {
328
- const { runEnvCommand } = await import('./lib/env-cli.mjs');
329
- await runEnvCommand(process.argv.slice(3));
330
- process.exit(0);
331
- }
332
-
333
333
  // ── WHICH PROJECT THIS START SERVES — said, asked, or refused; never guessed.
334
334
  //
335
335
  // The store holds many projects since 0.55.0 and resolution is BY REPO
@@ -61,20 +61,77 @@ const KEEP = [
61
61
  'XDG_DATA_HOME',
62
62
  ];
63
63
 
64
+ /**
65
+ * THE SECOND GROUP: INFRA CREDENTIALS, AND ONLY BEHIND `deploy: true`.
66
+ *
67
+ * WHY IT EXISTS (2026-09-21). Deploy used to get its credentials from the
68
+ * secrets vault — `childEnv({ extra: deployCreds() })`, where `deployCreds()`
69
+ * was the deploy-scope half of a bundle Flowviant had decrypted onto this box.
70
+ * The vault is deleted (the owner: "no i dont want it"), so Flowviant holds no
71
+ * cloud credential for anybody, and the source of a deploy's secrets is now the
72
+ * MACHINE'S OWN ENVIRONMENT — the operator's `CLOUDFLARE_API_TOKEN`, exported
73
+ * in the shell they started the daemon in, exactly as it would be if they ran
74
+ * `wrangler deploy` themselves.
75
+ *
76
+ * But this file exists precisely because a spawned command is built from `{}`,
77
+ * so an operator's own token is absent by construction and `wrangler deploy`
78
+ * fails with an auth error that looks like a broken deploy. Hence an OPT-IN
79
+ * WIDENING for the one command that genuinely needs infra credentials.
80
+ *
81
+ * IT IS STILL AN ALLOWLIST. `deploy: true` does not mean `{...process.env}`; it
82
+ * means these names and nothing else. The whole argument of this module is that
83
+ * a denylist is a claim about a set you cannot see and rots the moment somebody
84
+ * upstream adds a secret — that argument does not weaken because the caller is
85
+ * a deploy.
86
+ *
87
+ * THE BLAST RADIUS IS EXACTLY THIS LIST, and it is worth naming out loud: a
88
+ * deploy target's `build` string is REPO-CONTROLLED (it is read from
89
+ * `.flowviant/deploy.json` on the base branch, so authoring it requires landing
90
+ * a commit — but it is still a string this daemon runs). Everything below is
91
+ * therefore reachable by a reviewed commit on main, and nothing else is.
92
+ *
93
+ * `FLOWVIANT_*` IS NEVER IN IT, at any opt-in. The machine credential is the
94
+ * one secret whose leak costs the project itself, and the bug this module was
95
+ * written for is that it rode into `target.build` under a comment saying it did
96
+ * not.
97
+ */
98
+ const DEPLOY_KEEP = [
99
+ 'CLOUDFLARE_API_TOKEN',
100
+ 'CLOUDFLARE_ACCOUNT_ID',
101
+ 'VERCEL_TOKEN',
102
+ 'NETLIFY_AUTH_TOKEN',
103
+ 'FLY_API_TOKEN',
104
+ 'AWS_ACCESS_KEY_ID',
105
+ 'AWS_SECRET_ACCESS_KEY',
106
+ 'AWS_REGION',
107
+ 'AWS_SESSION_TOKEN',
108
+ 'RAILWAY_TOKEN',
109
+ 'RENDER_API_KEY',
110
+ 'NPM_TOKEN',
111
+ ];
112
+
64
113
  /**
65
114
  * A child environment for a command Flowviant runs on the operator's behalf.
66
115
  *
67
- * `extra` is layered LAST and is the caller's own material — deploy
68
- * credentials, or nothing. It is never repo-supplied: the deleted preview
69
- * feature let `.flowviant/preview.json` contribute an `env` map that was
70
- * layered last and therefore won every collision, which is how a branch got to
71
- * set `PATH`.
116
+ * `deploy: true` additionally keeps `DEPLOY_KEEP` from `process.env` — the
117
+ * opt-in widening for the one command that needs infra credentials. Nothing
118
+ * else passes it, and passing it does not widen anything beyond that list.
119
+ *
120
+ * `extra` is layered LAST and is the caller's own material. It is never
121
+ * repo-supplied: the deleted preview feature let `.flowviant/preview.json`
122
+ * contribute an `env` map that was layered last and therefore won every
123
+ * collision, which is how a branch got to set `PATH`.
72
124
  */
73
- export function childEnv({ cwd, extra } = {}) {
125
+ export function childEnv({ cwd, extra, deploy = false } = {}) {
74
126
  const env = {};
75
127
  for (const k of KEEP) {
76
128
  if (typeof process.env[k] === 'string') env[k] = process.env[k];
77
129
  }
130
+ if (deploy) {
131
+ for (const k of DEPLOY_KEEP) {
132
+ if (typeof process.env[k] === 'string') env[k] = process.env[k];
133
+ }
134
+ }
78
135
  // Set by us rather than inherited.
79
136
  //
80
137
  // TERM=dumb: a process that believes it owns a TTY draws progress bars and
@@ -93,9 +150,15 @@ export function childEnv({ cwd, extra } = {}) {
93
150
  return extra ? { ...env, ...extra } : env;
94
151
  }
95
152
 
96
- /** The names this deliberately drops, for the test to assert against. NOT the
97
- * mechanism — the mechanism is the allowlist above, and this list is only ever
98
- * a sample of what it excludes. Adding a name here changes nothing. */
153
+ /** The names this deliberately drops on the ORDINARY path, for the test to
154
+ * assert against. NOT the mechanism — the mechanism is the allowlist above,
155
+ * and this list is only ever a sample of what it excludes. Adding a name here
156
+ * changes nothing.
157
+ *
158
+ * Two of these (`CLOUDFLARE_API_TOKEN`, `AWS_SECRET_ACCESS_KEY`) are in
159
+ * `DEPLOY_KEEP` and survive `deploy: true`. They stay in this sample on
160
+ * purpose: the default is what almost every caller gets, and the sample is
161
+ * about the default. */
99
162
  export const DROPPED_SAMPLE = [
100
163
  'FLOWVIANT_FLEET',
101
164
  'FLOWVIANT_FLEET_URL',
@@ -111,3 +174,59 @@ export const DROPPED_SAMPLE = [
111
174
  'AWS_SECRET_ACCESS_KEY',
112
175
  'npm_config__authToken',
113
176
  ];
177
+
178
+ /** The opt-in group, exported for the test: `deploy: true` keeps exactly these
179
+ * and nothing else, and a name that is NOT here must still be absent. */
180
+ export const DEPLOY_KEEP_NAMES = [...DEPLOY_KEEP];
181
+
182
+ /**
183
+ * THE SAME INFRA CREDENTIALS, HANDED TO THE REDACTOR — never to the wire
184
+ * (2026-09-21, the review).
185
+ *
186
+ * THE REGRESSION THIS CLOSES. Until the vault was deleted, `scrub()` was fed
187
+ * the decrypted bundle, and the deploy-scope half of that bundle WAS
188
+ * `CLOUDFLARE_API_TOKEN` and friends — so every line of `wrangler` output that
189
+ * echoed the operator's token was redacted on its way to the server. The
190
+ * replacement scrubber reads the CHECKOUT'S `.env*` files, and an operator's
191
+ * deploy credential is almost never in one: it is exported in the shell they
192
+ * started the daemon in, which is exactly why `DEPLOY_KEEP` above exists. So
193
+ * the one lane that PASSES a secret into a command it then streams to the
194
+ * server had stopped redacting it. `deploy.mjs`'s own header was meanwhile
195
+ * still asserting the opposite — "strictly more of what a deploy log can
196
+ * contain than the vault ever delivered" — which is the defended-local-decision
197
+ * shape: a confident comment over a guarantee that had quietly lapsed.
198
+ *
199
+ * WHY IT LIVES HERE AND NOT IN `env.mjs`. This file is the single definition of
200
+ * what a spawned command may be handed; the redaction list must be the same set
201
+ * or it rots the moment somebody adds a name to `DEPLOY_KEEP` and forgets the
202
+ * other half. Deriving it FROM that array is what makes forgetting impossible,
203
+ * and it is why the merge happens inside `scanEnvForScrub` rather than at a
204
+ * call site somebody has to remember. The dependency direction is the only one
205
+ * that is acyclic besides: this module imports nothing at all.
206
+ *
207
+ * TWO NAMES ARE HERE THAT ARE NOT IN `DEPLOY_KEEP`, and the asymmetry is the
208
+ * point. `CF_API_TOKEN` and `CF_ACCOUNT_ID` are wrangler's older spellings — it
209
+ * still reads them — so a box may well hold the token under that name while the
210
+ * deploy command is handed nothing. REDACTING a value we do not pass costs
211
+ * nothing and covers the operator who is still on the old spelling; PASSING one
212
+ * would widen the blast radius of a repo-controlled `build` string, which is
213
+ * the thing this module exists to bound. Redaction and admission are different
214
+ * questions and this is the one place they are allowed to differ.
215
+ *
216
+ * IT NEVER REACHES THE REPORT. `/fleet/env-report` is a statement about the
217
+ * CHECKOUT'S FILES — that is the comparison the owner asked for, "show the env
218
+ * of each of the machines (for comparison)" — and this process's environment is
219
+ * not a fact about the checkout. Relaying it would put a box's shell
220
+ * configuration on the wire under a heading that claims to be about a directory,
221
+ * which is both a lie and a wider disclosure than anybody asked for. Values
222
+ * never leave the box either way; names would, and a name is enough to tell
223
+ * somebody which credential to go looking for.
224
+ */
225
+ export function processEnvSecrets() {
226
+ const out = [];
227
+ for (const name of [...DEPLOY_KEEP, 'CF_API_TOKEN', 'CF_ACCOUNT_ID']) {
228
+ const value = process.env[name];
229
+ if (typeof value === 'string' && value) out.push({ name, value });
230
+ }
231
+ return out;
232
+ }
@@ -293,8 +293,36 @@ export const PROCESS_STARTED_AT = new Date(
293
293
  export let FLEET_TOKEN =
294
294
  argFlag('--fleet') || process.env.FLOWVIANT_FLEET || CREDENTIAL.entry?.fleetToken || '';
295
295
 
296
+ /**
297
+ * WHICH PROJECT THIS DAEMON SERVES, or null when nothing on this box names one.
298
+ *
299
+ * ADDED 2026-09-21 for one consumer: it SALTS the env report's value
300
+ * fingerprints (`env.mjs`), so an eight-character hash over `production` or
301
+ * `3000` stops being a dictionary lookup anybody with the stored report can
302
+ * perform. The whole comparison is between the boxes of ONE project, so a
303
+ * per-project salt costs the feature nothing.
304
+ *
305
+ * NULL IS A REAL ANSWER AND IS NOT PAPERED OVER. A credential handed in through
306
+ * `FLOWVIANT_FLEET` or `--fleet` names no project until the roster does, and the
307
+ * consumer's rule is that a box which cannot name its project reports NO env
308
+ * files rather than fingerprints salted with something else — incomparable
309
+ * hashes would render as a confident "these boxes differ" beside a box holding
310
+ * the identical value. Ignorance renders nothing.
311
+ *
312
+ * `let`, for the same reason `FLEET_TOKEN` is: when cli.mjs answers an
313
+ * ambiguous store with a picker, `adoptStoredCredential` is what settles both,
314
+ * and ES named imports are LIVE bindings so every importer sees the answer.
315
+ * A frozen copy read at import would salt a picked project's report with null
316
+ * and report nothing at all, for the whole life of the process.
317
+ */
318
+ export let PROJECT_ID = CREDENTIAL.entry?.projectId ?? null;
319
+
296
320
  /** cli.mjs's picker chose. Must run BEFORE runFleetDaemon — nothing here
297
321
  * re-authenticates a connection already made. */
298
322
  export function adoptStoredCredential(entry) {
299
323
  if (entry?.fleetToken) FLEET_TOKEN = entry.fleetToken;
324
+ // The project id travels WITH the token, always. They are one answer, and a
325
+ // picker that updated only the credential left the fingerprint salt null on
326
+ // exactly the boxes that had to be asked which project they serve.
327
+ if (entry?.projectId) PROJECT_ID = entry.projectId;
300
328
  }
@@ -1,13 +1,65 @@
1
1
  /**
2
2
  * Cloudflare DevOps — the daemon runs the user's own `wrangler`. Broker-not-
3
- * host: no cloud credential ever reaches Flowviant. A deploy-authorized daemon
4
- * claims deploy jobs off the roster, runs build → push prod secrets → deploy →
3
+ * host: no cloud credential ever reaches Flowviant. A daemon on a project with
4
+ * deploy allowed claims deploy jobs off the roster, runs build → deploy →
5
5
  * verify, and reports the outcome. It also reports its parsed
6
6
  * .flowviant/deploy.json so the app can list targets, and (basic) observes
7
7
  * out-of-band deployments.
8
8
  *
9
- * Every log line that leaves the machine passes through the env scrubber —
10
- * wrangler output routinely echoes secrets.
9
+ * ── THE MACHINE'S OWN ENVIRONMENT IS THE SOURCE OF A DEPLOY'S SECRETS
10
+ * (2026-09-21), BECAUSE FLOWVIANT NO LONGER HOLDS ANY ──
11
+ *
12
+ * This lane used to get both halves of its secrets from the end-to-end
13
+ * encrypted vault: `deployCreds()` put the deploy-scope credentials in the
14
+ * command's environment, and `appSecretsFor('prod')` fed `wrangler secret put`
15
+ * on stdin so a prod deploy pushed the project's app secrets to the provider's
16
+ * own store. The vault is DELETED — the owner: "no i dont want it. unless its
17
+ * needed where i want to show the env of each of the machines (for
18
+ * comparison)", and on the recovery passphrase that protected it, "no, its fine
19
+ * to repaste from providers".
20
+ *
21
+ * So both halves are gone, and what replaces them is the operator:
22
+ *
23
+ * - the deploy COMMAND runs with the infra credentials that are in this
24
+ * daemon's own environment, passed through `childEnv`'s opt-in `deploy: true`
25
+ * allowlist. If `wrangler` authenticates when the operator runs it in that
26
+ * shell, it authenticates here.
27
+ * - the `pushSecrets` step is DELETED outright rather than reimplemented
28
+ * against `process.env`. It existed to move values Flowviant was custodian
29
+ * of; with no custody there is nothing here that the operator does not
30
+ * already have in front of them, and `wrangler secret put` is a command they
31
+ * can run. Flowviant automating a push of secrets it does not hold, from an
32
+ * environment it does not own, into a provider store it cannot read back, is
33
+ * the shape of a feature that fails silently and invisibly. `target
34
+ * .pushSecrets` is still PARSED and reported (a dormant key is the standing
35
+ * call) — it simply drives nothing.
36
+ *
37
+ * ── WHAT A DEPLOY LOG IS ACTUALLY SCRUBBED AGAINST, corrected 2026-09-21 by
38
+ * the review that caught this paragraph asserting a guarantee it had lost ──
39
+ *
40
+ * Every log line that leaves the machine still passes through `scrub`. What
41
+ * this paragraph claimed for a few hours was that being fed from the checkout's
42
+ * `.env*` files was "strictly more of what a deploy log can contain than the
43
+ * vault ever delivered", and for THIS lane that was exactly backwards. The
44
+ * vault's deploy-scope half WAS `CLOUDFLARE_API_TOKEN` and friends, so those
45
+ * were redacted; an operator's own token lives in the shell they started the
46
+ * daemon in — which is the entire reason `childEnv`'s `deploy: true` widening
47
+ * exists — and almost never in a checkout file. So the one lane that hands a
48
+ * credential to a command and then streams that command's stdout to the server
49
+ * was the one lane no longer redacting it.
50
+ *
51
+ * It is fed from BOTH now, and the join is made in `scanEnvForScrub` rather
52
+ * than here so no call site has to remember: the checkout's `.env*` files, plus
53
+ * the PRESENT values of `childEnv`'s own `DEPLOY_KEEP` names out of
54
+ * `process.env` (see `processEnvSecrets`, which also explains why `CF_API_TOKEN`
55
+ * is redacted although it is never passed, and why none of this reaches the
56
+ * `/fleet/env-report` wire — that report is a statement about the checkout's
57
+ * files, and this box's shell is not one). Deriving the redaction list from the
58
+ * admission list is what stops the two drifting the next time a name is added.
59
+ *
60
+ * The honest residue, stated: a credential that is neither in `DEPLOY_KEEP` nor
61
+ * in a checkout `.env*` — one a `build` script fetches for itself, say — is not
62
+ * redacted, because this daemon has never seen it.
11
63
  */
12
64
 
13
65
 
@@ -15,7 +67,7 @@ import { spawn } from 'node:child_process';
15
67
  import { join } from 'node:path';
16
68
  import { FLEET_URL, FLEET_TOKEN, USER_AGENT, DAEMON_INSTANCE } from './config.mjs';
17
69
  import { c, note, ok, warn } from './ui.mjs';
18
- import { deployCreds, appSecretsFor, scrub, myPubB64 } from './env.mjs';
70
+ import { scrub, myPubB64 } from './env.mjs';
19
71
  import { childEnv } from './childEnv.mjs';
20
72
  import { git } from './git.mjs';
21
73
 
@@ -131,7 +183,7 @@ export async function reportDeployConfig(repoRoot, baseRef) {
131
183
  /** Run a shell command ASYNC (never blocks the daemon's event loop — the
132
184
  * reconcile poll + the deploy heartbeat must keep firing during a long
133
185
  * deploy). Captures combined + scrubbed output; resolves {ok,out,code}. */
134
- function run(command, { cwd, env, input }) {
186
+ function run(command, { cwd, env }) {
135
187
  return new Promise((resolve) => {
136
188
  const child = spawn(command, { cwd, env, shell: true, stdio: ['pipe', 'pipe', 'pipe'] });
137
189
  let buf = '';
@@ -148,21 +200,17 @@ function run(command, { cwd, env, input }) {
148
200
  /* already gone */
149
201
  }
150
202
  }, 30 * 60_000);
151
- // A broken pipe (child exits before draining stdin — e.g. a fast-failing
152
- // `wrangler secret put`) surfaces as an ASYNC 'error' on the stdin stream,
153
- // which the try/catch below can't catch. Without a listener it's an uncaught
154
- // exception that kills the whole daemon. Swallow it.
203
+ // A broken pipe (a child that exits before its stdin is closed) surfaces as
204
+ // an ASYNC 'error' on the stdin stream, which the try/catch around this
205
+ // cannot catch. Without a listener it is an uncaught exception that kills
206
+ // the whole daemon. Swallow it.
207
+ //
208
+ // The `input` parameter this used to take went with the prod-secret push —
209
+ // it existed so a value could reach `wrangler secret put` on stdin rather
210
+ // than argv, and there is no value left to send. Closing stdin immediately
211
+ // is now the only case.
155
212
  child.stdin.on('error', () => {});
156
- if (input != null) {
157
- try {
158
- child.stdin.write(input);
159
- child.stdin.end();
160
- } catch {
161
- /* stdin closed */
162
- }
163
- } else {
164
- child.stdin.end();
165
- }
213
+ child.stdin.end();
166
214
  child.on('close', (code) => {
167
215
  clearTimeout(timer);
168
216
  resolve({ ok: code === 0, code: code ?? -1, out: scrub(buf) });
@@ -198,9 +246,9 @@ const claiming = new Set(); // in-flight guard (single-flight per daemon process
198
246
  * A deploy is IRREVERSIBLE and the report is not: a transient 5xx, a DNS blip
199
247
  * or the 30s timeout meant the outcome never landed, the heartbeat stopped,
200
248
  * and three minutes later the server requeued the job and this same daemon ran
201
- * `wrangler rollback` — or a full prod deploy with every pushSecret re-pushed —
202
- * a SECOND time, leaving production two versions behind the intended one with
203
- * nothing recording that it happened twice.
249
+ * `wrangler rollback` — or a full prod deploy — a SECOND time, leaving
250
+ * production two versions behind the intended one with nothing recording that
251
+ * it happened twice.
204
252
  *
205
253
  * So the process remembers. Not a substitute for the report (see the retry
206
254
  * below, which is the real fix); a floor under it, for the case where the
@@ -212,8 +260,8 @@ const ran = new Set();
212
260
 
213
261
  /**
214
262
  * Process queued deploy jobs from the roster. `ctx` = { repoRoot, baseRef,
215
- * myPubB64 }. Each job: claim → build → push prod secrets → deploy → verify →
216
- * report. Runs concurrently but one-per-jobId.
263
+ * myPubB64 }. Each job: claim → build → deploy → verify → report. Runs
264
+ * concurrently but one-per-jobId.
217
265
  */
218
266
  export function processDeployJobs(jobs, ctx) {
219
267
  if (!Array.isArray(jobs) || !jobs.length) return;
@@ -315,7 +363,13 @@ async function runDeploy(job, target, ctx) {
315
363
  // command — and of `target.build`, which is a string the REPO controls —
316
364
  // under a comment asserting the opposite. A denylist is a claim about a set
317
365
  // you cannot see; this is built from {} instead.
318
- const env = childEnv({ cwd: ctx.repoRoot, extra: deployCreds() }); // infra creds; never a file
366
+ //
367
+ // `deploy: true` is the OPT-IN SECOND GROUP (childEnv.mjs): a named set of
368
+ // infra credential variables kept out of this daemon's own environment, and
369
+ // nothing else. It replaced `extra: deployCreds()` when the vault was deleted
370
+ // — the credentials are the OPERATOR's now, in the shell they started the
371
+ // daemon in, rather than values Flowviant decrypted onto the box.
372
+ const env = childEnv({ cwd: ctx.repoRoot, deploy: true }); // infra creds; never a file
319
373
  const logs = [];
320
374
  // Rollback is a single wrangler command; deploy is build → secrets → deploy.
321
375
  if (job.kind === 'rollback') {
@@ -331,24 +385,17 @@ async function runDeploy(job, target, ctx) {
331
385
  if (!b.ok) return { ok: false, message: `build failed:\n${logs.slice(-6).join('\n')}`, logs };
332
386
  }
333
387
 
334
- // Push prod app secrets to the provider's secret store (never written local).
335
- // `name` is always a validated vault key (alnum/underscore) or skipped, and
336
- // the VALUE goes only via stdin — never argv (no prod plaintext in ps).
337
- if (job.env === 'prod' && Array.isArray(target.pushSecrets) && target.pushSecrets.length) {
338
- const secrets = appSecretsFor('prod');
339
- for (const name of target.pushSecrets) {
340
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) || !(name in secrets)) {
341
- warn(`deploy: pushSecret "${name}" invalid or not in the vault at prod scope — skipping`);
342
- continue;
343
- }
344
- const putCmd = target.commands?.['secretPut']
345
- ? target.commands['secretPut'].replace('{name}', name)
346
- : `npx wrangler secret put ${name} --env production`;
347
- const s = await run(putCmd, { cwd: ctx.repoRoot, env, input: secrets[name] });
348
- if (!s.ok) return { ok: false, message: `pushing secret ${name} failed`, logs };
349
- }
350
- }
351
-
388
+ // THE PROD-SECRET PUSH IS DELETED (2026-09-21). What stood here read
389
+ // `appSecretsFor('prod')` out of the vault and piped each value into
390
+ // `wrangler secret put` on stdin — never argv, so no prod plaintext in `ps`.
391
+ // That care was right and it is moot: the vault is gone, Flowviant holds no
392
+ // app secret for anybody, and there is nothing left to push. It is not
393
+ // reimplemented against `process.env`, because a deploy that quietly pushes
394
+ // whatever happens to be exported in the daemon's shell into a provider's
395
+ // secret store is a worse feature than no feature — the operator can see and
396
+ // run `wrangler secret put`, and they are the only one who can tell which
397
+ // values belong there. `target.pushSecrets` stays parsed and reported, which
398
+ // is what every retired key in this product does.
352
399
  const cmd = target.commands?.[job.env] || target.command;
353
400
  const d = await run(cmd, { cwd: ctx.repoRoot, env });
354
401
  logs.push(...tailLines(d.out));
@@ -379,7 +426,7 @@ async function runDeploy(job, target, ctx) {
379
426
  * heartbeat, and the server — which requeues a running job after three minutes
380
427
  * without one — handed the SAME job back to the SAME daemon, which ran it
381
428
  * again. For a rollback that is production two versions behind the intended
382
- * one; for a prod deploy it is every pushSecret pushed twice. Nothing recorded
429
+ * one; for a prod deploy it is the whole deploy run twice. Nothing recorded
383
430
  * that it had happened at all.
384
431
  *
385
432
  * The heartbeat keeps running throughout (the caller's `finally` is what stops