@sema-agent/core 5.21.1 → 5.22.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/agents/send-message-tool.js +6 -3
  3. package/dist/agents/subagent.d.ts +6 -0
  4. package/dist/agents/subagent.js +45 -4
  5. package/dist/brain/errors.d.ts +20 -0
  6. package/dist/brain/errors.js +40 -0
  7. package/dist/brain/retry.d.ts +16 -2
  8. package/dist/brain/retry.js +3 -2
  9. package/dist/brain/status-sink.d.ts +9 -2
  10. package/dist/brain/stream-engine.d.ts +22 -0
  11. package/dist/brain/stream-engine.js +41 -10
  12. package/dist/core/ask-class.d.ts +48 -0
  13. package/dist/core/ask-class.js +33 -0
  14. package/dist/core/checkpoint-store.d.ts +103 -10
  15. package/dist/core/checkpoint-store.js +3 -1
  16. package/dist/core/governance-codes.d.ts +38 -0
  17. package/dist/core/governance-codes.js +11 -0
  18. package/dist/core/hooks.d.ts +39 -0
  19. package/dist/core/hooks.js +26 -2
  20. package/dist/core/locked-config.d.ts +7 -1
  21. package/dist/core/locked-config.js +2 -1
  22. package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
  23. package/dist/core/memory-engine/delegation-provenance.js +26 -0
  24. package/dist/core/memory-engine/engine.d.ts +67 -1
  25. package/dist/core/memory-engine/engine.js +270 -12
  26. package/dist/core/memory-engine/header-hints.d.ts +30 -0
  27. package/dist/core/memory-engine/header-hints.js +41 -0
  28. package/dist/core/memory-engine/index.d.ts +3 -2
  29. package/dist/core/memory-engine/index.js +3 -2
  30. package/dist/core/memory-engine/layout.d.ts +166 -0
  31. package/dist/core/memory-engine/layout.js +399 -0
  32. package/dist/core/memory-engine/tools.d.ts +30 -0
  33. package/dist/core/memory-engine/tools.js +108 -17
  34. package/dist/core/permission-rule-consent.d.ts +25 -9
  35. package/dist/core/permission-rule-consent.js +91 -20
  36. package/dist/core/permission-rule-model.d.ts +9 -1
  37. package/dist/core/permission-rule-model.js +2 -2
  38. package/dist/core/permission-rule-org.d.ts +161 -0
  39. package/dist/core/permission-rule-org.js +211 -0
  40. package/dist/core/permission-rule-store.d.ts +249 -6
  41. package/dist/core/permission-rule-store.js +313 -3
  42. package/dist/core/permission-rule-sync.d.ts +131 -0
  43. package/dist/core/permission-rule-sync.js +314 -0
  44. package/dist/core/runner/prepare-memory.js +35 -8
  45. package/dist/core/runner/prepare-task.d.ts +54 -1
  46. package/dist/core/runner/prepare-task.js +246 -27
  47. package/dist/core/runner/runtask.js +147 -6
  48. package/dist/core/shared-memory/contract.js +19 -4
  49. package/dist/core/shared-memory/normalize.d.ts +3 -1
  50. package/dist/core/shared-memory/tools.js +73 -17
  51. package/dist/core/shared-memory/types.d.ts +27 -1
  52. package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
  53. package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
  54. package/dist/core/task-notification.d.ts +5 -2
  55. package/dist/core/task-registry-agent.d.ts +1 -1
  56. package/dist/core/task-registry-agent.js +6 -2
  57. package/dist/core/task-registry-shared.d.ts +9 -2
  58. package/dist/core/task-registry.d.ts +9 -3
  59. package/dist/core/task-registry.js +2 -0
  60. package/dist/core/tool-policy.d.ts +120 -2
  61. package/dist/core/tool-policy.js +116 -6
  62. package/dist/core/trace.d.ts +32 -1
  63. package/dist/core/types.d.ts +56 -3
  64. package/dist/index.d.ts +12 -7
  65. package/dist/index.js +10 -5
  66. package/dist/stores/file/checkpoint-store.d.ts +4 -0
  67. package/dist/stores/file/checkpoint-store.js +1 -0
  68. package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
  69. package/dist/stores/file/permission-rule-adopt.js +95 -0
  70. package/dist/stores/file/permission-rule-store.d.ts +80 -2
  71. package/dist/stores/file/permission-rule-store.js +189 -46
  72. package/package.json +1 -1
@@ -774,6 +774,25 @@ export interface CheckpointState {
774
774
  * (`resume.parent_constraint_mismatch`) — a partial re-supply is a typed error, never a silently
775
775
  * thinner chain. Absent on older checkpoints ⇒ only the non-empty check applies. */
776
776
  parentConstraintCount?: number;
777
+ /**
778
+ * F-012 L1 — the persisted CONSTRAINT PROJECTIONS of the inherited chain, one ordered entry per
779
+ * `parentConstraints` layer: the layer's deterministic deny data
780
+ * ({@link import("./tool-policy.js").ToolPolicyProjection} content) or an explicit `{opaque:true}`
781
+ * placeholder for a layer that exports none. The resume leg EXECUTES these persisted projections
782
+ * (extra deny-narrowing layers + the pre-CAS edit re-adjudication) rather than trusting a
783
+ * re-supplied policy's execution half for the frozen deny data — a re-supplied closure that
784
+ * matches the digest but behaves differently cannot un-deny what the suspend froze. Present iff
785
+ * `requiresParentConstraint` on new mints; absent on older checkpoints (⇒ no frozen deny data —
786
+ * the count-only contract governs, and an EDIT resume is refused for want of a validatable chain). */
787
+ constraintChain?: ReadonlyArray<import("./tool-policy.js").ConstraintChainEntry>;
788
+ /**
789
+ * F-012 L1 — the content digest over `constraintChain` ({@link import("./tool-policy.js").constraintChainDigest}):
790
+ * binds the ordered chain topology (layer boundaries + opaque placeholders) and each projection's
791
+ * content. The resume pre-CAS ladder recomputes the digest over the RE-SUPPLIED chain's
792
+ * projections and rejects a mismatch (`resume.parent_constraint_mismatch`) — replacing the
793
+ * count-only shape check with a content contract (a same-length chain of different frozen deny
794
+ * data no longer passes). Present iff `constraintChain` is. */
795
+ constraintDigest?: string;
777
796
  };
