flowviant 0.27.0 → 0.27.1
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/lib/config.mjs +1 -1
- package/bin/lib/env.mjs +69 -26
- package/package.json +1 -1
package/bin/lib/config.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
|
|
7
|
-
export const VERSION = '0.27.
|
|
7
|
+
export const VERSION = '0.27.1';
|
|
8
8
|
|
|
9
9
|
// The model EVERY daemon Claude turn runs on — pinned so autonomous work never
|
|
10
10
|
// inherits your interactive `~/.claude/settings.json` default. That matters: a
|
package/bin/lib/env.mjs
CHANGED
|
@@ -64,18 +64,27 @@ export async function sodiumReady() {
|
|
|
64
64
|
/** 6-emoji key fingerprint — algorithm MUST match the web's pubkeyEmoji
|
|
65
65
|
* (EnvironmentSettings.tsx) so the human can compare terminal ↔ approve card. */
|
|
66
66
|
// MUST stay byte-identical to the web's pubkeyEmoji (EnvironmentSettings.tsx) —
|
|
67
|
-
// the human compares the two. 32 glyphs × 8 positions
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
const FP_EMOJI = ['🦊','🐙','🦕','🐝','🦉','🐬','🦁','🐸','🦄','🐢','🦋','🐺','🦜','🐳','🦔','🐌','🦩','🐿️','🦥','🐨','🦦','🐇','🦡','
|
|
67
|
+
// the human compares the two strings. 32 glyphs × 8 positions, effective ~40
|
|
68
|
+
// bits. Two FNV-1a rolling hashes over the whole key + a murmur3 finalizer per
|
|
69
|
+
// glyph (a plain additive sum collapsed the space to ~10 bits — grindable).
|
|
70
|
+
const FP_EMOJI = ['🦊','🐙','🦕','🐝','🦉','🐬','🦁','🐸','🦄','🐢','🦋','🐺','🦜','🐳','🦔','🐌','🦩','🐿️','🦥','🐨','🦦','🐇','🦡','🦂','🦨','🐜','🦢','🐋','🦭','🐞','🦚','🐊'];
|
|
71
71
|
export function pubkeyEmoji(pubkeyB64) {
|
|
72
|
+
let h1 = 0x811c9dc5 >>> 0;
|
|
73
|
+
let h2 = 0xc2b2ae35 >>> 0;
|
|
74
|
+
for (let i = 0; i < pubkeyB64.length; i++) {
|
|
75
|
+
const ch = pubkeyB64.charCodeAt(i);
|
|
76
|
+
h1 = Math.imul(h1 ^ ch, 0x01000193) >>> 0;
|
|
77
|
+
h2 = Math.imul(h2 ^ ch, 0x85ebca6b) >>> 0;
|
|
78
|
+
}
|
|
72
79
|
let out = '';
|
|
73
80
|
for (let i = 0; i < 8; i++) {
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
let x = (((i < 4 ? h1 : h2) + i * 0x9e3779b1) >>> 0);
|
|
82
|
+
x ^= x >>> 16;
|
|
83
|
+
x = Math.imul(x, 0x7feb352d) >>> 0;
|
|
84
|
+
x ^= x >>> 15;
|
|
85
|
+
x = Math.imul(x, 0x846ca68b) >>> 0;
|
|
86
|
+
x ^= x >>> 16;
|
|
87
|
+
out += FP_EMOJI[x & 31];
|
|
79
88
|
}
|
|
80
89
|
return out;
|
|
81
90
|
}
|
|
@@ -186,13 +195,15 @@ function readCache(projectId) {
|
|
|
186
195
|
}
|
|
187
196
|
}
|
|
188
197
|
|
|
189
|
-
/** Offline start: materialize from the encrypted cache before the first poll.
|
|
198
|
+
/** Offline start: materialize from the encrypted cache before the first poll.
|
|
199
|
+
* Also seeds knownTargetFiles so stale-file cleanup survives a restart. */
|
|
190
200
|
export async function loadCachedEnv(projectId) {
|
|
191
201
|
await ensureKeypair();
|
|
192
202
|
const cached = readCache(projectId);
|
|
193
203
|
if (!cached) return false;
|
|
194
204
|
values = cached.values ?? [];
|
|
195
205
|
bundleVersion = cached.bundleVersion ?? -1;
|
|
206
|
+
knownTargetFiles = new Set(cached.knownFiles ?? values.map((v) => v.targetFile));
|
|
196
207
|
cachedProjectId = projectId;
|
|
197
208
|
return values.length > 0;
|
|
198
209
|
}
|
|
@@ -253,9 +264,16 @@ function isTrackedInGit(wt, relPath) {
|
|
|
253
264
|
}
|
|
254
265
|
}
|
|
255
266
|
|
|
256
|
-
// Per-worktree: the target files we last materialized
|
|
257
|
-
// its keys (or a key that moved files) gets its stale copy removed.
|
|
267
|
+
// Per-worktree: the target files we last materialized THIS SESSION.
|
|
258
268
|
const lastFilesByWorktree = new Map();
|
|
269
|
+
// Project-global union of every target file we've ever materialized — PERSISTED
|
|
270
|
+
// in the cache and seeded on load, so a file whose key was deleted while the
|
|
271
|
+
// daemon was down still gets its stale plaintext copy cleaned on the next
|
|
272
|
+
// materialize (lastFilesByWorktree alone is empty after a restart, and
|
|
273
|
+
// `git clean -fd` never removes an info/exclude'd file).
|
|
274
|
+
let knownTargetFiles = new Set();
|
|
275
|
+
|
|
276
|
+
const MATERIALIZE_HEADER = '# Materialized by flowviant env sync';
|
|
259
277
|
|
|
260
278
|
/** Render KEY=value with values that contain newlines/= safely quoted so one
|
|
261
279
|
* value can't fabricate another key line. */
|
|
@@ -266,7 +284,21 @@ function renderEnvFile(list) {
|
|
|
266
284
|
const esc = v.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '');
|
|
267
285
|
return `${v.name}="${esc}"`;
|
|
268
286
|
});
|
|
269
|
-
return
|
|
287
|
+
return `${MATERIALIZE_HEADER} — DO NOT COMMIT.\n${lines.join('\n')}\n`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Delete a materialized file from a worktree, but ONLY if it's ours (carries
|
|
291
|
+
* our header) and not git-tracked — never touch a file we didn't write. */
|
|
292
|
+
function removeStaleEnvFile(wt, rel) {
|
|
293
|
+
if (isTrackedInGit(wt, rel)) return;
|
|
294
|
+
const abs = join(wt, rel);
|
|
295
|
+
try {
|
|
296
|
+
if (existsSync(abs) && readFileSync(abs, 'utf8').startsWith(MATERIALIZE_HEADER)) {
|
|
297
|
+
rmSync(abs, { force: true });
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
/* best-effort */
|
|
301
|
+
}
|
|
270
302
|
}
|
|
271
303
|
|
|
272
304
|
/** Write the decrypted env into ONE worktree. Never call on the wiki worktree. */
|
|
@@ -305,18 +337,15 @@ export function materializeInto(wt) {
|
|
|
305
337
|
}
|
|
306
338
|
}
|
|
307
339
|
|
|
308
|
-
// Remove
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
} catch {
|
|
316
|
-
/* best-effort */
|
|
317
|
-
}
|
|
318
|
-
}
|
|
340
|
+
// Remove any file we ever materialized (this session OR a prior one, via the
|
|
341
|
+
// persisted knownTargetFiles) that has no keys now — a deleted secret's
|
|
342
|
+
// plaintext file must not linger, even across a daemon restart.
|
|
343
|
+
const writtenSet = new Set(written);
|
|
344
|
+
const candidates = new Set([...(lastFilesByWorktree.get(wt) ?? []), ...knownTargetFiles]);
|
|
345
|
+
for (const stale of candidates) {
|
|
346
|
+
if (!writtenSet.has(stale)) removeStaleEnvFile(wt, stale);
|
|
319
347
|
}
|
|
348
|
+
for (const f of written) knownTargetFiles.add(f);
|
|
320
349
|
lastFilesByWorktree.set(wt, written);
|
|
321
350
|
if (written.length) excludeInWorktree(wt, written);
|
|
322
351
|
}
|
|
@@ -359,7 +388,18 @@ export async function handleRosterEnv(env, { projectId } = {}) {
|
|
|
359
388
|
// wedge registration until restart.
|
|
360
389
|
if (env.status === 'none' && !registeredOnce) {
|
|
361
390
|
const label = hostname() || 'daemon';
|
|
362
|
-
|
|
391
|
+
try {
|
|
392
|
+
await post('register', { pubkey: myPubB64(), label });
|
|
393
|
+
} catch (e) {
|
|
394
|
+
// A 429 = the project is at its machine cap; retrying every poll would
|
|
395
|
+
// just hammer it. Stop for this session (a restart re-tries).
|
|
396
|
+
if (/\(429/.test(e.message)) {
|
|
397
|
+
registeredOnce = true;
|
|
398
|
+
warn('env: this project is at its machine limit — env access not requested. Ask an admin to remove an old machine.');
|
|
399
|
+
return { changed: false };
|
|
400
|
+
}
|
|
401
|
+
throw e; // transient — retry next poll (registeredOnce still false)
|
|
402
|
+
}
|
|
363
403
|
registeredOnce = true;
|
|
364
404
|
const fp = pubkeyEmoji(myPubB64());
|
|
365
405
|
info(`${c.cyan('env')} · this machine requested env access as ${c.bold(label)}`);
|
|
@@ -443,7 +483,10 @@ export async function handleRosterEnv(env, { projectId } = {}) {
|
|
|
443
483
|
if (needSync) {
|
|
444
484
|
values = opened;
|
|
445
485
|
bundleVersion = bundle.bundleVersion;
|
|
446
|
-
|
|
486
|
+
// Fold the current target files into the persisted known set so stale
|
|
487
|
+
// cleanup survives a restart (a key deleted while down still gets swept).
|
|
488
|
+
for (const v of values) knownTargetFiles.add(v.targetFile);
|
|
489
|
+
if (cachedProjectId) writeCache(cachedProjectId, { values, bundleVersion, knownFiles: [...knownTargetFiles] });
|
|
447
490
|
ok(`${c.cyan('env')} ${c.dim(`— synced ${values.length} secret${values.length === 1 ? '' : 's'} (env v${bundleVersion})`)}`);
|
|
448
491
|
return { changed: true };
|
|
449
492
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"description": "Run your own Claude Code as headless build agents for Flowviant — on your own credentials. Claims dispatched work, opens PRs, captures review evidence, and routes questions back to you.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|