flowviant 0.96.0 → 0.99.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.
Files changed (45) hide show
  1. package/bin/cli.mjs +13 -3
  2. package/bin/lib/agentPlan.mjs +159 -37
  3. package/bin/lib/artifacts.mjs +174 -33
  4. package/bin/lib/authReject.mjs +32 -0
  5. package/bin/lib/authproxy.mjs +52 -16
  6. package/bin/lib/childEnv.mjs +33 -1
  7. package/bin/lib/claude.mjs +377 -45
  8. package/bin/lib/config.mjs +24 -1
  9. package/bin/lib/credentials.mjs +27 -4
  10. package/bin/lib/deploy.mjs +364 -30
  11. package/bin/lib/env.mjs +146 -12
  12. package/bin/lib/fleet.mjs +125 -30
  13. package/bin/lib/git.mjs +131 -3
  14. package/bin/lib/hooks/readGuard.mjs +162 -0
  15. package/bin/lib/instance.mjs +115 -14
  16. package/bin/lib/knowledge.mjs +460 -23
  17. package/bin/lib/landed.mjs +38 -30
  18. package/bin/lib/listeners.mjs +211 -0
  19. package/bin/lib/localSessions.mjs +29 -4
  20. package/bin/lib/login.mjs +2 -2
  21. package/bin/lib/machines.mjs +16 -7
  22. package/bin/lib/preview.mjs +95 -6
  23. package/bin/lib/printable.mjs +42 -0
  24. package/bin/lib/procRegistry.mjs +65 -0
  25. package/bin/lib/processes.mjs +20 -7
  26. package/bin/lib/projectTools.mjs +248 -0
  27. package/bin/lib/prompts.mjs +354 -32
  28. package/bin/lib/resources.mjs +46 -4
  29. package/bin/lib/runtimeOfferings.mjs +75 -0
  30. package/bin/lib/runtimes.mjs +152 -11
  31. package/bin/lib/shipMerge.mjs +8 -4
  32. package/bin/lib/trace.mjs +16 -6
  33. package/bin/lib/update.mjs +5 -0
  34. package/bin/lib/work.mjs +1261 -4476
  35. package/bin/lib/workAgentMerges.mjs +504 -0
  36. package/bin/lib/workAgentPlans.mjs +447 -0
  37. package/bin/lib/workAgentReview.mjs +702 -0
  38. package/bin/lib/workAgentTurns.mjs +1148 -0
  39. package/bin/lib/workDiffs.mjs +107 -0
  40. package/bin/lib/workPreviews.mjs +351 -0
  41. package/bin/lib/workProcesses.mjs +236 -0
  42. package/bin/lib/workPullRequests.mjs +313 -0
  43. package/bin/lib/workShip.mjs +385 -0
  44. package/bin/lib/worktreeDiff.mjs +99 -43
  45. package/package.json +2 -2
@@ -116,6 +116,10 @@ const MAX_SOURCES = 500;
116
116
 
117
117
  const digest = (s) => createHash('sha256').update(String(s)).digest();
118
118
 
119
+ /** An Authorization header in the Basic scheme — the only shape the gate's own
120
+ * password ever arrives in, and so the only one it grades. */
121
+ const isBasic = (v) => typeof v === 'string' && /^\s*basic(\s|$)/i.test(v);
122
+
119
123
  /** Constant-time over sha256 digests, so length never leaks and a missing
120
124
  * header costs the same as a wrong one. */