778
797
  /**
779
798
  * [c209-C] Q5 — the listing frames' announced NAME-SETS at suspend (agent types / skills the model
@@ -799,6 +818,16 @@ export interface CheckpointState {
799
818
  skills?: string[];
800
819
  models?: string[];
801
820
  };
821
+ /**
822
+ * design/180 R-4 (R-2 cross-process variant) — the armed delegation child's MONOTONIC runtime-provenance
823
+ * aggregate at suspend ({@link import("./memory-engine/delegation-provenance.js").DelegationProvenanceAggregate}:
824
+ * version + coverageStartTurn + the two never-cleared bits sawExternal/incomplete). Persisted so a
825
+ * resume — a cross-process one especially, which lost the in-memory ref — CONTINUES accumulating from
826
+ * the recorded bits instead of restarting a fresh aggregate that could prove "clean" on the post-resume
827
+ * segment alone. Plain data → JSON/structuredClone round-trips. Present ONLY when the suspended leg ran
828
+ * with a recorder armed; ABSENT on an unarmed leg AND on an older checkpoint — the resume side reads
829
+ * absence-on-an-armed-child fail-closed as `incomplete`. */
830
+ delegationProvenance?: import("./memory-engine/delegation-provenance.js").DelegationProvenanceAggregate;
802
831
  }
803
832
  /**
804
833
  * {@link CheckpointState} with **every** field made required-PRESENT, while each value keeps its original
@@ -867,13 +896,26 @@ export declare const TOKEN_CHECKPOINT_VERSION = 5;
867
896
  * memory see zero version movement.
868
897
  */
869
898
  export declare const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
899
+ /**
900
+ * F-012 (2026-08-09 — the ORG_ADMISSION_CHECKPOINT_VERSION precedent replayed a fifth time, codex
901
+ * adversarial finding): the schema version a suspend stamps when its state carries the F-012 durable
902
+ * enforcement fields — `inheritedGate.constraintChain`/`constraintDigest` (the frozen ancestor
903
+ * projection chain the resume-edit re-adjudication executes) or `delegationProvenance` (the monotonic
904
+ * provenance aggregate a resume re-seeds). Both enforcements live ENTIRELY in the resuming worker: a
905
+ * pre-F-012 worker (MAX_SUPPORTED=6) would accept the row, ignore constraintChain (so an approver-edited
906
+ * resume executes WITHOUT the frozen ancestor deny), and restart the aggregate clean (false-clean after
907
+ * pre-suspend external activity). Stamping v7 forces such a worker to reject PRE-CAS
908
+ * (`unsupported_version`, stays `pending`, retried on an enforcing worker). A checkpoint with NO F-012
909
+ * state keeps the historic stamps — deployments not delegating see zero version movement.
910
+ */
911
+ export declare const F012_CHECKPOINT_VERSION = 7;
870
912
  /** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
871
913
  * {@link CheckpointError} `unsupported_version` (the checkpoint stays `pending`, retryable on a newer worker).
872
- * Raised to 6 for org-admission-bearing checkpoints — this worker reads v1 (legacy human), v2
873
- * (resource), v3 (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the
874
- * retired wall-clock allocation), v5 (token-allocation-bearing resource/approval) and v6
875
- * (org-admission freeze state). */
876
- export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 6;
914
+ * Raised to 7 for F-012-bearing checkpoints — this worker reads v1 (legacy human), v2 (resource), v3
915
+ * (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the retired
916
+ * wall-clock allocation), v5 (token-allocation-bearing resource/approval), v6 (org-admission freeze
917
+ * state) and v7 (F-012 constraint-chain / delegation-provenance enforcement state). */
918
+ export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 7;
877
919
  /**
878
920
  * Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
879
921
  * written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
@@ -992,8 +1034,16 @@ export interface ResolvedOutcome {
992
1034
  * RETRY of the ALREADY-APPROVED action — the re-resume MUST replay the persisted {@link ResolvedOutcome}
993
1035
  * winner, never a new vote — while a `tool_unavailable` reopen (the action could not run, may now be
994
1036
  * invalid — P-7) lets a human RE-DECIDE with the tool present, so a fresh operator decision IS allowed.
1037
+ *
1038
+ * `constraint_rejected` (F-012 L3) carries the same FRESH-REDECISION semantics as `tool_unavailable`:
1039
+ * a resolve was retracted because its decided INPUT failed a constraint re-adjudication (a frozen
1040
+ * ancestor projection / resume-edit policy denied the approver's edit) — the ask itself is unconsumed,
1041
+ * so the returned-to-pending row takes a fresh operator decision, never a forced replay of the
1042
+ * rejected one. Core's own edit re-adjudication runs PRE-CAS (nothing to reopen there); this reason
1043
+ * exists for a deployment lane that discovers the same rejection only after its own resolve landed
1044
+ * and must return the row to pending without inventing an out-of-vocabulary reason.
995
1045
  */
