@sema-agent/core 5.39.0 → 5.40.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 +99 -11
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.js +9 -1
- package/dist/core/memory-engine/engine.d.ts +27 -0
- package/dist/core/memory-engine/engine.js +103 -1
- package/dist/core/memory-engine/export-bundle.d.ts +192 -0
- package/dist/core/memory-engine/export-bundle.js +306 -0
- package/dist/core/memory-engine/file-backend.d.ts +178 -1
- package/dist/core/memory-engine/file-backend.js +637 -6
- package/dist/core/memory-engine/index.d.ts +2 -1
- package/dist/core/memory-engine/index.js +1 -0
- package/dist/core/memory-engine/layout.d.ts +89 -1
- package/dist/core/memory-engine/layout.js +131 -1
- package/dist/core/memory-engine/memory-backend-contract.d.ts +1 -1
- package/dist/core/memory-engine/memory-backend-contract.js +52 -0
- package/dist/core/memory-engine/tools.js +8 -1
- package/dist/core/permission-rule-consent.js +14 -2
- package/dist/core/runner/prepare-task.js +13 -0
- package/dist/core/runner/runtask.js +5 -0
- package/dist/core/runner/synthetic-tools.js +3 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/write-protect.d.ts +0 -20
- package/dist/core/write-protect.js +4 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/safety.js +1 -1
- package/dist/tools/fs/search.d.ts +33 -0
- package/dist/tools/fs/search.js +72 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.40.0 — 2026-08-18
|
|
4
|
+
|
|
5
|
+
No BREAKING changes. Two behavioral narrowings disclosed below (the requestedCwd × workspace-restore
|
|
6
|
+
combination now refuses loudly; Grep's ripgrep leg withholds alias-spelled deny-listed paths it
|
|
7
|
+
previously returned).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- design/178 v2-c — governed memory export/import: `MemoryEngine.exportMemoryScopes` /
|
|
12
|
+
`importMemoryBundle` over a self-describing bundle (`MemoryExportBundle`: entries + governance
|
|
13
|
+
rows — unresolved challenges, durable pollution markers, committed lineage, custody evidence —
|
|
14
|
+
+ residuals + a recomputable integrity section). Import is two-phase: entries land under a
|
|
15
|
+
synthetic lineage LATCH (withheld from every model-visible read face) and only a durable,
|
|
16
|
+
fully-validated completion receipt releases them; a crash mid-governance converges by re-running
|
|
17
|
+
the SAME bundle import (idempotent). Tenant slicing is per-artifact three-way (refusal /
|
|
18
|
+
field redaction with an import-visible provenance stamp / global-residual); a `complete:false`
|
|
19
|
+
export refuses to mint a package at all. Integrity detects accidents, never forgery (self-attested
|
|
20
|
+
hashes; authenticity is the transport's job — the multi-tenant service form gates imports behind
|
|
21
|
+
its authenticated endpoint). New exports: `computeMemoryBundleHash` + `MemoryExportBundle` /
|
|
22
|
+
`MemoryImportReport` / `MemoryExportSnapshot` / `MemoryBundleImportPlan` / `BundleChallengeRow` /
|
|
23
|
+
`BundleLineageRow` / `BundlePollutedSession` types. New coded errors: `memory.export_incomplete`,
|
|
24
|
+
`memory.import_rejected` (input-shape errors use the existing `config.memory_export_request` /
|
|
25
|
+
`config.memory_import_request` family). Free-string fields (pollution/challenge reasons, custody
|
|
26
|
+
`req`, session ids) export VERBATIM; the bundle's `doc` lines disclose that sub-scope tenant
|
|
27
|
+
isolation does not extend into those literals.
|
|
28
|
+
- backlog #243 — new coded refusal `checkpoint.cwd_conflicts_restore` (see Fixed below).
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- backlog #243 — `RunInternals.requestedCwd` combined with a checkpoint workspace restore is now
|
|
33
|
+
REFUSED loudly instead of half-working: the restored workspace's mountPath is authoritative for
|
|
34
|
+
the task root, so the cwd was semantically ignored at best — and verifying it was prepare's FIRST
|
|
35
|
+
env RPC, which a lazy-connect remote adapter answers by connecting a fresh EMPTY sandbox before
|
|
36
|
+
`resumeVM` (silently running the resumed task on an empty workspace), or the check itself
|
|
37
|
+
deadlocked the approval on a phantom mismatch (`env_failed` reopen retrying deterministically).
|
|
38
|
+
Two gates: the resume ladder refuses PRE-CAS (`checkpoint.cwd_conflicts_restore` — the checkpoint
|
|
39
|
+
stays pending; re-resume without the cwd succeeds), and prepare carries a depth twin for direct
|
|
40
|
+
`runTaskStream(spec, resume, internals)` callers (`config.cwd_conflicts_restore`, refused before
|
|
41
|
+
the env factory mints anything). **Narrowing**: on eager adapters this combination previously
|
|
42
|
+
"worked"; it now refuses on every adapter. Core's own delegation drivers never mint the pair.
|
|
43
|
+
- backlog #303 — Grep's ripgrep leg gains a deny TRIPWIRE: rg's exclusion globs are spelled from
|
|
44
|
+
the pattern text and cannot express the win32 component-alias family (`.aws.` — trailing
|
|
45
|
+
dots/spaces the win32 namespace strips), so a directory planted under an alias spelling slipped
|
|
46
|
+
past the globs and its contents reached Grep results whenever ripgrep was installed, while the JS
|
|
47
|
+
leg correctly pruned it. Every rg output line is now judged with the same two-view matchPath the
|
|
48
|
+
JS walker prunes with; ANY deny hit — or any line the record format cannot account for (e.g. a
|
|
49
|
+
newline inside a path component splitting one rg record across physical lines) — abandons the rg
|
|
50
|
+
pass entirely and the JS scanner answers, pruning with the authoritative judge and declaring the
|
|
51
|
+
abandonment on both the text and structured surfaces. **Narrowing**: results no longer differ by
|
|
52
|
+
whether ripgrep is installed; rg remains the fast path when no guarded entry is involved
|
|
53
|
+
(`#306` tracks the structured `--json` terminal form).
|
|
54
|
+
- backlog #298 — a THROWING deployment-supplied `breakerOpen` on the auto-mode decider reads as
|
|
55
|
+
not-open: the peer-referral ask is still minted (one extra question), never a silent allow.
|
|
56
|
+
- backlog #300 — `WRITE_PROTECTED_DEFAULT_TABLE` and every resolved table are deep-frozen: an
|
|
57
|
+
admin-face in-place mutate (splice / `length = 0`) throws at the mutation site instead of
|
|
58
|
+
silently weakening the process-wide protection floor.
|
|
59
|
+
- backlog #301 — the CC settings-import preview guards the settings ROOT like it already guarded
|
|
60
|
+
the `permissions` slot: a valid-JSON document that is a string/number/array reports the layer as
|
|
61
|
+
UNREAD (`skipped`) instead of two clean zeros; a `null` document remains the readable
|
|
62
|
+
"nothing written".
|
|
63
|
+
- 5.40 pre-release rescan (memory-engine import/export hardening, all pinned):
|
|
64
|
+
- the import's retroactive pollution sweep mints its challenge events under a TOP-LEVEL
|
|
65
|
+
`imp-sweep:` prefix, structurally disjoint from the carried-challenge leg's
|
|
66
|
+
`imp:<hash>:<source eventId>` — a shared key with a stale frozen `challengedRev` (a
|
|
67
|
+
crash-window promote on the source drifting lineage `lastRev`) aborted the read-back verifier
|
|
68
|
+
AFTER the latch staged, and the documented re-import convergence reproduced the abort forever;
|
|
69
|
+
- BOTH export faces refuse over the durable chain-degradation marker
|
|
70
|
+
(`transfers.chain-degraded.json`): a healed (quarantined + truncated) evidence chain parses
|
|
71
|
+
clean, but the store has declared the lost rows unprovable, and a bundle minted over it
|
|
72
|
+
testified to a deletion history the source knew was incomplete (an erased id could resurrect on
|
|
73
|
+
the destination); the composite throws `memory.export_incomplete`, `governanceExport` answers
|
|
74
|
+
`complete:false` with the reason;
|
|
75
|
+
- the bundle-import completion receipt validates EVERY `MemoryImportReport` member — an 8-of-15
|
|
76
|
+
husk could release a standing latch and hand back a report whose missing arrays throw on first
|
|
77
|
+
touch.
|
|
78
|
+
- `deferMode: "auto"` never sweeps engine BUILT-INS (align-with-CC ruling): the pressure valve's
|
|
79
|
+
candidates narrow to caller-supplied specs — CC's own `isDeferredTool` defers MCP as a family
|
|
80
|
+
plus individually self-declared tools and has no pressure valve at all, so built-ins like
|
|
81
|
+
Grep/Glob/Read stay inline unconditionally; sweeping them made every small-window model pay a
|
|
82
|
+
first-use activation error on tools its training treats as always present. An explicit
|
|
83
|
+
`deferTools` naming a built-in still honors the deployment's stated intent.
|
|
84
|
+
- Out-of-root refusals no longer print a duplicated root: a directory declared through two doors
|
|
85
|
+
(`additionalDirectories` + the advertised scratchpad) deduplicates by canonical value in the
|
|
86
|
+
disclosure list (the judgment was already idempotent).
|
|
87
|
+
- backlog #309 — the out-of-root escape hint names the `readFace: "open"` knob beside the
|
|
88
|
+
per-directory seats; the unknown-skill refusal states that foreign skill directories are not
|
|
89
|
+
loaded and points at importing the content instead (signpost ruling).
|
|
90
|
+
|
|
3
91
|
## 5.39.0 — 2026-08-17
|
|
4
92
|
|
|
5
93
|
No BREAKING changes. Two behavioral narrowings disclosed below (write-protection default table;
|
|
@@ -63,7 +151,7 @@ baseline slots refuse at mount instead of silently mounting ungoverned).
|
|
|
63
151
|
spelling was already collapsed by the filesystem itself — a genuinely-resolved POSIX target keeps
|
|
64
152
|
its own reading and a real `.aws.` directory stays readable and writable. The arms that resolve
|
|
65
153
|
NOTHING do not report it and keep both views: UNC keys (minted with zero probes for liveness) and
|
|
66
|
-
not-yet-existing targets (whose tail is rejoined verbatim) — which is where the
|
|
154
|
+
not-yet-existing targets (whose tail is rejoined verbatim) — which is where the pre-creation path
|
|
67
155
|
lives. Unreported is the fail-closed default, so a caller that forgets to pass it loses precision,
|
|
68
156
|
never protection.
|
|
69
157
|
**Residual cost**: a target the filesystem has NOT resolved still over-matches — writing a
|
|
@@ -902,7 +990,7 @@ breaking change (rows without the new gate bit keep their historic stamps).
|
|
|
902
990
|
governed row (org origin) is refused PRE-CAS on a worker with no `permissionRuleOrg` wiring (the
|
|
903
991
|
human decision stays unspent, redeemable on an org-wired worker; a post-CAS belt remains as
|
|
904
992
|
defense in depth); a v8 row whose bit was stripped in storage, and a sub-v8 row carrying a bit no
|
|
905
|
-
release minted, are both refused pre-CAS (corruption/
|
|
993
|
+
release minted, are both refused pre-CAS (corruption/fabrication guards).
|
|
906
994
|
- **`run_in_background: true` is judged by the backgrounding doctrine (#125, P0).** On a
|
|
907
995
|
`shellGate:"classify"` deployment, `bashReversibilityProbe` used to read only the command text, so
|
|
908
996
|
`ls` + the parameter form auto-admitted exactly what `ls &` asks for. The parameter spelling now
|
|
@@ -1636,7 +1724,7 @@ mysterious runtime.
|
|
|
1636
1724
|
claiming the row is still pending. Terminal-state refusals name the observed status without
|
|
1637
1725
|
coercing store-supplied values.
|
|
1638
1726
|
- The approval lane's `reason` is validated as plain text at capture; malformed decisions and
|
|
1639
|
-
|
|
1727
|
+
unexpected text shapes get typed refusals instead of raising inside the refusal path.
|
|
1640
1728
|
- Orchestration guidance, the TaskOutput tool card, and the selective-recall affordance stop
|
|
1641
1729
|
teaching retired tool names: the workflow tool is named by its wire name, and
|
|
1642
1730
|
`composeSelectiveBody` accepts an optional caller-supplied `recallToolName` (additive).
|
|
@@ -2178,7 +2266,7 @@ live `onAsk` is wired — drop the approver (or arm `forceDurableGate`) to test
|
|
|
2178
2266
|
|
|
2179
2267
|
- **Memory recall: an unknown write time is incomparable, not 1970.** A note whose `ts` failed to parse used to sort as epoch 0 — it lost every `[[name]]` collision (permanently unreachable through links) and rendered a fabricated age ("written 678 months ago"). `MemoryNoteHeader.timestampMissing` (new, optional) is minted at the single parse point; the one recency comparison treats missing as a tie (the incumbent keeps the name — an unknown-time note neither sinks nor poses as newest), and the render face says "write time unknown — verify it's still current". A literal 1970 timestamp string stays a real time (the numeric value is never the signal; negative pins lock this). Bonus: a wire row with NO `ts` key used to throw and kill the whole scope's manifest build. **Consumer flip**: a probe pinning the fabricated-age wording or the sink-to-bottom collision outcome reds.
|
|
2180
2268
|
- **Three awaited rescue timers are REF'd + cleared-on-settle** (subagent observer drain, teacher abortFallback, WebFetch deadline): unref'd, the process could exit before the rescue fired when the wedged arm held no live handles — the settle never completed. The bg notify drain window keeps its unref by design (a background lane must not pin the process).
|
|
2181
|
-
- **A harness-side throw around ONE tool call becomes that call's disclosed error result — the batch survives.** Worker exceptions that are not cancellation (a rejected `tool_execution_update` delivery, a sink failure) used to reject the whole parallel batch, orphan the sibling calls, and on the worst path rewrite a COMMITTED side effect into `[tool execution harness failed: …]` — inviting the model to re-run an already-effective write. Progress deliveries now settle at one point; a delivery failure on an executed tool keeps `content`/`isError` untouched and rides `details.toolProgressDeliveryFailed` (total by construction:
|
|
2269
|
+
- **A harness-side throw around ONE tool call becomes that call's disclosed error result — the batch survives.** Worker exceptions that are not cancellation (a rejected `tool_execution_update` delivery, a sink failure) used to reject the whole parallel batch, orphan the sibling calls, and on the worst path rewrite a COMMITTED side effect into `[tool execution harness failed: …]` — inviting the model to re-run an already-effective write. Progress deliveries now settle at one point; a delivery failure on an executed tool keeps `content`/`isError` untouched and rides `details.toolProgressDeliveryFailed` (total by construction: throwing accessors or exotic `details` decline the annotation, never the outcome; `afterToolCall` cannot erase it). Cancellation — including one wrapped in the harness's own `AgentHarnessError` (`cause`-chain walk) — stays a hard batch failure on every lane, and the in-stream lane closes admission immediately. The sequential lane shares the same disclosure band. **Consumer flips**: probes pinning the old `[tool execution harness failed]` rewrite or `isError:true` on a delivery-failed success red — re-pin on the new shape plus a retired-dialect tripwire.
|
|
2182
2270
|
- **openai lane: an evidence-free `tool_calls` fragment no longer fabricates an unnamed-call disclosure.** The evidence gate sits at the finalize disclosure point (slot assignment also routes id-less continuations, so gating there corrupted two real calls); the stream's own verdict wins — `finish_reason:"tool_calls"` with zero survivors, or an empty slot beside real calls, discloses loudly; an isolated terminal fragment with `stop`/`length` stays silent.
|
|
2183
2271
|
- **A throwing tool keeps its machine-readable marks on BOTH legs.** Two strip points, one class: the `defineTool` adapter rewrapped a ToolSpec throw as a bare `Error` (marks died even LIVE — a ToolSpec throw carrying `details.code` got no `errorCode` lift while a native tool's identical error did), and the durable-resume batch's catch stringified the message alone (breaking the live/resume frame-parity contract). Both carry `details`/`errorKind` through now; message text unchanged.
|
|
2184
2272
|
- **Cache-break attribution: an aborted / usage-missing row is "unknown", not a cold cache.** Feeding it to the design/31 detector minted a false `server-or-ttl` finding and poisoned the warm baseline for the next real turn. Such rows are skipped; a genuine cacheRead collapse still fires.
|
|
@@ -2708,7 +2796,7 @@ _Emergency direct-publish path (CI publishing resumes 8/1); pre-tag `gate:blackb
|
|
|
2708
2796
|
**API retry chain (user-reported, rate-limited third-party providers)**
|
|
2709
2797
|
|
|
2710
2798
|
- `maxRetries` defaults to 10 (was 2), configurable via explicit brain options (highest priority, uncapped) or `SEMA_MAX_RETRIES` (clamped to [0,15]); `retryDelayMs` base is now 500.
|
|
2711
|
-
- Backoff is increasing-with-light-jitter — `min(500·2^(n-1), 32000) + rand·0.25·backoff` — replacing full-jitter (attempt N could previously wait less than attempt 1). `Retry-After` keeps its existing max-composition semantics; `anthropic-ratelimit-unified-reset` is now honored the same way (header values clamped to 60s against
|
|
2799
|
+
- Backoff is increasing-with-light-jitter — `min(500·2^(n-1), 32000) + rand·0.25·backoff` — replacing full-jitter (attempt N could previously wait less than attempt 1). `Retry-After` keeps its existing max-composition semantics; `anthropic-ratelimit-unified-reset` is now honored the same way (header values clamped to 60s against out-of-range headers).
|
|
2712
2800
|
- Retry liveness reaches the task event stream: `status` frames carry `attempt`/`maxRetries`/`retryInMs`, waits longer than 30s re-announce per slice, recovery emits a `recovered` terminal and exhaustion a `gave_up` terminal (phases added to `BrainStatusPhase`), and `circuit_open` carries the remaining cooldown. Clients can finally clear a stale error row the moment the stream recovers.
|
|
2713
2801
|
|
|
2714
2802
|
**Security (read-only bash boundary — bypass closure, disclosure correction for 2.8.0)**
|
|
@@ -3486,7 +3574,7 @@ Carried over from 1.410.0's open list: **RB-95** (the compaction cut-point's bac
|
|
|
3486
3574
|
|
|
3487
3575
|
## 1.410.0 (2026-07-25)
|
|
3488
3576
|
|
|
3489
|
-
The divergent-
|
|
3577
|
+
The divergent-probe round: four more probe agents, again forbidden to touch source, produced 44 reproducible defects across the approval chain, the session tree, the tool surface and the agent-team stores. 22 are fixed here, 2 are recorded as open with their reds resident. One of them was mine — 1.409.0's own fix introduced it, and it is the most serious thing in this release.
|
|
3490
3578
|
|
|
3491
3579
|
**Note on 1.409.0**: its tag was pushed but CI went red on `flaky-guard` and it never reached npm. The cause was not the release content — the guard's stale-allowlist check flagged a declaration for an untracked `*.local.test.ts` probe, a file that exists on a developer's machine and, by design, not in CI. The check now skips entries whose file is absent from the scan. Everything in 1.409.0 ships here.
|
|
3492
3580
|
|
|
@@ -3513,7 +3601,7 @@ A multi-dimensional red hunt (five independent probe agents + a first-party pass
|
|
|
3513
3601
|
- **fix (RB-64): the anti-re-vote guard was nested entirely inside the `policy_ask` arm**, so a `plan_review` checkpoint reopened after an `env_failed` retry accepted a decision contradicting the one the human actually made — and `winnerFromOutcome` recorded no winner for that gate, leaving nothing to replay against. Winners are now recorded for the review gates (with a synthetic `gate:plan_review` sentinel that can never collide with a real toolCallId) and the guard is gate-agnostic.
|
|
3514
3602
|
- **fix (RB-68): `durableApproval.ttlMs` reached `Date.now() + ttlMs` unvalidated** — `Number(unset_env_var)` produced `deadline = NaN`, and `NaN <= cutoff` is always false, so a checkpoint that carries a TTL and looks bounded could never be reaped by any cutoff. Fixed in both layers: mints normalize a non-finite/non-positive TTL to the documented default (a deliberate `undefined` still means unbounded), and `reap` treats an uncomparable deadline as reapable, since checkpoint rows are also written by deployment-side stores and older versions.
|
|
3515
3603
|
- **fix (RB-70): `resumeStream` never set `requireExistingSession`**, so the headline durable scenario — a restarted process resolving a checkpoint while only the CheckpointStore was wired durably — fabricated a brand-new empty session under the checkpoint's id and pointed a dangling leaf at it. Every other resume-shaped call site already set it. The resulting `resume.session_not_found` now reopens the checkpoint as `env_failed`: the action never ran and the human's decision is still valid, so the approval is retryable rather than burned.
|
|
3516
|
-
- **fix (RB-71): one throwing getter collapsed an entire object to a single `<unserializable>` sentinel**, so any two
|
|
3604
|
+
- **fix (RB-71): one throwing getter collapsed an entire object to a single `<unserializable>` sentinel**, so any two unserializable payloads hashed identically — and `boundInputHashOf` exists precisely to prove the args being executed are the args a human saw. The sentinel is now scoped to the offending key; totality is unchanged, resolution costs one key instead of the whole payload.
|
|
3517
3605
|
- **fix (RB-57): `findOrphanToolCalls` resolved tool calls against a global id set**, so a provider that reuses a toolCallId across turns (OpenAI-compatible gateways that number per request) hid a genuine orphan — and an unclosed `tool_use` makes every subsequent request on that session fail at a strict provider, not just one reconcile. Resolution is now positional.
|
|
3518
3606
|
- **fix (RB-65): `tightenTaskSpec` left `excludeTools`/`deferTools` outside its tighten-only guard** — an override replaced the base list instead of unioning with it, and `[]` cleared every base exclusion outright.
|
|
3519
3607
|
- **fix (RB-69): the CC task-list store's addressable id set and its enumerable id set were different** — a `007.json` counted toward the allocation high-water yet was unreachable through get/set/delete, and `list()` reported it as the id `"7"`. Checked against CC 2.1.219 itself (`edr`/`Uid`/`ste`): CC constrains a character class, not a numeric shape, and its list returns filenames verbatim, so `007` is a first-class id there. Both sides now use CC's charset; sema keeps the stricter half of the divergence (an off-charset id throws rather than being silently rewritten into a neighbouring file).
|
|
@@ -3737,7 +3825,7 @@ design/154 (shellGate classify granularity — server [1558]④): compound read-
|
|
|
3737
3825
|
- **feat: `classifyCompoundReadonly(command, allow)`** (exported) — textual scan-split on `;`/`&&`/`||`/`|`, every segment re-vetted by the single shared `parseLeadingCommandName` + bare-name allowlist; all-green ⇒ read-only. Whole-string hard rejects stay for `< > $ ( )` backtick newline CR backslash; a LONE `&` is hard-rejected too (backgrounding outlives the shell and escapes TaskStop lifecycle governance; `|&` rejects via the same rule). Soundness: with escapes/expansion/redirection hard-rejected, quotes can only HIDE connectors (real boundaries ⊆ textual boundaries), so every executed argv[0] is a vetted segment head — a quote-carrying head is parser-rejected, a clean head equals the executed program name. Error direction is strictly false-negative.
|
|
3738
3826
|
- **feat: `bashReversibilityProbe` upgraded** to the compound classifier (signature unchanged). The `bash_readonly` tool's `effect:"read"` declaration face deliberately KEEPS the strict single-command check (documented fork). The Monitor face intentionally shares the upgraded probe (design/135 G2: same env seam, classification is about the command text; a polled read-only compound stays read-only) — pinned by two wiring tests.
|
|
3739
3827
|
- **NOT included by design:** no git/`find`/subcommand allowlisting — "zero-flag" vetting operates on pre-expansion text while the shell expands quotes/braces/globs into argv, and the candidate git subcommands are not provably read-only under repo config (`core.fsmonitor`, `diff.external`/textconv, promisor fetches, index refresh). `git status` still asks — recorded false-negative; widening needs a dedicated review.
|
|
3740
|
-
- Verification: adversarial review (SOUND across five
|
|
3828
|
+
- Verification: adversarial review (SOUND across five probe directions) + a black-box harness in the server-[1558] methodology — red-first detector self-check, then 5000 seeded fuzz commands with EVERY green actually executed under real bash in a snapshot sandbox: zero filesystem mutations, zero surviving process groups, zero hangs; curated adversarial family all red or verified-harmless. Ask-rate on a realistic coding-task corpus: 69% → 43%.
|
|
3741
3829
|
- Backlog: RB-41 (availability residue — stdin-blocking greens like bare `cat` hang to tool timeout; not a reversibility issue, documented).
|
|
3742
3830
|
|
|
3743
3831
|
## 1.386.0 (2026-07-23)
|
|
@@ -3985,7 +4073,7 @@ CWD case core B-half ([1451]/[1452]/[1461]④): resume workspace-root fidelity +
|
|
|
3985
4073
|
- **hardening round 5 (1 high folded): a divergent restore under a PENDING APPROVED ACTION fails closed.** The approved args were authored + human-approved against the checkpointed root — internal path-state rebase cannot rewrite tool args (`rm /app/…` would run against a root the workspace no longer occupies, or burn the approval on a fence refusal). Divergence + pending approval → `resume.env_failed` → the existing reopen leg (resolved→pending) keeps the approval retryable until the adapter honors `priorHandle` or the knob rolls back (pinned end-to-end: fail → reopen → knob-back retry executes exactly once at the right root). The guard keys on the RESUME OUTCOME (`tool_approval` × an `allow` winner — `PrepareResume.executesApprovedAction`), not on the checkpoint kind: a DENY winner executes nothing and resolves under the diverged root without wedging (round 6); non-approval suspends (resource_limit etc.) likewise proceed under the rebase.
|
|
3986
4074
|
- **hardening round 4 (2 high folded):** the rebase contract is POSIX-ONLY — every remote lane's `mountPath` is a Linux container path; a backslash anywhere marks the value outside the domain and the path passes through UNCHANGED (un-rebased is observable via the divergence observation; wrongly-guessed Windows drive/UNC recomposition would silently corrupt cwd/read-state). And the divergence/misalignment `onError` observations are try/catch-isolated — a throwing observer sink must never turn an already-consumed (resolved) checkpoint into a terminal failure (observe-only means observe-only).
|
|
3987
4075
|
- **residuals ledgered (RB-35, adversarial round 7, LOW):** two documented tails of the narrow "rotation window × non-honoring adapter × durable suspend" pathology — canonical-vs-logical root prefixes can miss the rebase on symlinked mounts (observable degradation: cd failure / fence refusal / read-state miss, never a silent wrong write), and the fail-closed guard keys on the allow winner before zero-execution branches (walltime exhaustion, tightened rules) are known (a permanent rotation can loop fail→reopen with an explicit remedy in every error). Both wait on a real deployment shape.
|
|
3988
|
-
- **test: 13 new pins** — priorHandle rides resumeVM, mount-rotation divergence observation (non-fatal), POSITIVE honor leg (prompt cwd = checkpointed root across a rotation, zero divergence noise), resumeFacts render trio (honest sentences /
|
|
4076
|
+
- **test: 13 new pins** — priorHandle rides resumeVM, mount-rotation divergence observation (non-fatal), POSITIVE honor leg (prompt cwd = checkpointed root across a rotation, zero divergence noise), resumeFacts render trio (honest sentences / degraded note / fresh-task-never), resume-leg e2e gating, cwd-alignment guard both ways (honoring env silent; dropping env observed, task proceeds).
|
|
3989
4077
|
|
|
3990
4078
|
## 1.359.0 (2026-07-20)
|
|
3991
4079
|
|
|
@@ -4123,7 +4211,7 @@ Redaction tiering ([1295]② — the assertion-killer) + workflow schema teachin
|
|
|
4123
4211
|
|
|
4124
4212
|
- **fix: same-principal faces stop redacting paths/URLs.** `boundedRedactedSummary` (workflow agent outputs/results/errors flowing back to the launching model+script; run rows behind the scope-gated observe API) now applies `redactSecrets` only — the old blanket `[redacted-path]`/`[redacted-url]` strip killed the cross-agent assertion pattern ("return the path of the file you wrote") while protecting nothing on faces whose isolation is ACCESS CONTROL. CC redacts nothing here; the secret arm stays as a declared superset. Three host-error sites in the Workflow tool (script-path read failures) join the tier — their paths are the HOST'S own error text.
|
|
4125
4213
|
- **`redactSecrets` rides the centralized `scrubSecrets` scanner** (codex HIGH: a second hand-rolled vocabulary WAS the bypass — SAS `sig=`, JWTs, sk-prefixed path tokens all slipped it): prefixed tokens, JWT, PEM, Bearer, `secretKey=value`, plus two URL arms that matter once URLs survive — userinfo credentials stripped to the LAST `@` of the authority (a first-`@` regex left `p@ssword` half-visible and let an empty-username form escape entirely, codex HIGH), and signed-URL params (`sig`/`signature`/`sas`/`code`) redacted by key.
|
|
4126
|
-
- **External faces keep the FULL strip** (codex HIGH): `untrustedEgressForHuman` (human display; URL stripping also
|
|
4214
|
+
- **External faces keep the FULL strip** (codex HIGH): `untrustedEgressForHuman` (human display; URL stripping also removes outbound-link bait) and the cross-replica `WorkflowCompletionNotifier` payload (its contract promises host paths never escape) still apply `redactHostLeaks`. `subscribeWorkflow` remains in-process/by-runId — a deployment exposing it over HTTP scope-gates first (as `getWorkflowRun` does).
|
|
4127
4215
|
- **teaching ([1295]①④)**: the Workflow tool card now states standard-JSON-Schema union support (`{"type":["string","null"]}` / `anyOf` — typebox handles both, verified) and that cross-agent assertions belong in the SCRIPT body (plain JS between stages). Suite green, tsc 0, deepseek live 9/9.
|
|
4128
4216
|
|
|
4129
4217
|
## 1.346.0 (2026-07-19)
|
|
@@ -4204,7 +4292,7 @@ Approval display projection ([1245] — the "Run a dynamic workflow?" confirmati
|
|
|
4204
4292
|
- **feat: `approvalPreview` seam** — a tool may declare `approvalPreview(args)` (AgentTool/ToolSpec, mechanism-neutral): a pure display projection of its args for human approval surfaces. The gate mints it through ONE helper — alias-aware tool lookup, throw-swallowed, 16KiB serialized clamp, and **control-character sanitization on every string leaf** (terminal escapes/CR can never reach an approval renderer raw). Delivered on BOTH lanes: `AskRequest.preview` (live onAsk dialogs) and the durable checkpoint's `pendingAction.preview` + **`CheckpointSummary.preview`** (the one-call `listByScope` inbox — no N+1 get).
|
|
4205
4293
|
- **Trust contract (explicit)**: the preview is UNTRUSTED, ADVISORY display metadata — renderers must contextually escape it and approval surfaces should show the bound raw args alongside (args + boundInputHash remain the sole execution contract; adjudication and resume never read the preview).
|
|
4206
4294
|
- **run_workflow implements it**: script form → statically parsed meta (name/description/phases[{title,detail}], scriptChars, hasArgs); named/scriptPath forms → identity stubs; malformed meta → bounded parse-error head. Runtime agent prompts are statically unknowable — phase detail is the honest ceiling. Also pinned: run_workflow rides the tool gate with NO exemption (an ask-policy naming it parks/asks like any tool — the [1230] "manual mode ran without confirmation" was a policy name-list gap, not an engine bypass).
|
|
4207
|
-
- codex adversarial 2 rounds (
|
|
4295
|
+
- codex adversarial 2 rounds (malformed-metadata sanitization, alias-invoked preview loss, durable-summary projection all folded). Suite 6181 green, tsc 0, deepseek live 9/9.
|
|
4208
4296
|
|
|
4209
4297
|
## 1.337.0 (2026-07-19)
|
|
4210
4298
|
|
|
@@ -1610,6 +1610,12 @@ export declare class CheckpointError extends Error {
|
|
|
1610
1610
|
* Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
|
|
1611
1611
|
* (design/49 §2/§3, code-ready council round-2). */
|
|
1612
1612
|
| "checkpoint.unsupported_version"
|
|
1613
|
+
/** backlog #243: `resume`/`resumeStream` was handed `internals.requestedCwd` for a checkpoint that
|
|
1614
|
+
* carries a remote `workspaceHandle`. The restored workspace's mountPath is authoritative for the
|
|
1615
|
+
* task root, so the cwd would be ignored at best — and verifying it would touch the env BEFORE the
|
|
1616
|
+
* VM restore (a lazy-connect adapter then connects a fresh empty sandbox, the design/49 disease).
|
|
1617
|
+
* Rejected pre-CAS so the checkpoint stays `pending`; re-resume without `requestedCwd`. */
|
|
1618
|
+
| "checkpoint.cwd_conflicts_restore"
|
|
1613
1619
|
/** design/164: the checkpoint was written by a pre-164 worker AND its ledger carries the RETIRED
|
|
1614
1620
|
* cross-slice wall-clock allocation (`resourceLedger.totalWalltimeSec`). The axis no longer exists,
|
|
1615
1621
|
* so resuming would run the leg with the operator's time ceiling silently unenforced. Refused
|
|
@@ -1733,6 +1739,12 @@ export declare class CheckpointError extends Error {
|
|
|
1733
1739
|
* Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
|
|
1734
1740
|
* (design/49 §2/§3, code-ready council round-2). */
|
|
1735
1741
|
| "checkpoint.unsupported_version"
|
|
1742
|
+
/** backlog #243: `resume`/`resumeStream` was handed `internals.requestedCwd` for a checkpoint that
|
|
1743
|
+
* carries a remote `workspaceHandle`. The restored workspace's mountPath is authoritative for the
|
|
1744
|
+
* task root, so the cwd would be ignored at best — and verifying it would touch the env BEFORE the
|
|
1745
|
+
* VM restore (a lazy-connect adapter then connects a fresh empty sandbox, the design/49 disease).
|
|
1746
|
+
* Rejected pre-CAS so the checkpoint stays `pending`; re-resume without `requestedCwd`. */
|
|
1747
|
+
| "checkpoint.cwd_conflicts_restore"
|
|
1736
1748
|
/** design/164: the checkpoint was written by a pre-164 worker AND its ledger carries the RETIRED
|
|
1737
1749
|
* cross-slice wall-clock allocation (`resourceLedger.totalWalltimeSec`). The axis no longer exists,
|
|
1738
1750
|
* so resuming would run the leg with the operator's time ceiling silently unenforced. Refused
|
|
@@ -24,6 +24,8 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
24
24
|
"memory.erasure_selector_mismatch",
|
|
25
25
|
"memory.erasure_census_incomplete",
|
|
26
26
|
"memory.erasure_index_residue",
|
|
27
|
+
"memory.export_incomplete",
|
|
28
|
+
"memory.import_rejected",
|
|
27
29
|
]);
|
|
28
30
|
export function governanceRetryClass(code) {
|
|
29
31
|
if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
|
package/dist/core/hooks.js
CHANGED
|
@@ -398,7 +398,15 @@ export async function runToolGate(input) {
|
|
|
398
398
|
denySource = input.shellGated === true ? "shellGate" : "safety";
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
|
-
|
|
401
|
+
const breakerKnownOpen = (d) => {
|
|
402
|
+
try {
|
|
403
|
+
return d.breakerOpen();
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
if (input.peerMessage === true && decision.action === "allow" && input.autoMode !== undefined && !breakerKnownOpen(input.autoMode.decider)) {
|
|
402
410
|
decision = {
|
|
403
411
|
action: "ask",
|
|
404
412
|
message: `tool "${toolName}" sends a message to another agent — routed for classifier review in auto mode`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CommittedBinding, type EraseMemoryEntriesInput, type MemoryErasureAttestation, type TransferEvidence } from "./file-backend.js";
|
|
2
|
+
import { type MemoryExportBundle, type MemoryImportReport } from "./export-bundle.js";
|
|
2
3
|
import { type ChallengeAssignment, type ChallengeEvent, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionRecord } from "./layout.js";
|
|
3
4
|
import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemorySessionHandle, ScanFinding } from "./types.js";
|
|
4
5
|
/**
|
|
@@ -438,6 +439,32 @@ export declare class MemoryEngine {
|
|
|
438
439
|
* reported honestly, never fabricated and never a throw (#196 absence-reports-not-silent-green).
|
|
439
440
|
*/
|
|
440
441
|
provenanceOf(entryId: string): Promise<EntryProvenanceAccount>;
|
|
442
|
+
/**
|
|
443
|
+
* Export the requested scopes as a GOVERNANCE-COMPLETE bundle (design/178 v2 §4.3 + v2-c):
|
|
444
|
+
* committed entries (audit-snapshot semantics — shadow-backed rows a disk scan would miss are
|
|
445
|
+
* in; unavailable content is enumerated, never dressed), the scope-sliced custody chain, the
|
|
446
|
+
* exported entries' unresolved challenge events and lineage rows, and the named sessions'
|
|
447
|
+
* pollution markers — all read inside the backend's single-lock export composite, so a bundle is
|
|
448
|
+
* never a mixed-epoch scene. Refusals are loud and total (`memory.export_incomplete`): there is
|
|
449
|
+
* no degraded/partial bundle shape — "a truncated package that looks complete" is the one
|
|
450
|
+
* deliverable this API is forbidden to produce.
|
|
451
|
+
*/
|
|
452
|
+
exportMemoryScopes(scopes: readonly string[]): Promise<MemoryExportBundle>;
|
|
453
|
+
/**
|
|
454
|
+
* Import a bundle (design/178 v2-c §1/§7). Validation is TOTAL and lands nothing on failure
|
|
455
|
+
* (`memory.import_rejected`): integrity (fixed section-hash key set, per-entry content-rev
|
|
456
|
+
* recomputation, batch-unique ids/event ids), structure (the governance section is REQUIRED —
|
|
457
|
+
* there is no entries-only escape hatch), and — when `opts.expectedScopes` is present (a service
|
|
458
|
+
* endpoint pins its principal's grant set here) — the scope authorization envelope. Everything
|
|
459
|
+
* after validation runs inside the backend's import composite: the synthetic latch (imported ids
|
|
460
|
+
* stay withheld from every model-visible read face until governance fully lands), the five
|
|
461
|
+
* import judgments IN-LOCK, entry landing through the one transaction skeleton, the governance
|
|
462
|
+
* legs, and the receipt-gated latch release. A crash anywhere converges by re-importing the SAME
|
|
463
|
+
* bundle; a completed import answers its recorded report idempotently.
|
|
464
|
+
*/
|
|
465
|
+
importMemoryBundle(bundle: MemoryExportBundle, opts?: {
|
|
466
|
+
expectedScopes?: readonly string[];
|
|
467
|
+
}): Promise<MemoryImportReport>;
|
|
441
468
|
/** Host API: challenge every entry the lineage ledger attributes to `sessionId` (post-hoc source
|
|
442
469
|
* falsification — trustedTools misconfigured, a tool re-classified, late delegation evidence).
|
|
443
470
|
* Same requestId contract as {@link challengeEntries}. */
|
|
@@ -6,6 +6,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
|
|
|
6
6
|
import { formatMemoryAge } from "../memory-recall.js";
|
|
7
7
|
import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
8
8
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
|
|
9
|
+
import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
9
10
|
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, resolveChallengeEvent, stageLineagePending, } from "./layout.js";
|
|
10
11
|
import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
|
|
11
12
|
export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
|
|
@@ -521,6 +522,105 @@ export class MemoryEngine {
|
|
|
521
522
|
return { v: 1, id: entryId, binding, contributors: plane.contributors, ...(plane.exclusion !== undefined ? { exclusion: plane.exclusion } : {}), contentState, ...(ingest !== undefined ? { ingest } : {}), custody };
|
|
522
523
|
}
|
|
523
524
|
}
|
|
525
|
+
async exportMemoryScopes(scopes) {
|
|
526
|
+
if (!Array.isArray(scopes) || scopes.length === 0 || scopes.some((s) => typeof s !== "string" || s.length === 0) || new Set(scopes).size !== scopes.length) {
|
|
527
|
+
const e = new Error("exportMemoryScopes: scopes must be a non-empty array of unique non-empty scope names");
|
|
528
|
+
e.code = "config.memory_export_request";
|
|
529
|
+
throw e;
|
|
530
|
+
}
|
|
531
|
+
const requested = [...scopes];
|
|
532
|
+
const face = this.backend.exportSnapshotOf;
|
|
533
|
+
if (typeof face !== "function") {
|
|
534
|
+
const e = new Error("exportMemoryScopes: this backend has no export-snapshot capability (exportSnapshotOf) — a governance-complete bundle cannot be assembled from the generic read faces (they cannot fence the five store faces into one epoch), and a governance-less export is the laundering shape this API refuses by design.");
|
|
535
|
+
e.code = "memory.export_incomplete";
|
|
536
|
+
throw e;
|
|
537
|
+
}
|
|
538
|
+
const snap = await face.call(this.backend, requested);
|
|
539
|
+
const exportedIds = new Set([...snap.entries.map((e) => e.id), ...snap.contentUnavailable.map((r) => r.id)]);
|
|
540
|
+
const resolvedGenerations = new Set();
|
|
541
|
+
for (const e of snap.challenges)
|
|
542
|
+
if (e.kind === "resolve")
|
|
543
|
+
resolvedGenerations.add(`${e.entryId}#${e.generation}`);
|
|
544
|
+
const challenges = [];
|
|
545
|
+
for (const e of snap.challenges) {
|
|
546
|
+
if (e.kind !== "challenge" || !exportedIds.has(e.entryId) || resolvedGenerations.has(`${e.entryId}#${e.generation}`))
|
|
547
|
+
continue;
|
|
548
|
+
challenges.push({ eventId: e.eventId, entryId: e.entryId, reason: e.reason, at: e.at, ...(e.challengedRev !== undefined ? { challengedRev: e.challengedRev } : {}) });
|
|
549
|
+
}
|
|
550
|
+
const lineage = snap.lineage.filter((r) => exportedIds.has(r.entryId));
|
|
551
|
+
const namedSessions = new Set(lineage.map((r) => r.sessionId));
|
|
552
|
+
for (const row of snap.custody) {
|
|
553
|
+
const sessions = row.sessions;
|
|
554
|
+
if (Array.isArray(sessions))
|
|
555
|
+
for (const s of sessions)
|
|
556
|
+
if (typeof s === "string")
|
|
557
|
+
namedSessions.add(s);
|
|
558
|
+
}
|
|
559
|
+
const pollutedSessions = snap.pollutedSessions.filter((p) => namedSessions.has(p.sessionId));
|
|
560
|
+
return assembleMemoryExportBundle({
|
|
561
|
+
at: this.now(),
|
|
562
|
+
scopes: requested,
|
|
563
|
+
storeId: snap.storeId,
|
|
564
|
+
entries: snap.entries,
|
|
565
|
+
challenges,
|
|
566
|
+
pollutedSessions,
|
|
567
|
+
lineage,
|
|
568
|
+
custody: snap.custody,
|
|
569
|
+
residuals: {
|
|
570
|
+
quarantined: snap.quarantined,
|
|
571
|
+
unbound: snap.fullStore ? snap.unbound : [],
|
|
572
|
+
pendingLatch: snap.fullStore ? snap.pendingLatch : [],
|
|
573
|
+
contentUnavailable: snap.contentUnavailable,
|
|
574
|
+
quarantineOpaque: snap.quarantineOpaque,
|
|
575
|
+
unsliceableCustody: snap.unsliceableCustody,
|
|
576
|
+
unboundCount: snap.unbound.length,
|
|
577
|
+
pendingLatchCount: snap.pendingLatch.length,
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
async importMemoryBundle(bundle, opts) {
|
|
582
|
+
let inert;
|
|
583
|
+
try {
|
|
584
|
+
inert = JSON.parse(JSON.stringify(bundle));
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
const e = new Error(`importMemoryBundle: the bundle is not JSON-serializable (${err instanceof Error ? err.message : String(err)})`);
|
|
588
|
+
e.code = "config.memory_import_request";
|
|
589
|
+
throw e;
|
|
590
|
+
}
|
|
591
|
+
const bad = memoryBundleInvalid(inert, { ...(opts?.expectedScopes !== undefined ? { expectedScopes: opts.expectedScopes } : {}) });
|
|
592
|
+
if (bad !== undefined) {
|
|
593
|
+
const e = new Error(`importMemoryBundle: ${bad} — the whole package is refused, nothing landed`);
|
|
594
|
+
e.code = "memory.import_rejected";
|
|
595
|
+
throw e;
|
|
596
|
+
}
|
|
597
|
+
const b = inert;
|
|
598
|
+
const face = this.backend.importBundleCommit;
|
|
599
|
+
if (typeof face !== "function") {
|
|
600
|
+
const e = new Error("importMemoryBundle: this backend has no bundle-import capability (importBundleCommit) — the import latch/receipt transaction requires it, and an unlatched import would serve entries before their governance lands; nothing was imported.");
|
|
601
|
+
e.code = "memory.import_rejected";
|
|
602
|
+
throw e;
|
|
603
|
+
}
|
|
604
|
+
const bundleHash = computeMemoryBundleHash(b.integrity.sectionHashes);
|
|
605
|
+
const custody = [];
|
|
606
|
+
const seenEv = new Set();
|
|
607
|
+
for (const row of b.governance.custody) {
|
|
608
|
+
if (seenEv.has(row.ev))
|
|
609
|
+
continue;
|
|
610
|
+
seenEv.add(row.ev);
|
|
611
|
+
custody.push(row);
|
|
612
|
+
}
|
|
613
|
+
const plan = {
|
|
614
|
+
bundleHash,
|
|
615
|
+
sourceStoreId: b.storeId,
|
|
616
|
+
entries: b.entries,
|
|
617
|
+
challenges: b.governance.challenges,
|
|
618
|
+
pollutedSessions: b.governance.pollutedSessions,
|
|
619
|
+
lineage: b.governance.lineage,
|
|
620
|
+
custody,
|
|
621
|
+
};
|
|
622
|
+
return face.call(this.backend, plan);
|
|
623
|
+
}
|
|
524
624
|
challengeSession(sessionId, reason, requestId) {
|
|
525
625
|
if (typeof requestId !== "string" || requestId === "") {
|
|
526
626
|
const e = new Error("challengeSession: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
|
|
@@ -822,7 +922,9 @@ export class MemoryEngine {
|
|
|
822
922
|
const rec = reconcileLineage(this.controlDir, this.now);
|
|
823
923
|
this.settlePromotions(rec.promoted);
|
|
824
924
|
for (const u of rec.undecidable) {
|
|
825
|
-
report.warnings.push(
|
|
925
|
+
report.warnings.push(u.kind === "latch-only"
|
|
926
|
+
? `memory bundle-import latch ${u.txnId} is unsettled (an import did not run to completion) — ${u.entryIds.length} entr${u.entryIds.length === 1 ? "y is" : "ies are"} latched (memory reads refuse them) until the SAME bundle is imported again to completion (importMemoryBundle; adjudication is refused on import latches)`
|
|
927
|
+
: `memory lineage transaction ${u.txnId} is unsettled (a crash landed between commit and its durable credential) — ${u.entryIds.length} entr${u.entryIds.length === 1 ? "y is" : "ies are"} latched (memory reads refuse them) until the host adjudicates it (adjudicatePendingLineage)`);
|
|
826
928
|
}
|
|
827
929
|
if (pollutedReason !== undefined && lineageSessionId !== undefined) {
|
|
828
930
|
const rec2 = this.sessionPollution(lineageSessionId);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { type TransferEvidence } from "./file-backend.js";
|
|
2
|
+
import type { MemoryEntry } from "./types.js";
|
|
3
|
+
/** One exported challenge event (unresolved generations only). Generations are DESTINATION-minted:
|
|
4
|
+
* the bundle deliberately carries no generation number — the importing store re-mints events under
|
|
5
|
+
* its own idempotent namespace and allocates generations there. */
|
|
6
|
+
export interface BundleChallengeRow {
|
|
7
|
+
eventId: string;
|
|
8
|
+
entryId: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
at: number;
|
|
11
|
+
challengedRev?: string;
|
|
12
|
+
}
|
|
13
|
+
/** One exported durable pollution marker (verbatim source record). */
|
|
14
|
+
export interface BundlePollutedSession {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
at: number;
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
/** One exported committed lineage contribution (verbatim `lastRev`/`lastAt`). */
|
|
20
|
+
export interface BundleLineageRow {
|
|
21
|
+
entryId: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
lastRev: string;
|
|
24
|
+
lastAt: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* design/178 v2 §4.3 + v2-c §6 — the governance-complete export bundle. The governance section is
|
|
28
|
+
* STRUCTURALLY REQUIRED (a bundle without it is a bad package — there is no "entries-only import"
|
|
29
|
+
* escape hatch: an optional hatch is a laundering channel, and the fixed section-hash key set makes
|
|
30
|
+
* a stripped section fail integrity outright). `doc` is human-readable disclosure text OUTSIDE the
|
|
31
|
+
* hashed sections (documentation, not governance data); unknown root keys are read-tolerated and
|
|
32
|
+
* never re-export-preserved (a re-export always mints a fresh bundle from store state).
|
|
33
|
+
*/
|
|
34
|
+
export interface MemoryExportBundle {
|
|
35
|
+
/** Envelope version — a reader meeting `bundle > 1` must refuse, never reinterpret. */
|
|
36
|
+
bundle: 1;
|
|
37
|
+
at: number;
|
|
38
|
+
scopes: string[];
|
|
39
|
+
/** The source store's durable identity (control-plane `store-identity.json`, minted at the first
|
|
40
|
+
* export) — the import side's `origin` stamp for custody rows that carry none. */
|
|
41
|
+
storeId: string;
|
|
42
|
+
/** Full-fidelity entries (provenance/trust/extra verbatim). A duplicate id is a bad package. */
|
|
43
|
+
entries: MemoryEntry[];
|
|
44
|
+
governance: {
|
|
45
|
+
challenges: BundleChallengeRow[];
|
|
46
|
+
pollutedSessions: BundlePollutedSession[];
|
|
47
|
+
lineage: BundleLineageRow[];
|
|
48
|
+
/** Evidence-chain rows sliced to the exported scopes (v2-c §2 table): open form — a consumer
|
|
49
|
+
* tolerates unknown keys; unknown CHANNELS never leave the exporter (they refuse the export). */
|
|
50
|
+
custody: TransferEvidence[];
|
|
51
|
+
};
|
|
52
|
+
/** Honest enumeration of what the bundle does NOT carry. The `unbound`/`pendingLatch` name lists
|
|
53
|
+
* are populated on a FULL-STORE export only (one trust domain); a subset export ships empty
|
|
54
|
+
* lists and the counts (an id list would leak another tenant's identifiers). The counts are
|
|
55
|
+
* always present and always equal the honest total, so a consumer reads ONE seat either way. */
|
|
56
|
+
residuals: {
|
|
57
|
+
quarantined: string[];
|
|
58
|
+
unbound: string[];
|
|
59
|
+
pendingLatch: string[];
|
|
60
|
+
contentUnavailable: Array<{
|
|
61
|
+
id: string;
|
|
62
|
+
scope: string;
|
|
63
|
+
slug: string;
|
|
64
|
+
rev: string;
|
|
65
|
+
reason: string;
|
|
66
|
+
}>;
|
|
67
|
+
quarantineOpaque: number;
|
|
68
|
+
/** Custody rows a SUBSET export could not slice (no scope to slice by — unbound-deletion
|
|
69
|
+
* history among them): the anti-resurrection guarantee for those ids does not travel. */
|
|
70
|
+
unsliceableCustody: number;
|
|
71
|
+
unboundCount: number;
|
|
72
|
+
pendingLatchCount: number;
|
|
73
|
+
};
|
|
74
|
+
/** Accident detection, not forgery-proofing (see module note). `sectionHashes` carries EXACTLY
|
|
75
|
+
* the four fixed keys (meta/entries/governance/residuals); `revs` and `entries` are mutually
|
|
76
|
+
* pointing (same id set, verbatim revs, `entryCount` ≡ both cardinalities). */
|
|
77
|
+
integrity: {
|
|
78
|
+
entryCount: number;
|
|
79
|
+
revs: Record<string, string>;
|
|
80
|
+
sectionHashes: Record<string, string>;
|
|
81
|
+
};
|
|
82
|
+
/** Human-readable disclosure sentences (free-string carriage, subset residuals). Outside the
|
|
83
|
+
* hashed sections by design — documentation, not governance data. */
|
|
84
|
+
doc: string[];
|
|
85
|
+
}
|
|
86
|
+
/** design/178 v2-c §7 — the import answer (the parent-design draft fields plus the v2-c report
|
|
87
|
+
* seats). Every negative disposition is REPORTED, never silent. */
|
|
88
|
+
export interface MemoryImportReport {
|
|
89
|
+
v: 1;
|
|
90
|
+
/** The bundle identity (sha256 over the canonical section-hash map) — the latch/remint salt. */
|
|
91
|
+
bundleHash: string;
|
|
92
|
+
sourceStoreId: string;
|
|
93
|
+
landed: Array<{
|
|
94
|
+
id: string;
|
|
95
|
+
scope: string;
|
|
96
|
+
slug: string;
|
|
97
|
+
}>;
|
|
98
|
+
/** id+rev+scope three-way idempotent skips (slug tolerates the deterministic `-n` suffix). */
|
|
99
|
+
alreadyPresent: string[];
|
|
100
|
+
/** The destination chain carries a delete row for the id and no live row survives — resurrection
|
|
101
|
+
* refused (no override; a legitimately re-created id has a live row and takes the ordinary
|
|
102
|
+
* CAS/conflict path instead). */
|
|
103
|
+
refusedErased: string[];
|
|
104
|
+
/** Fresh add of a `repo_file`-provenance entry with no `trust` marker — whitewash refused. */
|
|
105
|
+
refusedUntrusted: string[];
|
|
106
|
+
conflicts: Array<{
|
|
107
|
+
id: string;
|
|
108
|
+
reason: string;
|
|
109
|
+
}>;
|
|
110
|
+
/** Entry-attached governance (challenges/lineage) withheld because its entry neither landed nor
|
|
111
|
+
* matched already-present-at-the-same-rev — misattribution is worse than omission. */
|
|
112
|
+
governanceWithheld: string[];
|
|
113
|
+
/** Source chain carries a delete row while the destination holds a LIVE entry for the id: named
|
|
114
|
+
* for deployment adjudication, never deleted on the import's behalf. */
|
|
115
|
+
erasedAtSource: string[];
|
|
116
|
+
pollutionDivergence: Array<{
|
|
117
|
+
sessionId: string;
|
|
118
|
+
kept: "destination";
|
|
119
|
+
}>;
|
|
120
|
+
/** Governance rows pointing at ids outside entries ∪ the destination account (custody rows are
|
|
121
|
+
* exempt — an absent-id delete row is exactly the anti-resurrection payload). */
|
|
122
|
+
referentialOrphans: string[];
|
|
123
|
+
lineageDivergence: Array<{
|
|
124
|
+
entryId: string;
|
|
125
|
+
sessionId: string;
|
|
126
|
+
kept: "destination";
|
|
127
|
+
}>;
|
|
128
|
+
/** Custody rows withheld unappended: unknown channel, or a known-channel row failing the store's
|
|
129
|
+
* own validation — reported per row, never a whole-package refusal (a newer exporter must not
|
|
130
|
+
* brick an older importer) and never a verbatim append (an unvalidatable row would poison the
|
|
131
|
+
* fail-closed chain). `srcEv` is the row's source identity (its own `srcEv` when it carries one,
|
|
132
|
+
* else its `ev`). */
|
|
133
|
+
custodyWithheld: Array<{
|
|
134
|
+
srcEv: string;
|
|
135
|
+
channel: string;
|
|
136
|
+
}>;
|
|
137
|
+
/** Custody rows submitted to the destination chain under this bundle's namespace (idempotent
|
|
138
|
+
* re-imports answer the same number — the reconciliation handle). */
|
|
139
|
+
custodyAppended: number;
|
|
140
|
+
}
|
|
141
|
+
/** What the engine hands the File backend's `importBundleCommit` face after the pure validation
|
|
142
|
+
* pass: the bundle's governed content plus its computed identity. Everything here is inert plain
|
|
143
|
+
* data (the engine clones the caller's bundle through a JSON round-trip before validating). */
|
|
144
|
+
export interface MemoryBundleImportPlan {
|
|
145
|
+
bundleHash: string;
|
|
146
|
+
sourceStoreId: string;
|
|
147
|
+
entries: MemoryEntry[];
|
|
148
|
+
challenges: BundleChallengeRow[];
|
|
149
|
+
pollutedSessions: BundlePollutedSession[];
|
|
150
|
+
lineage: BundleLineageRow[];
|
|
151
|
+
/** Open custody rows (byte-equal duplicates already collapsed by validation). */
|
|
152
|
+
custody: Array<Record<string, unknown>>;
|
|
153
|
+
}
|
|
154
|
+
/** The four fixed integrity section keys (§6.3) — missing OR extra keys are a bad package. */
|
|
155
|
+
export declare const BUNDLE_SECTION_KEYS: readonly ["meta", "entries", "governance", "residuals"];
|
|
156
|
+
/** The canonical per-section hashes of a bundle's four sections (meta = the bundle/at/scopes/
|
|
157
|
+
* storeId scalars). Canonical JSON (recursively sorted keys) + LF, sha256 hex. */
|
|
158
|
+
export declare function computeBundleSectionHashes(bundle: Pick<MemoryExportBundle, "bundle" | "at" | "scopes" | "storeId" | "entries" | "governance" | "residuals">): Record<string, string>;
|
|
159
|
+
/**
|
|
160
|
+
* The bundle IDENTITY: sha256 over the canonical JSON of `integrity.sectionHashes` (full 64-hex —
|
|
161
|
+
* the import latch id, the challenge/custody remint salt, and the report echo all share it). The
|
|
162
|
+
* section hashes already seal the section contents, so their sorted map IS the package identity;
|
|
163
|
+
* a tampered package fails integrity before its hash ever matters.
|
|
164
|
+
*/
|
|
165
|
+
export declare function computeMemoryBundleHash(sectionHashes: Record<string, string>): string;
|
|
166
|
+
/**
|
|
167
|
+
* design/178 v2-c §1-2① — the PURE bundle validation (integrity/structure/self-consistency; zero
|
|
168
|
+
* store reads, zero side effects). Returns the FIRST violation, or undefined (valid). Every
|
|
169
|
+
* violation is a whole-package refusal at the caller — nothing lands, no latch is set.
|
|
170
|
+
*/
|
|
171
|
+
export declare function memoryBundleInvalid(raw: unknown, opts?: {
|
|
172
|
+
expectedScopes?: readonly string[];
|
|
173
|
+
}): string | undefined;
|
|
174
|
+
/** What the exporter feeds {@link assembleMemoryExportBundle} — the store-consistent snapshot plus
|
|
175
|
+
* the already-sliced governance pieces (pure data; the snapshot fence lives in the backend). */
|
|
176
|
+
export interface MemoryExportBundleInput {
|
|
177
|
+
at: number;
|
|
178
|
+
scopes: string[];
|
|
179
|
+
storeId: string;
|
|
180
|
+
entries: MemoryEntry[];
|
|
181
|
+
challenges: BundleChallengeRow[];
|
|
182
|
+
pollutedSessions: BundlePollutedSession[];
|
|
183
|
+
lineage: BundleLineageRow[];
|
|
184
|
+
custody: TransferEvidence[];
|
|
185
|
+
residuals: MemoryExportBundle["residuals"];
|
|
186
|
+
}
|
|
187
|
+
/** The C-1 disclosure sentence (recommended-arm ruling posture: free strings ride verbatim and the
|
|
188
|
+
* bundle SAYS so — coding them away would destroy the identity/attribution keys idempotency and
|
|
189
|
+
* the remint namespaces depend on). */
|
|
190
|
+
export declare const BUNDLE_FREE_TEXT_DISCLOSURE = "Free-string fields in this bundle (pollution/challenge reasons, challenge event ids, custody request ids, session ids) are carried verbatim and may contain text the exporting deployment put there; subset-export tenant isolation does not extend to those literals.";
|
|
191
|
+
/** Assemble the bundle: deterministic ordering, section hashes, disclosure doc. Pure. */
|
|
192
|
+
export declare function assembleMemoryExportBundle(input: MemoryExportBundleInput): MemoryExportBundle;
|