@sema-agent/core 5.21.1 → 5.23.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 +103 -0
- package/dist/agents/send-message-tool.js +6 -3
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +48 -6
- package/dist/brain/errors.d.ts +20 -0
- package/dist/brain/errors.js +40 -0
- package/dist/brain/retry.d.ts +16 -2
- package/dist/brain/retry.js +3 -2
- package/dist/brain/status-sink.d.ts +9 -2
- package/dist/brain/stream-engine.d.ts +22 -0
- package/dist/brain/stream-engine.js +41 -10
- package/dist/core/ask-class.d.ts +48 -0
- package/dist/core/ask-class.js +33 -0
- package/dist/core/checkpoint-store.d.ts +103 -10
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/governance-codes.d.ts +38 -0
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.d.ts +101 -1
- package/dist/core/hooks.js +116 -14
- package/dist/core/locked-config.d.ts +7 -1
- package/dist/core/locked-config.js +2 -1
- package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
- package/dist/core/memory-engine/delegation-provenance.js +26 -0
- package/dist/core/memory-engine/engine.d.ts +94 -1
- package/dist/core/memory-engine/engine.js +329 -12
- package/dist/core/memory-engine/header-hints.d.ts +30 -0
- package/dist/core/memory-engine/header-hints.js +41 -0
- package/dist/core/memory-engine/index.d.ts +3 -2
- package/dist/core/memory-engine/index.js +3 -2
- package/dist/core/memory-engine/layout.d.ts +232 -0
- package/dist/core/memory-engine/layout.js +469 -1
- package/dist/core/memory-engine/tools.d.ts +30 -0
- package/dist/core/memory-engine/tools.js +108 -17
- package/dist/core/permission-rule-consent.d.ts +25 -9
- package/dist/core/permission-rule-consent.js +93 -21
- package/dist/core/permission-rule-model.d.ts +9 -1
- package/dist/core/permission-rule-model.js +2 -2
- package/dist/core/permission-rule-org.d.ts +195 -0
- package/dist/core/permission-rule-org.js +234 -0
- package/dist/core/permission-rule-store.d.ts +249 -6
- package/dist/core/permission-rule-store.js +314 -3
- package/dist/core/permission-rule-sync.d.ts +139 -0
- package/dist/core/permission-rule-sync.js +343 -0
- package/dist/core/runner/prepare-memory.js +35 -8
- package/dist/core/runner/prepare-task.d.ts +64 -3
- package/dist/core/runner/prepare-task.js +357 -32
- package/dist/core/runner/runtask.js +166 -6
- package/dist/core/shared-memory/contract.js +19 -4
- package/dist/core/shared-memory/normalize.d.ts +3 -1
- package/dist/core/shared-memory/tools.js +73 -17
- package/dist/core/shared-memory/types.d.ts +27 -1
- package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
- package/dist/core/task-notification.d.ts +5 -2
- package/dist/core/task-registry-agent.d.ts +1 -1
- package/dist/core/task-registry-agent.js +6 -2
- package/dist/core/task-registry-shared.d.ts +9 -2
- package/dist/core/task-registry.d.ts +9 -3
- package/dist/core/task-registry.js +2 -0
- package/dist/core/tool-policy.d.ts +155 -4
- package/dist/core/tool-policy.js +148 -10
- package/dist/core/tool-result-store.d.ts +9 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +79 -1
- package/dist/core/types.d.ts +94 -3
- package/dist/core/wiring-manifest.d.ts +16 -1
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/index.d.ts +15 -8
- package/dist/index.js +12 -5
- package/dist/orchestration/goal.d.ts +10 -0
- package/dist/orchestration/goal.js +6 -5
- package/dist/stores/file/adoption/adopt.d.ts +146 -0
- package/dist/stores/file/adoption/adopt.js +616 -0
- package/dist/stores/file/adoption/marker.d.ts +194 -0
- package/dist/stores/file/adoption/marker.js +198 -0
- package/dist/stores/file/background-agent-store.js +2 -0
- package/dist/stores/file/checkpoint-store.d.ts +4 -0
- package/dist/stores/file/checkpoint-store.js +3 -0
- package/dist/stores/file/file-snapshot-store.js +2 -0
- package/dist/stores/file/index.d.ts +2 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/memory-store.js +2 -0
- package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
- package/dist/stores/file/permission-rule-adopt.js +95 -0
- package/dist/stores/file/permission-rule-store.d.ts +80 -2
- package/dist/stores/file/permission-rule-store.js +189 -46
- package/dist/stores/file/session-policy-store.js +2 -0
- package/dist/stores/file/session-store.js +2 -0
- package/dist/stores/file/task-list-store.js +2 -0
- package/dist/stores/file/tool-result-store.js +2 -0
- package/dist/stores/file/usage-window-store.js +2 -0
- package/dist/stores/file/workflow-journal-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -10
- package/package.json +3 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F-012 L2 — the ENGINE-INTERNAL ask-class envelope.
|
|
3
|
+
*
|
|
4
|
+
* An `ask` verdict's SEMANTIC CLASS decides whether a deployment-declared sandbox may auto-admit it
|
|
5
|
+
* (clay's ruling: a declared sandbox admits everything inside it — the boundary crossings park to a
|
|
6
|
+
* human, escape prevention belongs to the sandbox, not to engine re-policing). The class deliberately
|
|
7
|
+
* lives OUTSIDE the public `PermissionResult`: a decision object is caller-supplied data, and a
|
|
8
|
+
* self-reported "this ask is sandbox-local" field would be the same trust hole as the
|
|
9
|
+
* `decisionReason:"safety"` self-declaration this codebase already refuses. Instead, BUILT-IN policy
|
|
10
|
+
* factories register their instances here (private WeakMap brand — nothing a deployment policy can
|
|
11
|
+
* forge by shaping its return value), and everything unregistered reads as `unknown`.
|
|
12
|
+
*
|
|
13
|
+
* Classes and their dominance (STRICT — merge keeps the strongest; only an all-`sandbox_local` set
|
|
14
|
+
* may auto-admit):
|
|
15
|
+
* - `sandbox_local` — an engine-authored "confirm a potentially destructive LOCAL action" ask
|
|
16
|
+
* (name/pattern confirmation lists). Sandbox containment answers it.
|
|
17
|
+
* - `external_authority` — an ask whose whole point is real judgment (the safety-rule policies with
|
|
18
|
+
* `requiresRealApproval`); containment answers nothing about it.
|
|
19
|
+
* - `hook` — a screening-face ask (a deployment's PreToolUse verdict folded to policy
|
|
20
|
+
* form); the face's authority is the deployment's, not the sandbox's.
|
|
21
|
+
* - `mandate` — an ask bound to durable-park semantics (inherited durable mandate /
|
|
22
|
+
* approver-unavailable float); it must reach the park untouched.
|
|
23
|
+
* - `unknown` — everything unregistered, every deployment policy. Fail-closed: never
|
|
24
|
+
* admitted.
|
|
25
|
+
*/
|
|
26
|
+
export type AskClass = "sandbox_local" | "external_authority" | "hook" | "mandate" | "unknown";
|
|
27
|
+
/**
|
|
28
|
+
* Brand a built-in policy instance with its ask class (engine factories only — not exported from the
|
|
29
|
+
* package). The `check` binding is LOCKED as a non-writable, non-configurable DATA property before the
|
|
30
|
+
* policy is returned (codex adversarial round 4, HIGH): reading `policy.check` and INVOKING it must
|
|
31
|
+
* name the same callable, atomically. A plain `===` re-read was TOCTOU-able by an accessor — a getter
|
|
32
|
+
* could return the branded function to `policyAskClassOf` and a deployment ask to the gate's
|
|
33
|
+
* invocation. Locking the property makes the callable un-swappable and un-redefinable, so the class
|
|
34
|
+
* travels with the exact decision function. An `check` that is ALREADY accessor-backed (or a frozen
|
|
35
|
+
* object we cannot lock) is refused branding — it reads back `unknown` (fail-closed), never trusted.
|
|
36
|
+
*/
|
|
37
|
+
export declare function brandPolicyAskClass<T extends {
|
|
38
|
+
check: unknown;
|
|
39
|
+
}>(policy: T, cls: AskClass): T;
|
|
40
|
+
/** The registered class of a policy instance — `unknown` for anything unregistered, whose `check` is
|
|
41
|
+
* no longer the locked data value, or whose `check` became accessor-backed (fail-closed: a
|
|
42
|
+
* replaced/decorated/getter-shadowed decision function is not the branded one). Reads the property
|
|
43
|
+
* DESCRIPTOR's value (never invokes a getter), so the check is atomic with what the gate will call. */
|
|
44
|
+
export declare function policyAskClassOf(policy: (object & {
|
|
45
|
+
check?: unknown;
|
|
46
|
+
}) | null | undefined): AskClass;
|
|
47
|
+
/** The strongest class in a set (strict dominance — any non-`sandbox_local` member dominates). */
|
|
48
|
+
export declare function dominantAskClass(classes: readonly AskClass[]): AskClass;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const RANK = { sandbox_local: 0, external_authority: 1, hook: 2, mandate: 3, unknown: 4 };
|
|
2
|
+
const registry = new WeakMap();
|
|
3
|
+
export function brandPolicyAskClass(policy, cls) {
|
|
4
|
+
const desc = Object.getOwnPropertyDescriptor(policy, "check");
|
|
5
|
+
if (desc === undefined || desc.get !== undefined || desc.set !== undefined)
|
|
6
|
+
return policy;
|
|
7
|
+
try {
|
|
8
|
+
Object.defineProperty(policy, "check", { value: policy.check, writable: false, configurable: false, enumerable: desc.enumerable ?? true });
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return policy;
|
|
12
|
+
}
|
|
13
|
+
registry.set(policy, { cls, check: policy.check });
|
|
14
|
+
return policy;
|
|
15
|
+
}
|
|
16
|
+
export function policyAskClassOf(policy) {
|
|
17
|
+
if (policy === null || policy === undefined)
|
|
18
|
+
return "unknown";
|
|
19
|
+
const entry = registry.get(policy);
|
|
20
|
+
if (entry === undefined)
|
|
21
|
+
return "unknown";
|
|
22
|
+
const desc = Object.getOwnPropertyDescriptor(policy, "check");
|
|
23
|
+
if (desc === undefined || desc.get !== undefined || desc.set !== undefined)
|
|
24
|
+
return "unknown";
|
|
25
|
+
return desc.value === entry.check ? entry.cls : "unknown";
|
|
26
|
+
}
|
|
27
|
+
export function dominantAskClass(classes) {
|
|
28
|
+
let strongest = "sandbox_local";
|
|
29
|
+
for (const c of classes)
|
|
30
|
+
if (RANK[c] > RANK[strongest])
|
|
31
|
+
strongest = c;
|
|
32
|
+
return strongest;
|
|
33
|
+
}
|
|
@@ -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
|
|
873
|
-
* (
|
|
874
|
-
*
|
|
875
|
-
* (
|
|
876
|
-
export declare const MAX_SUPPORTED_CHECKPOINT_VERSION =
|
|
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` —
|
|
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` —
|
|
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"
|
|
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
|
|
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,12 @@ 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",
|
|
20
|
+
"memory.challenge_ledger_oversize",
|
|
21
|
+
"memory.control_plane_rebuilt",
|
|
22
|
+
"memory.control_plane_not_corrupt",
|
|
17
23
|
]);
|
|
18
24
|
export function governanceRetryClass(code) {
|
|
19
25
|
if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
|
|
@@ -23,3 +29,11 @@ export function governanceRetryClass(code) {
|
|
|
23
29
|
return "terminal";
|
|
24
30
|
return undefined;
|
|
25
31
|
}
|
|
32
|
+
export const RULE_SYNC_DROP_CODES = {
|
|
33
|
+
invalid_rule_text: "inbound-refused-or-local-quarantined",
|
|
34
|
+
metadata_mismatch: "inbound-refused-or-local-quarantined",
|
|
35
|
+
own_actor_forged: "inbound-refused",
|
|
36
|
+
dot_identity_conflict: "inbound-refused",
|
|
37
|
+
below_gc_frontier: "local-quarantined",
|
|
38
|
+
server_rejected: "local-quarantined",
|
|
39
|
+
};
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -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):
|
|
@@ -160,8 +161,13 @@ export interface Hooks {
|
|
|
160
161
|
* - `"shellGate"` — same tighten-deny, but the tool's irreversibility tier was installed by the COARSE
|
|
161
162
|
* `TaskSpec.shellGate` doctrine (design/80 D-E), not an explicit per-tool mark.
|
|
162
163
|
* - `"planMode"` — the design/108 plan-mode write-deny short-circuit (a read-only fidelity gate).
|
|
164
|
+
* - `"org"` (design/182 §7) — the ORG layer: an organization deny rule, or the fail-closed tighten an
|
|
165
|
+
* org-governed deployment applies while it cannot adjudicate against a snapshot (that tighten's ask
|
|
166
|
+
* then resolving to deny). Its own word rather than `"policy"` because the authority is the ORG's
|
|
167
|
+
* published snapshot, not this deployment's `ToolPolicy` — an operator reading "policy" on a call
|
|
168
|
+
* their own policy allowed would be looking in the wrong place.
|
|
163
169
|
*/
|
|
164
|
-
export type PermissionDeniedSource = "policy" | "hook" | "safety" | "shellGate" | "planMode" | "classifier";
|
|
170
|
+
export type PermissionDeniedSource = "policy" | "hook" | "safety" | "shellGate" | "planMode" | "classifier" | "org";
|
|
165
171
|
/** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + `source`). */
|
|
166
172
|
export interface PermissionDeniedPayload {
|
|
167
173
|
toolName: string;
|
|
@@ -519,6 +525,26 @@ export interface ParkAttemptFailed {
|
|
|
519
525
|
/** Why the park could not be minted — a sentence, already worded for a reader. */
|
|
520
526
|
parkFailed: string;
|
|
521
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* design/182 §7 — what the ORG layer answers for ONE call (see `ToolGateInput.orgRules`).
|
|
530
|
+
*
|
|
531
|
+
* The two arms are distinguished by `status`, NEVER by the absence of a verdict: "the org published no
|
|
532
|
+
* rule for this call" and "this deployment cannot see the org's rules" are opposite facts, and reading
|
|
533
|
+
* an empty answer as the first one is exactly the governance fail-open the availability contract exists
|
|
534
|
+
* to prevent.
|
|
535
|
+
*/
|
|
536
|
+
export type OrgGateVerdict = {
|
|
537
|
+
status: "available";
|
|
538
|
+
/** The org rule that speaks for this call, if any. Deny outranks ask; there is no allow bucket. */
|
|
539
|
+
verdict?: {
|
|
540
|
+
behavior: "deny" | "ask";
|
|
541
|
+
rule: string;
|
|
542
|
+
};
|
|
543
|
+
} | {
|
|
544
|
+
status: "unavailable";
|
|
545
|
+
/** The overlay's own disclosure lines (engine/deployment-authored), for the operator channel. */
|
|
546
|
+
disclosures: readonly string[];
|
|
547
|
+
};
|
|
522
548
|
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort signal. */
|
|
523
549
|
export interface ToolGateInput {
|
|
524
550
|
/** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
|
|
@@ -712,6 +738,80 @@ export interface ToolGateInput {
|
|
|
712
738
|
rule: string;
|
|
713
739
|
}) => void;
|
|
714
740
|
};
|
|
741
|
+
/**
|
|
742
|
+
* design/182 §7 — the ORG layer. Present ONLY when the deployment DECLARED org governance (the
|
|
743
|
+
* overlay constructor is the boot gate: a governed declaration with no snapshot provider refuses to
|
|
744
|
+
* boot). Absent ⇒ the whole step does not exist and the decision path is byte-identical.
|
|
745
|
+
*
|
|
746
|
+
* The org layer is the outermost TIGHTENING authority, so it runs after every fold and every engine
|
|
747
|
+
* tighten and before the ask-resolution chain — "org deny > org ask (non-dismissable) > personal
|
|
748
|
+
* allow rule > bare ask". It never loosens: there is structurally no org allow bucket.
|
|
749
|
+
*
|
|
750
|
+
* The `unavailable` arm is the load-bearing one. An org-governed deployment that cannot reach a
|
|
751
|
+
* snapshot fails the WHOLE decision boundary closed — every terminal allow (a deployment-policy
|
|
752
|
+
* allow and the no-policy default allow included) becomes an ask carrying
|
|
753
|
+
* `decisionReason: "org_unavailable"` AND `requiresRealApproval: true`, and both ask→allow seams (the
|
|
754
|
+
* personal-rule lane and the auto-mode classifier) are disarmed. Tightening only the two seams was
|
|
755
|
+
* refuted during design: a call that is ALREADY allow never passes through either seam, so it would
|
|
756
|
+
* have sailed past the org deny that could not be read. The `requiresRealApproval` bit closes the
|
|
757
|
+
* third loosening path — a blanket `onAsk: "allow"` would otherwise clear the synthetic ask.
|
|
758
|
+
*/
|
|
759
|
+
orgRules?: {
|
|
760
|
+
/** Adjudicate this call against the current org overlay. MUST NOT throw — a throw is read as
|
|
761
|
+
* `unavailable` (fail-closed), never as "no org rule spoke". */
|
|
762
|
+
adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
|
|
763
|
+
/** True iff the reserved question-tool NAME on this leg resolves to the ENGINE's own content-ask
|
|
764
|
+
* tool. It is the exemption's real predicate: the name alone is not, because a caller may mount
|
|
765
|
+
* its own tool under that name when the first-party one is not mounted, and such a tool has
|
|
766
|
+
* arbitrary side effects (codex review, confirmed MED). Absent ⇒ `false` ⇒ the call is governed
|
|
767
|
+
* like any other, which is the fail-closed default. */
|
|
768
|
+
contentAskToolMounted?: boolean;
|
|
769
|
+
/** Disclosure sink for the unavailable arm (trace `permission.org_snapshot_unavailable`). Never
|
|
770
|
+
* affects the outcome. */
|
|
771
|
+
onUnavailable?: (info: {
|
|
772
|
+
toolName: string;
|
|
773
|
+
toolCallId: string;
|
|
774
|
+
message: string;
|
|
775
|
+
}) => void;
|
|
776
|
+
};
|
|
777
|
+
/**
|
|
778
|
+
* F-012 L2 — the SANDBOX ADMISSION leg. Present ONLY when the runner armed the mode: the execution
|
|
779
|
+
* env DECLARES isolation (`RemoteExecutionEnv.capabilities.isolation === true` — a Runner-resolved
|
|
780
|
+
* fact, never caller self-report) AND the park prerequisites hold (durable park configured + a
|
|
781
|
+
* checkpoint store that explicitly declares `redecision.reopen`). Absent ⇒ the whole step does not
|
|
782
|
+
* exist and the decision path is byte-identical to a build without it.
|
|
783
|
+
*
|
|
784
|
+
* Semantics (clay's ruling): inside a declared sandbox, an engine-authored "confirm a potentially
|
|
785
|
+
* destructive local action" ask is answered by containment itself — auto-allow + durable
|
|
786
|
+
* disclosure (`permission.sandbox_admitted`), never a headless deny that pollutes the model.
|
|
787
|
+
* STRICT dominance governs: the leg admits only when EVERY surviving ask on the call is classified
|
|
788
|
+
* `sandbox_local` in the engine's private envelope (deployment-policy asks are `unknown`, hook
|
|
789
|
+
* asks `hook`, marked calls `mandate`, `requiresRealApproval` asks external — none admit), and
|
|
790
|
+
* the call crosses NO declared boundary (`boundaryCapable`: egress-marked tools and
|
|
791
|
+
* protocol-mounted (MCP/A2A) tools — a server's self-report is always boundary-capable). A
|
|
792
|
+
* boundary-crossing ask under the armed mode takes the ordinary durable-park leg below — "park to
|
|
793
|
+
* a human", never an auto-deny.
|
|
794
|
+
*/
|
|
795
|
+
sandboxAdmission?: {
|
|
796
|
+
/** The ask classes recorded for this call's fold-layer asks (with their source layers), or
|
|
797
|
+
* `undefined` when no fold layer asked — the gate's own safety tighten is then the only asker. */
|
|
798
|
+
askClassesOf: (toolCallId: string) => ReadonlyArray<{
|
|
799
|
+
cls: AskClass;
|
|
800
|
+
layer: string;
|
|
801
|
+
}> | undefined;
|
|
802
|
+
/** This call's tool crosses a DECLARED boundary (egress axis / protocol-mounted) — never admitted. */
|
|
803
|
+
boundaryCapable: boolean;
|
|
804
|
+
/** The durable disclosure sink — fired on every admission BEFORE the allow is returned. */
|
|
805
|
+
onAdmitted: (info: {
|
|
806
|
+
toolCallId: string;
|
|
807
|
+
toolName: string;
|
|
808
|
+
askClasses: ReadonlyArray<{
|
|
809
|
+
cls: AskClass;
|
|
810
|
+
layer: string;
|
|
811
|
+
}>;
|
|
812
|
+
boundary: "sandbox_internal";
|
|
813
|
+
}) => void;
|
|
814
|
+
};
|
|
715
815
|
}
|
|
716
816
|
/**
|
|
717
817
|
* Fold a {@link Hooks.preToolUse} callback into a {@link ToolPolicy} layer, so a screening face a task
|