996
- export type ReopenReason = "env_failed" | "tool_unavailable";
1046
+ export type ReopenReason = "env_failed" | "tool_unavailable" | "constraint_rejected";
997
1047
  /**
998
1048
  * design/80 D-1 (atomicity fix): the optimistic-concurrency precondition a {@link CheckpointStore.resolve}
999
1049
  * caller passes so its CAS is atomic with the validation it did against an earlier `get()` snapshot. The
@@ -1361,10 +1411,23 @@ export declare class CheckpointError extends Error {
1361
1411
  | "resume.parent_constraint_missing"
1362
1412
  /** Parent effective-policy inheritance (shape check): the resume DID re-supply
1363
1413
  * `internals.inheritedGate.parentConstraints`, but its length differs from the checkpoint's recorded
1364
- * `parentConstraintCount` — a partial/mismatched re-supply would silently run the child under a
1414
+ * `parentConstraintCount` — or (F-012 L1) its projection CONTENT digests differently from the
1415
+ * recorded `constraintDigest` — a partial/mismatched re-supply would silently run the child under a
1365
1416
  * thinner (or different) ancestor chain than it suspended with. Rejected pre-CAS (the checkpoint
1366
1417
  * stays `pending`); re-resume with the full original chain. */
1367
- | "resume.parent_constraint_mismatch";
1418
+ | "resume.parent_constraint_mismatch"
1419
+ /** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
1420
+ * constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
1421
+ * edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
1422
+ * the same token remains decidable (approve as shown, deny, or edit differently) — the rejection
1423
+ * consumed the decision's INPUT, never the ask itself. */
1424
+ | "resume.constraint_rejected"
1425
+ /** F-012 L3: the resume carried an approver EDIT but the inherited ancestor chain cannot be
1426
+ * re-adjudicated statically — an OPAQUE layer is present, a layer declares a live remainder beyond
1427
+ * its projection, or the checkpoint predates the projection chain. "Would have been allowed" is not
1428
+ * computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
1429
+ * checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
1430
+ | "resume.constraint_unprojectable";
1368
1431
  /** Structured discrimination a caller can branch on where the code alone is ambiguous. Today one
1369
1432
  * member: `field` names WHICH part of the decision payload was rejected on a
1370
1433
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
@@ -1457,10 +1520,23 @@ export declare class CheckpointError extends Error {
1457
1520
  | "resume.parent_constraint_missing"
1458
1521
  /** Parent effective-policy inheritance (shape check): the resume DID re-supply
1459
1522
  * `internals.inheritedGate.parentConstraints`, but its length differs from the checkpoint's recorded
1460
- * `parentConstraintCount` — a partial/mismatched re-supply would silently run the child under a
1523
+ * `parentConstraintCount` — or (F-012 L1) its projection CONTENT digests differently from the
1524
+ * recorded `constraintDigest` — a partial/mismatched re-supply would silently run the child under a
1461
1525
  * thinner (or different) ancestor chain than it suspended with. Rejected pre-CAS (the checkpoint
1462
1526
  * stays `pending`); re-resume with the full original chain. */
1463
- | "resume.parent_constraint_mismatch", message: string,
1527
+ | "resume.parent_constraint_mismatch"
1528
+ /** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
1529
+ * constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
1530
+ * edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
1531
+ * the same token remains decidable (approve as shown, deny, or edit differently) — the rejection
1532
+ * consumed the decision's INPUT, never the ask itself. */
1533
+ | "resume.constraint_rejected"
1534
+ /** F-012 L3: the resume carried an approver EDIT but the inherited ancestor chain cannot be
1535
+ * re-adjudicated statically — an OPAQUE layer is present, a layer declares a live remainder beyond
1536
+ * its projection, or the checkpoint predates the projection chain. "Would have been allowed" is not
1537
+ * computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
1538
+ * checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
1539
+ | "resume.constraint_unprojectable", message: string,
1464
1540
  /** Structured discrimination a caller can branch on where the code alone is ambiguous. Today one
1465
1541
  * member: `field` names WHICH part of the decision payload was rejected on a
1466
1542
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
@@ -1633,6 +1709,19 @@ export interface CheckpointStore {
1633
1709
  * that still matches.
1634
1710
  */
1635
1711
  readonly fidelity?: StoreFidelity;
1712
+ /**
1713
+ * F-012 L2/L3 — the RE-DECISION capability declaration: whether this store's `reopen` (and, when
1714
+ * present, a durable validating-lease discipline) actually works — a promise, not a method-presence
1715
+ * sniff (`reopen` is optional and a stub that returns `false` still satisfies the type). The
1716
+ * sandbox-admission mode's pre-flight REQUIRES `redecision.reopen === true` (its boundary asks park
1717
+ * to a human and must be re-decidable); absent reads fail-closed as "no declared re-decision face"
1718
+ * and the mode refuses to arm, loudly. Same "declaration, never duck-typing" posture as
1719
+ * {@link retention}/{@link durability}/{@link fidelity}.
1720
+ */
1721
+ readonly redecision?: {
1722
+ reopen: boolean;
1723
+ validatingLease?: boolean;
1724
+ };
1636
1725
  /** Create-once. Throws {@link CheckpointError} `already_exists` on a token collision (never a silent
1637
1726
  * overwrite — a reused token would clobber a live suspension). */
