@sema-agent/core 7.3.0 → 7.4.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 +49 -0
- package/dist/agents/peer-admission.d.ts +18 -3
- package/dist/agents/peer-admission.js +79 -4
- package/dist/agents/peer-held-queue.d.ts +101 -0
- package/dist/agents/peer-held-queue.js +229 -0
- package/dist/agents/peer-idle.d.ts +109 -0
- package/dist/agents/peer-idle.js +240 -0
- package/dist/agents/peer-notice-route.d.ts +33 -0
- package/dist/agents/peer-notice-route.js +46 -0
- package/dist/agents/peer-notices.d.ts +103 -0
- package/dist/agents/peer-notices.js +206 -0
- package/dist/agents/peer-session-drain.d.ts +39 -4
- package/dist/agents/peer-session-drain.js +248 -42
- package/dist/agents/send-message-tool.d.ts +8 -1
- package/dist/agents/send-message-tool.js +96 -30
- package/dist/agents/subagent.js +1 -0
- package/dist/brain/status-sink.d.ts +10 -0
- package/dist/brain/status-sink.js +13 -4
- package/dist/brain/stream-engine.d.ts +11 -0
- package/dist/brain/stream-engine.js +39 -3
- package/dist/core/arg-summary.d.ts +13 -3
- package/dist/core/arg-summary.js +138 -7
- package/dist/core/auto-mode-defaults.d.ts +11 -0
- package/dist/core/auto-mode-defaults.js +2 -0
- package/dist/core/auto-mode.d.ts +59 -0
- package/dist/core/auto-mode.js +57 -1
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +8 -0
- package/dist/core/hooks.d.ts +30 -0
- package/dist/core/hooks.js +43 -8
- package/dist/core/mailbox-store.d.ts +33 -1
- package/dist/core/mailbox-store.js +42 -2
- package/dist/core/runner/assemble-result.d.ts +5 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/denial-limit-arms.d.ts +149 -0
- package/dist/core/runner/denial-limit-arms.js +91 -0
- package/dist/core/runner/edited-files-ledger.d.ts +33 -0
- package/dist/core/runner/edited-files-ledger.js +14 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +5 -0
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-task.d.ts +62 -1
- package/dist/core/runner/prepare-task.js +135 -89
- package/dist/core/runner/runtask.js +12 -0
- package/dist/core/sensitive-path-policy.d.ts +27 -6
- package/dist/core/sensitive-path-policy.js +57 -2
- package/dist/core/task-notification.d.ts +24 -2
- package/dist/core/task-notification.js +6 -1
- package/dist/core/tool-policy.d.ts +55 -4
- package/dist/core/tool-policy.js +28 -5
- package/dist/core/tools.js +1 -0
- package/dist/core/types.d.ts +251 -15
- package/dist/core/wiring-manifest.d.ts +41 -5
- package/dist/core/wiring-manifest.js +8 -0
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +3 -0
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/loop/agent-loop.d.ts +7 -0
- package/dist/engine/loop/agent-loop.js +79 -0
- package/dist/engine/loop/types.d.ts +42 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +10 -4
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/orchestration/workflow.js +7 -3
- package/dist/tools/fs/fs-write.d.ts +4 -4
- package/dist/tools/fs/fs-write.js +99 -14
- package/dist/tools/fs/index.d.ts +7 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/safety.d.ts +29 -8
- package/dist/tools/fs/safety.js +11 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +181 -1
package/dist/core/auto-mode.d.ts
CHANGED
|
@@ -98,3 +98,62 @@ export interface AutoModeDecider {
|
|
|
98
98
|
* One instance per run/session — the breaker state is the session's "退回非 auto" latch.
|
|
99
99
|
*/
|
|
100
100
|
export declare function createAutoModeDecider(opts: AutoModeDeciderOptions): AutoModeDecider;
|
|
101
|
+
/** The deployment's bounds for the denial limit (`RunnerDeps.autoMode.denialLimit`). Every member
|
|
102
|
+
* optional; an omitted member takes its CC default. A present member with a bad value is REFUSED
|
|
103
|
+
* loudly at construction (never clamped, never read as the default). */
|
|
104
|
+
export interface AutoModeDenialLimitOptions {
|
|
105
|
+
/** Consecutive classifier blocks (no classifier/human allow between them) that fall back to a person.
|
|
106
|
+
* Default 3. A positive integer. */
|
|
107
|
+
maxConsecutive?: number;
|
|
108
|
+
/** Total classifier blocks per run that fall back to a person and reset the budget. Default 20. A
|
|
109
|
+
* positive integer. */
|
|
110
|
+
maxTotal?: number;
|
|
111
|
+
/** The fallback ask's auto-deny window, ms. Default 120_000; `0` = no window (the ask waits on the
|
|
112
|
+
* approver alone). A non-negative integer no greater than 2147483647 (a host timer truncates larger
|
|
113
|
+
* delays and fires at once — the fail-closed direction, and invisible). */
|
|
114
|
+
autoDenyAfterMs?: number;
|
|
115
|
+
}
|
|
116
|
+
/** The additive member a denial-limit fallback ask carries (`PermissionResult` ask arm, `AskRequest`):
|
|
117
|
+
* the counts that tripped the bound and the auto-deny window this particular ask runs under. */
|
|
118
|
+
export interface DenialLimitFallback {
|
|
119
|
+
/** Consecutive blocks INCLUDING the one that tripped the bound. */
|
|
120
|
+
readonly consecutive: number;
|
|
121
|
+
/** Total blocks this run INCLUDING the one that tripped the bound (read before the total-bound reset). */
|
|
122
|
+
readonly total: number;
|
|
123
|
+
/** Which bound tripped. */
|
|
124
|
+
readonly limit: "consecutive" | "total";
|
|
125
|
+
/** The auto-deny window for THIS ask, ms; `0` = none (disarmed by configuration, or the timed card was
|
|
126
|
+
* already shown for this streak). The ask resolver reads this member to arm its deadline. */
|
|
127
|
+
readonly autoDenyAfterMs: number;
|
|
128
|
+
}
|
|
129
|
+
export type DenialLimitVerdict = {
|
|
130
|
+
limitReached: false;
|
|
131
|
+
} | {
|
|
132
|
+
limitReached: true;
|
|
133
|
+
fallback: DenialLimitFallback;
|
|
134
|
+
};
|
|
135
|
+
/** The per-run denial tracker. Lives beside {@link AutoModeDecider} (same owner, same lifetime). */
|
|
136
|
+
export interface AutoModeDenialTracker {
|
|
137
|
+
/** A classifier `block`: count first, judge second (CC `eme` → `tme`). Returns whether THIS block is
|
|
138
|
+
* the one that falls back to a person, with the fallback's own snapshot. */
|
|
139
|
+
recordBlock(): DenialLimitVerdict;
|
|
140
|
+
/** A classifier `allow`, or a person's allow of a fallback ask (CC `yR`): consecutive → 0 and the
|
|
141
|
+
* timed-card mark cleared. A rule/fast-path allow must NOT call this. */
|
|
142
|
+
recordAllow(): void;
|
|
143
|
+
/** The current counts (diagnostics / pins). */
|
|
144
|
+
snapshot(): {
|
|
145
|
+
consecutive: number;
|
|
146
|
+
total: number;
|
|
147
|
+
timedFallbackShown: boolean;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Build the per-run tracker. Bad knob values throw (the deployment face is read at prepare; a refusal
|
|
151
|
+
* there is the loud exit the knob doctrine requires). */
|
|
152
|
+
export declare function createAutoModeDenialTracker(opts?: AutoModeDenialLimitOptions): AutoModeDenialTracker;
|
|
153
|
+
/** The fallback ask's text — CC's sentence family, single-sourced for every mint site (the main gate
|
|
154
|
+
* and the inherited-lane arms): `Classifier denial limit exceeded, falling back to prompting: <limit
|
|
155
|
+
* sentence>` + a blank line + `Latest blocked action: <the classifier's own reason, or the tool name>`.
|
|
156
|
+
* The reason is the classifier MODEL's text — the caller neutralizes it before it gets here. */
|
|
157
|
+
export declare function denialLimitFallbackMessage(fallback: DenialLimitFallback, latestBlockedAction: string): string;
|
|
158
|
+
/** The limit sentence alone (CC's two forms) — shared by the fallback ask and the headless terminal. */
|
|
159
|
+
export declare function denialLimitSentence(fallback: DenialLimitFallback): string;
|
package/dist/core/auto-mode.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AUTO_MODE_DEFAULT_FAILURE_THRESHOLD, AUTO_MODE_DEFAULT_TIMEOUT_MS } from "./auto-mode-defaults.js";
|
|
1
|
+
import { AUTO_MODE_DEFAULT_FAILURE_THRESHOLD, AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS } from "./auto-mode-defaults.js";
|
|
2
2
|
export function parseAutoModeResponse(text) {
|
|
3
3
|
const t = text
|
|
4
4
|
.replace(/<thinking>[\s\S]*?<\/thinking>/g, "")
|
|
@@ -99,3 +99,59 @@ class AutoModeTimeout extends Error {
|
|
|
99
99
|
super("auto-mode classify timeout");
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
103
|
+
export function createAutoModeDenialTracker(opts = {}) {
|
|
104
|
+
const bound = (name, v, dflt) => {
|
|
105
|
+
if (v === undefined)
|
|
106
|
+
return dflt;
|
|
107
|
+
if (!Number.isInteger(v) || v < 1) {
|
|
108
|
+
throw new Error(`RunnerDeps.autoMode.denialLimit.${name} must be a positive integer (got ${JSON.stringify(v)}) — omit it for the default ${dflt}`);
|
|
109
|
+
}
|
|
110
|
+
return v;
|
|
111
|
+
};
|
|
112
|
+
const maxConsecutive = bound("maxConsecutive", opts.maxConsecutive, AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxConsecutive);
|
|
113
|
+
const maxTotal = bound("maxTotal", opts.maxTotal, AUTO_MODE_DENIAL_LIMIT_DEFAULTS.maxTotal);
|
|
114
|
+
let autoDenyAfterMs = AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS;
|
|
115
|
+
if (opts.autoDenyAfterMs !== undefined) {
|
|
116
|
+
if (!Number.isInteger(opts.autoDenyAfterMs) || opts.autoDenyAfterMs < 0 || opts.autoDenyAfterMs > MAX_TIMER_DELAY_MS) {
|
|
117
|
+
throw new Error(`RunnerDeps.autoMode.denialLimit.autoDenyAfterMs must be an integer between 0 and ${MAX_TIMER_DELAY_MS} (got ${JSON.stringify(opts.autoDenyAfterMs)}) — 0 disarms the window; omit it for the default ${AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS}`);
|
|
118
|
+
}
|
|
119
|
+
autoDenyAfterMs = opts.autoDenyAfterMs;
|
|
120
|
+
}
|
|
121
|
+
let consecutive = 0;
|
|
122
|
+
let total = 0;
|
|
123
|
+
let timedFallbackShown = false;
|
|
124
|
+
return {
|
|
125
|
+
recordBlock() {
|
|
126
|
+
consecutive += 1;
|
|
127
|
+
total += 1;
|
|
128
|
+
if (consecutive < maxConsecutive && total < maxTotal)
|
|
129
|
+
return { limitReached: false };
|
|
130
|
+
const totalTripped = total >= maxTotal;
|
|
131
|
+
const windowMs = timedFallbackShown || totalTripped ? 0 : autoDenyAfterMs;
|
|
132
|
+
const fallback = { consecutive, total, limit: totalTripped ? "total" : "consecutive", autoDenyAfterMs: windowMs };
|
|
133
|
+
if (totalTripped) {
|
|
134
|
+
consecutive = 0;
|
|
135
|
+
total = 0;
|
|
136
|
+
timedFallbackShown = false;
|
|
137
|
+
}
|
|
138
|
+
else if (windowMs > 0) {
|
|
139
|
+
timedFallbackShown = true;
|
|
140
|
+
}
|
|
141
|
+
return { limitReached: true, fallback };
|
|
142
|
+
},
|
|
143
|
+
recordAllow() {
|
|
144
|
+
consecutive = 0;
|
|
145
|
+
timedFallbackShown = false;
|
|
146
|
+
},
|
|
147
|
+
snapshot: () => ({ consecutive, total, timedFallbackShown }),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function denialLimitFallbackMessage(fallback, latestBlockedAction) {
|
|
151
|
+
return `Classifier denial limit exceeded, falling back to prompting: ${denialLimitSentence(fallback)}\n\nLatest blocked action: ${latestBlockedAction}`;
|
|
152
|
+
}
|
|
153
|
+
export function denialLimitSentence(fallback) {
|
|
154
|
+
return fallback.limit === "total"
|
|
155
|
+
? `${fallback.total} actions were blocked this session. Please review the transcript before continuing.`
|
|
156
|
+
: `${fallback.consecutive} consecutive actions were blocked. Please review the transcript before continuing.`;
|
|
157
|
+
}
|
|
@@ -125,7 +125,7 @@ export function buildRiskDescriptor(input) {
|
|
|
125
125
|
if (shell || toolName === "Bash") {
|
|
126
126
|
const cmd = isPlainRecord(args) ? safeDataValue(args, "command") : undefined;
|
|
127
127
|
if (typeof cmd === "string" && cmd.length > 0)
|
|
128
|
-
summary = renderUntrustedCommandText(redactSecrets(
|
|
128
|
+
summary = renderUntrustedCommandText(stripFormatCharacters(redactSecrets(cmd)), SUMMARY_CMD_MAX);
|
|
129
129
|
const bg = isPlainRecord(args) ? safeDataValue(args, "run_in_background") : undefined;
|
|
130
130
|
if (bg === true)
|
|
131
131
|
summary = `[background persistent process — no per-step recheck] ${summary ?? ""}`.trimEnd();
|
|
@@ -140,7 +140,7 @@ export function buildRiskDescriptor(input) {
|
|
|
140
140
|
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
141
141
|
const encDigest = (s) => s.replace(/%/g, "%25").replace(/=/g, "%3D").replace(/ /g, "%20");
|
|
142
142
|
const k = encDigest(renderUntrustedCommandText(key, 40));
|
|
143
|
-
const val = encDigest(renderUntrustedCommandText(redactSecrets(
|
|
143
|
+
const val = encDigest(renderUntrustedCommandText(stripFormatCharacters(redactSecrets(String(v))), SUMMARY_VALUE_MAX));
|
|
144
144
|
parts.push(`${k}=${val}`);
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -103,7 +103,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
103
103
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
104
104
|
* no longer minted.
|
|
105
105
|
*/
|
|
106
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.durable_gate_unavailable", "config.peer_lane_unmounted", "peer.inbound_disposition", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
106
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
107
107
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
108
108
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
109
109
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -102,8 +102,12 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
102
102
|
"config.tool_model_gate_unknown_class",
|
|
103
103
|
"config.tool_model_gate_env_invalid",
|
|
104
104
|
"config.durable_gate_unavailable",
|
|
105
|
+
"config.peer_admission_out_of_range",
|
|
105
106
|
"config.peer_lane_unmounted",
|
|
106
107
|
"peer.inbound_disposition",
|
|
108
|
+
"peer.held_settled",
|
|
109
|
+
"peer.idle_subscription",
|
|
110
|
+
"classifier.denial_limit",
|
|
107
111
|
"delegation.transcript_integrity",
|
|
108
112
|
"mcp.revocation_probe_failed",
|
|
109
113
|
"workflow.governance_key_stripped",
|
|
@@ -142,6 +146,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
142
146
|
"memory.hold_disposed": "user",
|
|
143
147
|
"task.user_steer_undrained": "user",
|
|
144
148
|
"task.user_followup_undrained": "user",
|
|
149
|
+
"classifier.denial_limit": "user",
|
|
145
150
|
"task.turn_interrupted": "user",
|
|
146
151
|
"steering.parked_input_blocked": "user",
|
|
147
152
|
"task.halt_unconsumed": "user",
|
|
@@ -159,7 +164,10 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
159
164
|
"config.tool_model_gate_unknown_class": "operator",
|
|
160
165
|
"config.tool_model_gate_env_invalid": "operator",
|
|
161
166
|
"config.peer_lane_unmounted": "operator",
|
|
167
|
+
"config.peer_admission_out_of_range": "operator",
|
|
162
168
|
"peer.inbound_disposition": "user",
|
|
169
|
+
"peer.held_settled": "user",
|
|
170
|
+
"peer.idle_subscription": "user",
|
|
163
171
|
"delegation.transcript_integrity": "operator",
|
|
164
172
|
"mcp.revocation_probe_failed": "operator",
|
|
165
173
|
"workflow.governance_key_stripped": "operator",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from
|
|
|
2
2
|
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
|
|
3
3
|
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
4
|
import { type AskClass } from "./ask-class.js";
|
|
5
|
+
import { type AutoModeDenialTracker } from "./auto-mode.js";
|
|
5
6
|
import type { WiringLegKind } from "./wiring-manifest.js";
|
|
6
7
|
/**
|
|
7
8
|
* In-process hook seam (design/37) — a provider-agnostic interception layer modeled on CC's hooks,
|
|
@@ -429,6 +430,10 @@ export interface PermissionDeniedPayload {
|
|
|
429
430
|
* screen; a policy's direct deny, a hook deny, and the crash/plan-mode/compliance emissions carry
|
|
430
431
|
* none. See {@link import("./tool-policy.js").AskDenyResolution}. */
|
|
431
432
|
resolution?: import("./tool-policy.js").AskDenyResolution;
|
|
433
|
+
/** #548 — the deny is the classifier denial-limit fallback's AUTO-DENY (core's own window elapsed;
|
|
434
|
+
* see {@link import("./tool-policy.js").ResolvedAsk.autoDenied}). Carried verbatim from the resolver;
|
|
435
|
+
* absent on every other deny. */
|
|
436
|
+
autoDenied?: true;
|
|
432
437
|
/** {@link HookSeatSignal} — this invocation's own abort signal. On an OBSERVATION seat the deny has
|
|
433
438
|
* already happened and nothing this callback does can change it, so the signal says exactly one
|
|
434
439
|
* thing: stop reading, nobody is waiting for your answer any more. */
|
|
@@ -1458,7 +1463,32 @@ export interface ToolGateInput {
|
|
|
1458
1463
|
*/
|
|
1459
1464
|
autoMode?: {
|
|
1460
1465
|
decider: import("./auto-mode.js").AutoModeDecider;
|
|
1466
|
+
/**
|
|
1467
|
+
* #548 — the per-run DENIAL-LIMIT tracker beside the decider (CC 2.1.250 `FO`/`Wie`; same owner,
|
|
1468
|
+
* same lifetime). Absent ⇒ the pre-#548 chain byte for byte: every block is a deny, without bound.
|
|
1469
|
+
* Present ⇒ a block first counts, then judges; the block that reaches a bound becomes the fallback
|
|
1470
|
+
* ask (`requiresRealApproval: true` + `denialLimitFallback`), and a classifier allow or a person's
|
|
1471
|
+
* allow of that ask resets the consecutive count. The Runner builds it from
|
|
1472
|
+
* `RunnerDeps.autoMode.denialLimit`; a host driving the gate directly may hand its own.
|
|
1473
|
+
*/
|
|
1474
|
+
denialTracking?: AutoModeDenialTracker;
|
|
1461
1475
|
};
|
|
1476
|
+
/**
|
|
1477
|
+
* #548 — the HEADLESS arm of the denial-limit fallback: the fallback ask was resolved by a deny no
|
|
1478
|
+
* person made (no approver wired; a blanket `"allow"` refused as no approver; an approver that
|
|
1479
|
+
* reported unavailable with no park to take it). The fallback had nowhere to go, so the caller — which
|
|
1480
|
+
* owns the run — stops it (`TaskResult.errorCode = "classifier.denial_limit"`, CC's "too many
|
|
1481
|
+
* classifier denials in headless mode" abort). The deny itself still stands; this gate only names the
|
|
1482
|
+
* fact. A TOP-LEVEL seat, deliberately NOT under `autoMode`: the fallback can arrive from an ANCESTOR's
|
|
1483
|
+
* frozen tracker through the fold (a delegated child with no armed classifier of its own), and that
|
|
1484
|
+
* child's run is the one that has to stop. Observe-only for the gate: a throwing seat never alters
|
|
1485
|
+
* the deny.
|
|
1486
|
+
*/
|
|
1487
|
+
onHeadlessDenialLimit?: (info: {
|
|
1488
|
+
toolName: string;
|
|
1489
|
+
toolCallId: string;
|
|
1490
|
+
fallback: import("./auto-mode.js").DenialLimitFallback;
|
|
1491
|
+
}) => void;
|
|
1462
1492
|
/**
|
|
1463
1493
|
* design/153 §2/§7.4 (件4 复审, MED): true means this call's ask is MARKED — an inherited ancestor
|
|
1464
1494
|
* constraint already determined "no synchronous layer may resolve this ask" (the ancestor's frozen
|
package/dist/core/hooks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { coreMintedResolutionOf, decisionText, describeThrown, isAskDenyResolution, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
1
|
+
import { coreMintedAutoDeniedOf, coreMintedResolutionOf, decisionText, describeThrown, isAskDenyResolution, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
2
2
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
3
|
+
import { denialLimitFallbackMessage } from "./auto-mode.js";
|
|
3
4
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
4
5
|
import { mintSystemReminder } from "./reminder-mint.js";
|
|
5
6
|
import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
|
|
@@ -443,6 +444,7 @@ export async function runToolGate(input) {
|
|
|
443
444
|
let hookAsk;
|
|
444
445
|
let parkFailed;
|
|
445
446
|
let askDenyResolution;
|
|
447
|
+
let askAutoDenied = false;
|
|
446
448
|
const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
|
|
447
449
|
const hookSeatMs = resolveHookTimeoutMs(input.hookTimeoutMs, (err) => traceHookCrash(input, err, notifier));
|
|
448
450
|
const seatBound = (abortEnds) => ({
|
|
@@ -804,12 +806,14 @@ export async function runToolGate(input) {
|
|
|
804
806
|
decision.action === "ask" &&
|
|
805
807
|
decision.decisionReason !== "hook" &&
|
|
806
808
|
decision.matchedAskRule === undefined &&
|
|
809
|
+
decision.denialLimitFallback === undefined &&
|
|
807
810
|
req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
808
811
|
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
|
|
809
812
|
const verdict = await input.autoMode.decider
|
|
810
813
|
.decide({ req, askMessage: decisionText(decision) }, input.abortSignal)
|
|
811
814
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
812
815
|
if (verdict.kind === "allow") {
|
|
816
|
+
input.autoMode.denialTracking?.recordAllow();
|
|
813
817
|
decision = {
|
|
814
818
|
action: "allow",
|
|
815
819
|
message: "auto-mode classifier allowed this call",
|
|
@@ -820,12 +824,25 @@ export async function runToolGate(input) {
|
|
|
820
824
|
else if (verdict.kind === "block") {
|
|
821
825
|
const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
|
|
822
826
|
const category = verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
827
|
+
const tracked = input.autoMode.denialTracking?.recordBlock();
|
|
828
|
+
if (tracked?.limitReached === true) {
|
|
829
|
+
decision = {
|
|
830
|
+
...decision,
|
|
831
|
+
message: denialLimitFallbackMessage(tracked.fallback, reason || category || toolName),
|
|
832
|
+
decisionReason: "classifier",
|
|
833
|
+
requiresRealApproval: true,
|
|
834
|
+
denialLimitFallback: tracked.fallback,
|
|
835
|
+
};
|
|
836
|
+
denySource = "classifier";
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
decision = {
|
|
840
|
+
action: "deny",
|
|
841
|
+
message: `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}`,
|
|
842
|
+
decisionReason: "classifier",
|
|
843
|
+
};
|
|
844
|
+
denySource = "classifier";
|
|
845
|
+
}
|
|
829
846
|
}
|
|
830
847
|
}
|
|
831
848
|
if (input.sandboxAdmission !== undefined &&
|
|
@@ -910,6 +927,8 @@ export async function runToolGate(input) {
|
|
|
910
927
|
resolvedApprover = resolved.approver;
|
|
911
928
|
if (resolved.action === "deny" && isAskDenyResolution(resolved.resolution))
|
|
912
929
|
askDenyResolution = resolved.resolution;
|
|
930
|
+
if (resolved.action === "deny" && resolved.autoDenied === true)
|
|
931
|
+
askAutoDenied = true;
|
|
913
932
|
decision = resolved;
|
|
914
933
|
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
|
|
915
934
|
const parkArgs = [req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeMandated : undefined];
|
|
@@ -921,6 +940,21 @@ export async function runToolGate(input) {
|
|
|
921
940
|
return { suspend: suspended, preToolContext };
|
|
922
941
|
}
|
|
923
942
|
}
|
|
943
|
+
if (askBeforeResolve.action === "ask" && askBeforeResolve.denialLimitFallback !== undefined) {
|
|
944
|
+
if (decision.action === "allow") {
|
|
945
|
+
input.autoMode?.denialTracking?.recordAllow();
|
|
946
|
+
}
|
|
947
|
+
else if (askDenyResolution === "no_approver" ||
|
|
948
|
+
askDenyResolution === "blanket_allow_refused" ||
|
|
949
|
+
askDenyResolution === "approver_unavailable" ||
|
|
950
|
+
resolved.approverUnavailable === true) {
|
|
951
|
+
try {
|
|
952
|
+
input.onHeadlessDenialLimit?.({ toolName, toolCallId, fallback: askBeforeResolve.denialLimitFallback });
|
|
953
|
+
}
|
|
954
|
+
catch {
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
924
958
|
if (decision.action === "allow" && decision.updatedInput !== undefined) {
|
|
925
959
|
let editArgs = decision.updatedInput;
|
|
926
960
|
let editDenied;
|
|
@@ -1062,7 +1096,8 @@ export async function runToolGate(input) {
|
|
|
1062
1096
|
currentInput = decision.updatedInput;
|
|
1063
1097
|
}
|
|
1064
1098
|
const denyResolution = askDenyResolution ?? coreMintedResolutionOf(decision, { toolCallId, toolName });
|
|
1065
|
-
|
|
1099
|
+
const denyAutoDenied = askAutoDenied || coreMintedAutoDeniedOf(decision, { toolCallId, toolName });
|
|
1100
|
+
await notifyPermissionDeniedSeat({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource, ...(denyResolution !== undefined ? { resolution: denyResolution } : {}), ...(denyAutoDenied ? { autoDenied: true } : {}), ...(input.identity !== undefined ? { identity: input.identity } : {}) });
|
|
1066
1101
|
const denySettledBy = decision.settledBy;
|
|
1067
1102
|
const denyApprover = denySettledBy !== undefined ? resolvedApprover : undefined;
|
|
1068
1103
|
return {
|
|
@@ -35,11 +35,43 @@ export interface MailboxPeerMeta {
|
|
|
35
35
|
fromScope?: string;
|
|
36
36
|
/** Cross-principal delivery only — the delivery gate's receipt id (audit back-reference). */
|
|
37
37
|
gateReceiptId?: string;
|
|
38
|
+
/** Notice-kind records only (design/385 §4.4 / §5.2): the typed notice the requester's drain renders
|
|
39
|
+
* — a delivery receipt's state (`kind: "delivery_notice"`) or an idle notice's kind
|
|
40
|
+
* (`kind: "idle_notice"`). The record's `content` is a human-readable mirror; THIS is the authority. */
|
|
41
|
+
notice?: MailboxPeerNoticeMeta;
|
|
42
|
+
}
|
|
43
|
+
/** design/385 — the typed body of a notice-kind record. `state` is drawn from the receipt states on a
|
|
44
|
+
* `delivery_notice` (`held|denied|expired|delivered|refused|dropped`) and from the idle kinds on an
|
|
45
|
+
* `idle_notice` (`idle|exited|unavailable|expired`); the validator admits the union, the drain checks
|
|
46
|
+
* the pairing (a state that does not fit its kind is settled `notice_unrouted`, never rendered). */
|
|
47
|
+
export interface MailboxPeerNoticeMeta {
|
|
48
|
+
state: string;
|
|
49
|
+
/** delivery_notice: the box seq of the SENDER'S message this receipt is about. */
|
|
50
|
+
refSeq?: number;
|
|
51
|
+
/** delivery_notice: the recipient's display label (as its directory row spells it). */
|
|
52
|
+
recipient?: string;
|
|
53
|
+
/** delivery_notice `dropped`: the ingress guard's reason. */
|
|
54
|
+
dropReason?: string;
|
|
55
|
+
/** idle_notice `idle`/`exited`: when the target finished its turn (epoch ms). */
|
|
56
|
+
finishedAt?: number;
|
|
57
|
+
/** idle_notice `idle`: the target's one-line detail (already bounded by the producer). */
|
|
58
|
+
detail?: string;
|
|
59
|
+
/** idle_notice `expired` (the requester answering ITSELF): the label the ask carried for the target
|
|
60
|
+
* that never answered — the record's author is the requester, so the target must be named here. */
|
|
61
|
+
target?: string;
|
|
38
62
|
}
|
|
39
63
|
export declare const MAILBOX_PEER_FROM_MODES: readonly ["bypass", "prompting"];
|
|
40
64
|
export type MailboxPeerFromMode = (typeof MAILBOX_PEER_FROM_MODES)[number];
|
|
41
|
-
|
|
65
|
+
/** design/385: `peer_message` runs the parity judgment; `idle_subscription` registers the sender as an
|
|
66
|
+
* idle subscriber of the box owner (§5.2, the store form of CC's `notify_when_idle` frame);
|
|
67
|
+
* `idle_notice` / `delivery_notice` land on the notice face (rendered, never enveloped as a peer's words). */
|
|
68
|
+
export declare const MAILBOX_PEER_RECORD_KINDS: readonly ["peer_message", "idle_notice", "delivery_notice", "idle_subscription"];
|
|
42
69
|
export type MailboxPeerRecordKind = (typeof MAILBOX_PEER_RECORD_KINDS)[number];
|
|
70
|
+
/** The `notice.state` vocabulary the validator admits (the union of both notice families; the drain
|
|
71
|
+
* checks the kind↔state pairing). Spelled here, beside the record kinds, so the store contract is
|
|
72
|
+
* self-contained — the lane's own closed sets re-derive from these. */
|
|
73
|
+
export declare const MAILBOX_PEER_NOTICE_STATES: readonly ["held", "denied", "expired", "delivered", "refused", "dropped", "idle", "exited", "unavailable"];
|
|
74
|
+
export type MailboxPeerNoticeState = (typeof MAILBOX_PEER_NOTICE_STATES)[number];
|
|
43
75
|
/** The `append` refusal code for a malformed `peerMeta` (design/385): a garbage record is refused up
|
|
44
76
|
* front with this code, never silently stripped or stored — a drain that reads a half-typed record
|
|
45
77
|
* would judge on fabricated inputs. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertRetentionPolicy } from "./retention-policy.js";
|
|
2
2
|
export const MAILBOX_PEER_FROM_MODES = ["bypass", "prompting"];
|
|
3
|
-
export const MAILBOX_PEER_RECORD_KINDS = ["peer_message", "idle_notice", "delivery_notice"];
|
|
3
|
+
export const MAILBOX_PEER_RECORD_KINDS = ["peer_message", "idle_notice", "delivery_notice", "idle_subscription"];
|
|
4
|
+
export const MAILBOX_PEER_NOTICE_STATES = ["held", "denied", "expired", "delivered", "refused", "dropped", "idle", "exited", "unavailable"];
|
|
4
5
|
export const MAILBOX_INVALID_PEER_META_CODE = "mailbox.invalid_peer_meta";
|
|
5
6
|
export const MAILBOX_CROSS_PROCESS_UNSAFE_CODE = "mailbox.cross_process_unsafe";
|
|
6
7
|
const PEER_META_STRING_KEYS = ["fromSession", "senderKey", "fromScope", "gateReceiptId"];
|
|
@@ -38,14 +39,53 @@ export function readMailboxPeerMeta(raw) {
|
|
|
38
39
|
return refuse(`${k} must be a non-empty string`);
|
|
39
40
|
out[k] = v;
|
|
40
41
|
}
|
|
42
|
+
else if (k === "notice") {
|
|
43
|
+
out.notice = readMailboxPeerNoticeMeta(v, refuse);
|
|
44
|
+
}
|
|
41
45
|
else {
|
|
42
46
|
return refuse(`carries an unknown key ${JSON.stringify(k)}`);
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
49
|
return out;
|
|
46
50
|
}
|
|
51
|
+
function readMailboxPeerNoticeMeta(raw, refuse) {
|
|
52
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
53
|
+
return refuse("notice must be a plain object");
|
|
54
|
+
const proto = Object.getPrototypeOf(raw);
|
|
55
|
+
if (proto !== Object.prototype && proto !== null)
|
|
56
|
+
return refuse("notice must be a plain object (a class instance, Date, Map or similar is not)");
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const key of Reflect.ownKeys(raw)) {
|
|
59
|
+
if (typeof key !== "string")
|
|
60
|
+
return refuse("notice carries a symbol-keyed member");
|
|
61
|
+
const v = raw[key];
|
|
62
|
+
if (v === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
if (key === "state") {
|
|
65
|
+
if (!MAILBOX_PEER_NOTICE_STATES.includes(v))
|
|
66
|
+
return refuse(`notice.state must be one of ${MAILBOX_PEER_NOTICE_STATES.join("|")}`);
|
|
67
|
+
out.state = v;
|
|
68
|
+
}
|
|
69
|
+
else if (key === "recipient" || key === "dropReason" || key === "detail" || key === "target") {
|
|
70
|
+
if (typeof v !== "string" || v === "")
|
|
71
|
+
return refuse(`notice.${key} must be a non-empty string`);
|
|
72
|
+
out[key] = v;
|
|
73
|
+
}
|
|
74
|
+
else if (key === "refSeq" || key === "finishedAt") {
|
|
75
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0)
|
|
76
|
+
return refuse(`notice.${key} must be a non-negative finite number`);
|
|
77
|
+
out[key] = v;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
return refuse(`notice carries an unknown key ${JSON.stringify(key)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (out.state === undefined)
|
|
84
|
+
return refuse("notice.state is required");
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
47
87
|
export function cloneMailboxPeerMeta(meta) {
|
|
48
|
-
return meta === undefined ? undefined : { ...meta };
|
|
88
|
+
return meta === undefined ? undefined : { ...meta, ...(meta.notice !== undefined ? { notice: { ...meta.notice } } : {}) };
|
|
49
89
|
}
|
|
50
90
|
export const MAILBOX_TOMBSTONED_RECIPIENT_CODE = "mailbox.recipient_tombstoned";
|
|
51
91
|
export class MailboxStoreError extends Error {
|
|
@@ -117,6 +117,11 @@ export interface ResultFlags {
|
|
|
117
117
|
* Pure pass-through — assembly neither adds nor filters (a rewind that FAILED never reaches here; it
|
|
118
118
|
* throws at prepare and lands in the `threw` slot as a terminal errorCode). */
|
|
119
119
|
rewindNotes?: TaskResult["rewindNotes"];
|
|
120
|
+
/** The run's OWN edited-file ledger, echoed on `TaskResult.editedFiles`. Pure pass-through on
|
|
121
|
+
* EVERY terminal — what this run's hands changed is a fact about the leg that ran, whatever
|
|
122
|
+
* terminal it reached (a failed/aborted run that edited files is exactly the case a "restore the
|
|
123
|
+
* code this message changed" affordance is for). Undefined ⇒ the key is omitted. */
|
|
124
|
+
editedFiles?: TaskResult["editedFiles"];
|
|
120
125
|
/** The run's final turn was halted by a person's BARE rejection of a tool call (the parent-thread
|
|
121
126
|
* control-flow boundary) — echoed on `TaskResult.haltedOnUserRejection`. Pure pass-through on
|
|
122
127
|
* every terminal: the fact is about the leg that ran, whatever terminal it reached (on the normal
|
|
@@ -181,5 +181,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
181
181
|
if (flags.unpricedSpend)
|
|
182
182
|
delete publicStats.costMicroUsd;
|
|
183
183
|
const stampHaltedByUser = flags.userHalted === true && status !== "suspended" && status !== "needs_review";
|
|
184
|
-
return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
184
|
+
return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
185
185
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type DenialLimitFallback } from "../auto-mode.js";
|
|
2
|
+
import type { PermissionResult, ResolvedAsk, ToolCallRequest } from "../tool-policy.js";
|
|
3
|
+
import { type EngineNotice } from "../types.js";
|
|
4
|
+
import type { Prepared } from "./prepare-task.js";
|
|
5
|
+
/**
|
|
6
|
+
* #548 — the classifier DENIAL-LIMIT arms of the tool gate's inherited (delegation) lane, extracted
|
|
7
|
+
* from `prepareTask` as a phase module (design/238 D-7: extract, don't accrete). The gate's OWN
|
|
8
|
+
* classifier block site lives in tool-policy; these are the pieces the runner threads around it:
|
|
9
|
+
* · the tracker attached to a re-supplied chain entry ({@link attachRebuiltDenialTrackers});
|
|
10
|
+
* · the run's ONE typed stop seat + the closure that fills it ({@link createDenialLimitStop});
|
|
11
|
+
* · the frozen classifier's judgment with the count-then-fallback step, shared by the two inherited
|
|
12
|
+
* wrapper arms and the approved-edit re-check ({@link judgeInheritedClassifier});
|
|
13
|
+
* · the post-resolution settlement of a fallback ask — a person's allow resets the streak, a deny
|
|
14
|
+
* nobody made stops the run ({@link settleDenialLimitFallback}).
|
|
15
|
+
* Each helper is pure over what prepare hands it; the fold/ask seams themselves stay in the runner.
|
|
16
|
+
*/
|
|
17
|
+
/** An ancestor chain entry's classifier half as the arms read it: the decider and, beside it, the
|
|
18
|
+
* per-run denial tracker (absent on an entry that counts nothing). */
|
|
19
|
+
export interface InheritedAutoMode {
|
|
20
|
+
decider: AutoModeDecider;
|
|
21
|
+
denialTracking?: AutoModeDenialTracker;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A re-supplied chain entry (a cross-process redemption rebuilt its ancestor's classifier from the
|
|
25
|
+
* recorded recipe — `rebuildAutoModeDecider` hands back a decider and its arming, nothing live)
|
|
26
|
+
* carries no denial tracker, and an entry with a decider but no tracker would count nothing: the
|
|
27
|
+
* rebuilt classifier could block without bound on the redeemed leg, the exact gap the limit closes.
|
|
28
|
+
* Attach a FRESH tracker under this deployment's own bounds (a fresh count is the documented
|
|
29
|
+
* cross-process semantics — the breaker restarts the same way). Entries that already carry one (the
|
|
30
|
+
* in-process chain from a live ancestor) are kept BY IDENTITY; only a tracker-less armed entry is
|
|
31
|
+
* re-wrapped (its `policy`, the identity the fold and the pass-through compare, is untouched).
|
|
32
|
+
* ONE tracker per DECIDER identity, not per entry: a parent attaches its one decider to both its
|
|
33
|
+
* hook entry and its caller-policy entry, and a count split across two trackers could alternate
|
|
34
|
+
* forever without either reaching the bound. The tracker is minted lazily, so a bad bound refuses
|
|
35
|
+
* only a prepare that actually has such an entry (the armed leg's own mint screens the knob first).
|
|
36
|
+
*/
|
|
37
|
+
export declare function attachRebuiltDenialTrackers<T extends {
|
|
38
|
+
autoMode?: InheritedAutoMode;
|
|
39
|
+
}>(entries: ReadonlyArray<T> | undefined, denialLimit: AutoModeDenialLimitOptions | undefined): T[] | undefined;
|
|
40
|
+
/** The one closure that stops a run for a headless denial-limit fallback (see {@link createDenialLimitStop}). */
|
|
41
|
+
export type StopForDenialLimit = (info: {
|
|
42
|
+
toolName: string;
|
|
43
|
+
toolCallId: string;
|
|
44
|
+
fallback: DenialLimitFallback;
|
|
45
|
+
}) => void;
|
|
46
|
+
/**
|
|
47
|
+
* The gate's typed stop seat (`Prepared.gateStopRef`) and the one closure that fills it — the headless
|
|
48
|
+
* arm of the denial-limit fallback: no approver to fall back to ⇒ stop the run with
|
|
49
|
+
* `classifier.denial_limit` (CC throws "Agent aborted: too many classifier denials in headless
|
|
50
|
+
* mode"). Once per run (the first headless fallback owns the terminal); the deny that triggered it
|
|
51
|
+
* stands as that call's result. Minted once so every mint site — the main gate's seat and the
|
|
52
|
+
* inherited-lane arms — stops the SAME run through the SAME seat. `abort` is the run's own abort
|
|
53
|
+
* (controller + harness), handed in because the harness is built after the seat is.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createDenialLimitStop(opts: {
|
|
56
|
+
sessionId: string;
|
|
57
|
+
runId: string;
|
|
58
|
+
onNotice: ((notice: EngineNotice) => void) | undefined;
|
|
59
|
+
abort: () => void;
|
|
60
|
+
}): {
|
|
61
|
+
gateStopRef: Prepared["gateStopRef"];
|
|
62
|
+
stopForDenialLimit: StopForDenialLimit;
|
|
63
|
+
};
|
|
64
|
+
/** The ask arm of a permission decision — what the inherited lane arbitrates. */
|
|
65
|
+
export type InheritedAsk = Extract<PermissionResult, {
|
|
66
|
+
action: "ask";
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* The delegation half of the two classifier exclusions the LIVE gate enforces: an ask raised by a hook
|
|
70
|
+
* (`decisionReason:"hook"`, engine-stamped, never self-supplied) and an ask minted by an explicit `ask`
|
|
71
|
+
* permission rule (`matchedAskRule`) are exactly as out of the FROZEN classifier's reach as the live
|
|
72
|
+
* one's — the person's / hook's standing decision travels with the frozen entry, and a frozen decider
|
|
73
|
+
* resolving it would re-open the same hole one process over. An ask that already IS a denial-limit
|
|
74
|
+
* fallback (a deeper ancestor's) is not re-judged either. Excluded asks fall through to the frozen
|
|
75
|
+
* approver chain, the delegation lane's original chain.
|
|
76
|
+
*/
|
|
77
|
+
export declare function frozenClassifierExcluded(d: {
|
|
78
|
+
decisionReason?: string;
|
|
79
|
+
matchedAskRule?: string;
|
|
80
|
+
denialLimitFallback?: unknown;
|
|
81
|
+
}): boolean;
|
|
82
|
+
/** What {@link judgeInheritedClassifier} hands back to the arm that called it. */
|
|
83
|
+
export type InheritedClassifierJudgment<A extends InheritedAsk> =
|
|
84
|
+
/** The frozen classifier allowed — the arm returns its own allow shape (with or without the ask's rewrite). */
|
|
85
|
+
{
|
|
86
|
+
kind: "allow";
|
|
87
|
+
}
|
|
88
|
+
/** A block below the bound — the exact deny the ancestor's own gate would have produced. */
|
|
89
|
+
| {
|
|
90
|
+
kind: "deny";
|
|
91
|
+
result: PermissionResult;
|
|
92
|
+
}
|
|
93
|
+
/** Proceed to the frozen approver with `ask`: the incoming ask unchanged (classifier unavailable,
|
|
94
|
+
* excluded, or not armed), or — at the bound — the fallback ask minted from it (`mintedHere`).
|
|
95
|
+
* `fallback` is the member riding the ask either way (an incoming one is a deeper ancestor's). */
|
|
96
|
+
| {
|
|
97
|
+
kind: "resolve";
|
|
98
|
+
ask: A;
|
|
99
|
+
fallback: DenialLimitFallback | undefined;
|
|
100
|
+
mintedHere: boolean;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* The ancestor's frozen classifier, run BEFORE its frozen approver in the ancestor's own gate order,
|
|
104
|
+
* with the denial-limit count folded in (CC 2.1.250 count-then-judge): `allow` ⇒ the classifier's
|
|
105
|
+
* auto-allow (and the streak resets); `block` below the bound ⇒ deny, the exact verdict the ancestor's
|
|
106
|
+
* gate would have produced; `block` AT the bound ⇒ the ask is re-spoken in the classifier's voice as
|
|
107
|
+
* the fallback (`decisionReason:"classifier"`, `requiresRealApproval`, the member carrying counts +
|
|
108
|
+
* window) for the arm to resolve at the frozen approver; anything else (unavailable / parse error /
|
|
109
|
+
* excluded / no classifier) ⇒ the incoming ask, unchanged. The member may ALREADY be on the incoming
|
|
110
|
+
* ask (a deeper ancestor's fallback, minted inside the nested fold — the case the exclusion names):
|
|
111
|
+
* it rides to the approver and the headless arm unchanged, whichever tracker minted it; only a
|
|
112
|
+
* tracker that counted THIS streak is reset by the person's later allow (`mintedHere`).
|
|
113
|
+
* `req` is the request as the classifier must see it (the arm's presented / edited args);
|
|
114
|
+
* `subject` is the noun the deny names ("this call" on the wrapper arms, "the approved edit" on the
|
|
115
|
+
* re-check).
|
|
116
|
+
*/
|
|
117
|
+
export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
|
|
118
|
+
autoMode: InheritedAutoMode | undefined;
|
|
119
|
+
ask: A;
|
|
120
|
+
req: ToolCallRequest;
|
|
121
|
+
signal: AbortSignal;
|
|
122
|
+
subject: "this call" | "the approved edit";
|
|
123
|
+
}): Promise<InheritedClassifierJudgment<A>>;
|
|
124
|
+
/** The resolver's answer as the settlement reads it. */
|
|
125
|
+
export type FallbackResolution = Pick<ResolvedAsk, "action" | "resolution" | "approverUnavailable">;
|
|
126
|
+
/** A deny nobody made, at a wrapper arm: no approver wired, or a blanket `onAsk:"allow"` refused as
|
|
127
|
+
* no approver. `approver_unavailable` is NOT headless here — it floats to the main gate, which owns
|
|
128
|
+
* that arm (a park, or the fail-closed refusal). */
|
|
129
|
+
export declare function headlessDenyAtFold(r: FallbackResolution): boolean;
|
|
130
|
+
/** The re-check's reading: the approved-edit helper has NO park route (an approved edit is
|
|
131
|
+
* re-adjudicated in place), so an approver that reports unavailable is as headless as none at all. */
|
|
132
|
+
export declare function headlessDenyAtRecheck(r: FallbackResolution): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* After the frozen approver answered a fallback ask (CC `yR`): a person's allow ends the ancestor's
|
|
135
|
+
* streak — only on the tracker that counted it (`mintedHere`; a deeper ancestor's fallback leaves
|
|
136
|
+
* this entry's count alone); a deny nobody made (`headless`, the arm's own reading) is the headless
|
|
137
|
+
* arm — the run stops through the one seat (`stop`). Any other answer settles nothing here. A no-op
|
|
138
|
+
* when the ask was not a fallback.
|
|
139
|
+
*/
|
|
140
|
+
export declare function settleDenialLimitFallback(opts: {
|
|
141
|
+
fallback: DenialLimitFallback | undefined;
|
|
142
|
+
mintedHere: boolean;
|
|
143
|
+
tracker: AutoModeDenialTracker | undefined;
|
|
144
|
+
resolved: FallbackResolution;
|
|
145
|
+
headless: (r: FallbackResolution) => boolean;
|
|
146
|
+
stop: StopForDenialLimit;
|
|
147
|
+
toolName: string;
|
|
148
|
+
toolCallId: string;
|
|
149
|
+
}): void;
|