121
125
  function sameSecret(a, b) {
@@ -137,7 +141,18 @@ function sameSecret(a, b) {
137
141
  * URL under attack. A single source is blocked on its own, at MAX_FAILED,
138
142
  * without ending anybody else's share.
139
143
  */
140
- export function startAuthProxy({ targetPort, log, onAbuse, grantSecret, shareId, authorizeUrl }) {
144
+ export function startAuthProxy({
145
+ targetPort,
146
+ // The address that reaches the ATTRIBUTED socket (listeners.mjs `originFor`)
147
+ // — `::1` for a dev server bound to the IPv6 loopback. Defaults to the IPv4
148
+ // loopback it always dialled, for a caller that measured nothing.
149
+ targetHost = '127.0.0.1',
150
+ log,
151
+ onAbuse,
152
+ grantSecret,
153
+ shareId,
154
+ authorizeUrl,
155
+ }) {
141
156
  // ALL THREE OR NONE. Two of the three is a gate that cannot bounce anybody:
142
157
  // a secret with no authorize URL has nowhere to send them, an authorize URL
143
158
  // with no secret cannot verify what comes back. An older SERVER sends none of
@@ -217,7 +232,36 @@ export function startAuthProxy({ targetPort, log, onAbuse, grantSecret, shareId,
217
232
  // whole thing down. An expired-but-validly-signed grant must never count
218
233
  // either, or a viewer who left a tab open overnight closes the share on
219
234
  // their own reload.
220
- if (req.headers['authorization']) {
235
+ //
236
+ // AND ONLY A *BASIC* HEADER IS A PASSWORD ATTEMPT (audit 2026-09-24). Any
237
+ // Authorization header used to be graded against the gate password, so a
238
+ // previewed SPA calling its own `/api` with `Authorization: Bearer …` was
239
+ // answered 401 with a Basic challenge — over a valid grant cookie — and 25
240
+ // such calls blocked that viewer, eight viewers tore the share down, and
241
+ // the origin never saw its own header. A Bearer (or any other scheme) is
242
+ // the APP's credential: it is not ours to grade and it rides through.
243
+ //
244
+ // A VALID GRANT COOKIE IS ASKED FIRST, for the same reason: an app that
245
+ // itself speaks Basic sends a header that is not our password, and a
246
+ // viewer the app has vouched for must not be graded on it. A grant is
247
+ // HMAC-verified, so asking it first opens no brute-force path.
248
+ let grantVerdict = 'none';
249
+ if (grants) {
250
+ // EVERY value for our name, not the first — a duplicate must not shadow.
251
+ for (const raw of cookieValues(req.headers.cookie, GRANT_COOKIE)) {
252
+ const r = verifyGrant(raw, { secret: grantSecret, shareId });
253
+ if (r.ok) return 'ok';
254
+ if (r.reason === 'exp' && grantVerdict === 'none') {
255
+ lastExpired = r.payload;
256
+ grantVerdict = 'expired';
257
+ }
258
+ }
259
+ if (grantVerdict === 'none' && String(req.headers.cookie ?? '').includes(GRANT_COOKIE)) {
260
+ grantVerdict = 'forged';
261
+ }
262
+ }
263
+
264
+ if (isBasic(req.headers['authorization'])) {
221
265
  // A blocked source is refused BEFORE the comparison — a block that
222
266
  // still grades guesses would let the brute force run to a correct hit.
223
267
  if (sourceBlocked(req)) return 'blocked';
@@ -230,18 +274,7 @@ export function startAuthProxy({ targetPort, log, onAbuse, grantSecret, shareId,
230
274
  noteFailure(req);
231
275
  return 'badpass';
232
276
  }
233
-
234
- if (!grants) return 'none';
235
- // EVERY value for our name, not the first — a duplicate must not shadow.
236
- for (const raw of cookieValues(req.headers.cookie, GRANT_COOKIE)) {
237
- const r = verifyGrant(raw, { secret: grantSecret, shareId });
238
- if (r.ok) return 'ok';
239
- if (r.reason === 'exp') {
240
- lastExpired = r.payload;
241
- return 'expired';
242
- }
243
- }
244
- return String(req.headers.cookie ?? '').includes(GRANT_COOKIE) ? 'forged' : 'none';
277
+ return grantVerdict;
245
278
  };
246
279
 
247
280
  /**
@@ -338,7 +371,10 @@ export function startAuthProxy({ targetPort, log, onAbuse, grantSecret, shareId,
338
371
  // its request would be us editing their app's input.
339
372
  const forwardOpts = (req) => {
340
373
  const headers = { ...req.headers };
341
- delete headers.authorization;
374
+ // Only when it IS the gate credential. Anything else in Authorization is
375
+ // the previewed app's own (a Bearer to its API, its own Basic realm) and
376
+ // the app breaks without it; ours must still never reach branch code.
377
+ if (sameSecret(headers.authorization, expected)) delete headers.authorization;
342
378
  delete headers['proxy-authorization'];
343
379
  // ONLY OURS. The driver's app owns its own cookies and breaks without them;
344
380
  // our grant is a signed bearer token and must not reach branch code.
@@ -346,7 +382,7 @@ export function startAuthProxy({ targetPort, log, onAbuse, grantSecret, shareId,
346
382
  if (rest) headers.cookie = rest;
347
383
  else delete headers.cookie; // never send a bare empty `cookie:`
348
384
  return {
349
- host: '127.0.0.1',
385
+ host: targetHost,
350
386
  port: targetPort,
351
387
  method: req.method,
352
388
  path: req.url,
@@ -223,9 +223,41 @@ export const DEPLOY_KEEP_NAMES = [...DEPLOY_KEEP];
223
223
  * never leave the box either way; names would, and a name is enough to tell
224
224
  * somebody which credential to go looking for.
225
225
  */
226
+ /**
227
+ * …AND THE DAEMON'S OWN CREDENTIALS, REDACTED AND NEVER ADMITTED (2026-09-24,
228
+ * the audit).
229
+ *
230
+ * The list above was written about the deploy lane, and it left out every
231
+ * credential the DAEMON itself runs on: the machine credential
232
+ * (`FLOWVIANT_MACHINE_TOKEN`, or its permanent `FLOWVIANT_FLEET` fallback on a
233
+ * headless box), and whatever signs the operator's CLIs and `gh` in. Those are
234
+ * the values a CLI turn can most easily reach — a turn inherits this process's
235
+ * environment, and `env` is one Bash call — and a turn trace, an answer and an
236
+ * artifact are all posted to the server. Redacting a value we never pass costs
237
+ * nothing (the `CF_*` argument above), so they join the REDACTION list only;
238
+ * `DEPLOY_KEEP` is untouched and `childEnv` still hands none of them to anybody.
239
+ *
240
+ * The machine credential is ALSO caught by shape in `env.mjs` (`fva_…`),
241
+ * because the copy that matters most is not in any environment variable at all:
242
+ * it is every project's `fleetToken` in `~/.flowviant/credentials.json`, plus
243
+ * the per-turn work/capture tokens the server mints. The names here are belt.
244
+ */
245
+ const REDACT_ONLY = [
246
+ 'CF_API_TOKEN',
247
+ 'CF_ACCOUNT_ID',
248
+ 'FLOWVIANT_MACHINE_TOKEN',
249
+ 'FLOWVIANT_FLEET',
250
+ 'ANTHROPIC_API_KEY',
251
+ 'ANTHROPIC_AUTH_TOKEN',
252
+ 'CLAUDE_CODE_OAUTH_TOKEN',
253
+ 'OPENAI_API_KEY',
254
+ 'GH_TOKEN',
255
+ 'GITHUB_TOKEN',
256
+ ];
257
+
226
258
  export function processEnvSecrets() {
227
259
  const out = [];
228
- for (const name of [...DEPLOY_KEEP, 'CF_API_TOKEN', 'CF_ACCOUNT_ID']) {
260
+ for (const name of [...DEPLOY_KEEP, ...REDACT_ONLY]) {
229
261
  const value = process.env[name];
230
262
  if (typeof value === 'string' && value) out.push({ name, value });
231
263
  }