1638
1727
  put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
@@ -1887,6 +1976,10 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
1887
1976
  /** Honest declaration on the fidelity axis: `put`/`get` are `structuredClone`, so a row keeps the
1888
1977
  * whole structured-clone domain — a `Date` filed here is a `Date` when it comes back. */
1889
1978
  readonly fidelity: "structured-clone";
1979
+ /** F-012 L2: this store's `reopen` is a real resolved→pending CAS (below) — declared, not sniffed. */
1980
+ readonly redecision: {
1981
+ readonly reopen: true;
1982
+ };
1890
1983
  private cps;
1891
1984
  private fault;
1892
1985
  put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
@@ -133,7 +133,8 @@ export const RESOURCE_CHECKPOINT_VERSION = 2;
133
133
  export const BINDING_CHECKPOINT_VERSION = 3;
134
134
  export const TOKEN_CHECKPOINT_VERSION = 5;
135
135
  export const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
136
- export const MAX_SUPPORTED_CHECKPOINT_VERSION = 6;
136
+ export const F012_CHECKPOINT_VERSION = 7;
137
+ export const MAX_SUPPORTED_CHECKPOINT_VERSION = 7;
137
138
  export function checkpointVersionOf(cp) {
138
139
  return cp.version ?? 0;
139
140
  }
@@ -458,6 +459,7 @@ export class InMemoryCheckpointStore {
458
459
  retention = "none";
459
460
  durability = "process-local";
460
461
  fidelity = "structured-clone";
462
+ redecision = { reopen: true };
461
463
  cps = new Map();
462
464
  fault = null;
463
465
  async put(token, cp) {
@@ -29,3 +29,41 @@ export declare const NON_GOVERNANCE_MEMORY_CODES: ReadonlySet<string>;
29
29
  * `undefined` (NOT governance — the caller falls through to its ordinary taxonomy).
30
30
  */
31
31
  export declare function governanceRetryClass(code: string): GovernanceRetryClass | undefined;
32
+ /**
33
+ * The CLOSED registry of reasons a permission-rule sync round may refuse an inbound record or move a
34
+ * local row into the quarantine area (design/182 §4.3/§5.2/§8.1). Same doctrine as
35
+ * {@link GOVERNANCE_CODES}: the machine reads THIS table, never free text — "silently dropped" is a
36
+ * contract violation, so every drop names a registered code and a consumer can branch on it.
37
+ *
38
+ * These are DISCLOSURE codes, not subagent error codes: they never enter {@link GOVERNANCE_CODES}
39
+ * (that table classifies a failed child's error for retry, and its gate enforces a suffix convention
40
+ * these codes do not carry). The value states which side of the wire loses bytes:
41
+ * `inbound-refused` = the peer's copy was refused here (the peer still holds it — no loss);
42
+ * `local-quarantined` = a LOCAL row left the live view into the quarantine area (bytes preserved,
43
+ * disclosed, re-enterable only through a new consent).
44
+ */
45
+ export declare const RULE_SYNC_DROP_CODES: {
46
+ /** The single validator refused the rule text (e.g. a bare-interpreter prefix). Both directions. */
47
+ readonly invalid_rule_text: "inbound-refused-or-local-quarantined";
48
+ /** Stored tool/match/command disagree with the re-projection of the rule text. Both directions. */
49
+ readonly metadata_mismatch: "inbound-refused-or-local-quarantined";
50
+ /** An inbound dot claims THIS replica's actor above its minted counter — forgery on our own name.
51
+ * Covers adds, a tombstone's `deletedBy`, AND a tombstone's `removedDots` (a tombstone naming our
52
+ * future dot would pre-position a kill for an approval not yet given). */
53
+ readonly own_actor_forged: "inbound-refused";
54
+ /** An inbound add reuses a dot that already identifies a DIFFERENT (rule, scope) row — a dot is
55
+ * minted once for one add, so a reuse is damage or forgery, and admitting it would let quarantine
56
+ * or tombstone identity on one row reach the other. */
57
+ readonly dot_identity_conflict: "inbound-refused";
58
+ /** A live local add at-or-below the collected GC frontier and absent from the inbound state — the
59
+ * residue of an already-recycled tombstone. Never resurrected; quarantined and disclosed. */
60
+ readonly below_gc_frontier: "local-quarantined";
61
+ /** The server refused this local row (its response `dropped` names it); it leaves the live view so
62
+ * it does not ride — and get refused on — every future round. */
63
+ readonly server_rejected: "local-quarantined";
64
+ };
65
+ /** Every reason a sync round may drop or quarantine a record. Closed set; free text is not a member. */
66
+ export type RuleSyncDropReason = keyof typeof RULE_SYNC_DROP_CODES;
67
+ /** The subset that may appear on a LOCAL quarantined row (design/182 §8.1 `quarantine` instruction /
68
+ * fence arm / local screening). `own_actor_forged` is inbound-only by construction. */
69
+ export type RuleQuarantineReason = Exclude<RuleSyncDropReason, "own_actor_forged">;
@@ -14,6 +14,9 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
14
14
  "memory.partition_split",
15
15
  "memory.tail",
16
16
  "memory.pollution_mark_failed",
17
+ "memory.challenge_sweep_failed",
18
+ "memory.lineage_settle_failed",
19
+ "memory.challenge_ledger_corrupt",
17
20
  ]);
