@sema-agent/core 5.22.0 → 5.24.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 +169 -1
- package/dist/agents/subagent.js +3 -2
- package/dist/core/checkpoint-store.d.ts +38 -3
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +69 -2
- package/dist/core/hooks.js +100 -15
- package/dist/core/memory-engine/engine.d.ts +28 -1
- package/dist/core/memory-engine/engine.js +62 -3
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/layout.d.ts +69 -3
- package/dist/core/memory-engine/layout.js +75 -6
- package/dist/core/permission-rule-consent.js +2 -1
- package/dist/core/permission-rule-org.d.ts +36 -2
- package/dist/core/permission-rule-org.js +23 -0
- package/dist/core/permission-rule-store.d.ts +25 -14
- package/dist/core/permission-rule-store.js +7 -2
- package/dist/core/permission-rule-sync.d.ts +8 -0
- package/dist/core/permission-rule-sync.js +35 -6
- package/dist/core/runner/prepare-task.d.ts +10 -2
- package/dist/core/runner/prepare-task.js +120 -11
- package/dist/core/runner/runtask.js +46 -1
- package/dist/core/runner/session-file-state-replay.js +3 -0
- package/dist/core/tool-policy.d.ts +37 -4
- package/dist/core/tool-policy.js +49 -19
- package/dist/core/tool-result-store.d.ts +17 -1
- package/dist/core/tool-result-store.js +79 -4
- package/dist/core/trace.d.ts +47 -0
- package/dist/core/types.d.ts +45 -5
- package/dist/core/wiring-manifest.d.ts +16 -1
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/index.d.ts +18 -10
- package/dist/index.js +5 -3
- package/dist/orchestration/goal.d.ts +10 -0
- package/dist/orchestration/goal.js +6 -5
- package/dist/stores/file/adoption/adopt.d.ts +146 -0
- package/dist/stores/file/adoption/adopt.js +611 -0
- package/dist/stores/file/adoption/marker.d.ts +202 -0
- package/dist/stores/file/adoption/marker.js +205 -0
- package/dist/stores/file/background-agent-store.js +2 -0
- package/dist/stores/file/checkpoint-store.js +2 -0
- package/dist/stores/file/file-snapshot-store.js +2 -0
- package/dist/stores/file/index.d.ts +2 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/memory-store.js +2 -0
- package/dist/stores/file/session-policy-store.d.ts +11 -1
- package/dist/stores/file/session-policy-store.js +9 -2
- package/dist/stores/file/session-store.js +2 -0
- package/dist/stores/file/task-list-store.js +2 -0
- package/dist/stores/file/tool-result-store.js +2 -0
- package/dist/stores/file/usage-window-store.js +2 -0
- package/dist/stores/file/workflow-journal-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -10
- package/dist/tools/fs/fs-bash.js +7 -4
- package/dist/tools/monitor.js +3 -3
- package/package.json +3 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseAllowRuleText } from "./permission-rule-model.js";
|
|
2
2
|
import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, ruleSyncVector, sameScope, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
|
|
3
|
+
import { emitTrace } from "./trace.js";
|
|
3
4
|
export const PERMISSION_RULE_SYNC_PATH = "/v1/permission-rules/sync";
|
|
4
5
|
export const LOCAL_OWNER_UNSYNCABLE_CODE = "permission_rules.local_owner_unsyncable";
|
|
5
6
|
function refuseLocalOwnerSync() {
|
|
@@ -19,6 +20,34 @@ export async function syncPermissionRules(opts) {
|
|
|
19
20
|
if (typeof opts.principal !== "string" || opts.principal === "") {
|
|
20
21
|
throw new Error("syncPermissionRules requires a verified principal — an unauthenticated deployment has no cloud bucket to sync");
|
|
21
22
|
}
|
|
23
|
+
const traceClock = opts.now ?? Date.now;
|
|
24
|
+
const disclose = (result) => {
|
|
25
|
+
const tracer = opts.tracer;
|
|
26
|
+
if (tracer === undefined)
|
|
27
|
+
return result;
|
|
28
|
+
for (const r of result.resurrected) {
|
|
29
|
+
emitTrace(tracer, () => ({
|
|
30
|
+
kind: "permission.rule_sync_resurrected",
|
|
31
|
+
version: 1,
|
|
32
|
+
principal: opts.principal,
|
|
33
|
+
rule: r.rule,
|
|
34
|
+
scopeKind: r.scope.kind,
|
|
35
|
+
ts: traceClock(),
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
for (const d of result.dropped) {
|
|
39
|
+
emitTrace(tracer, () => ({
|
|
40
|
+
kind: "permission.rule_sync_dropped",
|
|
41
|
+
version: 1,
|
|
42
|
+
principal: opts.principal,
|
|
43
|
+
rule: d.rule,
|
|
44
|
+
scopeKind: d.scope.kind,
|
|
45
|
+
reason: d.reason,
|
|
46
|
+
ts: traceClock(),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
22
51
|
const store = opts.provider.forPrincipal(opts.principal);
|
|
23
52
|
const writer = writerOf(store);
|
|
24
53
|
if (writer === undefined) {
|
|
@@ -111,7 +140,7 @@ export async function syncPermissionRules(opts) {
|
|
|
111
140
|
res = await writer.apply(delta, { expectedRev: current.rev });
|
|
112
141
|
}
|
|
113
142
|
catch (err) {
|
|
114
|
-
return {
|
|
143
|
+
return disclose({
|
|
115
144
|
ok: false,
|
|
116
145
|
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
117
146
|
landed: { newAdds: 0, newTombstones: 0 },
|
|
@@ -119,7 +148,7 @@ export async function syncPermissionRules(opts) {
|
|
|
119
148
|
dropped,
|
|
120
149
|
rev: current.rev,
|
|
121
150
|
warnings: [...warnings, `the store refused the sync landing: ${errText(err)} — nothing landed, the local state is unchanged`],
|
|
122
|
-
};
|
|
151
|
+
});
|
|
123
152
|
}
|
|
124
153
|
if ("conflict" in res) {
|
|
125
154
|
current = await writer.readRaw();
|
|
@@ -156,7 +185,7 @@ export async function syncPermissionRules(opts) {
|
|
|
156
185
|
if (!dropped.some((x) => x.dot.actor === q.dot.actor && x.dot.counter === q.dot.counter && x.reason === q.reason))
|
|
157
186
|
dropped.push(q);
|
|
158
187
|
}
|
|
159
|
-
return {
|
|
188
|
+
return disclose({
|
|
160
189
|
ok: dropped.length === 0 && warnings.length === 0,
|
|
161
190
|
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
162
191
|
landed: { newAdds, newTombstones },
|
|
@@ -164,9 +193,9 @@ export async function syncPermissionRules(opts) {
|
|
|
164
193
|
dropped,
|
|
165
194
|
rev: landedRaw.rev,
|
|
166
195
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
167
|
-
};
|
|
196
|
+
});
|
|
168
197
|
}
|
|
169
|
-
return {
|
|
198
|
+
return disclose({
|
|
170
199
|
ok: false,
|
|
171
200
|
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
172
201
|
landed: { newAdds: 0, newTombstones: 0 },
|
|
@@ -174,7 +203,7 @@ export async function syncPermissionRules(opts) {
|
|
|
174
203
|
dropped,
|
|
175
204
|
rev: current.rev,
|
|
176
205
|
warnings: [...warnings, `optimistic-concurrency retries exhausted after ${SYNC_MAX_ATTEMPTS} attempts — nothing landed, the local state is unchanged`],
|
|
177
|
-
};
|
|
206
|
+
});
|
|
178
207
|
}
|
|
179
208
|
function isDot(v) {
|
|
180
209
|
const d = v;
|
|
@@ -8,10 +8,10 @@ import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.
|
|
|
8
8
|
import { StoredSession } from "../session.js";
|
|
9
9
|
import type { SessionStore } from "../session.js";
|
|
10
10
|
import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
|
|
11
|
-
import type { OnAsk, ToolPolicy } from "../tool-policy.js";
|
|
11
|
+
import type { OnAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
|
|
12
12
|
import { type ActiveSkillFrame } from "./active-skill-scope.js";
|
|
13
13
|
import type { SessionPermissionRules } from "../session-policy-store.js";
|
|
14
|
-
import { type Hooks } from "../hooks.js";
|
|
14
|
+
import { type Hooks, type OrgGateVerdict } from "../hooks.js";
|
|
15
15
|
import { type RecoveredOrphan } from "../session-reconcile.js";
|
|
16
16
|
import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
|
|
17
17
|
import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
|
|
@@ -214,6 +214,14 @@ export interface Prepared {
|
|
|
214
214
|
/** RB-63: the deployment's own caller policy, re-checked on a durable resume ONLY when the approver
|
|
215
215
|
* rewrote the pending call's args (see the composition site for why the edit case is special). */
|
|
216
216
|
basePolicyForResumeEdit?: ToolPolicy;
|
|
217
|
+
/** design/182 §7 — the ORG adjudication face, re-resolved on a durable RESUME before an approved
|
|
218
|
+
* pending call executes. The resume path bypasses the harness gate by design (a human already
|
|
219
|
+
* adjudicated the checkpointed call), which is exactly where org policy skew is most likely: the
|
|
220
|
+
* suspend may have outlived the snapshot revision that was current when it was minted. Present only
|
|
221
|
+
* on a governed deployment. */
|
|
222
|
+
permissionRuleOrg?: {
|
|
223
|
+
adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
|
|
224
|
+
};
|
|
217
225
|
/** Removes the `spec.signal` abort listener on task end (else a long-lived signal leaks listeners). */
|
|
218
226
|
releaseSignal: () => void;
|
|
219
227
|
/**
|
|
@@ -30,6 +30,7 @@ import { policyAskClassOf } from "../ask-class.js";
|
|
|
30
30
|
import { emitTrace } from "../trace.js";
|
|
31
31
|
import { createSessionRulePolicy } from "./session-rule-policy.js";
|
|
32
32
|
import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, runToolGate } from "../hooks.js";
|
|
33
|
+
import { orgRuleVerdictFor } from "../permission-rule-org.js";
|
|
33
34
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
34
35
|
import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
|
|
35
36
|
import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
|
|
@@ -84,7 +85,7 @@ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-
|
|
|
84
85
|
import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
|
|
85
86
|
import { resolveKey } from "../../tools/fs/safety.js";
|
|
86
87
|
import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
|
|
87
|
-
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
88
|
+
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
88
89
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
89
90
|
import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
|
|
90
91
|
import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
|
|
@@ -3144,12 +3145,22 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3144
3145
|
};
|
|
3145
3146
|
const permissionRuleLane = (() => {
|
|
3146
3147
|
const provider = deps.permissionRuleStore;
|
|
3148
|
+
const localOwnerDeclared = deps.localOwnerRules === true;
|
|
3149
|
+
if (localOwnerDeclared) {
|
|
3150
|
+
if (provider === undefined) {
|
|
3151
|
+
throw new Error("RunnerDeps.localOwnerRules is declared but no permissionRuleStore provider is wired — there is no bucket for the local owner to hold rules in; refusing rather than running as if the declaration were absent");
|
|
3152
|
+
}
|
|
3153
|
+
if (provider.forLocalOwner === undefined) {
|
|
3154
|
+
throw new Error("RunnerDeps.localOwnerRules is declared but the wired permissionRuleStore provider implements no forLocalOwner() face — a provider without a local-owner bucket cannot honor the declaration; refusing rather than silently resolving zero rules");
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3147
3157
|
if (provider === undefined)
|
|
3148
3158
|
return undefined;
|
|
3149
3159
|
const root = taskRootPath;
|
|
3150
3160
|
return {
|
|
3151
3161
|
admits: async (req) => {
|
|
3152
|
-
|
|
3162
|
+
const anonymous = spec.principal === undefined || spec.principal === "";
|
|
3163
|
+
if (anonymous && !localOwnerDeclared)
|
|
3153
3164
|
return undefined;
|
|
3154
3165
|
if (req.toolName !== PERSISTED_RULE_TOOL)
|
|
3155
3166
|
return undefined;
|
|
@@ -3158,7 +3169,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3158
3169
|
return undefined;
|
|
3159
3170
|
let listed;
|
|
3160
3171
|
try {
|
|
3161
|
-
listed =
|
|
3172
|
+
listed = anonymous
|
|
3173
|
+
?
|
|
3174
|
+
await provider.forLocalOwner().list()
|
|
3175
|
+
: await provider.forPrincipal(spec.principal).list();
|
|
3162
3176
|
}
|
|
3163
3177
|
catch (err) {
|
|
3164
3178
|
emitTrace(deps.tracer, () => ({
|
|
@@ -3174,6 +3188,31 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3174
3188
|
},
|
|
3175
3189
|
};
|
|
3176
3190
|
})();
|
|
3191
|
+
const permissionRuleOrgLane = (() => {
|
|
3192
|
+
const overlay = deps.permissionRuleOrg;
|
|
3193
|
+
if (overlay === undefined)
|
|
3194
|
+
return undefined;
|
|
3195
|
+
return {
|
|
3196
|
+
adjudicate: async (req) => {
|
|
3197
|
+
if (req.toolName === ASK_USER_QUESTION_TOOL_NAME && questionToolMounted)
|
|
3198
|
+
return { status: "available" };
|
|
3199
|
+
let resolution;
|
|
3200
|
+
try {
|
|
3201
|
+
resolution = await overlay.resolve();
|
|
3202
|
+
}
|
|
3203
|
+
catch (err) {
|
|
3204
|
+
return { status: "unavailable", disclosures: [`the org rule overlay threw: ${err instanceof Error ? err.message : String(err)}`] };
|
|
3205
|
+
}
|
|
3206
|
+
if (resolution.status === "unavailable")
|
|
3207
|
+
return { status: "unavailable", disclosures: resolution.disclosures };
|
|
3208
|
+
const command = req.args?.command;
|
|
3209
|
+
if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
|
|
3210
|
+
return { status: "available" };
|
|
3211
|
+
const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
|
|
3212
|
+
return verdict === undefined ? { status: "available" } : { status: "available", verdict };
|
|
3213
|
+
},
|
|
3214
|
+
};
|
|
3215
|
+
})();
|
|
3177
3216
|
const ruleSuggestionsOf = (toolName, args) => {
|
|
3178
3217
|
if (permissionRuleLane === undefined || toolName !== PERSISTED_RULE_TOOL)
|
|
3179
3218
|
return {};
|
|
@@ -3621,6 +3660,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3621
3660
|
sessionDurability: resolveDeclaredDurability(sessions, "sessionStore"),
|
|
3622
3661
|
backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
|
|
3623
3662
|
permissionRuleStoreWired: deps.permissionRuleStore !== undefined,
|
|
3663
|
+
permissionRuleSyncWired: deps.permissionRuleSyncWired === true,
|
|
3664
|
+
permissionRuleOrgGoverned: deps.permissionRuleOrg !== undefined,
|
|
3624
3665
|
hostChildEventSinkWired: deps.onBackgroundChildEvent !== undefined,
|
|
3625
3666
|
lockedConfigWired: deps.lockedConfig !== undefined,
|
|
3626
3667
|
complianceWired: deps.compliancePostureResolver !== undefined,
|
|
@@ -3837,8 +3878,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3837
3878
|
return { ok: false };
|
|
3838
3879
|
if (memoryEngineSession)
|
|
3839
3880
|
await memoryEngineSession.harvest("checkpoint");
|
|
3840
|
-
|
|
3841
|
-
await checkpointStore.put(token, cp);
|
|
3881
|
+
const announceCommittedScreeningPark = () => {
|
|
3842
3882
|
const committedCount = cp.state.inheritedGate?.parentConstraintCount;
|
|
3843
3883
|
if (cp.state.inheritedGate?.requiresParentConstraint === true &&
|
|
3844
3884
|
!screeningParkDisclosedRef.done &&
|
|
@@ -3855,10 +3895,59 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3855
3895
|
catch {
|
|
3856
3896
|
}
|
|
3857
3897
|
}
|
|
3898
|
+
};
|
|
3899
|
+
const confirmPutOutcome = async (putErr) => {
|
|
3900
|
+
if (putErr?.code === "checkpoint.already_exists")
|
|
3901
|
+
return "absent";
|
|
3902
|
+
try {
|
|
3903
|
+
const row = await checkpointStore.get(token);
|
|
3904
|
+
if (row == null)
|
|
3905
|
+
return "absent";
|
|
3906
|
+
const sameCall = (a, b) => a.kind === b.kind && (a.kind !== "tool_approval" || b.kind !== "tool_approval" || a.toolCallId === b.toolCallId);
|
|
3907
|
+
const isOurs = row.scope === cp.scope && row.sessionId === cp.sessionId && row.leafId === cp.leafId && sameCall(row.pendingAction, cp.pendingAction);
|
|
3908
|
+
return isOurs ? "committed" : "absent";
|
|
3909
|
+
}
|
|
3910
|
+
catch (readErr) {
|
|
3911
|
+
deps.onError?.(readErr, { phase: "config", sessionId });
|
|
3912
|
+
return "unknown";
|
|
3913
|
+
}
|
|
3914
|
+
};
|
|
3915
|
+
try {
|
|
3916
|
+
await checkpointStore.put(token, cp);
|
|
3917
|
+
announceCommittedScreeningPark();
|
|
3858
3918
|
return { ok: true };
|
|
3859
3919
|
}
|
|
3860
3920
|
catch (putErr) {
|
|
3861
3921
|
deps.onError?.(putErr, { phase: "config", sessionId });
|
|
3922
|
+
const confirmed = await confirmPutOutcome(putErr);
|
|
3923
|
+
if (confirmed === "committed") {
|
|
3924
|
+
try {
|
|
3925
|
+
deps.onError?.(new Error(`durable approval checkpoint: the store's write reported a failure but the row EXISTS — the commit ` +
|
|
3926
|
+
`landed and its acknowledgement was lost (a network backend can complete the INSERT and drop the ` +
|
|
3927
|
+
`connection before replying). The run suspends on that row rather than continuing, which would leave ` +
|
|
3928
|
+
`a redeemable pending checkpoint behind. This backend's acknowledgement path is lossy — the operator ` +
|
|
3929
|
+
`face carries the store's own message.`), { phase: "degraded", sessionId, classification: "checkpoint-put-confirmation-lost" });
|
|
3930
|
+
}
|
|
3931
|
+
catch {
|
|
3932
|
+
}
|
|
3933
|
+
announceCommittedScreeningPark();
|
|
3934
|
+
return { ok: true };
|
|
3935
|
+
}
|
|
3936
|
+
if (confirmed === "unknown") {
|
|
3937
|
+
const unknownReason = "the approval checkpoint's state cannot be established (the store rejected the write and then could not be read back; a committed-but-unacknowledged row may exist) — the run is stopped rather than continued past an approval whose durable record is unknown";
|
|
3938
|
+
try {
|
|
3939
|
+
deps.onError?.(new Error(`durable approval checkpoint: ${unknownReason}`), {
|
|
3940
|
+
phase: "degraded",
|
|
3941
|
+
sessionId,
|
|
3942
|
+
classification: "checkpoint-put-outcome-unknown",
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
catch {
|
|
3946
|
+
}
|
|
3947
|
+
abortController.abort();
|
|
3948
|
+
void harness.abort();
|
|
3949
|
+
return { ok: false, reason: unknownReason };
|
|
3950
|
+
}
|
|
3862
3951
|
const reason = "the approval checkpoint could not be persisted (the store rejected the write; the deployment's error face carries the store's own message)";
|
|
3863
3952
|
if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
|
|
3864
3953
|
const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
|
|
@@ -4147,7 +4236,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4147
4236
|
}
|
|
4148
4237
|
};
|
|
4149
4238
|
const suspendAsk = parkLaneArmed && checkpointStore !== undefined
|
|
4150
|
-
? async (req, postHookArgs, safety, liveFaceUnavailable) => {
|
|
4239
|
+
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval) => {
|
|
4151
4240
|
const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
|
|
4152
4241
|
if (syncFirstEligible &&
|
|
4153
4242
|
runtimeCaps?.forceDurableGate !== true &&
|
|
@@ -4256,12 +4345,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4256
4345
|
...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
|
|
4257
4346
|
});
|
|
4258
4347
|
gate =
|
|
4259
|
-
safety !== undefined
|
|
4348
|
+
safety !== undefined || realApproval !== undefined
|
|
4260
4349
|
? {
|
|
4261
4350
|
kind: "irreversible_ask",
|
|
4262
|
-
reason:
|
|
4351
|
+
reason: safety !== undefined
|
|
4352
|
+
? `human approval required before safety-tightened tool "${req.toolName}"`
|
|
4353
|
+
: `real human approval required for tool "${req.toolName}" (non-budgetable: ${realApproval.origin})`,
|
|
4263
4354
|
toolName: req.toolName,
|
|
4264
|
-
safetyAxis: safety,
|
|
4355
|
+
...(safety !== undefined ? { safetyAxis: safety } : {}),
|
|
4356
|
+
...(realApproval !== undefined ? { realApproval } : {}),
|
|
4265
4357
|
riskDescriptor,
|
|
4266
4358
|
}
|
|
4267
4359
|
: durableApproval
|
|
@@ -4284,7 +4376,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4284
4376
|
const mintedAt = Date.now();
|
|
4285
4377
|
cp = {
|
|
4286
4378
|
token,
|
|
4287
|
-
version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4379
|
+
version: realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4288
4380
|
scope,
|
|
4289
4381
|
sessionId,
|
|
4290
4382
|
leafId,
|
|
@@ -4422,6 +4514,23 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4422
4514
|
},
|
|
4423
4515
|
}
|
|
4424
4516
|
: {}),
|
|
4517
|
+
...(permissionRuleOrgLane
|
|
4518
|
+
? {
|
|
4519
|
+
orgRules: {
|
|
4520
|
+
adjudicate: permissionRuleOrgLane.adjudicate,
|
|
4521
|
+
contentAskToolMounted: questionToolMounted,
|
|
4522
|
+
onUnavailable: (info) => emitTrace(deps.tracer, () => ({
|
|
4523
|
+
kind: "permission.org_snapshot_unavailable",
|
|
4524
|
+
version: 1,
|
|
4525
|
+
taskId: spec.taskId ?? sessionId,
|
|
4526
|
+
toolName: info.toolName,
|
|
4527
|
+
toolCallId: info.toolCallId,
|
|
4528
|
+
message: info.message,
|
|
4529
|
+
ts: Date.now(),
|
|
4530
|
+
})),
|
|
4531
|
+
},
|
|
4532
|
+
}
|
|
4533
|
+
: {}),
|
|
4425
4534
|
isMarkedUnresolvable: (toolCallId) => inheritedUnavailableAsks.has(toolCallId),
|
|
4426
4535
|
...(sandboxAdmissionArmed
|
|
4427
4536
|
? {
|
|
@@ -4750,7 +4859,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4750
4859
|
: undefined;
|
|
4751
4860
|
overheadState.promptChars = systemPrompt.length;
|
|
4752
4861
|
const preparedHolder = {};
|
|
4753
|
-
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettledBy, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4862
|
+
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettledBy, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4754
4863
|
const prepared = buildPrepared();
|
|
4755
4864
|
preparedHolder.current = prepared;
|
|
4756
4865
|
return prepared;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
2
2
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
3
|
-
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
3
|
+
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
4
4
|
import { engineVersion } from "../version.js";
|
|
5
5
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
6
6
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
@@ -9,6 +9,7 @@ import { ASK_USER_QUESTION_TOOL_NAME, canonicalizeCapturedPlainData, classifyQue
|
|
|
9
9
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
10
10
|
import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
|
|
11
11
|
import { emitTrace } from "../trace.js";
|
|
12
|
+
import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
|
|
12
13
|
import { emitTaskOutcome } from "../task-outcome.js";
|
|
13
14
|
import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
|
|
14
15
|
import { primaryActivityArg } from "../arg-summary.js";
|
|
@@ -3703,6 +3704,21 @@ export class Runner {
|
|
|
3703
3704
|
if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
3704
3705
|
throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`);
|
|
3705
3706
|
}
|
|
3707
|
+
const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
|
|
3708
|
+
const preCasBitWellFormed = preCasGateBit !== undefined &&
|
|
3709
|
+
typeof preCasGateBit === "object" &&
|
|
3710
|
+
(preCasGateBit.origin === "org_rule" ||
|
|
3711
|
+
preCasGateBit.origin === "org_unavailable" ||
|
|
3712
|
+
preCasGateBit.origin === "policy");
|
|
3713
|
+
if (checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? !preCasBitWellFormed : preCasGateBit !== undefined) {
|
|
3714
|
+
throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3715
|
+
? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed non-budgetable realApproval gate bit (origin org_rule/org_unavailable/policy) on an irreversible_ask gate — this row does not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
|
|
3716
|
+
: `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`);
|
|
3717
|
+
}
|
|
3718
|
+
if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
|
|
3719
|
+
this.deps.permissionRuleOrg === undefined) {
|
|
3720
|
+
throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`);
|
|
3721
|
+
}
|
|
3706
3722
|
const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
|
|
3707
3723
|
if (retiredWalltimeTotal !== undefined) {
|
|
3708
3724
|
throw new CheckpointError("checkpoint.walltime_axis_retired", "this checkpoint's ledger carries a cross-slice WALL-CLOCK allocation (resourceLedger.totalWalltimeSec), an axis this engine version retired — " +
|
|
@@ -4008,6 +4024,35 @@ export class Runner {
|
|
|
4008
4024
|
return;
|
|
4009
4025
|
}
|
|
4010
4026
|
}
|
|
4027
|
+
const gateRealApproval = resume.cp.gate.kind === "irreversible_ask" ? resume.cp.gate.realApproval : undefined;
|
|
4028
|
+
const gateOrgGoverned = gateRealApproval?.origin === "org_rule" || gateRealApproval?.origin === "org_unavailable";
|
|
4029
|
+
if (gateOrgGoverned && prepared.permissionRuleOrg === undefined) {
|
|
4030
|
+
const unwiredDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: its approval was minted under organization governance (${gateRealApproval.origin}), and this worker has no org adjudication wiring — a governed approval may only be redeemed where governance can be enforced. This approval is spent; re-issue the call on an org-wired worker.`);
|
|
4031
|
+
emitEnd(true, { content: unwiredDenial });
|
|
4032
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, unwiredDenial, true));
|
|
4033
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4036
|
+
if (prepared.permissionRuleOrg !== undefined) {
|
|
4037
|
+
const orgVerdict = prepared.permissionRuleOrg
|
|
4038
|
+
.adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId })
|
|
4039
|
+
.catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
|
|
4040
|
+
const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
4041
|
+
const blocked = org.status === "unavailable"
|
|
4042
|
+
? gateRealApproval?.origin === "org_unavailable"
|
|
4043
|
+
? undefined
|
|
4044
|
+
: "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
|
|
4045
|
+
: org.verdict?.behavior === "deny"
|
|
4046
|
+
? `an organization policy rule (${org.verdict.rule}) denies it`
|
|
4047
|
+
: undefined;
|
|
4048
|
+
if (blocked !== undefined) {
|
|
4049
|
+
const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}. This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
|
|
4050
|
+
emitEnd(true, { content: orgDenial });
|
|
4051
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
|
|
4052
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4011
4056
|
if (prepared.denyNarrowingPolicy) {
|
|
4012
4057
|
const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({
|
|
4013
4058
|
toolName: pendingAction.toolName,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isAbsolutePathForm } from "../../tools/fs/safety.js";
|
|
2
|
+
import { isOffloadedDetailReplacement } from "../tool-result-store.js";
|
|
2
3
|
const READ_TOOL = "Read";
|
|
3
4
|
const WRITE_TOOL = "Write";
|
|
4
5
|
const RETRACTING_RESULTS = [
|
|
@@ -50,6 +51,8 @@ export function wholeFileRecordsFromTranscript(messages) {
|
|
|
50
51
|
const content = isRead ? wholeFileFromReadCard(rest) : typeof rest.content === "string" ? rest.content : undefined;
|
|
51
52
|
if (content === undefined)
|
|
52
53
|
continue;
|
|
54
|
+
if (isOffloadedDetailReplacement(content))
|
|
55
|
+
continue;
|
|
53
56
|
byPath.set(filePath, { path: filePath, content, at: m.timestamp });
|
|
54
57
|
}
|
|
55
58
|
return [...byPath.values()];
|
|
@@ -69,8 +69,16 @@ export interface ToolCallRequest {
|
|
|
69
69
|
/** Where a decision came from, for audit (design/37). `"sandbox"` (F-012 L2) marks an allow the
|
|
70
70
|
* sandbox-admission leg resolved: the deployment declared an isolated execution env, every surviving
|
|
71
71
|
* ask on the call was engine-classified sandbox-local, and the call crosses no declared boundary —
|
|
72
|
-
* disclosed durably as `permission.sandbox_admitted`.
|
|
73
|
-
|
|
72
|
+
* disclosed durably as `permission.sandbox_admitted`.
|
|
73
|
+
*
|
|
74
|
+
* design/182 §7 adds the two ORG-layer words, both TIGHTENING-only:
|
|
75
|
+
* - `"org_rule"` — an organization policy rule spoke (a deny, or an ask that no configuration can
|
|
76
|
+
* dismiss). Distinct from `"rule"` (a deployment `ToolPolicy` rule) because the AUTHORITY differs:
|
|
77
|
+
* an org rule comes from the org's server-published snapshot, not from this deployment's policy.
|
|
78
|
+
* - `"org_unavailable"` — an org-governed deployment could not adjudicate against a snapshot, so the
|
|
79
|
+
* whole decision boundary failed closed (see `ORG_UNAVAILABLE_DECISION_REASON`, the single
|
|
80
|
+
* spelling this word is minted from). */
|
|
81
|
+
export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier" | "persisted_rule" | "sandbox" | "org_rule" | "org_unavailable";
|
|
74
82
|
/**
|
|
75
83
|
* WHO (or what) ENDED an approval — the machine-readable twin of a settlement's human-readable text,
|
|
76
84
|
* so a consumer tells "a person decided this" from "nobody answered" without prose-matching a sentence.
|
|
@@ -78,10 +86,20 @@ export type DecisionReason = "rule" | "mode" | "hook" | "safety" | "classifier"
|
|
|
78
86
|
* what ended the WAIT.
|
|
79
87
|
*
|
|
80
88
|
* - `"human"` — a person, or the approver acting for one, returned a final verdict (allow or deny).
|
|
81
|
-
* - `"timeout"` —
|
|
89
|
+
* - `"timeout"` — an approval window elapsed with no answer.
|
|
82
90
|
* - `"aborted"` — every other NON-HUMAN end: the task aborted, the approver threw or reported nobody
|
|
83
91
|
* reachable, the decision arrived out of contract, a store or transport gave way, retries ran out.
|
|
84
92
|
*
|
|
93
|
+
* **Which windows `"timeout"` speaks for** (#114①, 2026-08-09 — the promise this note used to make was
|
|
94
|
+
* wider than the code): the engine stamps it at the waits IT owns — `createApprovalPolicy`'s
|
|
95
|
+
* `approvalTimeoutMs` window, and the durable park's TTL. The SYNCHRONOUS `onAsk` leg is not one of
|
|
96
|
+
* them: there the deployment owns the window (the engine starts no timer for a callback it does not
|
|
97
|
+
* schedule), so an unanswered card and a refused one arrive as the same `false` and the engine records
|
|
98
|
+
* `"human"` rather than inventing a cause it did not observe. A host that DOES time its own card out
|
|
99
|
+
* can say so — {@link AskOutcome}'s object arm carries an optional `settledBy` for exactly this — but a
|
|
100
|
+
* host that does not is indistinguishable, by construction. Read an absent `"timeout"` as "no window
|
|
101
|
+
* the engine owns elapsed", never as "nobody's window elapsed".
|
|
102
|
+
*
|
|
85
103
|
* The three words are exhaustive and mutually exclusive over the ways an approval can end, and the
|
|
86
104
|
* minimum discrimination a consumer needs — someone refused vs nobody answered — is `"human"` vs the
|
|
87
105
|
* other two.
|
|
@@ -694,17 +712,32 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
694
712
|
* untyped bridge is a fail-closed deny naming the defect (the historical truthy leniency was a
|
|
695
713
|
* fail-open on the security face with no live producer).
|
|
696
714
|
* - `"unavailable"` — the G1 per-ask routing verdict (see {@link OnAsk}).
|
|
697
|
-
* - `{ allow, updatedInput? }` — verdict PLUS an operator EDIT of the presented args
|
|
715
|
+
* - `{ allow, updatedInput?, settledBy? }` — verdict PLUS an operator EDIT of the presented args
|
|
698
716
|
* (whole-replacement form, e.g. ctrl+g "edit script in $EDITOR"): the human approved a MODIFIED
|
|
699
717
|
* action, and executing the un-edited args would betray that consent. `allow` folds STRICTLY
|
|
700
718
|
* (`allow === true`) — an object arm is a deliberate caller, so no truthy leniency — and
|
|
701
719
|
* `updatedInput` is honored only on allow (a deny edit never executes anything). The edit is
|
|
702
720
|
* applied as the resolved decision's own rewrite: the gate re-validates it against the tool
|
|
703
721
|
* schema exactly like a hook/policy rewrite.
|
|
722
|
+
*
|
|
723
|
+
* #114② (2026-08-09) — `settledBy` on the object arm is the SYNCHRONOUS leg's only channel for saying
|
|
724
|
+
* what ended the wait. This leg's window belongs to the HOST (`onAsk` is a callback the deployment
|
|
725
|
+
* owns; the engine starts no timer for it), so a host that timed its own approval card out could
|
|
726
|
+
* previously only report a plain `false`, which the engine correctly recorded as `"human"` — a person
|
|
727
|
+
* refusing. The two words the host may self-report:
|
|
728
|
+
* - `"human"` — a person answered (identical to omitting the field);
|
|
729
|
+
* - `"timeout"` — the host's own approval window elapsed with no answer. Legal ONLY with
|
|
730
|
+
* `allow: false`: a window that elapsed cannot be the thing that approved an action, so
|
|
731
|
+
* `{allow: true, settledBy: "timeout"}` is a contradiction and is refused fail-closed (the same
|
|
732
|
+
* narrowing the `PermissionResult` allow arm already spells in its type).
|
|
733
|
+
* `"aborted"` is deliberately NOT accepted here — that word names the engine's OWN fail-closed ends
|
|
734
|
+
* (abort, throw, unavailable, out-of-contract value), each already stamped at its own arm, and a
|
|
735
|
+
* self-reported one would let a host relabel its refusal as an engine failure.
|
|
704
736
|
*/
|
|
705
737
|
export type AskOutcome = boolean | "unavailable" | {
|
|
706
738
|
allow: boolean;
|
|
707
739
|
updatedInput?: unknown;
|
|
740
|
+
settledBy?: Extract<ApprovalSettledBy, "human" | "timeout">;
|
|
708
741
|
};
|
|
709
742
|
/**
|
|
710
743
|
* ruled 2026-08-04 — forward an approver into a delegated child, stamping every ask it raises with the
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -122,12 +122,9 @@ export function createAllowDenyPolicy(opts) {
|
|
|
122
122
|
if (entry.startsWith("mcp__")) {
|
|
123
123
|
const segments = entry.slice("mcp__".length).split("__");
|
|
124
124
|
if (segments.some((seg) => seg.length === 0)) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
message: `"${entry}" is a malformed MCP tool name (empty segment) — it can never match any mounted tool. ` +
|
|
129
|
-
`Use \`mcp__<server>\` for every tool of a server, or \`mcp__<server>__<tool>\` for one tool.`,
|
|
130
|
-
});
|
|
125
|
+
const lesson = `a malformed MCP tool name (empty segment) — it can never match any mounted tool. ` +
|
|
126
|
+
`Use \`mcp__<server>\` for every tool of a server, or \`mcp__<server>__<tool>\` for one tool.`;
|
|
127
|
+
invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
|
|
131
128
|
continue;
|
|
132
129
|
}
|
|
133
130
|
}
|
|
@@ -135,14 +132,11 @@ export function createAllowDenyPolicy(opts) {
|
|
|
135
132
|
kept.push(entry);
|
|
136
133
|
continue;
|
|
137
134
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`and route the narrowing through the lane that speaks this grammar: parameter rules via ` +
|
|
144
|
-
`createPermissionRulePolicy, Bash command prefixes via the persisted allow-rule lane.`,
|
|
145
|
-
});
|
|
135
|
+
const lesson = `a rule CONTENT form, not a tool name — a name set matches raw tool names, so this entry ` +
|
|
136
|
+
`can never match any mounted tool (in an allow list it removes the tool entirely). Use the tool NAME here, ` +
|
|
137
|
+
`and route the narrowing through the lane that speaks this grammar: parameter rules via ` +
|
|
138
|
+
`createPermissionRulePolicy, Bash command prefixes via the persisted allow-rule lane.`;
|
|
139
|
+
invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
|
|
146
140
|
}
|
|
147
141
|
return kept;
|
|
148
142
|
};
|
|
@@ -150,8 +144,16 @@ export function createAllowDenyPolicy(opts) {
|
|
|
150
144
|
const screenedDeny = screen(opts.deny, "deny");
|
|
151
145
|
if (invalid.length > 0) {
|
|
152
146
|
if ((opts.onInvalidName ?? "throw") === "throw") {
|
|
147
|
+
const byLesson = new Map();
|
|
148
|
+
for (const i of invalid) {
|
|
149
|
+
const group = byLesson.get(i.lesson) ?? [];
|
|
150
|
+
group.push({ entry: i.entry, list: i.list });
|
|
151
|
+
byLesson.set(i.lesson, group);
|
|
152
|
+
}
|
|
153
153
|
const e = new Error(`createAllowDenyPolicy: ${invalid.length} entr${invalid.length === 1 ? "y is" : "ies are"} not tool name(s):\n` +
|
|
154
|
-
|
|
154
|
+
[...byLesson.entries()]
|
|
155
|
+
.map(([lesson, group]) => ` Each of the following is ${lesson}\n` + group.map((g) => ` [${g.list}] "${g.entry}"`).join("\n"))
|
|
156
|
+
.join("\n"));
|
|
155
157
|
e.code = "config.invalid_tool_name_set";
|
|
156
158
|
e.issues = invalid;
|
|
157
159
|
throw e;
|
|
@@ -844,12 +846,40 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
844
846
|
};
|
|
845
847
|
}
|
|
846
848
|
if (typeof ok === "object" && ok !== null) {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
+
const supplied = ok.settledBy;
|
|
850
|
+
const allowed = ok.allow;
|
|
851
|
+
const suppliedEdit = ok.updatedInput;
|
|
852
|
+
if (supplied !== undefined && supplied !== "human" && supplied !== "timeout") {
|
|
853
|
+
return {
|
|
854
|
+
action: "deny",
|
|
855
|
+
message: `the approver for "${req.toolName}" reported settledBy "${typeof supplied === "string" ? containThrownText(supplied) : supplied === null ? "null" : typeof supplied}", which is outside what a synchronous ` +
|
|
856
|
+
`approver may self-report — it is exactly "human" or "timeout" (or omitted); denied fail-closed`,
|
|
857
|
+
decisionReason: "mode",
|
|
858
|
+
settledBy: "aborted",
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
if (supplied === "timeout" && allowed === true) {
|
|
862
|
+
return {
|
|
863
|
+
action: "deny",
|
|
864
|
+
message: `the approver for "${req.toolName}" returned an allow settled by "timeout" — an elapsed approval window cannot be ` +
|
|
865
|
+
`what approved a call; denied fail-closed (report timeout with allow:false, or allow with settledBy "human"/omitted)`,
|
|
866
|
+
decisionReason: "mode",
|
|
867
|
+
settledBy: "timeout",
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (allowed !== true) {
|
|
871
|
+
return {
|
|
872
|
+
action: "deny",
|
|
873
|
+
message: supplied === "timeout"
|
|
874
|
+
? `approval for "${req.toolName}" was not answered before the approver's own window elapsed: ${req.message}`
|
|
875
|
+
: `approval denied for "${req.toolName}": ${req.message}`,
|
|
876
|
+
decisionReason: "mode",
|
|
877
|
+
settledBy: supplied === "timeout" ? "timeout" : "human",
|
|
878
|
+
};
|
|
849
879
|
}
|
|
850
|
-
if (
|
|
880
|
+
if (suppliedEdit === undefined)
|
|
851
881
|
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settledBy: "human" };
|
|
852
|
-
const edit = tryCloneArgs(
|
|
882
|
+
const edit = tryCloneArgs(suppliedEdit);
|
|
853
883
|
if (!edit.ok) {
|
|
854
884
|
return {
|
|
855
885
|
action: "deny",
|