@sema-agent/core 7.40.1 → 7.41.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 +188 -0
- package/dist/agents/agent-transcript-tool.d.ts +10 -2
- package/dist/agents/agent-transcript-tool.js +52 -26
- package/dist/agents/list-agents-tool.d.ts +17 -2
- package/dist/agents/list-agents-tool.js +28 -16
- package/dist/agents/peer-session-drain.d.ts +3 -1
- package/dist/agents/peer-session-drain.js +1 -0
- package/dist/agents/subagent-steps.d.ts +12 -2
- package/dist/agents/subagent-steps.js +8 -4
- package/dist/agents/subagent.d.ts +1 -17
- package/dist/agents/subagent.js +12 -24
- package/dist/core/agent-definition.d.ts +19 -40
- package/dist/core/agent-state.d.ts +91 -0
- package/dist/core/agent-state.js +79 -0
- package/dist/core/ask-origin.d.ts +28 -6
- package/dist/core/ask-origin.js +9 -5
- package/dist/core/ask-unresolvable-notice.d.ts +27 -4
- package/dist/core/ask-unresolvable-notice.js +8 -0
- package/dist/core/auto-promote.d.ts +14 -27
- package/dist/core/auto-promote.js +5 -5
- package/dist/core/checkpoint-store.d.ts +11 -12
- package/dist/core/decision-receipt.d.ts +5 -2
- package/dist/core/decision-receipt.js +1 -1
- package/dist/core/engine-notice.d.ts +7 -1
- package/dist/core/gate-crash-trace.d.ts +1 -1
- package/dist/core/gate-fold.js +7 -3
- package/dist/core/gate-lanes.js +22 -3
- package/dist/core/governance-codes.d.ts +3 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +20 -6
- package/dist/core/hooks.js +28 -28
- package/dist/core/mcp-tool-meta.d.ts +24 -0
- package/dist/core/mcp-tool-meta.js +17 -0
- package/dist/core/mcp.d.ts +1 -6
- package/dist/core/mcp.js +6 -7
- package/dist/core/mode-policy.d.ts +25 -0
- package/dist/core/mode-policy.js +8 -0
- package/dist/core/protocol-table.d.ts +3 -0
- package/dist/core/protocol-table.js +3 -0
- package/dist/core/reminder-mint.d.ts +21 -1
- package/dist/core/reminder-mint.js +20 -0
- package/dist/core/retired-keys.d.ts +2 -1
- package/dist/core/retired-keys.js +13 -2
- package/dist/core/rule-denial-sentence.d.ts +2 -2
- package/dist/core/runner/call-hook-stations.d.ts +106 -0
- package/dist/core/runner/call-hook-stations.js +118 -0
- package/dist/core/runner/contracts.d.ts +35 -25
- package/dist/core/runner/gate-exit.d.ts +5 -1
- package/dist/core/runner/gate-exit.js +10 -6
- package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
- package/dist/core/runner/prepare-delegation-surface.js +4 -4
- package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
- package/dist/core/runner/prepare-gate-stations.js +13 -70
- package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
- package/dist/core/runner/prepare-inherited-gate.js +5 -3
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
- package/dist/core/runner/prepare-policy-chain.js +14 -25
- package/dist/core/runner/prepare-project-context.d.ts +15 -16
- package/dist/core/runner/prepare-project-context.js +15 -11
- package/dist/core/runner/prepare-run-refs.d.ts +8 -8
- package/dist/core/runner/prepare-run-refs.js +3 -3
- package/dist/core/runner/prepare-safety-scan.js +3 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
- package/dist/core/runner/prepare-suspend-saga.js +2 -1
- package/dist/core/runner/prepare-task.js +14 -12
- package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
- package/dist/core/runner/prepare-turn-wiring.js +16 -13
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-apply.js +12 -23
- package/dist/core/runner/resume-checkpoint-screen.js +3 -0
- package/dist/core/runner/run-leg.js +61 -47
- package/dist/core/runner/runtask.d.ts +1 -1
- package/dist/core/runner/session-rule-policy.d.ts +7 -19
- package/dist/core/runner/session-rule-policy.js +1 -2
- package/dist/core/runner/skill-base-dir.d.ts +10 -0
- package/dist/core/runner/skill-base-dir.js +4 -0
- package/dist/core/runner/synthetic-tools.d.ts +11 -17
- package/dist/core/runner/synthetic-tools.js +7 -8
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +2 -0
- package/dist/core/session-policy-store.d.ts +63 -11
- package/dist/core/session-policy-store.js +52 -6
- package/dist/core/skill-grant.d.ts +239 -0
- package/dist/core/skill-grant.js +313 -0
- package/dist/core/skills-directory.d.ts +5 -34
- package/dist/core/skills-directory.js +9 -58
- package/dist/core/task-registry-agent.d.ts +14 -2
- package/dist/core/task-registry-agent.js +23 -8
- package/dist/core/task-registry-shared.d.ts +6 -0
- package/dist/core/task-registry.d.ts +16 -0
- package/dist/core/task-registry.js +19 -6
- package/dist/core/task-spec.d.ts +10 -0
- package/dist/core/tool-face.d.ts +3 -2
- package/dist/core/tool-name-aliases.d.ts +38 -12
- package/dist/core/tool-name-aliases.js +16 -0
- package/dist/core/tool-name-readings.d.ts +6 -2
- package/dist/core/tool-name-readings.js +6 -4
- package/dist/core/tool-policy.d.ts +2 -2
- package/dist/core/tool-registry.d.ts +1 -4
- package/dist/core/tool-registry.js +0 -3
- package/dist/core/tool-spec.d.ts +11 -13
- package/dist/core/tools.d.ts +1 -1
- package/dist/core/trace.d.ts +15 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/harness/agent-harness.d.ts +12 -1
- package/dist/engine/harness/agent-harness.js +17 -13
- package/dist/engine/loop/agent-loop.d.ts +16 -1
- package/dist/engine/loop/agent-loop.js +9 -15
- package/dist/engine/loop/types.d.ts +2 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/stores/file/adoption/adopt.js +23 -16
- package/dist/stores/file/index.d.ts +8 -2
- package/dist/stores/file/index.js +2 -2
- package/dist/stores/file/mailbox-store.js +2 -2
- package/dist/stores/file/permission-rule-adopt.js +10 -1
- package/dist/stores/file/permission-rule-store.d.ts +6 -4
- package/dist/stores/file/permission-rule-store.js +127 -59
- package/dist/stores/file/session-policy-store.d.ts +86 -14
- package/dist/stores/file/session-policy-store.js +135 -118
- package/dist/stores/file/task-list-store.d.ts +5 -0
- package/dist/stores/file/task-list-store.js +18 -4
- package/dist/stores/file/usage-window-store.d.ts +9 -1
- package/dist/stores/file/usage-window-store.js +47 -33
- package/dist/tools/fs/safety.d.ts +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +45 -5
- package/wire-changes/core@7.41.0.json +1865 -0
- package/dist/core/runner/active-skill-scope.d.ts +0 -58
- package/dist/core/runner/active-skill-scope.js +0 -133
- package/dist/core/skill-tool-specifier.d.ts +0 -80
- package/dist/core/skill-tool-specifier.js +0 -57
|
@@ -282,7 +282,7 @@ export type RealApprovalTableCoversEveryOrigin = AssertAllKeysHandled<Exclude<As
|
|
|
282
282
|
* the ask's CLASS SET ({@link classifyAsk}, {@link MODE_ARBITRATION}) — never by this one word.
|
|
283
283
|
*
|
|
284
284
|
* - `operator_always` — the operator's `always` floor: per-call confirmation for shell commands.
|
|
285
|
-
* - `tool_marks` — the tool's own egress / irreversibility declaration.
|
|
285
|
+
* - `tool_marks` — the tool's own egress / irreversibility / interaction declaration.
|
|
286
286
|
* - `probe_mandate` — this call's reversibility probe declared the demotion STRUCTURAL.
|
|
287
287
|
* - `probe_unanswered` — the probe was consulted and did not answer: the boundary is UNKNOWN.
|
|
288
288
|
* - `write_protection` — the table covers this write's target. A write onto load-executed configuration
|
|
@@ -300,7 +300,7 @@ declare const MANDATE_DISCLOSURE: {
|
|
|
300
300
|
readonly probe_mandate: "the reversibility check declared this call structurally gated (the built-in shell check raises this for a read outside the directories allowed for this session) — it is cleared by confirming this call, never by a standing allow rule";
|
|
301
301
|
readonly probe_unanswered: "the reversibility check did not answer for this call (it timed out or failed), so whether the call reads outside the directories allowed for this session is unknown — it is cleared by confirming this call, never by a standing allow rule";
|
|
302
302
|
readonly operator_always: "this deployment mandates per-call confirmation for shell commands (shellGate: always)";
|
|
303
|
-
readonly tool_marks: "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)";
|
|
303
|
+
readonly tool_marks: "this tool carries egress/irreversibility/interaction marks (a mandated confirmation a rule cannot clear)";
|
|
304
304
|
readonly write_protection: "this write's target is on the deployment's write-protection table (the agent's own configuration is guarded one look per write) — it is cleared by confirming this call, never by a standing allow rule";
|
|
305
305
|
readonly write_protection_unresolved: "the write-protection judge could not resolve this write's target, so whether it lands on the protected table is unknown — it is cleared by confirming this call: no standing rule and no automatic decider answers for it, though a wired approver seat can";
|
|
306
306
|
};
|
|
@@ -346,6 +346,10 @@ export declare function persistedRuleMandateOf(marks: MandateMarks): PersistedRu
|
|
|
346
346
|
* `irreversibility`) beside the two per-call facts the gate stamps. */
|
|
347
347
|
export interface MandateMarks {
|
|
348
348
|
egress?: boolean;
|
|
349
|
+
/** The tool's server declared that a call must be put to a person
|
|
350
|
+
* ({@link import("../engine/loop/types.js").AgentTool.mcpRequiresUserInteraction}): a mark of the tool's own, like `egress` — no
|
|
351
|
+
* persisted allow rule or standing grant clears the ask, and the class `requires_user_interaction` rides every ask on the call. */
|
|
352
|
+
requiresUserInteraction?: boolean;
|
|
349
353
|
shellGated?: boolean;
|
|
350
354
|
irreversibility?: "never" | "maybe" | "always";
|
|
351
355
|
/** The surviving ask's engine-stamped `probeMandated` — see
|
|
@@ -436,10 +440,13 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
|
|
|
436
440
|
* most-immune-first order; every lane reads the folded row ({@link foldArbitrationRow}), never the word.
|
|
437
441
|
* `shell_floor` is the operator's `shellGate: "always"` floor on a shell-running seat — the question the `operator_always`
|
|
438
442
|
* mandate word discloses, as a class of its own so a set that holds another forced question still holds it.
|
|
443
|
+
* `requires_user_interaction` is the SERVER's declaration on the called tool (`_meta["anthropic/requiresUserInteraction"] === true`): a
|
|
444
|
+
* permission question, unlike `content_question` (the reserved question tool's content-routing signal) — it carries that class's arbitration
|
|
445
|
+
* row cell by cell, because the reference implementation judges the declaration ahead of its allow-rule lookup and its bypass release.
|
|
439
446
|
* `unstated` is the mode's answer for a call nobody stated: a tool whose permission seat is `passthrough` (it states no
|
|
440
447
|
* permission of its own — a remote peer's tool) reached the end of the fold with an allow no layer stated.
|
|
441
448
|
*/
|
|
442
|
-
export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
|
|
449
|
+
export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
|
|
443
450
|
export type AskClass = (typeof ASK_CLASSES)[number];
|
|
444
451
|
/** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
|
|
445
452
|
export declare function isAskClass(v: unknown): v is AskClass;
|
|
@@ -542,6 +549,13 @@ export declare const MODE_ARBITRATION: {
|
|
|
542
549
|
readonly ruleMayClear: false;
|
|
543
550
|
readonly hookAskFloor: false;
|
|
544
551
|
};
|
|
552
|
+
readonly requires_user_interaction: {
|
|
553
|
+
readonly bypassImmune: true;
|
|
554
|
+
readonly classifierEligible: false;
|
|
555
|
+
readonly sandboxAdmission: false;
|
|
556
|
+
readonly ruleMayClear: false;
|
|
557
|
+
readonly hookAskFloor: true;
|
|
558
|
+
};
|
|
545
559
|
readonly operator_forced: {
|
|
546
560
|
readonly bypassImmune: true;
|
|
547
561
|
readonly classifierEligible: true;
|
|
@@ -631,7 +645,8 @@ export declare const MODE_ARBITRATION: {
|
|
|
631
645
|
export type ArbitrationTableCoversEveryClass = AssertAllKeysHandled<Exclude<AskClass, keyof typeof MODE_ARBITRATION>>;
|
|
632
646
|
/** The classes whose own mint puts the `requiresRealApproval` bit on the ask — a bit one of them explains is not
|
|
633
647
|
* the bare bit `policy_rra` names. The mark classes (probe / write protection / tool marks / operator forced / the
|
|
634
|
-
* shell floor) mint no bit and explain none
|
|
648
|
+
* shell floor) mint no bit and explain none — except `requires_user_interaction`, whose declaration the origin stamp
|
|
649
|
+
* turns into the bit on every ask of the tool. */
|
|
635
650
|
export declare const RRA_EXPLAINERS: ReadonlySet<AskClass>;
|
|
636
651
|
/** The classes a permission mode can RELEASE (the `bypassImmune: false` rows) — the only words that can appear on
|
|
637
652
|
* `GateDisposition.released.askClass`; every other class reaches a person under every mode. */
|
|
@@ -709,6 +724,10 @@ export declare function classifyAsk(d: Extract<PermissionResult, {
|
|
|
709
724
|
* (2.1.280) — the call did NOT run because this session has no approval surface — with the run's own fact beside it
|
|
710
725
|
* and the ask's prompt as what was requested. The gate appends the caller-side remedy. */
|
|
711
726
|
export declare function headlessDenialMessage(toolName: string, askMessage: string): string;
|
|
727
|
+
/** The state of the approver seat a remedy is written for: `absent` — no approver is wired for the leg; `unavailable` — one is
|
|
728
|
+
* wired and answered that nobody is reachable for this ask. Absent in the input reads as `absent` (the posture the
|
|
729
|
+
* function was written for). */
|
|
730
|
+
export type HeadlessApproverSeat = "absent" | "unavailable";
|
|
712
731
|
/**
|
|
713
732
|
* The ONE sentence a headless auto-deny (an ask that reached its final deny because no approver is wired) adds
|
|
714
733
|
* after its refusal: the caller-side change that would have let this call run. Read off the effective mode, the
|
|
@@ -720,8 +739,10 @@ export declare function headlessDenialMessage(toolName: string, askMessage: stri
|
|
|
720
739
|
* · the same ask under `acceptEdits` — the target lies outside the working directories: add its directory to
|
|
721
740
|
* `additionalDirectories`;
|
|
722
741
|
* · any other releasable ask — `bypassPermissions` releases it (disclosed on the call).
|
|
723
|
-
* Every arm also names the approver
|
|
724
|
-
*
|
|
742
|
+
* Every arm also names the approver, in the clause of the seat's own state ({@link HeadlessApproverSeat}): none wired ⇒
|
|
743
|
+
* wire one; one wired that answered `unavailable` for this ask ⇒ make a person reachable through it (a wired approver is
|
|
744
|
+
* never told to be wired). The refusal's tool text and the `delegation.ask_unresolvable` notice both carry this sentence —
|
|
745
|
+
* they call this function, neither spells it.
|
|
725
746
|
*/
|
|
726
747
|
export declare function headlessApprovalRemedy(input: {
|
|
727
748
|
readonly mode: PermissionMode | undefined;
|
|
@@ -730,6 +751,7 @@ export declare function headlessApprovalRemedy(input: {
|
|
|
730
751
|
readonly unplacedOptions?: {
|
|
731
752
|
readonly program: "cp" | "mv";
|
|
732
753
|
};
|
|
754
|
+
readonly approverSeat?: HeadlessApproverSeat;
|
|
733
755
|
}): string;
|
|
734
756
|
/** The reference implementation's sentence (2.1.280) for a `cp` / `mv` the mode will not answer because it carries option
|
|
735
757
|
* words — the approval card's message under `acceptEdits` and the head of the headless remedy. */
|
package/dist/core/ask-origin.js
CHANGED
|
@@ -16,7 +16,7 @@ export function stampAskOrigin(decision, facts, classification) {
|
|
|
16
16
|
const classed = askClassMembersOf(classification);
|
|
17
17
|
const { mandate: _selfDeclared, ...unstamped } = decision;
|
|
18
18
|
const clearance = ruleClearanceOf(unstamped, facts.mandateMarks);
|
|
19
|
-
const mandated = { ...unstamped, ...(clearance.mandated ? { mandated: true } : {}), ...(clearance.mandate !== undefined ? { mandate: clearance.mandate } : {}) };
|
|
19
|
+
const mandated = { ...unstamped, ...(clearance.mandated ? { mandated: true } : {}), ...(clearance.mandate !== undefined ? { mandate: clearance.mandate } : {}), ...(facts.mandateMarks.requiresUserInteraction === true ? { requiresRealApproval: true } : {}) };
|
|
20
20
|
if (mandated.probeReadRootCandidate === undefined || TIGHTEN_ONLY_ORIGINS.has(origin))
|
|
21
21
|
return { ...mandated, origin, ...classed };
|
|
22
22
|
const { probeReadRootCandidate: _withdrawn, ...kept } = mandated;
|
|
@@ -112,7 +112,7 @@ const MANDATE_DISCLOSURE = {
|
|
|
112
112
|
probe_mandate: "the reversibility check declared this call structurally gated (the built-in shell check raises this for a read outside the directories allowed for this session) — it is cleared by confirming this call, never by a standing allow rule",
|
|
113
113
|
probe_unanswered: "the reversibility check did not answer for this call (it timed out or failed), so whether the call reads outside the directories allowed for this session is unknown — it is cleared by confirming this call, never by a standing allow rule",
|
|
114
114
|
operator_always: "this deployment mandates per-call confirmation for shell commands (shellGate: always)",
|
|
115
|
-
tool_marks: "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)",
|
|
115
|
+
tool_marks: "this tool carries egress/irreversibility/interaction marks (a mandated confirmation a rule cannot clear)",
|
|
116
116
|
write_protection: "this write's target is on the deployment's write-protection table (the agent's own configuration is guarded one look per write) — it is cleared by confirming this call, never by a standing allow rule",
|
|
117
117
|
write_protection_unresolved: "the write-protection judge could not resolve this write's target, so whether it lands on the protected table is unknown — it is cleared by confirming this call: no standing rule and no automatic decider answers for it, though a wired approver seat can",
|
|
118
118
|
};
|
|
@@ -127,7 +127,7 @@ export function persistedRuleMandateOf(marks) {
|
|
|
127
127
|
return "write_protection_unresolved";
|
|
128
128
|
if (marks.writeProtected === true)
|
|
129
129
|
return "write_protection";
|
|
130
|
-
return marks.egress === true ? "tool_marks" : word;
|
|
130
|
+
return marks.egress === true || marks.requiresUserInteraction === true ? "tool_marks" : word;
|
|
131
131
|
}
|
|
132
132
|
function irreversibilityMandateOf(marks) {
|
|
133
133
|
return marks.probeMandated === true
|
|
@@ -177,6 +177,7 @@ export const ASK_CLASSES = Object.freeze([
|
|
|
177
177
|
"rule_unreadable",
|
|
178
178
|
"ask_rule",
|
|
179
179
|
"denial_limit_fallback",
|
|
180
|
+
"requires_user_interaction",
|
|
180
181
|
"operator_forced",
|
|
181
182
|
"shell_floor",
|
|
182
183
|
"write_protection_unresolved",
|
|
@@ -205,6 +206,7 @@ export const MODE_ARBITRATION = {
|
|
|
205
206
|
rule_unreadable: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
|
|
206
207
|
ask_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
|
|
207
208
|
denial_limit_fallback: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
|
|
209
|
+
requires_user_interaction: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
|
|
208
210
|
operator_forced: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
|
|
209
211
|
shell_floor: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
|
|
210
212
|
write_protection_unresolved: { bypassImmune: true, classifierEligible: false, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: true },
|
|
@@ -218,7 +220,7 @@ export const MODE_ARBITRATION = {
|
|
|
218
220
|
unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
|
|
219
221
|
ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
|
|
220
222
|
};
|
|
221
|
-
export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read"]);
|
|
223
|
+
export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
|
|
222
224
|
export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
|
|
223
225
|
export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
|
|
224
226
|
const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
|
|
@@ -295,6 +297,8 @@ export function classifyAsk(d, facts) {
|
|
|
295
297
|
if (facts.ruleCallUnreadable === true)
|
|
296
298
|
classes.add("rule_unreadable");
|
|
297
299
|
const marks = facts.mandateMarks;
|
|
300
|
+
if (marks.requiresUserInteraction === true)
|
|
301
|
+
classes.add("requires_user_interaction");
|
|
298
302
|
if (marks.shellGated === true && marks.irreversibility === "always")
|
|
299
303
|
classes.add("shell_floor");
|
|
300
304
|
const toolMarked = marks.shellGated !== true && (marks.egress === true || marks.irreversibility === "always" || marks.irreversibility === "maybe");
|
|
@@ -332,7 +336,7 @@ export function headlessDenialMessage(toolName, askMessage) {
|
|
|
332
336
|
return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (no approver is wired for "${toolName}" — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
|
|
333
337
|
}
|
|
334
338
|
export function headlessApprovalRemedy(input) {
|
|
335
|
-
const approver = "wire an approver (`onAsk`) to put it to a person";
|
|
339
|
+
const approver = input.approverSeat === "unavailable" ? "make a person reachable through the approver that is wired (it answered `unavailable` for this ask)" : "wire an approver (`onAsk`) to put it to a person";
|
|
336
340
|
const c = input.classification;
|
|
337
341
|
if (input.mode === "acceptEdits" && input.unplacedOptions !== undefined && (c === undefined || (c.classes.size === 1 && c.classes.has("ordinary"))))
|
|
338
342
|
return `${fileCommandOptionsSentence(input.unplacedOptions.program)} Spell the ${input.unplacedOptions.program} without option words for the mode to answer it, or ${approver}`;
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* is pure.
|
|
28
28
|
*/
|
|
29
29
|
import type { EngineNotice } from "./engine-notice.js";
|
|
30
|
+
import type { AssertAllKeysHandled, HeadlessApproverSeat } from "./ask-origin.js";
|
|
30
31
|
/** WHY the ask could not be resolved — the closed set (see the module note for each word). */
|
|
31
32
|
export declare const ASK_UNRESOLVABLE_CAUSES: readonly ["approver_unavailable", "no_approver_wired", "mandate_unreconstructible"];
|
|
32
33
|
export type AskUnresolvableCause = (typeof ASK_UNRESOLVABLE_CAUSES)[number];
|
|
@@ -45,10 +46,31 @@ export interface AskUnresolvableFacts {
|
|
|
45
46
|
/** Whether a durable approval park lane was ARMED for this run (`true`: the park had its one attempt and
|
|
46
47
|
* did not take the call — it declined or failed; `false`: no park lane at all, the usual shape). */
|
|
47
48
|
readonly parkLaneExisted: boolean;
|
|
48
|
-
/**
|
|
49
|
-
* refusal's tool text (`headlessApprovalRemedy`, ask-origin.ts)
|
|
49
|
+
/** Present for the causes that say nobody could be asked ({@link askUnresolvableRemedySeat}): the caller-side change that
|
|
50
|
+
* would have let the call run, as the gate minted it for the refusal's tool text (`headlessApprovalRemedy`, ask-origin.ts)
|
|
51
|
+
* — carried verbatim, never re-composed here. */
|
|
50
52
|
readonly remedy?: string;
|
|
51
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Which causes take the caller-side remedy (`headlessApprovalRemedy`, ask-origin.ts), and in which seat's clause — the table IS
|
|
56
|
+
* the rule, and it is total over the closed set (a cause with no row is a red build: {@link CauseRemedySeatCoversEveryCause}).
|
|
57
|
+
* The two causes that say NOBODY COULD BE ASKED take it — no approver is wired (`absent`: the clause says to wire one), or the
|
|
58
|
+
* wired approver reported nobody reachable (`unavailable`: the clause says to make a person reachable through it, never to
|
|
59
|
+
* wire what is wired): one fact, one sentence about the way out, whichever way nobody was there. The inherited-mandate cause
|
|
60
|
+
* takes none (`undefined`): its recovery is a park facility on this leg, which neither a permission mode nor an `onAsk` here
|
|
61
|
+
* supplies, so the remedy's arms would name a change that does not release the call. (The settlement-axis table
|
|
62
|
+
* `SETTLEMENT_NOBODY_JUDGED` keeps `approver_unavailable` false because there a park route may still take the ask; this table is
|
|
63
|
+
* read at the deny exit, where every park route has had its attempt.)
|
|
64
|
+
*/
|
|
65
|
+
declare const CAUSE_REMEDY_SEAT: {
|
|
66
|
+
readonly approver_unavailable: "unavailable";
|
|
67
|
+
readonly no_approver_wired: "absent";
|
|
68
|
+
readonly mandate_unreconstructible: undefined;
|
|
69
|
+
};
|
|
70
|
+
export type CauseRemedySeatCoversEveryCause = AssertAllKeysHandled<Exclude<AskUnresolvableCause, keyof typeof CAUSE_REMEDY_SEAT>>;
|
|
71
|
+
/** The seat whose clause the refusal for `cause` carries its remedy in, or `undefined` when the cause takes no remedy
|
|
72
|
+
* ({@link CAUSE_REMEDY_SEAT}); read once at the deny exit, which hands the sentence to the tool text and the notice alike. */
|
|
73
|
+
export declare function askUnresolvableRemedySeat(cause: AskUnresolvableCause): HeadlessApproverSeat | undefined;
|
|
52
74
|
/**
|
|
53
75
|
* Compose the `delegation.ask_unresolvable` notice from the gate's deny-exit facts.
|
|
54
76
|
*
|
|
@@ -58,7 +80,8 @@ export interface AskUnresolvableFacts {
|
|
|
58
80
|
* an empty session or tool name, or a cause outside the closed set, is refused at the mint with a
|
|
59
81
|
* `TypeError` rather than shaped into a notice that would route nowhere or say nothing.
|
|
60
82
|
*
|
|
61
|
-
* `detail: { sessionId, toolName, toolCallId, cause, parkLaneExisted, remedy? }` — `remedy` rides
|
|
62
|
-
*
|
|
83
|
+
* `detail: { sessionId, toolName, toolCallId, cause, parkLaneExisted, remedy? }` — `remedy` rides the causes that say nobody
|
|
84
|
+
* could be asked ({@link askUnresolvableRemedySeat}: the sentence the refusal's tool text carries); `sessionId` is lifted onto the typed key by {@link import("./engine-notice.js").deliverEngineNotice}, the one throat.
|
|
63
85
|
*/
|
|
64
86
|
export declare function askUnresolvableNotice(facts: AskUnresolvableFacts): EngineNotice;
|
|
87
|
+
export {};
|
|
@@ -9,6 +9,14 @@ const CAUSE_CLAUSE = {
|
|
|
9
9
|
no_approver_wired: "no approver (or question face) is wired for this run — the headless posture",
|
|
10
10
|
mandate_unreconstructible: "an inherited durable-approval mandate binds this ask and this leg has no park facility to honor it",
|
|
11
11
|
};
|
|
12
|
+
const CAUSE_REMEDY_SEAT = {
|
|
13
|
+
approver_unavailable: "unavailable",
|
|
14
|
+
no_approver_wired: "absent",
|
|
15
|
+
mandate_unreconstructible: undefined,
|
|
16
|
+
};
|
|
17
|
+
export function askUnresolvableRemedySeat(cause) {
|
|
18
|
+
return CAUSE_REMEDY_SEAT[cause];
|
|
19
|
+
}
|
|
12
20
|
export function askUnresolvableNotice(facts) {
|
|
13
21
|
const { sessionId, toolName, toolCallId, cause, parkLaneExisted, remedy } = facts;
|
|
14
22
|
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
@@ -1,39 +1,25 @@
|
|
|
1
1
|
import type { OracleIsolationVerdict } from "./oracle-isolation.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SkillSpec } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* The terminal of {@link decideAutoPromote}.
|
|
5
5
|
* - `"auto_promote"` — the ONLY exit that skips human review. Reachable only when all four prior guards
|
|
6
|
-
* (tripwire / irreversible / statistics / concurrence) clear AND all
|
|
6
|
+
* (tripwire / irreversible / statistics / concurrence) clear AND all three gates are present+passing.
|
|
7
7
|
* - `"candidate_only"` — surface the artifact as a candidate; the SOLE shortfall is the oracle isolation class
|
|
8
8
|
* being downgraded to `in_process_probe_only` (oracle-isolation.ts:34) with everything else passing.
|
|
9
9
|
* - `"needs_human"` — fail-closed default: escalate to a human (any guard tripped, any other gate failing).
|
|
10
10
|
*/
|
|
11
11
|
export type AutoPromoteTerminal = "auto_promote" | "candidate_only" | "needs_human";
|
|
12
12
|
/**
|
|
13
|
-
* Gate-2
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* co-sourced" is the bug this prevents). `lineageId` keys the active-skill-scope frame (active-skill-scope.ts).
|
|
13
|
+
* Gate-2 handle (裁决净增③): the verdict the promoted artifact's OWN promotable write landed with, held in one place
|
|
14
|
+
* so the decision reads one verdict and never a loose boolean. (It used to carry the artifact's lineage id as the scope
|
|
15
|
+
* key of the retired per-skill narrowing; with that gone the lineage keys nothing the fold reads.)
|
|
17
16
|
*/
|
|
18
17
|
export interface PromotedRecord {
|
|
19
|
-
/** Stable lineage id of the promoted artifact; the Gate-3 scope key. */
|
|
20
|
-
lineageId: string;
|
|
21
18
|
/**
|
|
22
19
|
* Gate-2 chokepoint result: the promotable write landed WITH a positive utility verdict (memory.ts:240),
|
|
23
20
|
* or it was absent/ungated. Only `"present_positive"` is gate-passing; `"absent_or_ungated"` ⇒ needs_human.
|
|
24
21
|
*/
|
|
25
22
|
gate2Verdict: "present_positive" | "absent_or_ungated";
|
|
26
|
-
/** Gate-3 capability manifest — co-located with `gate2Verdict` in the same store record, never diverging. */
|
|
27
|
-
manifest: SkillManifest;
|
|
28
|
-
/**
|
|
29
|
-
* Gate-3 scope is armed AND has propagated across the subagent boundary (design/79 §4, design/77 §3/§7).
|
|
30
|
-
* `true` ONLY when either (a) the manifest lists NO subagent-spawn tool, or (b) its scope has propagated to
|
|
31
|
-
* spawned children. The skill→subagent manifest-scope propagation just landed in this branch (commit
|
|
32
|
-
* `04f5104`); for an artifact that lists a subagent-spawn tool whose scope has NOT propagated, this is
|
|
33
|
-
* `false` → the decision falls to `needs_human` (裁决净增⑤ / P1). A bare in-process `gate4Armed`-style
|
|
34
|
-
* boolean is intentionally NOT enough here: the propagation hole is encoded structurally into this flag.
|
|
35
|
-
*/
|
|
36
|
-
gate3Scoped: boolean;
|
|
37
23
|
}
|
|
38
24
|
/**
|
|
39
25
|
* Inputs to {@link decideAutoPromote}. Every field is a PRESENCE/boolean/set the profile has already computed —
|
|
@@ -44,7 +30,7 @@ export interface PromotedRecord {
|
|
|
44
30
|
export interface AutoPromoteInputs {
|
|
45
31
|
/** Gate-1 verdict (oracle-isolation.ts:37); the caller re-asserted it before calling this function. */
|
|
46
32
|
oracle: OracleIsolationVerdict;
|
|
47
|
-
/** Gate-2
|
|
33
|
+
/** Gate-2 lineage-bound single handle (净增③). */
|
|
48
34
|
record: PromotedRecord;
|
|
49
35
|
/** Gate-4: the irreversibility tighten is in place (or the artifact touches no irreversible tool). */
|
|
50
36
|
gate4Armed: boolean;
|
|
@@ -79,9 +65,9 @@ export interface AutoPromoteInputs {
|
|
|
79
65
|
* 2. `touchesIrreversibleTool` → needs_human (§6 permanent human door — 净增①; even if all else passes)
|
|
80
66
|
* 3. `!statisticsPresent` → needs_human (statistics must have RAN — core-required; thresholds are profile)
|
|
81
67
|
* 4. `concurringSignals.size < 2` → needs_human (≥2 DISTINCT KIND mechanical signals concur — 净增②)
|
|
82
|
-
* 5. the
|
|
68
|
+
* 5. the three-gate present-passing check; if not all pass, the SOLE-shortfall arm:
|
|
83
69
|
* `in_process_probe_only` oracle (and every other gate passing) → `candidate_only`; otherwise needs_human.
|
|
84
|
-
* 6. all
|
|
70
|
+
* 6. all three gates present+passing (with guards clear) → `auto_promote` (the only auto exit).
|
|
85
71
|
*/
|
|
86
72
|
export declare function decideAutoPromote(i: AutoPromoteInputs): AutoPromoteTerminal;
|
|
87
73
|
/**
|
|
@@ -104,13 +90,13 @@ export declare const FROZEN_DENYLIST_FLOOR: ReadonlySet<string>;
|
|
|
104
90
|
/**
|
|
105
91
|
* Build a tripwire verdict over a promoted artifact's text. **ESCALATE-ONLY** (design/79 §3,守 design/76
|
|
106
92
|
* §5.1 r3): it can ONLY ever raise `escalate:true`; a clean artifact yields `{escalate:false, reasons:[]}` and
|
|
107
|
-
* NEVER clears anything — the
|
|
108
|
-
* this advisory. Two mechanical scans, both escalate-only:
|
|
93
|
+
* NEVER clears anything — it contains nothing itself: the real protection is the out-of-process boundary and the
|
|
94
|
+
* permission gate at call time, not this advisory. Two mechanical scans, both escalate-only:
|
|
109
95
|
*
|
|
110
96
|
* 1. **symbol-mention drift** via {@link extractSymbols} (repo-map.ts). extractSymbols is a v1 heuristic:
|
|
111
97
|
* column-0 / source-only, it skips indented lines and is structurally INCAPABLE of being a containment
|
|
112
98
|
* proof (it can't enumerate everything the artifact can call). So it is used ONLY to ESCALATE — if a
|
|
113
|
-
* surfaced top-level symbol names a tool/capability that is NOT in `declared.
|
|
99
|
+
* surfaced top-level symbol names a tool/capability that is NOT in `declared.allowedTools`, escalate.
|
|
114
100
|
* 2. **raw-byte denylist** over the artifact's RAW bytes INCLUDING markdown bytes — {@link FROZEN_DENYLIST_FLOOR}
|
|
115
101
|
* UNIONed with `profileTokens` (add-only). A substring hit → escalate, with the matched token in `reasons`.
|
|
116
102
|
*
|
|
@@ -119,7 +105,8 @@ export declare const FROZEN_DENYLIST_FLOOR: ReadonlySet<string>;
|
|
|
119
105
|
* contribution, never block" case, which is the caller's concern, not a throw inside the scan.)
|
|
120
106
|
*
|
|
121
107
|
* @param artifactText the artifact's raw text (markdown body included).
|
|
122
|
-
* @param declared the artifact's declared
|
|
108
|
+
* @param declared the artifact's declared tool list (the `SkillSpec` member the loader copies) — its `allowedTools`
|
|
109
|
+
* entries, each read as a permission rule for the tool it names, are the symbol-drift baseline.
|
|
123
110
|
* @param profileTokens optional ADD-ONLY extra denylist tokens (UNIONed onto the frozen floor; cannot remove).
|
|
124
111
|
*/
|
|
125
|
-
export declare function deriveTripwire(artifactText: string, declared:
|
|
112
|
+
export declare function deriveTripwire(artifactText: string, declared: Pick<SkillSpec, "allowedTools">, profileTokens?: ReadonlySet<string>): TripwireResult;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extractSymbols } from "../tools/fs/repo-map.js";
|
|
2
|
-
import {
|
|
2
|
+
import { parsePermissionRule } from "./permission-rules.js";
|
|
3
3
|
import { thrownText } from "./thrown-value.js";
|
|
4
4
|
export function decideAutoPromote(i) {
|
|
5
5
|
if (i.tripwire.escalate)
|
|
@@ -12,10 +12,10 @@ export function decideAutoPromote(i) {
|
|
|
12
12
|
return "needs_human";
|
|
13
13
|
const g1ok = i.oracle.protected && i.oracle.isolationClass === "out_of_process";
|
|
14
14
|
const g2ok = i.record.gate2Verdict === "present_positive";
|
|
15
|
-
const allGatesPresentPassing = g1ok && g2ok && i.
|
|
15
|
+
const allGatesPresentPassing = g1ok && g2ok && i.gate4Armed;
|
|
16
16
|
if (!allGatesPresentPassing) {
|
|
17
17
|
return (i.oracle.protected && i.oracle.isolationClass === "in_process_probe_only"
|
|
18
|
-
&& g2ok && i.
|
|
18
|
+
&& g2ok && i.gate4Armed)
|
|
19
19
|
? "candidate_only"
|
|
20
20
|
: "needs_human";
|
|
21
21
|
}
|
|
@@ -60,12 +60,12 @@ export function deriveTripwire(artifactText, declared, profileTokens) {
|
|
|
60
60
|
const reasons = [];
|
|
61
61
|
let escalate = false;
|
|
62
62
|
try {
|
|
63
|
-
const allow = new Set((declared.
|
|
63
|
+
const allow = new Set((declared.allowedTools ?? []).map((entry) => parsePermissionRule(entry).toolName));
|
|
64
64
|
const symbols = extractSymbols(artifactText);
|
|
65
65
|
for (const sym of symbols) {
|
|
66
66
|
if (!allow.has(sym)) {
|
|
67
67
|
escalate = true;
|
|
68
|
-
reasons.push(`symbol-drift: surfaced symbol "${sym}" not in declared.
|
|
68
|
+
reasons.push(`symbol-drift: surfaced symbol "${sym}" not in declared.allowedTools`);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -56,12 +56,6 @@ export declare function mintCheckpointId(): string;
|
|
|
56
56
|
* that {@link ResumeOutcome} must match at the resume entry (council #3: prevents a `task_done` gate
|
|
57
57
|
* being resumed with a `policy_ask` outcome → type confusion / corruption).
|
|
58
58
|
*/
|
|
59
|
-
/** design/74: why a resource-slice suspend fired. `"budget"` covers cost OR tokens (both are checked by
|
|
60
|
-
* `overBudget`); `"walltime"` is the soft slice deadline; `"turns"` the slice turn cap.
|
|
61
|
-
* design/80 Seam #2: `"preempt"` is NOT a resource limit — it is an EXTERNAL scheduler yield (the scheduler
|
|
62
|
-
* raised `spec.preemptSignal` to free resources for a higher-priority task). It rides the SAME durable-suspend
|
|
63
|
-
* mechanism: the gate.kind stays `resource_limit` (the mechanism), so the resume path is byte-identical to a
|
|
64
|
-
* budget/turns/walltime resume (continue the work, no decision); `"preempt"` is only the CAUSE. */
|
|
65
59
|
/**
|
|
66
60
|
* Why a `resource_limit` suspend was minted. design/164 split the single `budget` member: a token
|
|
67
61
|
* window and a $ window are different allowances with different owners, and a scheduler reading a
|
|
@@ -80,6 +74,7 @@ export declare function mintCheckpointId(): string;
|
|
|
80
74
|
* turn boundary the attribution order is
|
|
81
75
|
* `preempt > env_lifetime > usage_window > budget_tokens > budget_cost > turns > walltime`
|
|
82
76
|
* (external causes above self-chosen allowances; preempt keeps the top seat it always had).
|
|
77
|
+
* `"preempt"` is a scheduler's yield (`spec.preemptSignal`), not a limit, riding the same suspend: its resume continues the work.
|
|
83
78
|
*
|
|
84
79
|
* **Consumers pinning the closed set again**: a `switch` that was exhaustive over the five 5.x members
|
|
85
80
|
* now falls through on these two.
|
|
@@ -854,6 +849,8 @@ export type PendingAction = {
|
|
|
854
849
|
* pre-1a.2 checkpoint (deserialized without it), in which case the resume skips the hash check.
|
|
855
850
|
*/
|
|
856
851
|
boundInputHash: string;
|
|
852
|
+
/** A parked `Skill` call's binding to its card — the skill's name and its two lists ({@link import("./skill-grant.js").skillGrantDigest}, judged by {@link import("./skill-grant.js").parkedSkillApprovalRefusal}); only that row carries it. */
|
|
853
|
+
boundGrantDigest?: string;
|
|
857
854
|
/**
|
|
858
855
|
* design/388 §2.5 boundary ② (B18; additive — no checkpoint-version bump, the `previewWithheld`
|
|
859
856
|
* precedent: an optional group an older reader ignores): the IDENTITY of the row the park minted
|
|
@@ -1014,6 +1011,8 @@ export interface CheckpointState {
|
|
|
1014
1011
|
writeFace?: {
|
|
1015
1012
|
face: "open" | "roots";
|
|
1016
1013
|
};
|
|
1014
|
+
/** The grant rows the run grew ({@link import("./skill-grant.js").frozenSkillGrants}); absent = none, damaged = refused pre-CAS. */
|
|
1015
|
+
skillGrants?: import("./skill-grant.js").SkillGrantRecord[];
|
|
1017
1016
|
/**
|
|
1018
1017
|
* design/319 (A ticket) — the session's reminder provenance mark
|
|
1019
1018
|
* ({@link import("./reminder-mint.js").mintReminderMark}), persisted so a durable resume keeps
|
|
@@ -2034,14 +2033,14 @@ export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
|
|
|
2034
2033
|
* steer seat, which no worker version reads and the host decides itself, or a retired mode-seed member of
|
|
2035
2034
|
* `inheritedGate`, which an engine at 7.38.x or earlier still reads once).
|
|
2036
2035
|
* · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`, `"real_approval_forged"`,
|
|
2037
|
-
* `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"constraint_chain_missing"` (all terminal for the row's
|
|
2036
|
+
* `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"skill_grants_damaged"`, `"constraint_chain_missing"` (all terminal for the row's
|
|
2038
2037
|
* current bytes — no worker version redeems a damaged row).
|
|
2039
2038
|
*/
|
|
2040
2039
|
export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged"
|
|
2041
2040
|
/** `state.writeFace` is present but not a well-formed section. No `_forged` twin: the section is
|
|
2042
2041
|
* legal-and-absent at every version, so there is no version to judge a fabrication against — and a
|
|
2043
2042
|
* fabricated "open" still needs the LIVE resolution to say open too. */
|
|
2044
|
-
| "write_face_damaged" | "constraint_chain_missing";
|
|
2043
|
+
| "write_face_damaged" | "skill_grants_damaged" | "constraint_chain_missing";
|
|
2045
2044
|
/**
|
|
2046
2045
|
* The codes a RESUME's pre-claim configuration screen carries as `detail.code` of a `checkpoint.config_refused` — the four
|
|
2047
2046
|
* words prepare's mode, output-style and recorded-mode doors throw, held unchanged so one refusal reads the same on a fresh
|
|
@@ -2219,7 +2218,7 @@ export declare class CheckpointError extends Error {
|
|
|
2219
2218
|
* members. `field` names WHICH part of the decision payload was rejected on a
|
|
2220
2219
|
* `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
|
|
2221
2220
|
* re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
|
|
2222
|
-
* action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
|
|
2221
|
+
* action: re-review), `"boundGrantDigest"` (a parked `Skill` approval does not cover the skill the decision selects — another skill, or the card's with other lists: deny, re-activate), or `"answer"` (the content-ask answer is missing, or was attached to a
|
|
2223
2222
|
* decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
|
|
2224
2223
|
* `"hostDecision"` (the decision facts are missing, name a decider outside `person`/`sla_timeout`/`policy`,
|
|
2225
2224
|
* pair the sweep's `sla_timeout` with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
|
|
@@ -2231,7 +2230,7 @@ export declare class CheckpointError extends Error {
|
|
|
2231
2230
|
* 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
|
|
2232
2231
|
* discriminant. */
|
|
2233
2232
|
readonly detail?: {
|
|
2234
|
-
field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
|
|
2233
|
+
field?: "boundCallId" | "boundInputHash" | "boundGrantDigest" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
|
|
2235
2234
|
/** On `checkpoint.config_refused` — prepare's own code for the refusal ({@link CheckpointConfigRefusalCode}), the
|
|
2236
2235
|
* word a fresh run of the same bad config reports as its `errorCode`. Absent on every other code. */
|
|
2237
2236
|
code?: CheckpointConfigRefusalCode;
|
|
@@ -2411,7 +2410,7 @@ export declare class CheckpointError extends Error {
|
|
|
2411
2410
|
* members. `field` names WHICH part of the decision payload was rejected on a
|
|
2412
2411
|
* `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
|
|
2413
2412
|
* re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
|
|
2414
|
-
* action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
|
|
2413
|
+
* action: re-review), `"boundGrantDigest"` (a parked `Skill` approval does not cover the skill the decision selects — another skill, or the card's with other lists: deny, re-activate), or `"answer"` (the content-ask answer is missing, or was attached to a
|
|
2415
2414
|
* decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
|
|
2416
2415
|
* `"hostDecision"` (the decision facts are missing, name a decider outside `person`/`sla_timeout`/`policy`,
|
|
2417
2416
|
* pair the sweep's `sla_timeout` with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
|
|
@@ -2423,7 +2422,7 @@ export declare class CheckpointError extends Error {
|
|
|
2423
2422
|
* 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
|
|
2424
2423
|
* discriminant. */
|
|
2425
2424
|
detail?: {
|
|
2426
|
-
field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
|
|
2425
|
+
field?: "boundCallId" | "boundInputHash" | "boundGrantDigest" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
|
|
2427
2426
|
/** On `checkpoint.config_refused` — prepare's own code for the refusal ({@link CheckpointConfigRefusalCode}), the
|
|
2428
2427
|
* word a fresh run of the same bad config reports as its `errorCode`. Absent on every other code. */
|
|
2429
2428
|
code?: CheckpointConfigRefusalCode;
|
|
@@ -4,8 +4,11 @@ export declare const refusedOutOfContract: (message: string) => PermissionResult
|
|
|
4
4
|
* `receiveHookResult` is one of two things, and this tells them apart. */
|
|
5
5
|
export declare const isEngineRefusal: (d: unknown) => boolean;
|
|
6
6
|
/** A decision's `decisionReason` — the closed set; the type is derived FROM this list, so a word added to either side is a
|
|
7
|
-
* compile error rather than a drift between the type and the screen. Module-private (the package publishes the type).
|
|
8
|
-
|
|
7
|
+
* compile error rather than a drift between the type and the screen. Module-private (the package publishes the type).
|
|
8
|
+
* `skill`: an active skill's list spoke — the allow layer's skill member cleared the call (`allowed-tools`), or this
|
|
9
|
+
* run's deny row refused it (`disallowed-tools`); distinct from `rule` (a deployment policy) and `persisted_rule` (a
|
|
10
|
+
* person's stored row) because the authority is the skill the run activated. */
|
|
11
|
+
declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill"];
|
|
9
12
|
export type DecisionReason = (typeof DECISION_REASONS)[number];
|
|
10
13
|
export declare const RETIRED_WINDOW_MEMBER_DENY_MESSAGE: string;
|
|
11
14
|
export declare const carriesRetiredWindow: (fallback: unknown) => boolean;
|
|
@@ -8,7 +8,7 @@ export const refusedOutOfContract = (message) => {
|
|
|
8
8
|
return refused;
|
|
9
9
|
};
|
|
10
10
|
export const isEngineRefusal = (d) => typeof d === "object" && d !== null && ENGINE_REFUSALS.has(d);
|
|
11
|
-
const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"]);
|
|
11
|
+
const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill"]);
|
|
12
12
|
const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
|
|
13
13
|
const GUARDS = Object.keys({ unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
|
|
14
14
|
const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0 });
|
|
@@ -57,6 +57,11 @@ export interface EngineNotice {
|
|
|
57
57
|
* at least one type is listed name-only (`nameOnly`) or not at all (`dropped`). Audience `operator`.
|
|
58
58
|
* `detail: { budgetBytes, total, kept, nameOnly: [name], dropped: [name], sessionId }`; the same
|
|
59
59
|
* firing rule as its skills sibling below (once per delivered frame, on the carrying message's receipt).
|
|
60
|
+
* - `"skills.allowed_tool_not_granted"` — a loaded skill's `allowed-tools` / `disallowed-tools` holds entries that grant
|
|
61
|
+
* (or refuse) nothing: `*` (it voids an allow list; on a deny list it reads as nothing and the rest still refuses), `mcp__*` (no server named), a write-path or domain
|
|
62
|
+
* entry, a shape the rule grammar does not read. The run goes on and asks for those tools as usual. Audience
|
|
63
|
+
* `operator`; once per prepared task, only when such an entry exists. `detail: { sessionId, entries: [{ skill, list,
|
|
64
|
+
* entry, reason }] }` (`reason` ∈ `wildcard_all` / `namespace_wildcard` / `path_pattern` / `domain` / `content_unsupported` / `grammar`).
|
|
60
65
|
* - `"skills.listing_truncated"` — the `<skills>` disclosure block did not fit its byte budget, so
|
|
61
66
|
* at least one skill is listed without its description (`nameOnly`) or, in the pathological case,
|
|
62
67
|
* not listed at all (`dropped`); the `Skill` tool still serves every one of them. Audience
|
|
@@ -775,7 +780,8 @@ export interface EngineNotice {
|
|
|
775
780
|
* fail-closed sentence as the tool result); this is its person-facing half, minted at the gate's ONE
|
|
776
781
|
* deny exit, once per tool call, by `ask-unresolvable-notice.ts`. `detail: { sessionId, toolName,
|
|
777
782
|
* toolCallId, cause, parkLaneExisted, remedy? }` — `parkLaneExisted` says whether a park lane was armed and
|
|
778
|
-
* declined/failed (`true`) or none existed (`false`, the usual shape); `remedy` (the
|
|
783
|
+
* declined/failed (`true`) or none existed (`false`, the usual shape); `remedy` (the two causes that say nobody
|
|
784
|
+
* could be asked — `approver_unavailable` and `no_approver_wired`; never `mandate_unreconstructible`)
|
|
779
785
|
* is the one sentence naming the caller-side change that would have let the call run, as the tool text carries it. Audience `"user"`: the refused
|
|
780
786
|
* work is the session user's own; the operator hears it through the sink like every code.
|
|
781
787
|
*
|
|
@@ -4,4 +4,4 @@ import type { SafeNotifier } from "./safe-notify.js";
|
|
|
4
4
|
* throwing sink alter the fail-closed outcome — the same swallow posture as every other observer here.
|
|
5
5
|
* Shared by the gate's dispatch (the phase-1 crash arm, the seat-bound refusal), the reversibility probe's
|
|
6
6
|
* two failure arms, the approved-edit re-screen and the bounded-await deadline. */
|
|
7
|
-
export declare function traceHookCrash(input: ToolGateInput, err: unknown, notifier: SafeNotifier): void;
|
|
7
|
+
export declare function traceHookCrash(input: Pick<ToolGateInput, "onHookError">, err: unknown, notifier: SafeNotifier): void;
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -8,7 +8,7 @@ import { writeProtectionMandated, writeProtectionTightenDecision, writeProtectio
|
|
|
8
8
|
import { createGateDeadline, awaitWriteProtectionJudgment, gateWaitFailureOf, withGateTimeout } from "./gate-bounded-await.js";
|
|
9
9
|
import { thrownText } from "./thrown-value.js";
|
|
10
10
|
import { refuseOutOfContractDecision } from "./tool-policy.js";
|
|
11
|
-
import { asksUnstated, unstatedAskMessage } from "./mode-policy.js";
|
|
11
|
+
import { asksUnstated, permissionSeatAt, unstatedAskMessage } from "./mode-policy.js";
|
|
12
12
|
export async function runGateFold(pass) {
|
|
13
13
|
const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass;
|
|
14
14
|
const boundedAwaitDeadlineMs = createGateDeadline(input, notifier);
|
|
@@ -69,6 +69,10 @@ export async function runGateFold(pass) {
|
|
|
69
69
|
applyShellReadVerdict(pass, verdict, toolName, input.shellGated === true);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
if (input.requiresUserInteraction === true && pass.decision.action === "allow") {
|
|
73
|
+
pass.decision = { action: "ask", message: markTightenMessage(toolName, "interaction"), decisionReason: "safety" };
|
|
74
|
+
pass.tightenedBy = "safety";
|
|
75
|
+
}
|
|
72
76
|
if (input.egress && pass.decision.action === "allow") {
|
|
73
77
|
pass.decision = {
|
|
74
78
|
action: "ask",
|
|
@@ -169,8 +173,8 @@ export async function runGateFold(pass) {
|
|
|
169
173
|
pass.deniedBy = "write_protection";
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
|
-
if (asksUnstated(pass.decision, input.
|
|
173
|
-
pass.decision = { action: "ask", message: unstatedAskMessage(toolName), ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}) };
|
|
176
|
+
if (asksUnstated(pass.decision, permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.seat, input.permissionMode?.(), pass.hookAllowed === true && pass.decision.updatedInput === undefined)) {
|
|
177
|
+
pass.decision = { action: "ask", message: permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.question ?? unstatedAskMessage(toolName), ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}) };
|
|
174
178
|
pass.tightenedBy = "unstated";
|
|
175
179
|
}
|
|
176
180
|
return await pass.afterFold(pass);
|