18
21
  export function governanceRetryClass(code) {
19
22
  if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
@@ -23,3 +26,11 @@ export function governanceRetryClass(code) {
23
26
  return "terminal";
24
27
  return undefined;
25
28
  }
29
+ export const RULE_SYNC_DROP_CODES = {
30
+ invalid_rule_text: "inbound-refused-or-local-quarantined",
31
+ metadata_mismatch: "inbound-refused-or-local-quarantined",
32
+ own_actor_forged: "inbound-refused",
33
+ dot_identity_conflict: "inbound-refused",
34
+ below_gc_frontier: "local-quarantined",
35
+ server_rejected: "local-quarantined",
36
+ };
@@ -1,6 +1,7 @@
1
1
  import type { DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
2
2
  import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
3
3
  import type { PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
4
+ import { type AskClass } from "./ask-class.js";
4
5
  /**
5
6
  * In-process hook seam (design/37) — a small, provider-agnostic interception layer modeled on CC's
6
7
  * hooks but reduced to three process-internal callbacks (no shell/HTTP executors, no settings files):
@@ -712,6 +713,44 @@ export interface ToolGateInput {
712
713
  rule: string;
713
714
  }) => void;
714
715
  };
716
+ /**
717
+ * F-012 L2 — the SANDBOX ADMISSION leg. Present ONLY when the runner armed the mode: the execution
718
+ * env DECLARES isolation (`RemoteExecutionEnv.capabilities.isolation === true` — a Runner-resolved
719
+ * fact, never caller self-report) AND the park prerequisites hold (durable park configured + a
720
+ * checkpoint store that explicitly declares `redecision.reopen`). Absent ⇒ the whole step does not
721
+ * exist and the decision path is byte-identical to a build without it.
722
+ *
723
+ * Semantics (clay's ruling): inside a declared sandbox, an engine-authored "confirm a potentially
724
+ * destructive local action" ask is answered by containment itself — auto-allow + durable
725
+ * disclosure (`permission.sandbox_admitted`), never a headless deny that pollutes the model.
726
+ * STRICT dominance governs: the leg admits only when EVERY surviving ask on the call is classified
727
+ * `sandbox_local` in the engine's private envelope (deployment-policy asks are `unknown`, hook
728
+ * asks `hook`, marked calls `mandate`, `requiresRealApproval` asks external — none admit), and
729
+ * the call crosses NO declared boundary (`boundaryCapable`: egress-marked tools and
730
+ * protocol-mounted (MCP/A2A) tools — a server's self-report is always boundary-capable). A
731
+ * boundary-crossing ask under the armed mode takes the ordinary durable-park leg below — "park to
732
+ * a human", never an auto-deny.
733
+ */
734
+ sandboxAdmission?: {
735
+ /** The ask classes recorded for this call's fold-layer asks (with their source layers), or
736
+ * `undefined` when no fold layer asked — the gate's own safety tighten is then the only asker. */
737
+ askClassesOf: (toolCallId: string) => ReadonlyArray<{
738
+ cls: AskClass;
739
+ layer: string;
740
+ }> | undefined;
741
+ /** This call's tool crosses a DECLARED boundary (egress axis / protocol-mounted) — never admitted. */
742
+ boundaryCapable: boolean;
743
+ /** The durable disclosure sink — fired on every admission BEFORE the allow is returned. */
744
+ onAdmitted: (info: {
745
+ toolCallId: string;
746
+ toolName: string;
747
+ askClasses: ReadonlyArray<{
748
+ cls: AskClass;
749
+ layer: string;
750
+ }>;
751
+ boundary: "sandbox_internal";
752
+ }) => void;
753
+ };
715
754
  }
716
755
  /**
717
756
  * Fold a {@link Hooks.preToolUse} callback into a {@link ToolPolicy} layer, so a screening face a task
@@ -1,4 +1,5 @@
1
1
  import { decisionText, refuseOutOfContractDecision } from "./tool-policy.js";
2
+ import { brandPolicyAskClass } from "./ask-class.js";
2
3
  import { inlineUntrusted } from "./untrusted-text.js";
3
4
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
4
5
  import { createSafeNotifier } from "./safe-notify.js";
@@ -99,7 +100,7 @@ function screenPreToolUseResult(r) {
99
100
  return refuseOutOfContractDecision(r);
100
101
  }
101
102
  export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash) {
102
- return {
103
+ return brandPolicyAskClass({
103
104
  check: async (req) => {
104
105
  let r;
105
106
  try {
@@ -128,7 +129,7 @@ export function createPreToolUseConstraintPolicy(preToolUse, env, onCrash) {
128
129
  }
129
130
  return r.updatedInput !== undefined ? { action: "allow", updatedInput: r.updatedInput } : { action: "allow" };
130
131
  },
131
- };
132
+ }, "hook");
132
133
  }
133
134
  function traceHookCrash(input, err, notifier) {
134
135
  notifier.notify(() => input.onHookError?.(err), "toolGate.onHookError");
@@ -293,6 +294,29 @@ export async function runToolGate(input) {
293
294
  denySource = "classifier";
294
295
  }
295
296
  }
297
+ if (input.sandboxAdmission !== undefined &&
298
+ decision.action === "ask" &&
299
+ req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
300
+ input.isMarkedUnresolvable?.(input.event.toolCallId) !== true &&
301
+ decision.requiresRealApproval !== true &&
302
+ decision.decisionReason !== "hook" &&
303
+ hookAsk === undefined &&
304
+ input.egress !== true &&
305
+ input.sandboxAdmission.boundaryCapable !== true) {
306
+ const recorded = input.sandboxAdmission.askClassesOf(toolCallId);
307
+ const admissible = recorded !== undefined && recorded.length > 0 && recorded.every((r) => r.cls === "sandbox_local");
308
+ if (admissible) {
309
+ const askClasses = recorded;
310
+ input.sandboxAdmission.onAdmitted({ toolCallId, toolName, askClasses, boundary: "sandbox_internal" });
311
+ preToolContext.push(`permission.sandbox_admitted: this call's pending approval was auto-admitted because the deployment declares an isolated sandbox execution env and the request stays inside it. Recorded for audit.`);
312
+ decision = {
313
+ action: "allow",
314
+ message: "admitted inside the deployment-declared sandbox (permission.sandbox_admitted)",
315
+ decisionReason: "sandbox",
316
+ ...(policyRewrite !== undefined ? { updatedInput: policyRewrite } : {}),
317
+ };
318
+ }
319
+ }
296
320
  const egressTool = input.egress === true;
297
321
  const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
298
322
  const safety = egressTool || irreversibleTool ? { egress: egressTool, irreversible: irreversibleTool } : undefined;
@@ -18,7 +18,7 @@ export declare const LOCKED_KEY_REGISTRY: {
18
18
  readonly specFields: readonly ["mcp"];
19
19
  };
20
20
  readonly toolPolicy: {
21
- readonly specFields: readonly ["toolPolicy"];
21
+ readonly specFields: readonly ["toolPolicy", "basePolicyForResumeEdit"];
22
22
  };
23
23
  readonly compliancePosture: {
24
24
  readonly specFields: readonly [];
@@ -46,6 +46,12 @@ export interface LockedPreflight {
46
46
  * always enforced, resolved ONCE here. With `toolPolicy` locked, the deployment's own
47
47
  * `deps.toolPolicy` is the whole answer (a spec-supplied policy has already been refused). */
48
48
  toolPolicy: ToolPolicy | undefined;
49
+ /** F-012 (codex round 8): the effective resume-edit re-adjudication OVERRIDE, resolved lock-aware in
50
+ * the SAME slot as {@link toolPolicy}. With `toolPolicy` locked, a spec-supplied
51
+ * `basePolicyForResumeEdit` has already been refused wholesale, so this is the deployment's own
52
+ * `deps.basePolicyForResumeEdit` (or undefined — the caller-policy fallback then applies at the use
53
+ * site). Unlocked: `spec.basePolicyForResumeEdit ?? deps.basePolicyForResumeEdit`. */
54
+ basePolicyForResumeEdit: ToolPolicy | undefined;
49
55
  }
50
56
  /**
51
57
  * Validate a deployment's lock declaration against the closed registry. Fail-loud on any unknown
@@ -1,6 +1,6 @@
1
1
  export const LOCKED_KEY_REGISTRY = {
2
2
  mcp: { specFields: ["mcp"] },
3
- toolPolicy: { specFields: ["toolPolicy"] },
3
+ toolPolicy: { specFields: ["toolPolicy", "basePolicyForResumeEdit"] },
4
4
  compliancePosture: { specFields: [] },
5
5
  retentionPolicy: { specFields: [] },
6
6
  };
@@ -38,5 +38,6 @@ export function preflightLockedConfig(spec, deps) {
38
38
  lockedKeys,
39
39
  mcp: lockedKeys.has("mcp") ? undefined : spec.mcp,
40
40
  toolPolicy: lockedKeys.has("toolPolicy") ? deps.toolPolicy : (spec.toolPolicy ?? deps.toolPolicy),
41
+ basePolicyForResumeEdit: lockedKeys.has("toolPolicy") ? deps.basePolicyForResumeEdit : (spec.basePolicyForResumeEdit ?? deps.basePolicyForResumeEdit),
41
42
  };
42
43
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * design/180 half A — delegation RUNTIME provenance: the three-value attestation and its monotonic
3
+ * aggregate. The static tool-face judgment ({@link import("./content-origin.js").delegationCallIsExternal})
4
+ * stays the PERMANENT FLOOR: runtime evidence may only retire the static verdict when a COMPLETE
5
+ * clean proof is in hand; any evidence gap falls back to the floor. "We could not prove it clean"
6
+ * and "it is clean" are different answers — replacing the floor outright would fail OPEN on every
7
+ * evidence gap (old child vintages, crashes, un-armed sessions).
8
+ *
9
+ * The aggregate is two MONOTONIC bits plus versioning:
10
+ * - `sawExternal` — an actually-observed polluting-class invocation (own recorder, or a delivered
11
+ * child attestation of `external`). Once set, never cleared.
12
+ * - `incomplete` — some stretch of the session's conduct is unprovable (a delegation delivered no /
13
+ * an unknown attestation, a resume lost the aggregate, the recorder was not armed from turn 0).
14
+ * Once set, never cleared.
15
+ * Reduction order is FIXED (design/180 r3): `sawExternal ⇒ external` > `incomplete ⇒ unknown` >
16
+ * `clean` — an observed external event always outranks an evidence gap (missing evidence can make
17
+ * "clean" unprovable; it can never make an observed event disappear).
18
+ */
19
+ /** The three-value delegation attestation. Only `clean` changes behavior (the parent skips the
20
+ * static mark); `unknown` is byte-equivalent to absence (static floor governs); `external` marks
21
+ * unconditionally, with the observed-event reason. */
22
+ export type DelegationAttestation = "clean" | "external" | "unknown";
23
+ /** The monotonic per-run provenance aggregate (in-process this slice; the durable-checkpoint
24
+ * persistence leg rides the background-face slice — absence after a resume reads `incomplete`,
25
+ * which is the fail-closed default this shape is built around). */
26
+ export interface DelegationProvenanceAggregate {
27
+ version: 1;
28
+ /** Turn index the recorder was armed from. 0 ⇔ armed at prepare (full coverage). */
29
+ coverageStartTurn: number;
30
+ sawExternal: boolean;
31
+ incomplete: boolean;
32
+ }
33
+ /** The frozen content-safety snapshot a parent hands its delegated children (design/180 A-2): the
34
+ * classification config the child's recorder must judge under. Inherit-narrow-only — see
35
+ * {@link narrowContentSafety}. */
36
+ export interface DelegationContentSafety {
37
+ trustedTools: readonly string[];
38
+ execIsExternalContent: boolean;
39
+ }
40
+ export declare function newDelegationProvenanceAggregate(): DelegationProvenanceAggregate;
41
+ /**
42
+ * Effective content-safety for a run: the parent chain's frozen snapshot ∩ the run's own config.
43
+ * A child may NARROW what the chain trusts (fewer trusted tools, stricter exec), never widen it —
44
+ * a wider child-side allowlist must not launder a chain-external tool into `local` on the way up.
45
+ * At least one input must be present (the caller only computes this when armed).
46
+ */
47
+ export declare function narrowContentSafety(parent: DelegationContentSafety | undefined, own: {
48
+ trustedTools: ReadonlySet<string>;
49
+ execIsExternalContent: boolean;
50
+ } | undefined): {
51
+ trustedTools: Set<string>;
52
+ execIsExternalContent: boolean;
53
+ };
54
+ /** Reduce an aggregate to the delivered attestation. `completed` = the child reached its terminal
55
+ * normally with its full result delivered — a salvage/partial/errored child can never prove clean
56
+ * (its conduct after the cut is unobserved), though an OBSERVED external event still reports. */
57
+ export declare function reduceDelegationAttestation(agg: DelegationProvenanceAggregate | undefined, opts: {
58
+ completed: boolean;
59
+ }): DelegationAttestation;
60
+ /** Read a delivered card's attestation field (the explicit-whitelist handback seat). Anything but
61
+ * the three spellings — including absence — reads as undefined, i.e. the static floor governs. */
62
+ export declare function readCardAttestation(details: unknown): DelegationAttestation | undefined;
@@ -0,0 +1,26 @@
1
+ export function newDelegationProvenanceAggregate() {
2
+ return { version: 1, coverageStartTurn: 0, sawExternal: false, incomplete: false };
3
+ }
4
+ export function narrowContentSafety(parent, own) {
5
+ if (parent === undefined) {
6
+ return { trustedTools: new Set(own?.trustedTools ?? []), execIsExternalContent: own?.execIsExternalContent ?? false };
7
+ }
8
+ const fromParent = new Set(parent.trustedTools);
9
+ const trustedTools = own === undefined ? fromParent : new Set([...fromParent].filter((t) => own.trustedTools.has(t)));
10
+ return { trustedTools, execIsExternalContent: parent.execIsExternalContent || (own?.execIsExternalContent ?? false) };
11
+ }
12
+ export function reduceDelegationAttestation(agg, opts) {
13
+ if (agg === undefined)
14
+ return "unknown";
15
+ if (agg.sawExternal)
16
+ return "external";
17
+ if (agg.incomplete || !opts.completed || agg.version !== 1 || agg.coverageStartTurn !== 0)
18
+ return "unknown";
19
+ return "clean";
20
+ }
21
+ export function readCardAttestation(details) {
22
+ if (details === null || typeof details !== "object")
23
+ return undefined;
24
+ const v = details.attestation;
25
+ return v === "clean" || v === "external" || v === "unknown" ? v : undefined;
26
+ }
@@ -1,4 +1,4 @@
1
- import { type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionRecord } from "./layout.js";
1
+ import { type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionRecord } from "./layout.js";
2
2
  import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemorySessionHandle, ScanFinding } from "./types.js";
