@skillsmith/core 0.9.0 → 0.10.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/CHANGELOG.md +12 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +6 -0
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/client.test.d.ts +9 -0
- package/dist/src/api/client.test.d.ts.map +1 -0
- package/dist/src/api/client.test.js +81 -0
- package/dist/src/api/client.test.js.map +1 -0
- package/dist/src/exports/services.d.ts +3 -1
- package/dist/src/exports/services.d.ts.map +1 -1
- package/dist/src/exports/services.js +7 -1
- package/dist/src/exports/services.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/install/agent-config-merge.json-array.d.ts +29 -0
- package/dist/src/install/agent-config-merge.json-array.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.json-array.js +100 -0
- package/dist/src/install/agent-config-merge.json-array.js.map +1 -0
- package/dist/src/install/agent-config-merge.json.d.ts +37 -0
- package/dist/src/install/agent-config-merge.json.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.json.js +134 -0
- package/dist/src/install/agent-config-merge.json.js.map +1 -0
- package/dist/src/install/agent-config-merge.toml-block.d.ts +48 -0
- package/dist/src/install/agent-config-merge.toml-block.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.toml-block.js +107 -0
- package/dist/src/install/agent-config-merge.toml-block.js.map +1 -0
- package/dist/src/install/agent-config-merge.types.d.ts +87 -0
- package/dist/src/install/agent-config-merge.types.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.types.js +83 -0
- package/dist/src/install/agent-config-merge.types.js.map +1 -0
- package/dist/src/install/agent-config-merge.yaml.d.ts +46 -0
- package/dist/src/install/agent-config-merge.yaml.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.yaml.js +133 -0
- package/dist/src/install/agent-config-merge.yaml.js.map +1 -0
- package/dist/src/install/agent-config-merge.yaml.test.d.ts +2 -0
- package/dist/src/install/agent-config-merge.yaml.test.d.ts.map +1 -0
- package/dist/src/install/agent-config-merge.yaml.test.js +210 -0
- package/dist/src/install/agent-config-merge.yaml.test.js.map +1 -0
- package/dist/src/install/agent-harness-targets.d.ts +98 -0
- package/dist/src/install/agent-harness-targets.d.ts.map +1 -0
- package/dist/src/install/agent-harness-targets.js +154 -0
- package/dist/src/install/agent-harness-targets.js.map +1 -0
- package/dist/src/install/agent-home-relocate.d.ts +29 -0
- package/dist/src/install/agent-home-relocate.d.ts.map +1 -0
- package/dist/src/install/agent-home-relocate.js +38 -0
- package/dist/src/install/agent-home-relocate.js.map +1 -0
- package/dist/src/install/agent-manifest-path-guard.d.ts +55 -0
- package/dist/src/install/agent-manifest-path-guard.d.ts.map +1 -0
- package/dist/src/install/agent-manifest-path-guard.js +109 -0
- package/dist/src/install/agent-manifest-path-guard.js.map +1 -0
- package/dist/src/install/agent-manifest-path-guard.test.d.ts +2 -0
- package/dist/src/install/agent-manifest-path-guard.test.d.ts.map +1 -0
- package/dist/src/install/agent-manifest-path-guard.test.js +72 -0
- package/dist/src/install/agent-manifest-path-guard.test.js.map +1 -0
- package/dist/src/install/agent-manifest.d.ts +58 -0
- package/dist/src/install/agent-manifest.d.ts.map +1 -0
- package/dist/src/install/agent-manifest.js +103 -0
- package/dist/src/install/agent-manifest.js.map +1 -0
- package/dist/src/install/agent-pack-installer.d.ts +39 -0
- package/dist/src/install/agent-pack-installer.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.entry.d.ts +55 -0
- package/dist/src/install/agent-pack-installer.entry.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.entry.js +90 -0
- package/dist/src/install/agent-pack-installer.entry.js.map +1 -0
- package/dist/src/install/agent-pack-installer.fs-helpers.d.ts +33 -0
- package/dist/src/install/agent-pack-installer.fs-helpers.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.fs-helpers.js +59 -0
- package/dist/src/install/agent-pack-installer.fs-helpers.js.map +1 -0
- package/dist/src/install/agent-pack-installer.harness.d.ts +66 -0
- package/dist/src/install/agent-pack-installer.harness.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.harness.js +310 -0
- package/dist/src/install/agent-pack-installer.harness.js.map +1 -0
- package/dist/src/install/agent-pack-installer.js +221 -0
- package/dist/src/install/agent-pack-installer.js.map +1 -0
- package/dist/src/install/agent-pack-installer.preserve.test.d.ts +11 -0
- package/dist/src/install/agent-pack-installer.preserve.test.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.preserve.test.js +72 -0
- package/dist/src/install/agent-pack-installer.preserve.test.js.map +1 -0
- package/dist/src/install/agent-pack-installer.test.d.ts +2 -0
- package/dist/src/install/agent-pack-installer.test.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.test.js +350 -0
- package/dist/src/install/agent-pack-installer.test.js.map +1 -0
- package/dist/src/install/agent-pack-installer.types.d.ts +51 -0
- package/dist/src/install/agent-pack-installer.types.d.ts.map +1 -0
- package/dist/src/install/agent-pack-installer.types.js +16 -0
- package/dist/src/install/agent-pack-installer.types.js.map +1 -0
- package/dist/src/install/agent-pack-uninstaller.d.ts +43 -0
- package/dist/src/install/agent-pack-uninstaller.d.ts.map +1 -0
- package/dist/src/install/agent-pack-uninstaller.js +113 -0
- package/dist/src/install/agent-pack-uninstaller.js.map +1 -0
- package/dist/src/install/agent-pack-uninstaller.test.d.ts +2 -0
- package/dist/src/install/agent-pack-uninstaller.test.d.ts.map +1 -0
- package/dist/src/install/agent-pack-uninstaller.test.js +212 -0
- package/dist/src/install/agent-pack-uninstaller.test.js.map +1 -0
- package/dist/src/install/index.d.ts +7 -0
- package/dist/src/install/index.d.ts.map +1 -1
- package/dist/src/install/index.js +7 -0
- package/dist/src/install/index.js.map +1 -1
- package/dist/src/install/paths.d.ts +1 -1
- package/dist/src/install/paths.d.ts.map +1 -1
- package/dist/src/install/paths.js +4 -0
- package/dist/src/install/paths.js.map +1 -1
- package/dist/src/install/paths.test.js +15 -0
- package/dist/src/install/paths.test.js.map +1 -1
- package/dist/src/journal/hash.d.ts +13 -0
- package/dist/src/journal/hash.d.ts.map +1 -0
- package/dist/src/journal/hash.js +16 -0
- package/dist/src/journal/hash.js.map +1 -0
- package/dist/src/journal/index.d.ts +15 -0
- package/dist/src/journal/index.d.ts.map +1 -0
- package/dist/src/journal/index.js +15 -0
- package/dist/src/journal/index.js.map +1 -0
- package/dist/src/journal/journal.test.d.ts +16 -0
- package/dist/src/journal/journal.test.d.ts.map +1 -0
- package/dist/src/journal/journal.test.js +171 -0
- package/dist/src/journal/journal.test.js.map +1 -0
- package/dist/src/journal/path.d.ts +44 -0
- package/dist/src/journal/path.d.ts.map +1 -0
- package/dist/src/journal/path.js +61 -0
- package/dist/src/journal/path.js.map +1 -0
- package/dist/src/journal/reader.d.ts +38 -0
- package/dist/src/journal/reader.d.ts.map +1 -0
- package/dist/src/journal/reader.js +93 -0
- package/dist/src/journal/reader.js.map +1 -0
- package/dist/src/journal/types.d.ts +87 -0
- package/dist/src/journal/types.d.ts.map +1 -0
- package/dist/src/journal/types.js +21 -0
- package/dist/src/journal/types.js.map +1 -0
- package/dist/src/journal/writer.d.ts +52 -0
- package/dist/src/journal/writer.d.ts.map +1 -0
- package/dist/src/journal/writer.js +125 -0
- package/dist/src/journal/writer.js.map +1 -0
- package/dist/src/paywall-triggers/index.d.ts +13 -0
- package/dist/src/paywall-triggers/index.d.ts.map +1 -0
- package/dist/src/paywall-triggers/index.js +13 -0
- package/dist/src/paywall-triggers/index.js.map +1 -0
- package/dist/src/paywall-triggers/path.d.ts +47 -0
- package/dist/src/paywall-triggers/path.d.ts.map +1 -0
- package/dist/src/paywall-triggers/path.js +73 -0
- package/dist/src/paywall-triggers/path.js.map +1 -0
- package/dist/src/paywall-triggers/store.d.ts +75 -0
- package/dist/src/paywall-triggers/store.d.ts.map +1 -0
- package/dist/src/paywall-triggers/store.js +122 -0
- package/dist/src/paywall-triggers/store.js.map +1 -0
- package/dist/src/paywall-triggers/store.test.d.ts +2 -0
- package/dist/src/paywall-triggers/store.test.d.ts.map +1 -0
- package/dist/src/paywall-triggers/store.test.js +121 -0
- package/dist/src/paywall-triggers/store.test.js.map +1 -0
- package/dist/src/paywall-triggers/types.d.ts +24 -0
- package/dist/src/paywall-triggers/types.d.ts.map +1 -0
- package/dist/src/paywall-triggers/types.js +13 -0
- package/dist/src/paywall-triggers/types.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.compound.d.ts +13 -0
- package/dist/src/security/scanner/SecurityScanner.compound.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.compound.js +127 -0
- package/dist/src/security/scanner/SecurityScanner.compound.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.js +10 -1
- package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.pii.d.ts +23 -0
- package/dist/src/security/scanner/SecurityScanner.pii.d.ts.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.pii.js +152 -0
- package/dist/src/security/scanner/SecurityScanner.pii.js.map +1 -0
- package/dist/src/security/scanner/SecurityScanner.scanners.d.ts +5 -12
- package/dist/src/security/scanner/SecurityScanner.scanners.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.scanners.js +63 -137
- package/dist/src/security/scanner/SecurityScanner.scanners.js.map +1 -1
- package/dist/src/security/scanner/patterns.d.ts +2 -0
- package/dist/src/security/scanner/patterns.d.ts.map +1 -1
- package/dist/src/security/scanner/patterns.js +79 -11
- package/dist/src/security/scanner/patterns.js.map +1 -1
- package/dist/src/security/scanner/types.d.ts +2 -0
- package/dist/src/security/scanner/types.d.ts.map +1 -1
- package/dist/src/services/agent-pack/agent-pack.test.d.ts +18 -0
- package/dist/src/services/agent-pack/agent-pack.test.d.ts.map +1 -0
- package/dist/src/services/agent-pack/agent-pack.test.js +344 -0
- package/dist/src/services/agent-pack/agent-pack.test.js.map +1 -0
- package/dist/src/services/agent-pack/hooks.d.ts +29 -0
- package/dist/src/services/agent-pack/hooks.d.ts.map +1 -0
- package/dist/src/services/agent-pack/hooks.js +139 -0
- package/dist/src/services/agent-pack/hooks.js.map +1 -0
- package/dist/src/services/agent-pack/index.d.ts +26 -0
- package/dist/src/services/agent-pack/index.d.ts.map +1 -0
- package/dist/src/services/agent-pack/index.js +109 -0
- package/dist/src/services/agent-pack/index.js.map +1 -0
- package/dist/src/services/agent-pack/prompt-source.d.ts +47 -0
- package/dist/src/services/agent-pack/prompt-source.d.ts.map +1 -0
- package/dist/src/services/agent-pack/prompt-source.js +182 -0
- package/dist/src/services/agent-pack/prompt-source.js.map +1 -0
- package/dist/src/services/agent-pack/shims.d.ts +40 -0
- package/dist/src/services/agent-pack/shims.d.ts.map +1 -0
- package/dist/src/services/agent-pack/shims.js +96 -0
- package/dist/src/services/agent-pack/shims.js.map +1 -0
- package/dist/src/services/agent-pack/skill-md.d.ts +35 -0
- package/dist/src/services/agent-pack/skill-md.d.ts.map +1 -0
- package/dist/src/services/agent-pack/skill-md.js +89 -0
- package/dist/src/services/agent-pack/skill-md.js.map +1 -0
- package/dist/src/services/agent-pack/types.d.ts +118 -0
- package/dist/src/services/agent-pack/types.d.ts.map +1 -0
- package/dist/src/services/agent-pack/types.js +61 -0
- package/dist/src/services/agent-pack/types.js.map +1 -0
- package/dist/src/services/agent-tool-profile.d.ts +59 -0
- package/dist/src/services/agent-tool-profile.d.ts.map +1 -0
- package/dist/src/services/agent-tool-profile.js +76 -0
- package/dist/src/services/agent-tool-profile.js.map +1 -0
- package/dist/src/services/agent-tool-profile.test.d.ts +2 -0
- package/dist/src/services/agent-tool-profile.test.d.ts.map +1 -0
- package/dist/src/services/agent-tool-profile.test.js +28 -0
- package/dist/src/services/agent-tool-profile.test.js.map +1 -0
- package/dist/src/services/bundled-sibling-scan.d.ts +111 -0
- package/dist/src/services/bundled-sibling-scan.d.ts.map +1 -0
- package/dist/src/services/bundled-sibling-scan.js +170 -0
- package/dist/src/services/bundled-sibling-scan.js.map +1 -0
- package/dist/src/services/skill-installation.io.d.ts +16 -5
- package/dist/src/services/skill-installation.io.d.ts.map +1 -1
- package/dist/src/services/skill-installation.io.js +44 -20
- package/dist/src/services/skill-installation.io.js.map +1 -1
- package/dist/src/services/skill-installation.policy.d.ts +96 -0
- package/dist/src/services/skill-installation.policy.d.ts.map +1 -0
- package/dist/src/services/skill-installation.policy.js +144 -0
- package/dist/src/services/skill-installation.policy.js.map +1 -0
- package/dist/src/services/skill-installation.service.d.ts.map +1 -1
- package/dist/src/services/skill-installation.service.js +9 -3
- package/dist/src/services/skill-installation.service.js.map +1 -1
- package/dist/src/sources/index.d.ts +1 -0
- package/dist/src/sources/index.d.ts.map +1 -1
- package/dist/src/sources/index.js +4 -0
- package/dist/src/sources/index.js.map +1 -1
- package/dist/src/sync/index.d.ts +1 -1
- package/dist/src/sync/index.d.ts.map +1 -1
- package/dist/src/sync/index.js +1 -1
- package/dist/src/sync/index.js.map +1 -1
- package/dist/src/sync/inventory-client.d.ts +16 -0
- package/dist/src/sync/inventory-client.d.ts.map +1 -1
- package/dist/src/sync/inventory-client.js +54 -0
- package/dist/src/sync/inventory-client.js.map +1 -1
- package/dist/src/sync/inventory-client.test.js +67 -1
- package/dist/src/sync/inventory-client.test.js.map +1 -1
- package/dist/src/sync/inventory-collector.d.ts.map +1 -1
- package/dist/src/sync/inventory-collector.js +32 -5
- package/dist/src/sync/inventory-collector.js.map +1 -1
- package/dist/src/sync/inventory-collector.test.js +50 -1
- package/dist/src/sync/inventory-collector.test.js.map +1 -1
- package/dist/src/sync/inventory-types.d.ts +9 -0
- package/dist/src/sync/inventory-types.d.ts.map +1 -1
- package/dist/src/sync/inventory-types.js +3 -0
- package/dist/src/sync/inventory-types.js.map +1 -1
- package/dist/src/telemetry/agent-marker.d.ts +137 -0
- package/dist/src/telemetry/agent-marker.d.ts.map +1 -0
- package/dist/src/telemetry/agent-marker.js +242 -0
- package/dist/src/telemetry/agent-marker.js.map +1 -0
- package/dist/src/telemetry/agent-marker.test.d.ts +13 -0
- package/dist/src/telemetry/agent-marker.test.d.ts.map +1 -0
- package/dist/src/telemetry/agent-marker.test.js +202 -0
- package/dist/src/telemetry/agent-marker.test.js.map +1 -0
- package/dist/src/telemetry/index.d.ts +2 -1
- package/dist/src/telemetry/index.d.ts.map +1 -1
- package/dist/src/telemetry/index.js +6 -1
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/posthog.d.ts +10 -0
- package/dist/src/telemetry/posthog.d.ts.map +1 -1
- package/dist/src/telemetry/posthog.js +5 -1
- package/dist/src/telemetry/posthog.js.map +1 -1
- package/dist/src/telemetry/wrap.d.ts +28 -7
- package/dist/src/telemetry/wrap.d.ts.map +1 -1
- package/dist/src/telemetry/wrap.gate.test.d.ts +22 -0
- package/dist/src/telemetry/wrap.gate.test.d.ts.map +1 -0
- package/dist/src/telemetry/wrap.gate.test.js +197 -0
- package/dist/src/telemetry/wrap.gate.test.js.map +1 -0
- package/dist/src/telemetry/wrap.js +111 -26
- package/dist/src/telemetry/wrap.js.map +1 -1
- package/dist/src/telemetry/wrap.marker.test.d.ts +16 -0
- package/dist/src/telemetry/wrap.marker.test.d.ts.map +1 -0
- package/dist/src/telemetry/wrap.marker.test.js +185 -0
- package/dist/src/telemetry/wrap.marker.test.js.map +1 -0
- package/dist/src/telemetry/wrap.test.js +4 -0
- package/dist/src/telemetry/wrap.test.js.map +1 -1
- package/dist/tests/SecurityScanner.exec.test.js +60 -0
- package/dist/tests/SecurityScanner.exec.test.js.map +1 -1
- package/dist/tests/security/ContinuousSecurity.test.js +10 -4
- package/dist/tests/security/ContinuousSecurity.test.js.map +1 -1
- package/dist/tests/security/SecurityScanner.chmod-evasion.test.d.ts +2 -0
- package/dist/tests/security/SecurityScanner.chmod-evasion.test.d.ts.map +1 -0
- package/dist/tests/security/SecurityScanner.chmod-evasion.test.js +126 -0
- package/dist/tests/security/SecurityScanner.chmod-evasion.test.js.map +1 -0
- package/dist/tests/security/chmod-compound.test.d.ts +2 -0
- package/dist/tests/security/chmod-compound.test.d.ts.map +1 -0
- package/dist/tests/security/chmod-compound.test.js +188 -0
- package/dist/tests/security/chmod-compound.test.js.map +1 -0
- package/dist/tests/security/pii-detection.test.js +24 -0
- package/dist/tests/security/pii-detection.test.js.map +1 -1
- package/dist/tests/security/scanner-regression-guard.test.d.ts +23 -7
- package/dist/tests/security/scanner-regression-guard.test.d.ts.map +1 -1
- package/dist/tests/security/scanner-regression-guard.test.js +27 -11
- package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
- package/dist/tests/security/sensitive-path-fp.test.d.ts +2 -0
- package/dist/tests/security/sensitive-path-fp.test.d.ts.map +1 -0
- package/dist/tests/security/sensitive-path-fp.test.js +142 -0
- package/dist/tests/security/sensitive-path-fp.test.js.map +1 -0
- package/dist/tests/services/bundled-sibling-scan.test.d.ts +2 -0
- package/dist/tests/services/bundled-sibling-scan.test.d.ts.map +1 -0
- package/dist/tests/services/bundled-sibling-scan.test.js +139 -0
- package/dist/tests/services/bundled-sibling-scan.test.js.map +1 -0
- package/dist/tests/unit/services/skill-installation.io.test.js +166 -0
- package/dist/tests/unit/services/skill-installation.io.test.js.map +1 -1
- package/dist/tests/unit/services/skill-installation.policy.test.d.ts +8 -0
- package/dist/tests/unit/services/skill-installation.policy.test.d.ts.map +1 -0
- package/dist/tests/unit/services/skill-installation.policy.test.js +151 -0
- package/dist/tests/unit/services/skill-installation.policy.test.js.map +1 -0
- package/package.json +23 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Unit tests for the change-journal writer + reader
|
|
3
|
+
* (SMI-5456 Wave 1 Step 3 / SMI-5470).
|
|
4
|
+
* @module @skillsmith/core/journal/journal.test
|
|
5
|
+
*
|
|
6
|
+
* Covers two of the three P-5 invariants named in
|
|
7
|
+
* docs/internal/implementation/smi-5456-skillsmith-agent-wave1.md's
|
|
8
|
+
* "Shared-State / Coordination Audit" table for `~/.skillsmith/journal`:
|
|
9
|
+
* - "journal module: concurrent-apply serialization test" (below)
|
|
10
|
+
* - "journal module: chain-verification test" (below)
|
|
11
|
+
* The third (undo round-trip / refusal / scope fence) lives in
|
|
12
|
+
* `@skillsmith/mcp-server`'s `tests/unit/undo-apply.test.ts`, since it
|
|
13
|
+
* needs the mcp-server apply-family tools to produce a realistic changeset.
|
|
14
|
+
*/
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as os from 'node:os';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { appendJournalRecord, isCorruptTailError } from './writer.js';
|
|
20
|
+
import { verifyJournalChain } from './reader.js';
|
|
21
|
+
import { getJournalFilePath, resetJournalSessionIdForTests } from './path.js';
|
|
22
|
+
let TEST_DIR;
|
|
23
|
+
let PREV_OVERRIDE;
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
TEST_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'skillsmith-journal-'));
|
|
26
|
+
PREV_OVERRIDE = process.env['SKILLSMITH_JOURNAL_DIR'];
|
|
27
|
+
process.env['SKILLSMITH_JOURNAL_DIR'] = TEST_DIR;
|
|
28
|
+
resetJournalSessionIdForTests();
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (PREV_OVERRIDE !== undefined)
|
|
32
|
+
process.env['SKILLSMITH_JOURNAL_DIR'] = PREV_OVERRIDE;
|
|
33
|
+
else
|
|
34
|
+
delete process.env['SKILLSMITH_JOURNAL_DIR'];
|
|
35
|
+
if (TEST_DIR && fs.existsSync(TEST_DIR))
|
|
36
|
+
fs.rmSync(TEST_DIR, { recursive: true, force: true });
|
|
37
|
+
resetJournalSessionIdForTests();
|
|
38
|
+
});
|
|
39
|
+
function fixtureRecord(suggestionId) {
|
|
40
|
+
return {
|
|
41
|
+
schema: 1,
|
|
42
|
+
ts: Date.now(),
|
|
43
|
+
session_id: 'test-session',
|
|
44
|
+
tool: 'apply_namespace_rename',
|
|
45
|
+
action: 'apply',
|
|
46
|
+
suggestion_id: suggestionId,
|
|
47
|
+
target_path: `/tmp/skill-${suggestionId}.md`,
|
|
48
|
+
before_hash: 'before-hash',
|
|
49
|
+
after_hash: 'after-hash',
|
|
50
|
+
approval: 'apply',
|
|
51
|
+
backup_ref: '/tmp/backup',
|
|
52
|
+
detail: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
describe('journal chain verification', () => {
|
|
56
|
+
it('a valid chain (multiple sequential appends) passes verification', async () => {
|
|
57
|
+
await appendJournalRecord(fixtureRecord('a'));
|
|
58
|
+
await appendJournalRecord(fixtureRecord('b'));
|
|
59
|
+
await appendJournalRecord(fixtureRecord('c'));
|
|
60
|
+
const result = await verifyJournalChain();
|
|
61
|
+
expect(result.valid).toBe(true);
|
|
62
|
+
expect(result.records).toHaveLength(3);
|
|
63
|
+
expect(result.breakAt).toBeUndefined();
|
|
64
|
+
// The chain is genuinely linked, not just three independent records.
|
|
65
|
+
expect(result.records[1].prev_hash).toBe(result.records[0].record_hash);
|
|
66
|
+
expect(result.records[2].prev_hash).toBe(result.records[1].record_hash);
|
|
67
|
+
});
|
|
68
|
+
it('an empty / never-written journal is a valid, empty chain (never throws)', async () => {
|
|
69
|
+
const result = await verifyJournalChain();
|
|
70
|
+
expect(result).toEqual({ valid: true, records: [] });
|
|
71
|
+
});
|
|
72
|
+
it('a tampered middle record breaks the chain at the correct index', async () => {
|
|
73
|
+
await appendJournalRecord(fixtureRecord('a'));
|
|
74
|
+
await appendJournalRecord(fixtureRecord('b'));
|
|
75
|
+
await appendJournalRecord(fixtureRecord('c'));
|
|
76
|
+
const filePath = getJournalFilePath();
|
|
77
|
+
const lines = fs
|
|
78
|
+
.readFileSync(filePath, 'utf-8')
|
|
79
|
+
.split('\n')
|
|
80
|
+
.filter((l) => l.length > 0);
|
|
81
|
+
const tampered = JSON.parse(lines[1]);
|
|
82
|
+
tampered.suggestion_id = 'TAMPERED';
|
|
83
|
+
lines[1] = JSON.stringify(tampered);
|
|
84
|
+
fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
|
|
85
|
+
const result = await verifyJournalChain();
|
|
86
|
+
expect(result.valid).toBe(false);
|
|
87
|
+
expect(result.breakAt).toBe(1);
|
|
88
|
+
// Everything before the tamper is still reported as verified evidence.
|
|
89
|
+
expect(result.records).toHaveLength(1);
|
|
90
|
+
expect(result.records[0].suggestion_id).toBe('a');
|
|
91
|
+
});
|
|
92
|
+
it('a corrupt tail line is reported via breakAt, never thrown', async () => {
|
|
93
|
+
await appendJournalRecord(fixtureRecord('a'));
|
|
94
|
+
await appendJournalRecord(fixtureRecord('b'));
|
|
95
|
+
const filePath = getJournalFilePath();
|
|
96
|
+
fs.appendFileSync(filePath, 'this is not json\n', 'utf-8');
|
|
97
|
+
const outcome = await verifyJournalChain();
|
|
98
|
+
expect(outcome.valid).toBe(false);
|
|
99
|
+
expect(outcome.breakAt).toBe(2);
|
|
100
|
+
expect(outcome.records).toHaveLength(2);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('journal writer refuses to append past a corrupt tail (governance follow-up, SMI-5456)', () => {
|
|
104
|
+
it('rejects with a typed isCorruptTailError when the last line is unparseable', async () => {
|
|
105
|
+
await appendJournalRecord(fixtureRecord('a'));
|
|
106
|
+
const filePath = getJournalFilePath();
|
|
107
|
+
fs.appendFileSync(filePath, 'this is not json\n', 'utf-8');
|
|
108
|
+
let caught;
|
|
109
|
+
try {
|
|
110
|
+
await appendJournalRecord(fixtureRecord('b'));
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
caught = err;
|
|
114
|
+
}
|
|
115
|
+
expect(caught).toBeDefined();
|
|
116
|
+
expect(isCorruptTailError(caught)).toBe(true);
|
|
117
|
+
expect(caught.message).toMatch(/corrupt|unverifiable/);
|
|
118
|
+
// The writer never wrote 'b' past the corrupt tail — the file still
|
|
119
|
+
// has exactly the original record plus the corrupt line.
|
|
120
|
+
const lines = fs
|
|
121
|
+
.readFileSync(filePath, 'utf-8')
|
|
122
|
+
.split('\n')
|
|
123
|
+
.filter((l) => l.length > 0);
|
|
124
|
+
expect(lines).toHaveLength(2);
|
|
125
|
+
});
|
|
126
|
+
it('does not wedge the write queue for subsequent callers — a fixed tail lets the next append through', async () => {
|
|
127
|
+
await appendJournalRecord(fixtureRecord('a'));
|
|
128
|
+
const filePath = getJournalFilePath();
|
|
129
|
+
fs.appendFileSync(filePath, 'this is not json\n', 'utf-8');
|
|
130
|
+
await expect(appendJournalRecord(fixtureRecord('b'))).rejects.toSatisfy(isCorruptTailError);
|
|
131
|
+
// Repair the tail (what a real operator/future recovery tool would do)
|
|
132
|
+
// and prove the queue moved forward rather than staying wedged on the
|
|
133
|
+
// rejected promise.
|
|
134
|
+
const lines = fs
|
|
135
|
+
.readFileSync(filePath, 'utf-8')
|
|
136
|
+
.split('\n')
|
|
137
|
+
.filter((l) => l.length > 0);
|
|
138
|
+
lines.pop();
|
|
139
|
+
fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
|
|
140
|
+
const record = await appendJournalRecord(fixtureRecord('c'));
|
|
141
|
+
expect(record.suggestion_id).toBe('c');
|
|
142
|
+
const result = await verifyJournalChain();
|
|
143
|
+
expect(result.valid).toBe(true);
|
|
144
|
+
expect(result.records.map((r) => r.suggestion_id)).toEqual(['a', 'c']);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('journal concurrent-apply serialization (P-5 single-writer invariant)', () => {
|
|
148
|
+
it('N concurrent journal writes produce N well-formed, correctly-chained records with no interleaved/torn lines', async () => {
|
|
149
|
+
const N = 25;
|
|
150
|
+
// Fire all N appends without awaiting between them — simulates
|
|
151
|
+
// apply_namespace_rename and apply_recommended_edit racing inside the
|
|
152
|
+
// same server process.
|
|
153
|
+
await Promise.all(Array.from({ length: N }, (_, i) => appendJournalRecord(fixtureRecord(`concurrent-${i}`))));
|
|
154
|
+
const filePath = getJournalFilePath();
|
|
155
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
156
|
+
const lines = raw.split('\n').filter((l) => l.length > 0);
|
|
157
|
+
// No torn/interleaved lines: every line parses as JSON on its own.
|
|
158
|
+
expect(lines).toHaveLength(N);
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
expect(() => JSON.parse(line)).not.toThrow();
|
|
161
|
+
}
|
|
162
|
+
const result = await verifyJournalChain();
|
|
163
|
+
expect(result.valid).toBe(true);
|
|
164
|
+
expect(result.records).toHaveLength(N);
|
|
165
|
+
// Every suggestion_id we wrote shows up exactly once — no lost or
|
|
166
|
+
// duplicated writes from the race.
|
|
167
|
+
const seen = new Set(result.records.map((r) => r.suggestion_id));
|
|
168
|
+
expect(seen.size).toBe(N);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
//# sourceMappingURL=journal.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal.test.js","sourceRoot":"","sources":["../../../src/journal/journal.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Journal file-layout + process session-id resolution
|
|
3
|
+
* (SMI-5456 Wave 1 Step 3 / SMI-5470).
|
|
4
|
+
* @module @skillsmith/core/journal/path
|
|
5
|
+
*
|
|
6
|
+
* File layout: `<configDir>/journal/journal.jsonl` — JSON Lines (one JSON
|
|
7
|
+
* object per line), append-only. Chosen over a single JSON array because:
|
|
8
|
+
* - Appends are O(1) — no read-modify-rewrite of the whole file, unlike
|
|
9
|
+
* `audit/namespace-overrides.ts`'s ledger (appropriate there because
|
|
10
|
+
* that file is small, mutable config, not an ever-growing log).
|
|
11
|
+
* - A line is the unit of both a write and a crash boundary: fsync'ing
|
|
12
|
+
* one appended line can't corrupt any earlier line, so a torn last
|
|
13
|
+
* write degrades to "one unreadable tail line" rather than an
|
|
14
|
+
* unparseable whole file — exactly what `reader.ts`'s
|
|
15
|
+
* "corrupt tail = report, never throw" contract needs.
|
|
16
|
+
* - It's streamable / greppable for ad-hoc forensics without a parser.
|
|
17
|
+
*
|
|
18
|
+
* `SKILLSMITH_JOURNAL_DIR` overrides the default, mirroring
|
|
19
|
+
* `SKILLSMITH_AGENT_MARKER_DIR` (`../telemetry/agent-marker.ts`) — needed
|
|
20
|
+
* because macOS `os.homedir()` resolves via `getpwuid()` and ignores
|
|
21
|
+
* `process.env.HOME` mutations in tests.
|
|
22
|
+
*/
|
|
23
|
+
/** Env var overriding the journal directory (test isolation). */
|
|
24
|
+
export declare const JOURNAL_DIR_ENV_VAR = "SKILLSMITH_JOURNAL_DIR";
|
|
25
|
+
/** Absolute path to the journal file. Read-time resolved (not memoised) so
|
|
26
|
+
* tests can flip `SKILLSMITH_JOURNAL_DIR` between cases. */
|
|
27
|
+
export declare function getJournalFilePath(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Process-lifetime session id shared by every journal record this server
|
|
30
|
+
* process writes, and by the in-process undo stack
|
|
31
|
+
* (`@skillsmith/mcp-server`'s `tools/apply-session.helpers.ts`) that keys
|
|
32
|
+
* "session-scoped undo" off the same lifetime: restarting the MCP server
|
|
33
|
+
* both mints a new session id here AND resets the undo stack (a fresh
|
|
34
|
+
* module load), so the two always agree without explicit coordination.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getJournalSessionId(): string;
|
|
37
|
+
/**
|
|
38
|
+
* Test-only reset. The session id is memoised for the process lifetime by
|
|
39
|
+
* design, but a single Vitest process runs many logically-independent
|
|
40
|
+
* "sessions" (test cases) — call this in `afterEach`/`beforeEach` to avoid
|
|
41
|
+
* cross-test bleed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resetJournalSessionIdForTests(): void;
|
|
44
|
+
//# sourceMappingURL=path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../../src/journal/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,2BAA2B,CAAA;AAO3D;4DAC4D;AAC5D,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAK5C;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Journal file-layout + process session-id resolution
|
|
3
|
+
* (SMI-5456 Wave 1 Step 3 / SMI-5470).
|
|
4
|
+
* @module @skillsmith/core/journal/path
|
|
5
|
+
*
|
|
6
|
+
* File layout: `<configDir>/journal/journal.jsonl` — JSON Lines (one JSON
|
|
7
|
+
* object per line), append-only. Chosen over a single JSON array because:
|
|
8
|
+
* - Appends are O(1) — no read-modify-rewrite of the whole file, unlike
|
|
9
|
+
* `audit/namespace-overrides.ts`'s ledger (appropriate there because
|
|
10
|
+
* that file is small, mutable config, not an ever-growing log).
|
|
11
|
+
* - A line is the unit of both a write and a crash boundary: fsync'ing
|
|
12
|
+
* one appended line can't corrupt any earlier line, so a torn last
|
|
13
|
+
* write degrades to "one unreadable tail line" rather than an
|
|
14
|
+
* unparseable whole file — exactly what `reader.ts`'s
|
|
15
|
+
* "corrupt tail = report, never throw" contract needs.
|
|
16
|
+
* - It's streamable / greppable for ad-hoc forensics without a parser.
|
|
17
|
+
*
|
|
18
|
+
* `SKILLSMITH_JOURNAL_DIR` overrides the default, mirroring
|
|
19
|
+
* `SKILLSMITH_AGENT_MARKER_DIR` (`../telemetry/agent-marker.ts`) — needed
|
|
20
|
+
* because macOS `os.homedir()` resolves via `getpwuid()` and ignores
|
|
21
|
+
* `process.env.HOME` mutations in tests.
|
|
22
|
+
*/
|
|
23
|
+
import { randomUUID } from 'node:crypto';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
import { getConfigDir } from '../config/index.js';
|
|
26
|
+
/** Env var overriding the journal directory (test isolation). */
|
|
27
|
+
export const JOURNAL_DIR_ENV_VAR = 'SKILLSMITH_JOURNAL_DIR';
|
|
28
|
+
function getJournalDir() {
|
|
29
|
+
const override = process.env[JOURNAL_DIR_ENV_VAR];
|
|
30
|
+
return override && override.length > 0 ? override : join(getConfigDir(), 'journal');
|
|
31
|
+
}
|
|
32
|
+
/** Absolute path to the journal file. Read-time resolved (not memoised) so
|
|
33
|
+
* tests can flip `SKILLSMITH_JOURNAL_DIR` between cases. */
|
|
34
|
+
export function getJournalFilePath() {
|
|
35
|
+
return join(getJournalDir(), 'journal.jsonl');
|
|
36
|
+
}
|
|
37
|
+
let processSessionId;
|
|
38
|
+
/**
|
|
39
|
+
* Process-lifetime session id shared by every journal record this server
|
|
40
|
+
* process writes, and by the in-process undo stack
|
|
41
|
+
* (`@skillsmith/mcp-server`'s `tools/apply-session.helpers.ts`) that keys
|
|
42
|
+
* "session-scoped undo" off the same lifetime: restarting the MCP server
|
|
43
|
+
* both mints a new session id here AND resets the undo stack (a fresh
|
|
44
|
+
* module load), so the two always agree without explicit coordination.
|
|
45
|
+
*/
|
|
46
|
+
export function getJournalSessionId() {
|
|
47
|
+
if (processSessionId === undefined) {
|
|
48
|
+
processSessionId = randomUUID();
|
|
49
|
+
}
|
|
50
|
+
return processSessionId;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Test-only reset. The session id is memoised for the process lifetime by
|
|
54
|
+
* design, but a single Vitest process runs many logically-independent
|
|
55
|
+
* "sessions" (test cases) — call this in `afterEach`/`beforeEach` to avoid
|
|
56
|
+
* cross-test bleed.
|
|
57
|
+
*/
|
|
58
|
+
export function resetJournalSessionIdForTests() {
|
|
59
|
+
processSessionId = undefined;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path.js","sourceRoot":"","sources":["../../../src/journal/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEjD,iEAAiE;AACjE,MAAM,CAAC,MAAM,mBAAmB,GAAG,wBAAwB,CAAA;AAE3D,SAAS,aAAa;IACpB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;IACjD,OAAO,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,SAAS,CAAC,CAAA;AACrF,CAAC;AAED;4DAC4D;AAC5D,MAAM,UAAU,kBAAkB;IAChC,OAAO,IAAI,CAAC,aAAa,EAAE,EAAE,eAAe,CAAC,CAAA;AAC/C,CAAC;AAED,IAAI,gBAAoC,CAAA;AAExC;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACnC,gBAAgB,GAAG,UAAU,EAAE,CAAA;IACjC,CAAC;IACD,OAAO,gBAAgB,CAAA;AACzB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,6BAA6B;IAC3C,gBAAgB,GAAG,SAAS,CAAA;AAC9B,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Change-journal reader — hash-chain verification
|
|
3
|
+
* (SMI-5456 Wave 1 Step 3 / SMI-5470).
|
|
4
|
+
* @module @skillsmith/core/journal/reader
|
|
5
|
+
*
|
|
6
|
+
* Never throws. The journal exists to be evidence (PRD §7 / §10 exclusion
|
|
7
|
+
* 1); a reader that raises on the first bad byte can't produce that
|
|
8
|
+
* evidence when it matters most (after a tamper or a crash mid-write). A
|
|
9
|
+
* missing file is a valid, empty chain. A corrupt or tampered line is
|
|
10
|
+
* reported via `breakAt` — the caller (e.g. `undo_apply`, or a future audit
|
|
11
|
+
* surface) decides what to do with a break, but the reader never decides
|
|
12
|
+
* for them by throwing.
|
|
13
|
+
*/
|
|
14
|
+
import type { JournalRecord } from './types.js';
|
|
15
|
+
export interface ChainVerificationResult {
|
|
16
|
+
/** `true` iff every line parsed, chained, and hash-verified cleanly. */
|
|
17
|
+
valid: boolean;
|
|
18
|
+
/** Records verified so far — the full chain when `valid` is `true`, or
|
|
19
|
+
* every record BEFORE the break when `valid` is `false`. */
|
|
20
|
+
records: JournalRecord[];
|
|
21
|
+
/** 0-based line index of the first unparseable / malformed / tampered
|
|
22
|
+
* record. Present iff `valid` is `false`. */
|
|
23
|
+
breakAt?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Verify the journal's hash chain line-by-line: each record's `prev_hash`
|
|
27
|
+
* must equal the previous record's `record_hash` (or `JOURNAL_GENESIS_HASH`
|
|
28
|
+
* for the first line), and each record's own `record_hash` must match a
|
|
29
|
+
* fresh recomputation from its other fields (SMI-5456 §7 tamper signal).
|
|
30
|
+
*
|
|
31
|
+
* @param filePath - Override for tests; defaults to the resolved journal path.
|
|
32
|
+
*/
|
|
33
|
+
export declare function verifyJournalChain(filePath?: string): Promise<ChainVerificationResult>;
|
|
34
|
+
/** Convenience wrapper: the verified records, or as many as verified before
|
|
35
|
+
* a break. Callers that need the break position should use
|
|
36
|
+
* `verifyJournalChain` directly. */
|
|
37
|
+
export declare function readJournalRecords(filePath?: string): Promise<JournalRecord[]>;
|
|
38
|
+
//# sourceMappingURL=reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../../../src/journal/reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,WAAW,uBAAuB;IACtC,wEAAwE;IACxE,KAAK,EAAE,OAAO,CAAA;IACd;gEAC4D;IAC5D,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB;iDAC6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAwBD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA6C5F;AAED;;oCAEoC;AACpC,wBAAsB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAGpF"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Change-journal reader — hash-chain verification
|
|
3
|
+
* (SMI-5456 Wave 1 Step 3 / SMI-5470).
|
|
4
|
+
* @module @skillsmith/core/journal/reader
|
|
5
|
+
*
|
|
6
|
+
* Never throws. The journal exists to be evidence (PRD §7 / §10 exclusion
|
|
7
|
+
* 1); a reader that raises on the first bad byte can't produce that
|
|
8
|
+
* evidence when it matters most (after a tamper or a crash mid-write). A
|
|
9
|
+
* missing file is a valid, empty chain. A corrupt or tampered line is
|
|
10
|
+
* reported via `breakAt` — the caller (e.g. `undo_apply`, or a future audit
|
|
11
|
+
* surface) decides what to do with a break, but the reader never decides
|
|
12
|
+
* for them by throwing.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { getJournalFilePath } from './path.js';
|
|
16
|
+
import { computeRecordHash } from './writer.js';
|
|
17
|
+
import { JOURNAL_GENESIS_HASH } from './hash.js';
|
|
18
|
+
/** Narrow an arbitrary parsed JSON value down to the `JournalRecord` shape. */
|
|
19
|
+
function isJournalRecordShape(value) {
|
|
20
|
+
if (!value || typeof value !== 'object')
|
|
21
|
+
return false;
|
|
22
|
+
const r = value;
|
|
23
|
+
return (typeof r.schema === 'number' &&
|
|
24
|
+
typeof r.ts === 'number' &&
|
|
25
|
+
typeof r.session_id === 'string' &&
|
|
26
|
+
typeof r.tool === 'string' &&
|
|
27
|
+
(r.action === 'apply' || r.action === 'error' || r.action === 'undo') &&
|
|
28
|
+
(r.suggestion_id === null || typeof r.suggestion_id === 'string') &&
|
|
29
|
+
(r.target_path === null || typeof r.target_path === 'string') &&
|
|
30
|
+
(r.before_hash === null || typeof r.before_hash === 'string') &&
|
|
31
|
+
(r.after_hash === null || typeof r.after_hash === 'string') &&
|
|
32
|
+
(r.approval === null || typeof r.approval === 'string') &&
|
|
33
|
+
(r.backup_ref === null || typeof r.backup_ref === 'string') &&
|
|
34
|
+
(r.detail === null || typeof r.detail === 'string') &&
|
|
35
|
+
typeof r.prev_hash === 'string' &&
|
|
36
|
+
typeof r.record_hash === 'string');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Verify the journal's hash chain line-by-line: each record's `prev_hash`
|
|
40
|
+
* must equal the previous record's `record_hash` (or `JOURNAL_GENESIS_HASH`
|
|
41
|
+
* for the first line), and each record's own `record_hash` must match a
|
|
42
|
+
* fresh recomputation from its other fields (SMI-5456 §7 tamper signal).
|
|
43
|
+
*
|
|
44
|
+
* @param filePath - Override for tests; defaults to the resolved journal path.
|
|
45
|
+
*/
|
|
46
|
+
export async function verifyJournalChain(filePath) {
|
|
47
|
+
const path = filePath ?? getJournalFilePath();
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = await readFile(path, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Missing file (never written yet) — a valid, empty chain.
|
|
54
|
+
return { valid: true, records: [] };
|
|
55
|
+
}
|
|
56
|
+
const lines = raw.split('\n').filter((line) => line.trim().length > 0);
|
|
57
|
+
const records = [];
|
|
58
|
+
let expectedPrevHash = JOURNAL_GENESIS_HASH;
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(lines[i]);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Corrupt/torn line (e.g. a crash mid-append) — report, don't throw.
|
|
66
|
+
return { valid: false, records, breakAt: i };
|
|
67
|
+
}
|
|
68
|
+
if (!isJournalRecordShape(parsed)) {
|
|
69
|
+
return { valid: false, records, breakAt: i };
|
|
70
|
+
}
|
|
71
|
+
if (parsed.prev_hash !== expectedPrevHash) {
|
|
72
|
+
// Chain discontinuity — a record was deleted, reordered, or its
|
|
73
|
+
// neighbor was tampered.
|
|
74
|
+
return { valid: false, records, breakAt: i };
|
|
75
|
+
}
|
|
76
|
+
const { record_hash, ...rest } = parsed;
|
|
77
|
+
if (computeRecordHash(rest) !== record_hash) {
|
|
78
|
+
// The record's own content was edited after the fact.
|
|
79
|
+
return { valid: false, records, breakAt: i };
|
|
80
|
+
}
|
|
81
|
+
records.push(parsed);
|
|
82
|
+
expectedPrevHash = record_hash;
|
|
83
|
+
}
|
|
84
|
+
return { valid: true, records };
|
|
85
|
+
}
|
|
86
|
+
/** Convenience wrapper: the verified records, or as many as verified before
|
|
87
|
+
* a break. Callers that need the break position should use
|
|
88
|
+
* `verifyJournalChain` directly. */
|
|
89
|
+
export async function readJournalRecords(filePath) {
|
|
90
|
+
const result = await verifyJournalChain(filePath);
|
|
91
|
+
return result.records;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader.js","sourceRoot":"","sources":["../../../src/journal/reader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Change-journal record shape (SMI-5456 Wave 1 Step 3 / SMI-5470).
|
|
3
|
+
* @module @skillsmith/core/journal/types
|
|
4
|
+
*
|
|
5
|
+
* The journal is the trust-loop evidence base described in
|
|
6
|
+
* docs/internal/product/prd-skillsmith-agent.md §7: an append-only,
|
|
7
|
+
* hash-chained record of every mutation the apply-family MCP tools make
|
|
8
|
+
* (`apply_namespace_rename`, `apply_recommended_edit`) plus the `undo_apply`
|
|
9
|
+
* tool that reverses them. It is the substrate PRD §10 exclusion 1 points at
|
|
10
|
+
* before autonomy can ever be relaxed ("90 days of journal data shows ≥95%
|
|
11
|
+
* suggestion-acceptance and zero rollback incidents").
|
|
12
|
+
*
|
|
13
|
+
* Field naming is snake_case (not the package's usual camelCase) because the
|
|
14
|
+
* journal is a durable on-disk wire format, not an in-memory TS shape — it
|
|
15
|
+
* mirrors the telemetry wire format's convention (SMI-5012) for the same
|
|
16
|
+
* reason: an on-disk/over-the-wire record should read the same regardless of
|
|
17
|
+
* which language eventually parses it.
|
|
18
|
+
*/
|
|
19
|
+
/** Bump on a breaking shape change to `JournalRecord`. */
|
|
20
|
+
export declare const JOURNAL_SCHEMA_VERSION = 1;
|
|
21
|
+
/**
|
|
22
|
+
* `'apply'` — an apply-family tool successfully mutated a file.
|
|
23
|
+
* `'error'` — an apply-family tool attempted a mutation and it failed
|
|
24
|
+
* (the `detail` field carries the typed error kind).
|
|
25
|
+
* `'undo'` — `undo_apply` successfully reversed a prior `'apply'` record.
|
|
26
|
+
*/
|
|
27
|
+
export type JournalAction = 'apply' | 'error' | 'undo';
|
|
28
|
+
/**
|
|
29
|
+
* Every field the caller supplies. The writer (`writer.ts`) fills in
|
|
30
|
+
* `prev_hash` + `record_hash` to complete a `JournalRecord`.
|
|
31
|
+
*
|
|
32
|
+
* Field semantics:
|
|
33
|
+
* - `suggestion_id` — the `collisionId` the mutation was applied for.
|
|
34
|
+
* - `target_path` — the on-disk file whose CONTENT the record's hashes
|
|
35
|
+
* describe. For a single-file mutation (prose edit, command/agent rename)
|
|
36
|
+
* this is the mutated file itself. For a skill-directory rename
|
|
37
|
+
* (`rename_skill_dir_and_frontmatter`) this is `<newSkillDir>/SKILL.md` —
|
|
38
|
+
* the one file whose bytes actually changed; the directory rename itself
|
|
39
|
+
* is not content-hashable and is intentionally out of this record's scope
|
|
40
|
+
* (see `tools/apply-journal.helpers.ts` header in `@skillsmith/mcp-server`
|
|
41
|
+
* for the full rationale).
|
|
42
|
+
* - `before_hash` / `after_hash` — sha256 of `target_path`'s content
|
|
43
|
+
* immediately before / after the mutation. `null` where not applicable
|
|
44
|
+
* (e.g. an `'error'` record where the mutation never reached a
|
|
45
|
+
* content-changing step, so there is no "after" state to hash).
|
|
46
|
+
* - `approval` — the caller-supplied approval mode that authorized the
|
|
47
|
+
* mutation, e.g. `'apply'` / `'custom'` (namespace rename) or
|
|
48
|
+
* `'apply_with_confirmation'` (recommended edit). Always non-null for
|
|
49
|
+
* `'apply'`/`'error'` records (the confirmation gate ran); `'undo'` for
|
|
50
|
+
* undo records.
|
|
51
|
+
* - `backup_ref` — absolute path to the apply tool's own pre-mutation
|
|
52
|
+
* backup directory (from `createSkillBackup` / `createProseBackup`).
|
|
53
|
+
* `null` for `'error'` and `'undo'` records — an error may not have
|
|
54
|
+
* reached the backup step, and an undo record documents a restore that
|
|
55
|
+
* already happened rather than creating a new backup.
|
|
56
|
+
* - `detail` — free-text elaboration. Populated with the typed error
|
|
57
|
+
* `kind` (e.g. `'namespace.rename.backup_failed'`) on `'error'` records;
|
|
58
|
+
* `null` otherwise.
|
|
59
|
+
*/
|
|
60
|
+
export interface JournalRecordFields {
|
|
61
|
+
schema: number;
|
|
62
|
+
ts: number;
|
|
63
|
+
session_id: string;
|
|
64
|
+
tool: string;
|
|
65
|
+
action: JournalAction;
|
|
66
|
+
suggestion_id: string | null;
|
|
67
|
+
target_path: string | null;
|
|
68
|
+
before_hash: string | null;
|
|
69
|
+
after_hash: string | null;
|
|
70
|
+
approval: string | null;
|
|
71
|
+
backup_ref: string | null;
|
|
72
|
+
detail: string | null;
|
|
73
|
+
}
|
|
74
|
+
/** Input to `appendJournalRecord` — the writer computes the rest. */
|
|
75
|
+
export type JournalRecordInput = JournalRecordFields;
|
|
76
|
+
/**
|
|
77
|
+
* A fully-formed, on-disk journal record (one JSON-Lines row). `prev_hash`
|
|
78
|
+
* chains to the previous record's `record_hash` (or `JOURNAL_GENESIS_HASH`
|
|
79
|
+
* for the first record); `record_hash` commits to every other field
|
|
80
|
+
* INCLUDING `prev_hash`, so any single-record tamper is detectable by
|
|
81
|
+
* recomputing it — see `reader.ts:verifyJournalChain`.
|
|
82
|
+
*/
|
|
83
|
+
export interface JournalRecord extends JournalRecordFields {
|
|
84
|
+
prev_hash: string;
|
|
85
|
+
record_hash: string;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/journal/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,0DAA0D;AAC1D,eAAO,MAAM,sBAAsB,IAAI,CAAA;AAEvC;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,aAAa,CAAA;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,qEAAqE;AACrE,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,CAAA;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,aAAc,SAAQ,mBAAmB;IACxD,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACpB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Change-journal record shape (SMI-5456 Wave 1 Step 3 / SMI-5470).
|
|
3
|
+
* @module @skillsmith/core/journal/types
|
|
4
|
+
*
|
|
5
|
+
* The journal is the trust-loop evidence base described in
|
|
6
|
+
* docs/internal/product/prd-skillsmith-agent.md §7: an append-only,
|
|
7
|
+
* hash-chained record of every mutation the apply-family MCP tools make
|
|
8
|
+
* (`apply_namespace_rename`, `apply_recommended_edit`) plus the `undo_apply`
|
|
9
|
+
* tool that reverses them. It is the substrate PRD §10 exclusion 1 points at
|
|
10
|
+
* before autonomy can ever be relaxed ("90 days of journal data shows ≥95%
|
|
11
|
+
* suggestion-acceptance and zero rollback incidents").
|
|
12
|
+
*
|
|
13
|
+
* Field naming is snake_case (not the package's usual camelCase) because the
|
|
14
|
+
* journal is a durable on-disk wire format, not an in-memory TS shape — it
|
|
15
|
+
* mirrors the telemetry wire format's convention (SMI-5012) for the same
|
|
16
|
+
* reason: an on-disk/over-the-wire record should read the same regardless of
|
|
17
|
+
* which language eventually parses it.
|
|
18
|
+
*/
|
|
19
|
+
/** Bump on a breaking shape change to `JournalRecord`. */
|
|
20
|
+
export const JOURNAL_SCHEMA_VERSION = 1;
|
|
21
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/journal/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,0DAA0D;AAC1D,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Change-journal writer — single-writer, fsync'd, hash-chained
|
|
3
|
+
* append (SMI-5456 Wave 1 Step 3 / SMI-5470).
|
|
4
|
+
* @module @skillsmith/core/journal/writer
|
|
5
|
+
*
|
|
6
|
+
* P-5 single-writer invariant (docs/internal/implementation/smi-5456-skillsmith-agent-wave1.md
|
|
7
|
+
* "Shared-State / Coordination Audit"): the journal file is written by the
|
|
8
|
+
* apply-family MCP tools ONLY, and every writer in this process funnels
|
|
9
|
+
* through `appendJournalRecord`, which serializes behind ONE module-scoped
|
|
10
|
+
* promise queue. That queue — not any per-caller locking — is what makes
|
|
11
|
+
* concurrent `apply_namespace_rename` / `apply_recommended_edit` / `undo_apply`
|
|
12
|
+
* calls in the same server process produce well-formed, correctly-chained
|
|
13
|
+
* lines instead of interleaved/torn writes.
|
|
14
|
+
*
|
|
15
|
+
* Durability: each append opens the file, writes the line, calls
|
|
16
|
+
* `FileHandle.sync()` (fsync), then closes — so a record that returns
|
|
17
|
+
* successfully is durable on disk before the caller's apply/undo response
|
|
18
|
+
* goes out, matching the "fsync'd appends" requirement in the plan's P-5
|
|
19
|
+
* table.
|
|
20
|
+
*/
|
|
21
|
+
import type { JournalRecord, JournalRecordInput } from './types.js';
|
|
22
|
+
/**
|
|
23
|
+
* Recompute a record's `record_hash` from its other fields. Exported so the
|
|
24
|
+
* reader can independently verify each line without duplicating the
|
|
25
|
+
* canonicalization logic (drift between writer and reader would silently
|
|
26
|
+
* break every chain-verification call).
|
|
27
|
+
*/
|
|
28
|
+
export declare function computeRecordHash(record: Omit<JournalRecord, 'record_hash'>): string;
|
|
29
|
+
/**
|
|
30
|
+
* Error thrown when the journal's last line cannot be parsed as a valid
|
|
31
|
+
* record — the writer cannot safely determine the chain head to append
|
|
32
|
+
* after. This is distinct from the reader's "corrupt tail = report, never
|
|
33
|
+
* throw" contract: a reader is producing evidence about the past and must
|
|
34
|
+
* degrade gracefully; a writer is about to create NEW evidence and silently
|
|
35
|
+
* starting a fresh chain over a corrupt/tampered tail would hide exactly
|
|
36
|
+
* the tamper signal the chain exists to surface.
|
|
37
|
+
*/
|
|
38
|
+
export interface JournalCorruptTailError extends Error {
|
|
39
|
+
kind: 'journal.write.corrupt_tail';
|
|
40
|
+
}
|
|
41
|
+
export declare function isCorruptTailError(err: unknown): err is JournalCorruptTailError;
|
|
42
|
+
/**
|
|
43
|
+
* Append a record to the journal. Serialized behind the module-scoped
|
|
44
|
+
* write queue and fsync'd before the returned promise resolves.
|
|
45
|
+
*
|
|
46
|
+
* Callers (the apply-family tools) should treat a rejection as fail-soft —
|
|
47
|
+
* the journal is an evidence trail, not a gate on the user's mutation —
|
|
48
|
+
* and log rather than propagate. See `tools/apply-journal.helpers.ts` in
|
|
49
|
+
* `@skillsmith/mcp-server` for the call-site convention.
|
|
50
|
+
*/
|
|
51
|
+
export declare function appendJournalRecord(input: JournalRecordInput): Promise<JournalRecord>;
|
|
52
|
+
//# sourceMappingURL=writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../../src/journal/writer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AA4BnE;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,aAAa,CAAC,GAAG,MAAM,CAEpF;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAwB,SAAQ,KAAK;IACpD,IAAI,EAAE,4BAA4B,CAAA;CACnC;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,uBAAuB,CAK/E;AAkCD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC,CAQrF"}
|