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.
- package/bin/cli.mjs +13 -3
- package/bin/lib/agentPlan.mjs +159 -37
- package/bin/lib/artifacts.mjs +174 -33
- package/bin/lib/authReject.mjs +32 -0
- package/bin/lib/authproxy.mjs +52 -16
- package/bin/lib/childEnv.mjs +33 -1
- package/bin/lib/claude.mjs +377 -45
- package/bin/lib/config.mjs +24 -1
- package/bin/lib/credentials.mjs +27 -4
- package/bin/lib/deploy.mjs +364 -30
- package/bin/lib/env.mjs +146 -12
- package/bin/lib/fleet.mjs +125 -30
- package/bin/lib/git.mjs +131 -3
- package/bin/lib/hooks/readGuard.mjs +162 -0
- package/bin/lib/instance.mjs +115 -14
- package/bin/lib/knowledge.mjs +460 -23
- package/bin/lib/landed.mjs +38 -30
- package/bin/lib/listeners.mjs +211 -0
- package/bin/lib/localSessions.mjs +29 -4
- package/bin/lib/login.mjs +2 -2
- package/bin/lib/machines.mjs +16 -7
- package/bin/lib/preview.mjs +95 -6
- package/bin/lib/printable.mjs +42 -0
- package/bin/lib/procRegistry.mjs +65 -0
- package/bin/lib/processes.mjs +20 -7
- package/bin/lib/projectTools.mjs +248 -0
- package/bin/lib/prompts.mjs +354 -32
- package/bin/lib/resources.mjs +46 -4
- package/bin/lib/runtimeOfferings.mjs +75 -0
- package/bin/lib/runtimes.mjs +152 -11
- package/bin/lib/shipMerge.mjs +8 -4
- package/bin/lib/trace.mjs +16 -6
- package/bin/lib/update.mjs +5 -0
- package/bin/lib/work.mjs +1261 -4476
- package/bin/lib/workAgentMerges.mjs +504 -0
- package/bin/lib/workAgentPlans.mjs +447 -0
- package/bin/lib/workAgentReview.mjs +702 -0
- package/bin/lib/workAgentTurns.mjs +1148 -0
- package/bin/lib/workDiffs.mjs +107 -0
- package/bin/lib/workPreviews.mjs +351 -0
- package/bin/lib/workProcesses.mjs +236 -0
- package/bin/lib/workPullRequests.mjs +313 -0
- package/bin/lib/workShip.mjs +385 -0
- package/bin/lib/worktreeDiff.mjs +99 -43
- package/package.json +2 -2
package/bin/lib/authproxy.mjs
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
385
|
+
host: targetHost,
|
|
350
386
|
port: targetPort,
|
|
351
387
|
method: req.method,
|
|
352
388
|
path: req.url,
|
package/bin/lib/childEnv.mjs
CHANGED
|
@@ -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,
|
|
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
|
}
|