3
3
  /**
4
4
  * The CC `# Memory` instruction section — VERBATIM from the live capture
@@ -31,6 +31,15 @@ export declare function buildMemoryInstruction(memoryDir: string, instructionFil
31
31
  * dual-root instruction merge ("the write plane's instruction wins") never sees a second candidate.
32
32
  */
33
33
  export declare const MEMORY_RECALL_DISCIPLINE = "Before answering questions about earlier work, decisions, dates, people, or the user's preferences, look them up: `memory_search` finds entries by keyword and `memory_get` reads a full entry \u2014 the injected memory index only lists what exists. When a lookup comes up empty, say that you checked memory and found nothing instead of guessing.";
34
+ /**
35
+ * design/180 B-3 — the preference-header discipline paragraph. Same carriage as
36
+ * {@link MEMORY_RECALL_DISCIPLINE}: a runner-appended segment OUTSIDE `inject()`, so the
37
+ * CC-verbatim `# Memory` instruction stays byte-identical to its sha256-pinned capture. Appended
38
+ * only when a WRITE channel exists (prepare-memory gates it) — the lines it teaches are frontmatter
39
+ * the MODEL writes; the engine has no write arm for them (writing usage evidence for the model
40
+ * would forge the very account the three-tier discipline forbids forging).
41
+ */
42
+ export declare const MEMORY_PREFERENCE_DISCIPLINE = "When the user confirms a stored preference or fact still holds, refresh that entry's `last-confirmed: <YYYY-MM-DD>` frontmatter line (add it when absent). When you save a preference, add an `applies-when: <context>` frontmatter line naming when it applies. Both are plain frontmatter lines \u2014 write them yourself; nothing fills them in for you.";
34
43
  /** CC index-injection parameters: MEMORY.md's first 200 lines / 25KB enter the prompt. */
