@evomap/evolver 1.85.2 → 1.86.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/package.json +4 -1
- package/scripts/check-changelog.js +166 -0
- package/src/adapters/scripts/evolver-session-end.js +0 -1
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/claimNudge.js +10 -10
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/featureFlags.js +11 -8
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/localStateAwareness.js +8 -9
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/paths.js +204 -48
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/validator/stakeBootstrap.js +12 -11
- package/src/gep/workspaceKeychain.js +1 -0
- package/src/proxy/index.js +4 -4
- package/src/proxy/lifecycle/manager.js +44 -1
- package/src/webui/observer/interactions.js +4 -3
package/src/gep/paths.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
3
4
|
|
|
4
5
|
let _cachedRepoRoot = null;
|
|
5
6
|
|
|
@@ -42,34 +43,73 @@ function getRepoRoot() {
|
|
|
42
43
|
const legacyFlag = process.env.EVOLVER_USE_PARENT_GIT;
|
|
43
44
|
const legacyOptOut = typeof legacyFlag === 'string' && legacyFlag.toLowerCase() === 'false';
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
// Both upward walks below must stop at the parent of the nearest
|
|
47
|
+
// `node_modules` ancestor — never escape into whatever `.git` happens
|
|
48
|
+
// to live above it (issue #541). On macOS with Homebrew, the global
|
|
49
|
+
// install lives at `/opt/homebrew/lib/node_modules/@evomap/evolver`
|
|
50
|
+
// and `/opt/homebrew` is itself a git repo; an unbounded walk
|
|
51
|
+
// therefore resolves repoRoot to `/opt/homebrew`, sending
|
|
52
|
+
// workspaceRoot / memoryDir / evolutionDir to a directory that
|
|
53
|
+
// doesn't belong to the user and silently producing evolution
|
|
54
|
+
// proposals for the wrong codebase.
|
|
55
|
+
//
|
|
56
|
+
// Boundary semantics: returns the parent of the nearest `node_modules`
|
|
57
|
+
// ancestor (inclusive — a `.git` at that parent IS still picked up),
|
|
58
|
+
// or null if `dir` is not inside any `node_modules` (dev clone /
|
|
59
|
+
// user project root). Callers stop AFTER checking the boundary path
|
|
60
|
+
// itself.
|
|
61
|
+
//
|
|
62
|
+
// For a local install (`<project>/node_modules/@evomap/evolver`), the
|
|
63
|
+
// parent of node_modules IS the user's project, so the boundary
|
|
64
|
+
// includes `<project>` and `<project>/.git` is still picked up
|
|
65
|
+
// correctly. For a dev clone, the boundary is null and the walk is
|
|
66
|
+
// unbounded as before.
|
|
67
|
+
function _nodeModulesBoundary(dir) {
|
|
68
|
+
const segments = dir.split(path.sep);
|
|
69
|
+
const nmIdx = segments.lastIndexOf('node_modules');
|
|
70
|
+
if (nmIdx <= 0) return null;
|
|
71
|
+
return segments.slice(0, nmIdx).join(path.sep) || path.sep;
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let dir =
|
|
74
|
+
function _walkForGit(start) {
|
|
75
|
+
const stopAt = _nodeModulesBoundary(start);
|
|
76
|
+
let dir = start;
|
|
63
77
|
while (dir !== path.dirname(dir)) {
|
|
64
78
|
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
65
79
|
if (!process.env.EVOLVER_QUIET_PARENT_GIT) {
|
|
66
80
|
console.log('[evolver] Using host git repository at:', dir);
|
|
67
81
|
}
|
|
68
|
-
|
|
69
|
-
return _cachedRepoRoot;
|
|
82
|
+
return dir;
|
|
70
83
|
}
|
|
84
|
+
if (stopAt !== null && dir === stopAt) break;
|
|
71
85
|
dir = path.dirname(dir);
|
|
72
86
|
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Walk upward from process.cwd() — the project the user is standing in.
|
|
91
|
+
// Bounded the same way as the ownDir walk: a user who `cd`s into the
|
|
92
|
+
// global install (e.g. `cd /opt/homebrew/lib/node_modules/@evomap/evolver`
|
|
93
|
+
// to debug) would otherwise hit `/opt/homebrew/.git` here BEFORE the
|
|
94
|
+
// ownDir walk runs, defeating its boundary. The boundary still
|
|
95
|
+
// includes the parent of node_modules, so a user `cd`'d into
|
|
96
|
+
// `<their-project>/node_modules/lodash` still has `<their-project>/.git`
|
|
97
|
+
// picked correctly.
|
|
98
|
+
if (!noParent && !legacyOptOut) {
|
|
99
|
+
const hit = _walkForGit(process.cwd());
|
|
100
|
+
if (hit) {
|
|
101
|
+
_cachedRepoRoot = hit;
|
|
102
|
+
return _cachedRepoRoot;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Walk upward from ownDir's parent (local install inside node_modules).
|
|
107
|
+
if (!noParent && !legacyOptOut) {
|
|
108
|
+
const hit = _walkForGit(path.dirname(ownDir));
|
|
109
|
+
if (hit) {
|
|
110
|
+
_cachedRepoRoot = hit;
|
|
111
|
+
return _cachedRepoRoot;
|
|
112
|
+
}
|
|
73
113
|
}
|
|
74
114
|
|
|
75
115
|
// Fallback: evolver's own directory (dev mode or isolated install).
|
|
@@ -224,6 +264,27 @@ function getEvolverInstallRoot() {
|
|
|
224
264
|
return path.resolve(__dirname, '..', '..');
|
|
225
265
|
}
|
|
226
266
|
|
|
267
|
+
// Resolve the per-user `~/.evomap` directory, with `EVOLVER_HOME` env var
|
|
268
|
+
// override. Lazy (function call, not a module-level `const`) so tests can
|
|
269
|
+
// flip `EVOLVER_HOME` per case without monkey-patching `os.homedir`.
|
|
270
|
+
//
|
|
271
|
+
// Existing call sites used to duplicate `path.join(os.homedir(), '.evomap')`
|
|
272
|
+
// across ~9 modules; about two thirds silently ignored `EVOLVER_HOME` (it
|
|
273
|
+
// worked for stake bootstrap and claim nudge but not for node-id, device-id,
|
|
274
|
+
// feature flags, etc.). #114 consolidates onto this helper so the override
|
|
275
|
+
// is uniform and tests don't need to monkey-patch the global homedir
|
|
276
|
+
// function (which doesn't compose with `node --test` parallel execution).
|
|
277
|
+
function getEvomapDir() {
|
|
278
|
+
return process.env.EVOLVER_HOME || path.join(os.homedir(), '.evomap');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Join sub-segments under `~/.evomap`. Just a convenience wrapper so call
|
|
282
|
+
// sites don't have to `path.join(getEvomapDir(), 'mailbox', 'state.json')`
|
|
283
|
+
// in two pieces.
|
|
284
|
+
function getEvomapPath(...segments) {
|
|
285
|
+
return path.join(getEvomapDir(), ...segments);
|
|
286
|
+
}
|
|
287
|
+
|
|
227
288
|
// Per-workspace random secret used to attest that a memory_graph.jsonl
|
|
228
289
|
// entry was written by the same workspace that's now reading it. Stored
|
|
229
290
|
// at <workspace>/.evolver/workspace-id with mode 0600 and lazily created
|
|
@@ -238,32 +299,51 @@ function getEvolverInstallRoot() {
|
|
|
238
299
|
// claim a different workspace. workspace-id replaces that self-report
|
|
239
300
|
// with a secret that only the legitimate workspace's evolver knows
|
|
240
301
|
// (Bugbot PR #108 round-3 Agentic Security Review MEDIUM).
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
302
|
+
//
|
|
303
|
+
// Issue #111 Phase 1: optionally backs the secret with the OS keychain
|
|
304
|
+
// (`@napi-rs/keyring` optional dep) to close the same-uid readability
|
|
305
|
+
// gap. Mode is controlled by `EVOLVER_WORKSPACE_KEYCHAIN` (auto/force/
|
|
306
|
+
// off, default `auto`). FS file is RETAINED on successful keychain
|
|
307
|
+
// migration so bun-compiled binaries (which can't `require()` the
|
|
308
|
+
// addon yet — Phase 2) still see the same id when handing off to a
|
|
309
|
+
// node-CLI session in the same workspace.
|
|
245
310
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// #109 round-2 HIGH, Agentic Security Review).
|
|
311
|
+
// Read the FS-backed workspace-id at <workspace>/.evolver/workspace-id.
|
|
312
|
+
// Returns the id on a clean read, null on any error or missing file.
|
|
313
|
+
// Symlink rejection matches the pre-keychain hardening from PR #109.
|
|
314
|
+
function _readWorkspaceIdFromFs(file) {
|
|
315
|
+
const dir = path.dirname(file);
|
|
252
316
|
try {
|
|
253
317
|
const dirStat = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
254
318
|
if (dirStat && dirStat.isSymbolicLink()) return null;
|
|
255
319
|
const fileStat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
256
|
-
if (fileStat)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
320
|
+
if (!fileStat) return null;
|
|
321
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile()) return null;
|
|
322
|
+
const raw = fs.readFileSync(file, 'utf8').trim();
|
|
323
|
+
if (raw && /^[a-f0-9]{32,}$/i.test(raw)) return raw;
|
|
324
|
+
return null;
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Atomically create <workspace>/.evolver/workspace-id with the given id
|
|
331
|
+
// (or generate one if `id` is null). Returns the id that ended up on
|
|
332
|
+
// disk, or null on any unrecoverable error. EEXIST races re-read.
|
|
333
|
+
function _writeWorkspaceIdToFs(file, id) {
|
|
334
|
+
const dir = path.dirname(file);
|
|
264
335
|
try {
|
|
336
|
+
// Refuse to write if `.evolver` is a symlink. mkdirSync({recursive:true})
|
|
337
|
+
// happily traverses an existing symlinked directory and the subsequent
|
|
338
|
+
// open() lands the secret file in the attacker-controlled target —
|
|
339
|
+
// O_NOFOLLOW only guards the FINAL path component, not intermediate
|
|
340
|
+
// directories. The pre-refactor monolithic getWorkspaceId() returned
|
|
341
|
+
// null on a symlinked dir before reaching the write; preserve that
|
|
342
|
+
// here (Bugbot PR #121 round-1 HIGH; original guard PR #109 round-2 HIGH).
|
|
343
|
+
const dirStat = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
344
|
+
if (dirStat && dirStat.isSymbolicLink()) return null;
|
|
265
345
|
fs.mkdirSync(dir, { recursive: true });
|
|
266
|
-
const
|
|
346
|
+
const payload = id || require('crypto').randomBytes(16).toString('hex');
|
|
267
347
|
// Atomic create-and-fail-if-exists so we never overwrite an
|
|
268
348
|
// attacker-pre-placed file (TOCTOU between lstat and writeFileSync
|
|
269
349
|
// could otherwise race a symlink in). O_NOFOLLOW also refuses to
|
|
@@ -276,32 +356,106 @@ function getWorkspaceId() {
|
|
|
276
356
|
try {
|
|
277
357
|
fd = fs.openSync(file, flags, 0o600);
|
|
278
358
|
} catch (e) {
|
|
279
|
-
// EEXIST means another process beat us to it — re-read with the
|
|
280
|
-
// same symlink guards as above.
|
|
281
359
|
if (e && e.code === 'EEXIST') {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
try {
|
|
285
|
-
const raw = fs.readFileSync(file, 'utf8').trim();
|
|
286
|
-
if (raw && /^[a-f0-9]{32,}$/i.test(raw)) return raw;
|
|
287
|
-
} catch { /* unreadable */ }
|
|
288
|
-
return null;
|
|
360
|
+
// Another process beat us — re-read with the same symlink guards.
|
|
361
|
+
return _readWorkspaceIdFromFs(file);
|
|
289
362
|
}
|
|
290
363
|
// ELOOP / EMLINK from O_NOFOLLOW hitting a symlink — refuse.
|
|
291
364
|
return null;
|
|
292
365
|
}
|
|
293
366
|
try {
|
|
294
|
-
fs.writeSync(fd,
|
|
367
|
+
fs.writeSync(fd, payload + '\n', 0, 'utf8');
|
|
295
368
|
} finally {
|
|
296
369
|
fs.closeSync(fd);
|
|
297
370
|
}
|
|
298
371
|
try { fs.chmodSync(file, 0o600); } catch { /* best-effort */ }
|
|
299
|
-
return
|
|
372
|
+
return payload;
|
|
300
373
|
} catch {
|
|
301
374
|
return null;
|
|
302
375
|
}
|
|
303
376
|
}
|
|
304
377
|
|
|
378
|
+
function getWorkspaceId() {
|
|
379
|
+
if (process.env.EVOLVER_WORKSPACE_ID) return String(process.env.EVOLVER_WORKSPACE_ID);
|
|
380
|
+
const workspaceRoot = getWorkspaceRoot();
|
|
381
|
+
const dir = path.join(workspaceRoot, '.evolver');
|
|
382
|
+
const file = path.join(dir, 'workspace-id');
|
|
383
|
+
|
|
384
|
+
let mode = 'off';
|
|
385
|
+
let keychain = null;
|
|
386
|
+
try {
|
|
387
|
+
keychain = require('./workspaceKeychain');
|
|
388
|
+
mode = keychain.getMode();
|
|
389
|
+
} catch {
|
|
390
|
+
// workspaceKeychain.js missing — degrade silently to FS-only.
|
|
391
|
+
mode = 'off';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (mode !== 'off' && keychain) {
|
|
395
|
+
const addonAvailable = keychain.loadAddon() !== null;
|
|
396
|
+
if (mode === 'force' && !addonAvailable) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
'EVOLVER_WORKSPACE_KEYCHAIN=force but @napi-rs/keyring is not installed. ' +
|
|
399
|
+
'Install it (`npm i @napi-rs/keyring`) or set EVOLVER_WORKSPACE_KEYCHAIN=auto/off.'
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
if (addonAvailable) {
|
|
403
|
+
const hit = keychain.readFromKeychain(workspaceRoot);
|
|
404
|
+
if (hit.available && hit.id) return hit.id;
|
|
405
|
+
|
|
406
|
+
// `force` must NEVER fall back to FS read/write — that would
|
|
407
|
+
// silently re-introduce same-uid plaintext exposure of the
|
|
408
|
+
// workspace secret, which is exactly what `force` exists to
|
|
409
|
+
// prevent (Bugbot PR #121 round-2 MEDIUM Agentic Security).
|
|
410
|
+
// Generate a fresh id and write it ONLY to the keychain; if
|
|
411
|
+
// that write fails, throw rather than mirror to FS.
|
|
412
|
+
if (mode === 'force') {
|
|
413
|
+
if (hit.available) {
|
|
414
|
+
// Keychain reachable but empty — mint and write keychain-only.
|
|
415
|
+
const newId = require('crypto').randomBytes(16).toString('hex');
|
|
416
|
+
if (!keychain.writeToKeychain(workspaceRoot, newId)) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
'EVOLVER_WORKSPACE_KEYCHAIN=force: keychain write failed; ' +
|
|
419
|
+
'refusing to fall back to filesystem secret.'
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return newId;
|
|
423
|
+
}
|
|
424
|
+
// Addon loaded but read claims unavailable (e.g. locked
|
|
425
|
+
// keyring on Linux, no D-Bus session). Refuse rather than
|
|
426
|
+
// silently degrade.
|
|
427
|
+
throw new Error(
|
|
428
|
+
'EVOLVER_WORKSPACE_KEYCHAIN=force: keychain reports unavailable ' +
|
|
429
|
+
'(locked keyring / no session?); refusing to fall back to filesystem.'
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// mode === 'auto', keychain miss — try to migrate an existing
|
|
434
|
+
// FS secret in.
|
|
435
|
+
const fsId = _readWorkspaceIdFromFs(file);
|
|
436
|
+
if (fsId) {
|
|
437
|
+
keychain.writeToKeychain(workspaceRoot, fsId); // best-effort
|
|
438
|
+
return fsId;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// No secret anywhere — generate, write FS atomically, then
|
|
442
|
+
// mirror to keychain. FS write is the source of truth for the
|
|
443
|
+
// value (race-resistant via O_EXCL); keychain is the upgrade.
|
|
444
|
+
const newId = _writeWorkspaceIdToFs(file, null);
|
|
445
|
+
if (!newId) return null;
|
|
446
|
+
keychain.writeToKeychain(workspaceRoot, newId); // best-effort
|
|
447
|
+
return newId;
|
|
448
|
+
}
|
|
449
|
+
// mode === 'auto' && addon unavailable → fall through to FS.
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// FS-only path (mode === 'off' or auto-fallback). Identical to the
|
|
453
|
+
// pre-#111 implementation in observable behavior.
|
|
454
|
+
const existing = _readWorkspaceIdFromFs(file);
|
|
455
|
+
if (existing) return existing;
|
|
456
|
+
return _writeWorkspaceIdToFs(file, null);
|
|
457
|
+
}
|
|
458
|
+
|
|
305
459
|
module.exports = {
|
|
306
460
|
getRepoRoot,
|
|
307
461
|
getEvolverInstallRoot,
|
|
@@ -319,4 +473,6 @@ module.exports = {
|
|
|
319
473
|
getNarrativePath,
|
|
320
474
|
getEvolutionPrinciplesPath,
|
|
321
475
|
getReflectionLogPath,
|
|
476
|
+
getEvomapDir,
|
|
477
|
+
getEvomapPath,
|
|
322
478
|
};
|