@ijfw/memory-server 1.4.3 → 1.5.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/fixtures/truncation-corpus/_generate-corpus.js +367 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/intent-journal.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-01-clean-exit-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/snapshots/v-midO-1-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/snapshots/v-midO-2-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/snapshots/v-midO-3-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/snapshots/v-midO-4-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/snapshots/v-midO-5-advance.json +11 -0
- package/fixtures/truncation-corpus/fx-02-mid-overwrite-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-01/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-03/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/events.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-03-mid-append-05/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/snapshots/v-noEv-1-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/snapshots/v-noEv-2-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-02/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/snapshots/v-noEv-3-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/snapshots/v-noEv-4-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-04/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/events.jsonl +0 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/intent-journal.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/snapshots/v-noEv-5-set-phase.json +11 -0
- package/fixtures/truncation-corpus/fx-04-no-events-05/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/snapshots/v-errT-1-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-01/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-02/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/snapshots/v-errT-3-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-03/target/.ijfw/state/workflow.json +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-04/target/.ijfw/blackboard/decisions.jsonl +1 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/events.jsonl +2 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/intent-journal.jsonl +3 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/meta.json +18 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/snapshots/v-errT-5-partial.json +11 -0
- package/fixtures/truncation-corpus/fx-05-error-terminated-05/target/.ijfw/state/workflow.json +1 -0
- package/package.json +1 -1
- package/src/active-extension-writer.js +144 -64
- package/src/api-client.js +43 -5
- package/src/audit-roster.js +80 -5
- package/src/blackboard.js +298 -6
- package/src/cli-run.js +33 -5
- package/src/codex-agents.js +96 -5
- package/src/cost/aggregator.js +39 -9
- package/src/cost/pricing.js +57 -0
- package/src/cost/readers/gemini.js +1 -1
- package/src/cross-audit-chunker.js +189 -0
- package/src/cross-dispatcher.js +124 -21
- package/src/cross-orchestrator-cli.js +550 -14
- package/src/cross-orchestrator.js +1171 -10
- package/src/cross-project-search.js +195 -9
- package/src/dashboard-client-planning.html +273 -0
- package/src/dashboard-client-waves.html +304 -0
- package/src/dashboard-client.html +17 -2
- package/src/dashboard-server.js +152 -0
- package/src/deploy-alerts.js +150 -0
- package/src/design/iframe-bridge.js +242 -0
- package/src/design-companion.js +144 -0
- package/src/dispatch/checkpoint-cli.js +97 -0
- package/src/dispatch/colon-syntax.js +81 -1
- package/src/dispatch/extension.js +27 -1
- package/src/dispatch/registry-cli.js +4 -1
- package/src/dispatch/wave-cli.js +323 -0
- package/src/dispatch/worktree-cli.js +40 -0
- package/src/dispatch-planner.js +97 -2
- package/src/dream/runner.mjs +47 -11
- package/src/dream/stage-runner.js +40 -0
- package/src/dream/state-file.js +102 -0
- package/src/extension-installer.js +70 -24
- package/src/extension-quota-tracker.js +4 -2
- package/src/extension-registry.js +289 -35
- package/src/feedback-detector.js +26 -0
- package/src/fs-lock.js +259 -7
- package/src/gate-result.js +95 -1
- package/src/hero-line.js +86 -5
- package/src/intent-router.js +35 -0
- package/src/lib/a11y-contract.js +117 -0
- package/src/lib/atomic-io.js +29 -8
- package/src/lib/cache-keepalive.js +150 -0
- package/src/lib/jsonl-rotation.js +104 -0
- package/src/lib/lighthouse-pillar.js +121 -0
- package/src/lib/llm-call.js +121 -0
- package/src/lib/playwright-baseline.js +205 -0
- package/src/lib/rekor-bridge.js +221 -0
- package/src/lib/repo-map.js +392 -0
- package/src/lib/shasum-verify.js +164 -0
- package/src/lib/sketches-gc.js +132 -0
- package/src/lib/tmp-suffix.js +62 -0
- package/src/lib/ui-review-runner.js +554 -0
- package/src/lib/uispec-drift.js +301 -0
- package/src/lib/uispec-intake.js +381 -0
- package/src/lib/worktree-guards.js +118 -0
- package/src/lib/worktree-recovery.js +100 -0
- package/src/memory/auto-linker.js +152 -0
- package/src/memory/benchmark.js +498 -0
- package/src/memory/dedup.js +126 -0
- package/src/memory/embedding-cache.js +136 -0
- package/src/memory/fact-extractor.js +168 -0
- package/src/memory/fts5.js +65 -1
- package/src/memory/migrations/004-bitemporal.js +91 -0
- package/src/memory/migrations/005-vector-cache.js +61 -0
- package/src/memory/migrations/006-obsidian-graph.js +46 -0
- package/src/memory/migrations/007-skill-telemetry.js +24 -0
- package/src/memory/migrations/008-write-provenance.js +41 -0
- package/src/memory/obsidian-parser.js +91 -0
- package/src/memory/query-dataview.js +86 -0
- package/src/memory/search.js +10 -0
- package/src/memory/temporal.js +529 -0
- package/src/memory/tokenize.js +10 -0
- package/src/memory-facts-handler.js +37 -0
- package/src/memory-feedback.js +260 -2
- package/src/model-refresh.js +292 -0
- package/src/observability/cost-anomaly.js +166 -0
- package/src/observability/evaluator-checkpoint-contract.js +117 -0
- package/src/observability/trace-id.js +163 -0
- package/src/orchestrator/agents-md-blackboard.js +152 -0
- package/src/orchestrator/checkpoint-contract.md +140 -0
- package/src/orchestrator/debug-trident.js +570 -0
- package/src/orchestrator/merge-block-aware.js +350 -0
- package/src/orchestrator/plan-checker.js +475 -0
- package/src/orchestrator/post-done-runner.js +249 -0
- package/src/orchestrator/review.js +136 -0
- package/src/orchestrator/runtime-loop.js +430 -0
- package/src/orchestrator/skill-telemetry-sink.js +29 -0
- package/src/orchestrator/skill-telemetry.js +37 -0
- package/src/orchestrator/state-events.js +459 -0
- package/src/orchestrator/state-sdk.js +1764 -0
- package/src/orchestrator/status-protocol.js +235 -0
- package/src/orchestrator/subagent-telemetry.js +452 -0
- package/src/orchestrator/termination.js +160 -0
- package/src/orchestrator/verification-gate.js +281 -0
- package/src/orchestrator/wave-state.js +564 -0
- package/src/orchestrator/worktree-provision.js +77 -0
- package/src/override-use-registry.js +111 -5
- package/src/receipts.js +36 -4
- package/src/recovery/checkpoint.js +56 -3
- package/src/recovery/code-fixer.js +656 -0
- package/src/recovery/truncation.js +317 -0
- package/src/redactor.js +75 -6
- package/src/runtime-mediator.js +15 -0
- package/src/sanitizer.js +10 -0
- package/src/search-hybrid.js +139 -0
- package/src/server.js +603 -59
- package/src/swarm/worktree.js +27 -4
- package/src/swarm-config.js +113 -12
- package/src/team/domain-templates/book.json +51 -0
- package/src/team/domain-templates/business.json +41 -0
- package/src/team/domain-templates/content.json +50 -0
- package/src/team/domain-templates/design.json +44 -0
- package/src/team/domain-templates/research.json +41 -0
- package/src/team/domain-templates/software.json +40 -0
- package/src/team/generator.js +278 -3
- package/src/team/modify.js +203 -0
- package/src/team/schemas.js +48 -0
- package/src/update-apply.js +19 -3
|
@@ -21,12 +21,17 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { createPublicKey, createHash, verify as cryptoVerify } from 'node:crypto';
|
|
24
|
-
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
|
|
24
|
+
import { readFile, writeFile, mkdir, rename, readdir, stat, unlink } from 'node:fs/promises';
|
|
25
25
|
import { homedir } from 'node:os';
|
|
26
26
|
import { join, dirname } from 'node:path';
|
|
27
27
|
import https from 'node:https';
|
|
28
28
|
|
|
29
29
|
import { withFsLock } from './fs-lock.js';
|
|
30
|
+
import {
|
|
31
|
+
submitToRekor as _submitToRekor,
|
|
32
|
+
verifyRekorEntry as _verifyRekorEntry,
|
|
33
|
+
hasRekorClient as _hasRekorClient,
|
|
34
|
+
} from './lib/rekor-bridge.js';
|
|
30
35
|
|
|
31
36
|
// ---------------------------------------------------------------------------
|
|
32
37
|
// Embedded meta-key — compiled-in trust root for registry signature verification.
|
|
@@ -133,7 +138,12 @@ function sortKeysDeep(v) {
|
|
|
133
138
|
function registryCanonicalBytes(registry) {
|
|
134
139
|
const shallow = {};
|
|
135
140
|
for (const k of Object.keys(registry)) {
|
|
136
|
-
|
|
141
|
+
// Exclude `signature` (carries the sig itself) and `rekor` (added AFTER
|
|
142
|
+
// signing as a transparency-log anchor — v1.5.0 audit-H5.7). Both must
|
|
143
|
+
// be excluded so sign-time and verify-time produce byte-identical input.
|
|
144
|
+
// Backcompat: pre-v1.5.0 registries have no `rekor` field, so this is
|
|
145
|
+
// a no-op for them.
|
|
146
|
+
if (k === 'signature' || k === 'rekor') continue;
|
|
137
147
|
shallow[k] = registry[k];
|
|
138
148
|
}
|
|
139
149
|
return Buffer.from(JSON.stringify(sortKeysDeep(shallow)), 'utf8');
|
|
@@ -312,6 +322,27 @@ export function verifyRegistry(body, opts = {}) {
|
|
|
312
322
|
return { valid: false, registry: null, reason: 'revoked must be an array' };
|
|
313
323
|
}
|
|
314
324
|
|
|
325
|
+
// v1.5.0 audit-H5.7: when an embedded `rekor` anchor is present, validate
|
|
326
|
+
// its shape here (sync, applies to ALL signature paths including seed-mode).
|
|
327
|
+
// The actual network cross-check happens in `crossCheckRekor` (async).
|
|
328
|
+
if (parsed.rekor !== undefined && parsed.rekor !== null) {
|
|
329
|
+
const r = parsed.rekor;
|
|
330
|
+
if (
|
|
331
|
+
typeof r !== 'object' ||
|
|
332
|
+
Array.isArray(r) ||
|
|
333
|
+
typeof r.uuid !== 'string' ||
|
|
334
|
+
r.uuid.length === 0 ||
|
|
335
|
+
typeof r.logIndex !== 'number' ||
|
|
336
|
+
typeof r.integratedTime !== 'number'
|
|
337
|
+
) {
|
|
338
|
+
return {
|
|
339
|
+
valid: false,
|
|
340
|
+
registry: null,
|
|
341
|
+
reason: 'rekor field malformed (expected {uuid, logIndex, integratedTime})',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
315
346
|
// Signature verification — null signature is only accepted in seed/bootstrap mode.
|
|
316
347
|
if (parsed.signature === null) {
|
|
317
348
|
const allowSeed = opts.allowSeed === true || process.env.IJFW_ALLOW_SEED_REGISTRY === '1';
|
|
@@ -360,6 +391,50 @@ export function verifyRegistry(body, opts = {}) {
|
|
|
360
391
|
return { valid: true, registry: parsed, reason: 'ok' };
|
|
361
392
|
}
|
|
362
393
|
|
|
394
|
+
/**
|
|
395
|
+
* v1.5.0 audit-H5.7: optional Rekor transparency-log cross-check.
|
|
396
|
+
*
|
|
397
|
+
* Run AFTER `verifyRegistry` returns valid:true. When the registry has an
|
|
398
|
+
* embedded `rekor` field AND a Rekor client is available locally, this
|
|
399
|
+
* looks the entry up in the live log and confirms its payload hash matches
|
|
400
|
+
* the registry we just verified locally.
|
|
401
|
+
*
|
|
402
|
+
* Decision matrix:
|
|
403
|
+
* - no `rekor` field → returns { ok: true, reason: 'no rekor anchor' }
|
|
404
|
+
* (backcompat — registries signed before this
|
|
405
|
+
* lift still verify on Ed25519 alone).
|
|
406
|
+
* - no Rekor client available → returns { ok: true, reason: 'no rekor client' }
|
|
407
|
+
* (offline clients accept Ed25519 alone).
|
|
408
|
+
* - entry mismatch → returns { ok: false, reason: 'rekor payload mismatch — REJECT' }
|
|
409
|
+
* Caller MUST reject the registry; this is the
|
|
410
|
+
* detection signal for meta-key compromise.
|
|
411
|
+
* - entry matches → returns { ok: true, reason: 'rekor cross-check ok' }
|
|
412
|
+
*
|
|
413
|
+
* @param {object} registry verified registry object (from verifyRegistry)
|
|
414
|
+
* @returns {Promise<{ ok: boolean, reason: string }>}
|
|
415
|
+
*/
|
|
416
|
+
export async function crossCheckRekor(registry) {
|
|
417
|
+
if (!registry || typeof registry !== 'object' || registry.rekor === undefined || registry.rekor === null) {
|
|
418
|
+
return { ok: true, reason: 'no rekor anchor' };
|
|
419
|
+
}
|
|
420
|
+
const hasClient = await _hasRekorClient();
|
|
421
|
+
if (!hasClient) {
|
|
422
|
+
return { ok: true, reason: 'no rekor client' };
|
|
423
|
+
}
|
|
424
|
+
const bytes = registryCanonicalBytes(registry);
|
|
425
|
+
const result = await _verifyRekorEntry({ uuid: registry.rekor.uuid, payload: bytes });
|
|
426
|
+
if (result === null) {
|
|
427
|
+
// Lookup failed (network error, malformed response). Be conservative but
|
|
428
|
+
// backcompat-friendly: accept on Ed25519 alone — same posture as offline.
|
|
429
|
+
return { ok: true, reason: 'rekor lookup unavailable — accepting on ed25519' };
|
|
430
|
+
}
|
|
431
|
+
if (result === false) {
|
|
432
|
+
return { ok: false, reason: 'rekor payload mismatch — REJECT' };
|
|
433
|
+
}
|
|
434
|
+
return { ok: true, reason: 'rekor cross-check ok' };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
363
438
|
// ---------------------------------------------------------------------------
|
|
364
439
|
// loadRegistrySources — read ~/.ijfw/registries.json or fall back to single-source.
|
|
365
440
|
// ---------------------------------------------------------------------------
|
|
@@ -628,13 +703,104 @@ async function atomicWriteJson(filePath, payload) {
|
|
|
628
703
|
await rename(tmp, filePath);
|
|
629
704
|
}
|
|
630
705
|
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
707
|
+
// Federated per-source cache quota (v1.5.0 audit M10 / F-PRF-2).
|
|
708
|
+
//
|
|
709
|
+
// Per-source caches live at `~/.ijfw/state/registry-cache-<name>.json`. With
|
|
710
|
+
// the 1 MiB MAX_REGISTRY_BYTES per-body cap and N federated sources, the disk
|
|
711
|
+
// footprint can grow unbounded as users add/remove sources over time (orphan
|
|
712
|
+
// caches for deactivated sources are never cleaned up). We enforce two caps:
|
|
713
|
+
//
|
|
714
|
+
// - IJFW_FEDERATED_CACHE_MAX_SOURCES (default 32) — file count
|
|
715
|
+
// - IJFW_FEDERATED_CACHE_MAX_BYTES (default 64 MiB) — total bytes
|
|
716
|
+
//
|
|
717
|
+
// On overflow we LRU-evict (mtime ascending) until we're under both caps.
|
|
718
|
+
// Eviction skips the file we're about to write so a fresh write never
|
|
719
|
+
// self-evicts; this is a soft contract for the caller — the next overflow
|
|
720
|
+
// pass will re-evaluate.
|
|
721
|
+
// ---------------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
const REGISTRY_CACHE_PREFIX = 'registry-cache-';
|
|
724
|
+
const REGISTRY_CACHE_SUFFIX = '.json';
|
|
725
|
+
const DEFAULT_FEDERATED_CACHE_MAX_SOURCES = 32;
|
|
726
|
+
const DEFAULT_FEDERATED_CACHE_MAX_BYTES = 64 * 1024 * 1024;
|
|
727
|
+
|
|
728
|
+
function envIntCap(name, fallback) {
|
|
729
|
+
const raw = process.env[name];
|
|
730
|
+
if (typeof raw !== 'string' || raw.length === 0) return fallback;
|
|
731
|
+
const n = Number.parseInt(raw, 10);
|
|
732
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
733
|
+
return n;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function listSourceCacheFiles() {
|
|
737
|
+
const dir = ijfwStateDir();
|
|
738
|
+
let names;
|
|
739
|
+
try {
|
|
740
|
+
names = await readdir(dir);
|
|
741
|
+
} catch {
|
|
742
|
+
return [];
|
|
743
|
+
}
|
|
744
|
+
const out = [];
|
|
745
|
+
for (const name of names) {
|
|
746
|
+
if (!name.startsWith(REGISTRY_CACHE_PREFIX)) continue;
|
|
747
|
+
if (!name.endsWith(REGISTRY_CACHE_SUFFIX)) continue;
|
|
748
|
+
// Skip the legacy single-source cache (no source name infix).
|
|
749
|
+
if (name === 'registry-cache.json') continue;
|
|
750
|
+
const filePath = join(dir, name);
|
|
751
|
+
let st;
|
|
752
|
+
try {
|
|
753
|
+
st = await stat(filePath);
|
|
754
|
+
} catch {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (!st.isFile()) continue;
|
|
758
|
+
out.push({ path: filePath, name, size: st.size, mtimeMs: st.mtimeMs });
|
|
759
|
+
}
|
|
760
|
+
return out;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Enforce the federated cache quota by evicting oldest-mtime cache files until
|
|
765
|
+
* both the file-count and total-bytes caps are satisfied. `protectPath` is
|
|
766
|
+
* excluded from eviction so a freshly-written cache survives its own pass.
|
|
767
|
+
*
|
|
768
|
+
* Returns the list of evicted file paths (for observability / tests).
|
|
769
|
+
*/
|
|
770
|
+
export async function enforceFederatedCacheQuota({ protectPath = null } = {}) {
|
|
771
|
+
const maxSources = envIntCap('IJFW_FEDERATED_CACHE_MAX_SOURCES', DEFAULT_FEDERATED_CACHE_MAX_SOURCES);
|
|
772
|
+
const maxBytes = envIntCap('IJFW_FEDERATED_CACHE_MAX_BYTES', DEFAULT_FEDERATED_CACHE_MAX_BYTES);
|
|
773
|
+
const files = await listSourceCacheFiles();
|
|
774
|
+
if (files.length === 0) return [];
|
|
775
|
+
|
|
776
|
+
files.sort((a, b) => a.mtimeMs - b.mtimeMs); // oldest first
|
|
777
|
+
let totalBytes = files.reduce((acc, f) => acc + f.size, 0);
|
|
778
|
+
let count = files.length;
|
|
779
|
+
const evicted = [];
|
|
780
|
+
|
|
781
|
+
for (const f of files) {
|
|
782
|
+
if (count <= maxSources && totalBytes <= maxBytes) break;
|
|
783
|
+
if (protectPath && f.path === protectPath) continue;
|
|
784
|
+
try {
|
|
785
|
+
await unlink(f.path);
|
|
786
|
+
evicted.push(f.path);
|
|
787
|
+
totalBytes -= f.size;
|
|
788
|
+
count -= 1;
|
|
789
|
+
} catch {
|
|
790
|
+
// Best-effort: failure to unlink is non-fatal; the next pass retries.
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return evicted;
|
|
794
|
+
}
|
|
795
|
+
|
|
631
796
|
/**
|
|
632
797
|
* Mutate the per-source cache inside an exclusive fs-lock.
|
|
633
798
|
* @param {object} source — entry from loadRegistrySources()
|
|
634
799
|
* @param {(cache: object) => object|Promise<object>} mutator
|
|
635
800
|
*/
|
|
636
801
|
export async function withSourceCache(source, mutator) {
|
|
637
|
-
|
|
802
|
+
const targetPath = perSourceCachePath(source.name);
|
|
803
|
+
const result = await withFsLock(perSourceLockPath(source.name), async () => {
|
|
638
804
|
const { cache, corrupt, reason } = await readSourceCache(source);
|
|
639
805
|
if (corrupt) {
|
|
640
806
|
// The mutator still gets a fresh empty cache to write into. We surface
|
|
@@ -642,16 +808,26 @@ export async function withSourceCache(source, mutator) {
|
|
|
642
808
|
const next = await mutator({ ...cache, _corruptReason: reason });
|
|
643
809
|
if (next && typeof next === 'object') {
|
|
644
810
|
delete next._corruptReason;
|
|
645
|
-
await atomicWriteJson(
|
|
811
|
+
await atomicWriteJson(targetPath, next);
|
|
646
812
|
}
|
|
647
813
|
return { corrupt: true, reason };
|
|
648
814
|
}
|
|
649
815
|
const next = await mutator(cache);
|
|
650
816
|
if (next && typeof next === 'object') {
|
|
651
|
-
await atomicWriteJson(
|
|
817
|
+
await atomicWriteJson(targetPath, next);
|
|
652
818
|
}
|
|
653
819
|
return { corrupt: false };
|
|
654
820
|
});
|
|
821
|
+
// v1.5.0 audit M10: enforce the federated cache quota AFTER the write
|
|
822
|
+
// releases its per-source lock. We exclude the just-written file so a fresh
|
|
823
|
+
// write never self-evicts. Failures here are non-fatal — caller already has
|
|
824
|
+
// a consistent on-disk cache.
|
|
825
|
+
try {
|
|
826
|
+
await enforceFederatedCacheQuota({ protectPath: targetPath });
|
|
827
|
+
} catch {
|
|
828
|
+
// Silent: quota is a hygiene knob, not a correctness gate.
|
|
829
|
+
}
|
|
830
|
+
return result;
|
|
655
831
|
}
|
|
656
832
|
|
|
657
833
|
// ---------------------------------------------------------------------------
|
|
@@ -969,20 +1145,38 @@ export async function refreshTrustFromAllRegistries(opts = {}) {
|
|
|
969
1145
|
|
|
970
1146
|
const appliedSources = [];
|
|
971
1147
|
|
|
972
|
-
|
|
1148
|
+
// v1.5.0 audit M9 (F-SPD-3): parallelise the per-source pipeline. Previously
|
|
1149
|
+
// this was a sequential `for await` loop — with the 10s fetch timeout and N
|
|
1150
|
+
// federated sources, worst-case wait was N×10s (50s for 5 sources). The
|
|
1151
|
+
// network-bound and cache-IO phases for each source are independent, so we
|
|
1152
|
+
// fan out with Promise.all and process results in priority order afterward
|
|
1153
|
+
// (preserves deterministic warning order + applyMultiRegistry input order).
|
|
1154
|
+
//
|
|
1155
|
+
// Per-source failures are caught inside the worker so one slow/throwing
|
|
1156
|
+
// source can never block the others; the worker returns a result envelope
|
|
1157
|
+
// with `error` set, and the post-merge loop converts it into the same
|
|
1158
|
+
// skip-with-warning shape the old sequential code produced.
|
|
1159
|
+
async function processSource(source) {
|
|
973
1160
|
const fetchImpl = typeof opts.fetchImpl === 'function'
|
|
974
1161
|
? (url, part) => opts.fetchImpl(url, source, part)
|
|
975
1162
|
: null;
|
|
976
1163
|
|
|
977
1164
|
let cache;
|
|
978
1165
|
let corruptReason = null;
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1166
|
+
let workerWarnings = [];
|
|
1167
|
+
try {
|
|
1168
|
+
const cacheRead = await readSourceCache(source);
|
|
1169
|
+
cache = cacheRead.cache;
|
|
1170
|
+
if (cacheRead.corrupt) {
|
|
1171
|
+
corruptReason = cacheRead.reason;
|
|
1172
|
+
workerWarnings.push(`[ijfw] WARNING: cache for source '${source.name}' corrupt (${cacheRead.reason}) — ignored; falling back to network`);
|
|
1173
|
+
}
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
// Defensive: readSourceCache should always return; if it throws treat as
|
|
1176
|
+
// corrupt so we still fetch network and try to recover.
|
|
1177
|
+
cache = emptySourceCache(source);
|
|
1178
|
+
corruptReason = `read_throw:${err.message}`;
|
|
1179
|
+
workerWarnings.push(`[ijfw] WARNING: cache for source '${source.name}' read threw (${err.message}) — falling back to network`);
|
|
986
1180
|
}
|
|
987
1181
|
|
|
988
1182
|
const now = Date.now();
|
|
@@ -994,35 +1188,72 @@ export async function refreshTrustFromAllRegistries(opts = {}) {
|
|
|
994
1188
|
let mergedRegistry = null;
|
|
995
1189
|
let fetchError = null;
|
|
996
1190
|
|
|
997
|
-
// Always fetch the full registry when we want publishers (the response is
|
|
998
|
-
// the source of truth for both parts). When ONLY revocation is stale we
|
|
999
|
-
// still issue a fetch (server returns the same JSON; CDN can cache
|
|
1000
|
-
// separately if it cares about ?part=revoked).
|
|
1001
1191
|
if (wantPublishers || wantRevocation) {
|
|
1002
1192
|
const part = wantPublishers ? 'all' : 'revoked';
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const verified = verifyRegistry(fetched.body, {
|
|
1008
|
-
metaKeyPem: source.meta_key_pem,
|
|
1009
|
-
allowSeed: opts.allowSeed,
|
|
1010
|
-
});
|
|
1011
|
-
if (!verified.valid) {
|
|
1012
|
-
fetchError = `verify failed: ${verified.reason}`;
|
|
1193
|
+
try {
|
|
1194
|
+
const fetched = await fetchRegistry(source.url, { fetchImpl, part });
|
|
1195
|
+
if (!fetched.ok) {
|
|
1196
|
+
fetchError = fetched.error;
|
|
1013
1197
|
} else {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1198
|
+
const verified = verifyRegistry(fetched.body, {
|
|
1199
|
+
metaKeyPem: source.meta_key_pem,
|
|
1200
|
+
allowSeed: opts.allowSeed,
|
|
1201
|
+
});
|
|
1202
|
+
if (!verified.valid) {
|
|
1203
|
+
fetchError = `verify failed: ${verified.reason}`;
|
|
1204
|
+
} else {
|
|
1205
|
+
if (verified.warnings) {
|
|
1206
|
+
for (const w of verified.warnings) {
|
|
1207
|
+
workerWarnings.push(`[ijfw] WARNING (source=${source.name}): ${w}`);
|
|
1208
|
+
}
|
|
1019
1209
|
}
|
|
1210
|
+
mergedRegistry = verified.registry;
|
|
1020
1211
|
}
|
|
1021
|
-
mergedRegistry = verified.registry;
|
|
1022
1212
|
}
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
// Worker-level catch — if fetchRegistry/verifyRegistry throws we
|
|
1215
|
+
// surface it as a regular fetch error so the cache-fallback path
|
|
1216
|
+
// below can run, instead of poisoning the whole Promise.all batch.
|
|
1217
|
+
fetchError = `fetch threw: ${err.message}`;
|
|
1023
1218
|
}
|
|
1024
1219
|
}
|
|
1025
1220
|
|
|
1221
|
+
return {
|
|
1222
|
+
source,
|
|
1223
|
+
cache,
|
|
1224
|
+
corruptReason,
|
|
1225
|
+
wantPublishers,
|
|
1226
|
+
wantRevocation,
|
|
1227
|
+
mergedRegistry,
|
|
1228
|
+
fetchError,
|
|
1229
|
+
workerWarnings,
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Promise.all — N×10s worst case collapses to ~10s. safe-by-construction:
|
|
1234
|
+
// every worker catches its own errors and returns a result envelope.
|
|
1235
|
+
const processed = await Promise.all(sources.map((s) => processSource(s)));
|
|
1236
|
+
|
|
1237
|
+
// Post-merge: walk results in original priority order so warnings + cache
|
|
1238
|
+
// writes + appliedSources accumulate deterministically. This is the same
|
|
1239
|
+
// sequence the old `for` loop produced; only the network/cache I/O phase
|
|
1240
|
+
// moved to parallel.
|
|
1241
|
+
for (const result of processed) {
|
|
1242
|
+
const {
|
|
1243
|
+
source,
|
|
1244
|
+
cache,
|
|
1245
|
+
corruptReason,
|
|
1246
|
+
wantPublishers,
|
|
1247
|
+
mergedRegistry,
|
|
1248
|
+
fetchError,
|
|
1249
|
+
workerWarnings,
|
|
1250
|
+
} = result;
|
|
1251
|
+
|
|
1252
|
+
for (const w of workerWarnings) {
|
|
1253
|
+
process.stderr.write(w + '\n');
|
|
1254
|
+
warnings.push(w);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1026
1257
|
// Decide what cache to write + which registry to apply.
|
|
1027
1258
|
if (mergedRegistry) {
|
|
1028
1259
|
// Update cache.
|
|
@@ -1245,9 +1476,32 @@ export async function signRegistry(registryPath, opts = {}) {
|
|
|
1245
1476
|
|
|
1246
1477
|
registry.updated_at = new Date().toISOString();
|
|
1247
1478
|
delete registry.signature;
|
|
1479
|
+
delete registry.rekor;
|
|
1248
1480
|
const bytes = registryCanonicalBytes(registry);
|
|
1249
1481
|
const sigBuf = cryptoSign(null, bytes, privKey);
|
|
1250
|
-
|
|
1482
|
+
const signature = `ed25519:${sigBuf.toString('base64')}`;
|
|
1483
|
+
registry.signature = signature;
|
|
1484
|
+
|
|
1485
|
+
// v1.5.0 audit-H5.7: anchor the signed registry to Sigstore Rekor when a
|
|
1486
|
+
// client is available. The Rekor entry attests to {payload, signature,
|
|
1487
|
+
// publicKey} — downstream verifiers cross-check the embedded uuid against
|
|
1488
|
+
// the live log so a meta-key swap is detectable. Graceful no-op when the
|
|
1489
|
+
// peer dep is absent.
|
|
1490
|
+
try {
|
|
1491
|
+
const publicKeyPem = createPublicKey(privKey)
|
|
1492
|
+
.export({ type: 'spki', format: 'pem' })
|
|
1493
|
+
.toString();
|
|
1494
|
+
const rekorEntry = await _submitToRekor({
|
|
1495
|
+
payload: bytes,
|
|
1496
|
+
signature,
|
|
1497
|
+
publicKey: publicKeyPem,
|
|
1498
|
+
});
|
|
1499
|
+
if (rekorEntry !== null) {
|
|
1500
|
+
registry.rekor = rekorEntry;
|
|
1501
|
+
}
|
|
1502
|
+
} catch {
|
|
1503
|
+
// submitToRekor never throws by contract; this guard is belt-and-braces.
|
|
1504
|
+
}
|
|
1251
1505
|
|
|
1252
1506
|
try {
|
|
1253
1507
|
await writeFile(abs, JSON.stringify(registry, null, 2) + '\n', 'utf8');
|
|
@@ -1255,7 +1509,7 @@ export async function signRegistry(registryPath, opts = {}) {
|
|
|
1255
1509
|
return { ok: false, error: `write failed: ${err.message}` };
|
|
1256
1510
|
}
|
|
1257
1511
|
|
|
1258
|
-
return { ok: true };
|
|
1512
|
+
return { ok: true, rekor: registry.rekor || null };
|
|
1259
1513
|
}
|
|
1260
1514
|
|
|
1261
1515
|
export async function verifyRegistryFile(registryPath) {
|
package/src/feedback-detector.js
CHANGED
|
@@ -30,8 +30,34 @@ const PATTERNS = [
|
|
|
30
30
|
{ kind: 'rule', re: /\b(?:every time|each time|whenever|any time)\b/i },
|
|
31
31
|
];
|
|
32
32
|
|
|
33
|
+
// v1.5.0 audit-LOW-memory-#15: negation / sarcasm guards.
|
|
34
|
+
// When any of these phrases appears, suppress all feedback detection for the
|
|
35
|
+
// prompt — the user has signalled that the literal phrase shouldn't be taken
|
|
36
|
+
// as feedback (retraction, joke, hypothetical). High precision is more
|
|
37
|
+
// valuable than recall here; a false positive promotes a bogus rule into
|
|
38
|
+
// long-term memory which is expensive to unlearn.
|
|
39
|
+
const NEGATION_PATTERNS = [
|
|
40
|
+
/\bnever ?mind\s+(?:that|this|it)?\b/i,
|
|
41
|
+
/\bnvm\b/i,
|
|
42
|
+
/\bactually,?\s+wait\b/i,
|
|
43
|
+
/\bsarcasm:\s*yes\b/i,
|
|
44
|
+
/\b\/s\b/i, // tumblr/reddit sarcasm tag
|
|
45
|
+
/\bjust kidding\b/i,
|
|
46
|
+
/\bscratch that\b/i,
|
|
47
|
+
/\bdisregard (?:that|this|it|the (?:above|last))\b/i,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function isNegatedOrSarcastic(prompt) {
|
|
51
|
+
for (const re of NEGATION_PATTERNS) {
|
|
52
|
+
if (re.test(prompt)) return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
export function detectFeedback(prompt) {
|
|
34
58
|
if (typeof prompt !== 'string' || !prompt) return [];
|
|
59
|
+
// Suppress detection when negation/sarcasm signal present.
|
|
60
|
+
if (isNegatedOrSarcastic(prompt)) return [];
|
|
35
61
|
const hits = [];
|
|
36
62
|
for (const { kind, re } of PATTERNS) {
|
|
37
63
|
const m = prompt.match(re);
|