35
44
  export declare const MEMORY_INDEX_MAX_LINES = 200;
36
45
  export declare const MEMORY_INDEX_MAX_BYTES: number;
@@ -188,6 +197,62 @@ export declare class MemoryEngine {
188
197
  /** The session's pollution record (in-process mark first, then the durable marker) — undefined =
189
198
  * clean. Reads are side-effect-free. */
190
199
  sessionPollution(sessionId: string): SessionPollutionRecord | undefined;
200
+ /** Entry ids the session's memory tools RETURNED this process — the retrieved half of the
201
+ * challenged-history mechanical event (B-3). In-process on purpose: "same session" is one runner
202
+ * process by architecture, and the durable retrieved account is not session-keyed. */
203
+ private readonly retrievedThisSession;
204
+ /**
205
+ * The model-visible read faces' EXCLUSION view: entry id → why it is withheld.
206
+ * - `challenged` — an unresolved S-grade challenge generation exists (B-2');
207
+ * - `lineage_pending` — the dirty latch: a staged lineage row for this id has not been settled
208
+ * (credentialed-but-unpromoted, or the undecidable crash window) — "the account is not
209
+ * settled, the content does not go on the table" (r4).
210
+ * THROWS {@link import("./layout.js").ControlPlaneCorruptError} when either ledger's integrity is
211
+ * unknowable — consumers must refuse service loudly (corrupt ≠ clean), the same law the pollution
212
+ * marker already follows.
213
+ */
214
+ readChallengeExclusions(): Map<string, {
215
+ code: "challenged" | "lineage_pending";
216
+ generation?: number;
217
+ at?: number;
218
+ }>;
219
+ /** The shared challenge writer: append events idempotently, then announce OPAQUELY (id + code +
220
+ * generation only — the announcement is a model-visible face, so no name/description/free reason
221
+ * rides it; content stays on the host audit face). Failures are disclosed, never thrown past. */
222
+ private challengeAndAnnounce;
223
+ /**
224
+ * Host API (advisory, control-plane trust level — the same plane that can mark pollution):
225
+ * challenge entries by id. `requestId` is REQUIRED (the generation ledger's idempotency identity;
226
+ * this engine never mints one for the caller — a retried request must carry the SAME id so a lost
227
+ * ack cannot open a second generation). Each entry's eventId derives as `<requestId>:<entryId>`,
228
+ * so one batched request stays per-entry attributable.
229
+ */
230
+ challengeEntries(ids: readonly string[], reason: string, requestId: string): ChallengeAssignment[];
231
+ /** Host API: challenge every entry the lineage ledger attributes to `sessionId` (post-hoc source
232
+ * falsification — trustedTools misconfigured, a tool re-classified, late delegation evidence).
233
+ * Same requestId contract as {@link challengeEntries}. */
234
+ challengeSession(sessionId: string, reason: string, requestId: string): ChallengeAssignment[];
235
+ /** Host API: the TRUSTED resolve — closes exactly the named generation (a later challenge stays
236
+ * open; a resolve never blanket-clears, and there is no model-face path to this call). Returns
237
+ * false ⇔ no such generation was ever opened. */
238
+ resolveChallenge(entryId: string, generation: number, reason: string, requestId?: string): boolean;
239
+ /** Host API: adjudicate one UNDECIDABLE pending lineage transaction (the applyPatches-success →
240
+ * credential-write crash window; design/180 r5 — never auto-discarded). The host, holding its own
241
+ * deployment facts, states whether the commit landed. */
242
+ adjudicatePendingLineage(txnId: string, action: "promote" | "discard"): LineagePromotion[];
243
+ /** Host observability: the staged lineage transactions still awaiting settlement (their entry ids
244
+ * are latched — see {@link readChallengeExclusions}). Throws on a corrupt ledger. */
245
+ listPendingLineage(): Record<string, LineagePendingTxn>;
246
+ /** Host audit reads (throw on corruption — audit must not read a laundered ledger as clean). */
247
+ readChallengeLedger(): ChallengeEvent[];
248
+ /** Observability/tests — the collect-only challenged-history account (B-3; zero engine consumers). */
249
+ readChallengedHistoryAccount(): Record<string, ChallengedHistoryRow>;
250
+ /** Promotion settlement (shared by harvest step ⑤, reconciliation and host adjudication): a
251
+ * promoted contribution whose session ALREADY carries a durable pollution marker is challenged
252
+ * on the spot — "mark first, promote later" must converge exactly like the reverse order. The
253
+ * eventId anchors on the pollution record (sessionId + marked-at), so however many times this
254
+ * settlement replays the same pollution event, the generation is allocated once. */
255
+ private settlePromotions;
191
256
  materialize(scopes: readonly string[], writeScope: string | null): Promise<MemorySessionHandle>;
192
257
  inject(handle: MemorySessionHandle, opts?: {
193
258
  writeToolMounted?: boolean;
@@ -222,6 +287,7 @@ export declare class MemoryEngine {
222
287
  polluted?: {
223
288
  reason: string;
224
289
  };
290
+ sessionId?: string;
225
291
  }): Promise<HarvestReport>;
226
292
  private harvestCore;
227
293
  /**