@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
|
@@ -36,6 +36,92 @@ import os from 'node:os';
|
|
|
36
36
|
|
|
37
37
|
const SCHEMA_VERSION = '1.0';
|
|
38
38
|
|
|
39
|
+
// Maximum path length the registry will accept. Practical limits across the
|
|
40
|
+
// supported platforms are PATH_MAX=4096 (Linux), 1024 (macOS), 32767 (Windows
|
|
41
|
+
// long-path). 4096 is a comfortable cap that won't reject any real path but
|
|
42
|
+
// will reject a flood-of-bytes prompt-injection payload.
|
|
43
|
+
const MAX_PROJECT_ROOT_LEN = 4096;
|
|
44
|
+
|
|
45
|
+
// Unicode tag-block range (U+E0000–U+E007F) — invisible "ASCII Smuggler" code
|
|
46
|
+
// points historically used to hide prompt-injection payloads inside text the
|
|
47
|
+
// model sees but the human doesn't. Closed at the prelude in H1.4; we also
|
|
48
|
+
// reject at registry-write time to prevent contamination at the source.
|
|
49
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- single-char Unicode range class; no quantifier; not backtrack-exploitable
|
|
50
|
+
const UNICODE_TAG_BLOCK_RE = /[\u{E0000}-\u{E007F}]/u;
|
|
51
|
+
|
|
52
|
+
// ASCII control characters (excluding nothing — \n \r \t \0 etc are ALL
|
|
53
|
+
// rejected when present in a path key).
|
|
54
|
+
// eslint-disable-next-line no-control-regex -- this regex EXISTS to reject control chars; lint hit is a false positive on the gate itself
|
|
55
|
+
const CONTROL_CHAR_RE = /[\x00-\x1F\x7F]/;
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Validation
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* F-SEC-4 (HIGH, update-install-trust audit v1.5.0):
|
|
63
|
+
* recordOverrideUse persisted any non-empty string as a project key and that
|
|
64
|
+
* value flows verbatim into the cross-project promote suggestion that lands
|
|
65
|
+
* in the prelude the model reads — a prompt-injection vector.
|
|
66
|
+
*
|
|
67
|
+
* Validate projectRoot is shaped like a real absolute filesystem path with
|
|
68
|
+
* no `..` segments, no control characters, no Unicode tag-block smuggling
|
|
69
|
+
* code points, and within a sane length cap. Returns `{ ok: true }` on pass
|
|
70
|
+
* or `{ ok: false, reason }` on fail. We do NOT throw — the caller in
|
|
71
|
+
* override-resolver.js wraps the registry write in a non-fatal try/catch
|
|
72
|
+
* and we want a structured signal instead of a thrown Error so future
|
|
73
|
+
* callers can surface a clean rejection without try/catch noise.
|
|
74
|
+
*
|
|
75
|
+
* @param {unknown} projectRoot
|
|
76
|
+
* @returns {{ ok: true } | { ok: false, reason: string }}
|
|
77
|
+
*/
|
|
78
|
+
function validateProjectRoot(projectRoot) {
|
|
79
|
+
if (typeof projectRoot !== 'string' || projectRoot.length === 0) {
|
|
80
|
+
return { ok: false, reason: 'projectRoot must be a non-empty string' };
|
|
81
|
+
}
|
|
82
|
+
if (projectRoot.length > MAX_PROJECT_ROOT_LEN) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: `projectRoot exceeds max length ${MAX_PROJECT_ROOT_LEN}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (!path.isAbsolute(projectRoot)) {
|
|
89
|
+
return { ok: false, reason: 'projectRoot must be an absolute path' };
|
|
90
|
+
}
|
|
91
|
+
// Reject any `..` segment in the RAW path before normalization. path.normalize
|
|
92
|
+
// would collapse interior `..` (e.g. `/a/../b` -> `/b`) and silently rewrite
|
|
93
|
+
// the registry key, defeating the validation. A real filesystem path passed
|
|
94
|
+
// by override-resolver.js is already canonical, so a `..` is a tampering
|
|
95
|
+
// signal. Split on both POSIX and Windows separators so /a/../b and \a\..\b
|
|
96
|
+
// are both caught regardless of host platform.
|
|
97
|
+
const rawSegments = projectRoot.split(/[\\/]/);
|
|
98
|
+
if (rawSegments.includes('..')) {
|
|
99
|
+
return { ok: false, reason: 'projectRoot must not contain `..` segments' };
|
|
100
|
+
}
|
|
101
|
+
if (CONTROL_CHAR_RE.test(projectRoot)) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: 'projectRoot must not contain control characters',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (UNICODE_TAG_BLOCK_RE.test(projectRoot)) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
reason: 'projectRoot must not contain Unicode tag-block characters',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Reject prompt-injection / markdown / HTML tokens that are not meaningful
|
|
114
|
+
// in a real path but ARE meaningful when rendered into the prelude. These
|
|
115
|
+
// are not legal in any reasonable filesystem path.
|
|
116
|
+
if (/[<>`]/.test(projectRoot)) {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
reason: 'projectRoot must not contain `<`, `>`, or backtick',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return { ok: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
39
125
|
// ---------------------------------------------------------------------------
|
|
40
126
|
// Path helpers — read os.homedir()/HOME at call time so tests can swap HOME
|
|
41
127
|
// via process.env between calls.
|
|
@@ -111,21 +197,31 @@ async function readSettings() {
|
|
|
111
197
|
* Idempotent — re-recording the same (project, preset) updates applied_at
|
|
112
198
|
* rather than duplicating the entry.
|
|
113
199
|
*
|
|
114
|
-
*
|
|
200
|
+
* Returns `{ ok: true }` on success or `{ ok: false, reason }` on validation
|
|
201
|
+
* failure. Does NOT throw on bad input — the value flows into the prelude
|
|
202
|
+
* the model reads, so we want every untrusted projectRoot rejected loudly
|
|
203
|
+
* via the return contract instead of via an unhandled Error.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} projectRoot MUST be an absolute path, no `..`, no
|
|
206
|
+
* control / Unicode tag-block / HTML chars.
|
|
115
207
|
* @param {string} preset
|
|
116
208
|
* @param {string} scope 'base' | 'user' | 'org' | 'project'
|
|
117
209
|
* @param {string} [project_type] auto-detected project type, defaults to
|
|
118
210
|
* whatever is already on file or 'unknown'.
|
|
211
|
+
* @returns {Promise<{ ok: true } | { ok: false, reason: string }>}
|
|
119
212
|
*/
|
|
120
213
|
export async function recordOverrideUse(projectRoot, preset, scope, project_type) {
|
|
121
|
-
|
|
122
|
-
|
|
214
|
+
// F-SEC-4 (HIGH): full path validation on projectRoot before it enters the
|
|
215
|
+
// registry. See validateProjectRoot above for the threat-model rationale.
|
|
216
|
+
const pathCheck = validateProjectRoot(projectRoot);
|
|
217
|
+
if (!pathCheck.ok) {
|
|
218
|
+
return pathCheck;
|
|
123
219
|
}
|
|
124
220
|
if (typeof preset !== 'string' || !preset) {
|
|
125
|
-
|
|
221
|
+
return { ok: false, reason: 'preset must be a non-empty string' };
|
|
126
222
|
}
|
|
127
223
|
if (typeof scope !== 'string' || !scope) {
|
|
128
|
-
|
|
224
|
+
return { ok: false, reason: 'scope must be a non-empty string' };
|
|
129
225
|
}
|
|
130
226
|
|
|
131
227
|
const state = await readRegistry();
|
|
@@ -153,6 +249,7 @@ export async function recordOverrideUse(projectRoot, preset, scope, project_type
|
|
|
153
249
|
}
|
|
154
250
|
state.projects[projectRoot] = proj;
|
|
155
251
|
await writeRegistry(state);
|
|
252
|
+
return { ok: true };
|
|
156
253
|
}
|
|
157
254
|
|
|
158
255
|
/**
|
|
@@ -186,6 +283,9 @@ export async function findProjectsWithOverride(preset) {
|
|
|
186
283
|
const out = [];
|
|
187
284
|
for (const [project, entry] of Object.entries(state.projects || {})) {
|
|
188
285
|
if (!entry || !Array.isArray(entry.active_overrides)) continue;
|
|
286
|
+
// F-SEC-4 (HIGH) defense-in-depth: legacy registries written before
|
|
287
|
+
// v1.5.1 validation could contain unsafe keys. Refuse to surface them.
|
|
288
|
+
if (!validateProjectRoot(project).ok) continue;
|
|
189
289
|
const hit = entry.active_overrides.find(
|
|
190
290
|
(o) => o && o.preset === preset
|
|
191
291
|
);
|
|
@@ -226,6 +326,10 @@ export async function findProjectsWithSimilarOverrideSet(
|
|
|
226
326
|
for (const [project, entry] of Object.entries(state.projects || {})) {
|
|
227
327
|
if (exclude && project === exclude) continue;
|
|
228
328
|
if (!entry || !Array.isArray(entry.active_overrides)) continue;
|
|
329
|
+
// F-SEC-4 (HIGH) defense-in-depth: legacy registries written before
|
|
330
|
+
// v1.5.1 validation could contain unsafe keys that would flow into the
|
|
331
|
+
// promote suggestion text and the prelude. Refuse to surface them.
|
|
332
|
+
if (!validateProjectRoot(project).ok) continue;
|
|
229
333
|
const theirs = new Set(
|
|
230
334
|
entry.active_overrides
|
|
231
335
|
.filter((o) => o && typeof o.preset === 'string')
|
|
@@ -304,4 +408,6 @@ export const __test = {
|
|
|
304
408
|
settingsPath,
|
|
305
409
|
readRegistry,
|
|
306
410
|
writeRegistry,
|
|
411
|
+
validateProjectRoot,
|
|
412
|
+
MAX_PROJECT_ROOT_LEN,
|
|
307
413
|
};
|
package/src/receipts.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
+
// v1.5.0 N4.obs M1: tag every receipt with the orchestrator's trace_id so the
|
|
13
|
+
// dashboard can roll up sessions->traces->observations like Langfuse / Helicone.
|
|
14
|
+
import { getTraceId } from './observability/trace-id.js';
|
|
12
15
|
|
|
13
16
|
export function RECEIPTS_FILE(projectDir) {
|
|
14
17
|
return path.join(projectDir, '.ijfw', 'receipts', 'cross-runs.jsonl');
|
|
@@ -24,7 +27,13 @@ export function writeReceipt(projectDir, record) {
|
|
|
24
27
|
const dest = RECEIPTS_FILE(projectDir);
|
|
25
28
|
const dir = path.dirname(dest);
|
|
26
29
|
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
-
|
|
30
|
+
// v1.5.0 N4.obs M1: tag with trace_id if one is set + caller hasn't supplied
|
|
31
|
+
// one. Never overwrite an explicit caller-supplied trace_id.
|
|
32
|
+
const traceId = getTraceId();
|
|
33
|
+
const enriched = (traceId && record && typeof record === 'object' && !record.trace_id)
|
|
34
|
+
? { ...record, trace_id: traceId }
|
|
35
|
+
: record;
|
|
36
|
+
fs.appendFileSync(dest, JSON.stringify(enriched) + '\n');
|
|
28
37
|
_pruneReceipts(dest);
|
|
29
38
|
}
|
|
30
39
|
|
|
@@ -46,8 +55,27 @@ export function purgeReceipts(projectDir) {
|
|
|
46
55
|
return count;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
//
|
|
50
|
-
|
|
58
|
+
// v1.5.0 audit-LOW-obs-L2: per-tier cache-read savings rate.
|
|
59
|
+
//
|
|
60
|
+
// Anthropic prompt caching: a cached read costs ~10% of an uncached input
|
|
61
|
+
// token. Savings are computed as (uncached_input_rate - cached_read_rate),
|
|
62
|
+
// which collapses to (0.9 * input_rate) per cached-read token. Values are
|
|
63
|
+
// derived from the canonical pricing table (mcp-server/src/cost/pricing.js).
|
|
64
|
+
//
|
|
65
|
+
// Default tier remains sonnet (matches the prior single-constant behaviour
|
|
66
|
+
// and is the dominant model on the hero-line surface), but cache_stats
|
|
67
|
+
// records that carry a `model` field now dispatch to the right rate.
|
|
68
|
+
const SONNET_CACHE_SAVINGS_PER_TOKEN = 2.70 / 1_000_000;
|
|
69
|
+
const OPUS_CACHE_SAVINGS_PER_TOKEN = 13.50 / 1_000_000;
|
|
70
|
+
const HAIKU_CACHE_SAVINGS_PER_TOKEN = 0.72 / 1_000_000;
|
|
71
|
+
|
|
72
|
+
function cacheSavingsPerTokenFor(model) {
|
|
73
|
+
if (typeof model !== 'string' || !model) return SONNET_CACHE_SAVINGS_PER_TOKEN;
|
|
74
|
+
const m = model.toLowerCase();
|
|
75
|
+
if (m.includes('opus')) return OPUS_CACHE_SAVINGS_PER_TOKEN;
|
|
76
|
+
if (m.includes('haiku')) return HAIKU_CACHE_SAVINGS_PER_TOKEN;
|
|
77
|
+
return SONNET_CACHE_SAVINGS_PER_TOKEN;
|
|
78
|
+
}
|
|
51
79
|
|
|
52
80
|
// renderReceipt(record, phaseWave?, stepNum?)
|
|
53
81
|
// phaseWave -- caller-supplied label for the narration header. Default is
|
|
@@ -101,7 +129,11 @@ export function renderReceipt(record, phaseWave = 'Trident', stepNum = 1) {
|
|
|
101
129
|
lines.push(`Step ${stepNum}.4 -- cache created: ${cs.cache_creation_input_tokens} tokens`);
|
|
102
130
|
}
|
|
103
131
|
if (typeof cs.cache_read_input_tokens === 'number') {
|
|
104
|
-
|
|
132
|
+
// v1.5.0 audit-LOW-obs-L2: dispatch on cache_stats.model when set;
|
|
133
|
+
// fall back to receipt-level model; finally fall back to sonnet.
|
|
134
|
+
const modelForRate = cs.model || record.model || null;
|
|
135
|
+
const rate = cacheSavingsPerTokenFor(modelForRate);
|
|
136
|
+
const saved = cs.cache_read_input_tokens * rate;
|
|
105
137
|
const savedStr = saved >= 0.01 ? ` (~$${saved.toFixed(2)} saved)` : '';
|
|
106
138
|
lines.push(`Step ${stepNum}.5 -- cache read: ${cs.cache_read_input_tokens} tokens${savedStr}`);
|
|
107
139
|
}
|
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
// not a replacement for IJFW memory, but they make recovery possible when chat
|
|
5
5
|
// context or a generated memory summary goes missing.
|
|
6
6
|
|
|
7
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
8
8
|
import { basename, join, resolve } from 'node:path';
|
|
9
9
|
import { writeAtomic } from '../lib/atomic-io.js';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
appendBlackboardEvent,
|
|
12
|
+
blackboardPaths,
|
|
13
|
+
blackboardStatus,
|
|
14
|
+
readBlackboard,
|
|
15
|
+
} from '../blackboard.js';
|
|
11
16
|
import { readTeamAssembly } from '../team/generator.js';
|
|
12
17
|
import { buildSwarmPlan } from '../swarm/planner.js';
|
|
13
18
|
|
|
@@ -89,13 +94,43 @@ export function listCheckpoints(projectRoot = process.cwd()) {
|
|
|
89
94
|
.map((file) => join(paths.dir, file));
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
// v1.5.0 audit-LOW-work-L2: memoise buildSnapshot per (projectRoot, ms).
|
|
98
|
+
// Snapshot construction reads team + plan + blackboard, which are themselves
|
|
99
|
+
// I/O-heavy reads. The bb mtime cache already shortcuts the inner reads, but
|
|
100
|
+
// when a caller does back-to-back createCheckpoint() calls (e.g. on a wave
|
|
101
|
+
// boundary) we still re-build the wrapper N times. Memo is keyed on the
|
|
102
|
+
// project root + blackboard mtimes + ts so any state change invalidates;
|
|
103
|
+
// hot-path cache size is capped at 8 entries to stay tiny.
|
|
104
|
+
const SNAPSHOT_CACHE = new Map();
|
|
105
|
+
const SNAPSHOT_CACHE_MAX = 8;
|
|
106
|
+
|
|
107
|
+
function snapshotCacheKey(projectRoot, ts) {
|
|
108
|
+
const paths = blackboardPaths(projectRoot);
|
|
109
|
+
let tasksMtime = 0, claimsMtime = 0;
|
|
110
|
+
try { tasksMtime = statSync(paths.tasks).mtimeMs; } catch { /* default 0 */ }
|
|
111
|
+
try { claimsMtime = statSync(paths.claims).mtimeMs; } catch { /* default 0 */ }
|
|
112
|
+
return `${paths.root}::${ts}::${tasksMtime}::${claimsMtime}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
function buildSnapshot(projectRoot, meta) {
|
|
116
|
+
const cacheKey = snapshotCacheKey(projectRoot, meta.ts);
|
|
117
|
+
const cached = SNAPSHOT_CACHE.get(cacheKey);
|
|
118
|
+
if (cached) {
|
|
119
|
+
// Cached body is independent of meta.id / meta.label / meta.message;
|
|
120
|
+
// those are reapplied from the current call.
|
|
121
|
+
return {
|
|
122
|
+
...cached,
|
|
123
|
+
id: meta.id,
|
|
124
|
+
label: meta.label,
|
|
125
|
+
message: meta.message || null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
93
128
|
const blackboard = readBlackboard(projectRoot);
|
|
94
129
|
const team = readTeamAssembly(projectRoot);
|
|
95
130
|
const plan = buildSwarmPlan(projectRoot);
|
|
96
131
|
const status = blackboardStatus(projectRoot);
|
|
97
132
|
const tasks = blackboard.tasks.data.tasks || [];
|
|
98
|
-
|
|
133
|
+
const snapshot = {
|
|
99
134
|
schema_version: 'ijfw-checkpoint/v1',
|
|
100
135
|
id: meta.id,
|
|
101
136
|
label: meta.label,
|
|
@@ -122,6 +157,24 @@ function buildSnapshot(projectRoot, meta) {
|
|
|
122
157
|
recent: blackboard.recent,
|
|
123
158
|
next: recommendedNext(team, plan, tasks),
|
|
124
159
|
};
|
|
160
|
+
// Stash a meta-agnostic copy in the cache (id/label/message reapplied on hit).
|
|
161
|
+
SNAPSHOT_CACHE.set(cacheKey, {
|
|
162
|
+
...snapshot,
|
|
163
|
+
id: null,
|
|
164
|
+
label: null,
|
|
165
|
+
message: null,
|
|
166
|
+
});
|
|
167
|
+
// Narrow LRU: cap the cache size and drop the oldest insertion when over.
|
|
168
|
+
if (SNAPSHOT_CACHE.size > SNAPSHOT_CACHE_MAX) {
|
|
169
|
+
const firstKey = SNAPSHOT_CACHE.keys().next().value;
|
|
170
|
+
if (firstKey !== undefined) SNAPSHOT_CACHE.delete(firstKey);
|
|
171
|
+
}
|
|
172
|
+
return snapshot;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Exposed for tests / cache invalidation hooks.
|
|
176
|
+
export function _resetSnapshotCache() {
|
|
177
|
+
SNAPSHOT_CACHE.clear();
|
|
125
178
|
}
|
|
126
179
|
|
|
127
180
|
function renderCheckpoint(snapshot) {
|