@sema-agent/core 5.34.0 → 5.35.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 +46 -0
- package/dist/core/checkpoint-store.d.ts +33 -4
- package/dist/core/hooks.d.ts +98 -3
- package/dist/core/hooks.js +146 -8
- package/dist/core/park-selfcheck.d.ts +156 -0
- package/dist/core/park-selfcheck.js +251 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
- package/dist/core/runner/prepare-task.d.ts +12 -4
- package/dist/core/runner/prepare-task.js +60 -19
- package/dist/core/runner/runtask.d.ts +6 -1
- package/dist/core/runner/runtask.js +64 -14
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-policy.d.ts +125 -0
- package/dist/core/tool-policy.js +35 -2
- package/dist/core/types.d.ts +71 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/orchestration/workflow.d.ts +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +17 -1
|
@@ -20,7 +20,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents
|
|
|
20
20
|
import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
|
|
21
21
|
import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
|
|
22
22
|
import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
|
|
23
|
-
import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
|
|
23
|
+
import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
|
|
24
24
|
const PERSISTED_RULE_TOOL = "Bash";
|
|
25
25
|
import { findAdmittingRule, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
26
26
|
import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
|
|
@@ -88,6 +88,7 @@ import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
|
|
|
88
88
|
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
89
89
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
90
90
|
import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
|
|
91
|
+
import { durableParkGapFor } from "../park-selfcheck.js";
|
|
91
92
|
import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
|
|
92
93
|
import { deliverEngineNotice } from "../types.js";
|
|
93
94
|
const announcedMaterializeEnv = new Set();
|
|
@@ -192,6 +193,46 @@ async function forgetQuietly(sessions, sessionId) {
|
|
|
192
193
|
catch {
|
|
193
194
|
}
|
|
194
195
|
}
|
|
196
|
+
function screenGateSettlement(result, settling) {
|
|
197
|
+
const defects = [];
|
|
198
|
+
const reportedBy = result.settledBy;
|
|
199
|
+
let settledBy;
|
|
200
|
+
if (reportedBy !== undefined) {
|
|
201
|
+
if (!isApprovalSettledBy(reportedBy) || (!settling && reportedBy !== "human")) {
|
|
202
|
+
defects.push(`a tool-gate settlement reported settledBy "${String(reportedBy)}" on ${settling ? "a blocked" : "an executing"} call — ` +
|
|
203
|
+
`it is one of "human" / "timeout" / "aborted", and only "human" can be the source of a call that runs; the frame carries no source`);
|
|
204
|
+
}
|
|
205
|
+
else
|
|
206
|
+
settledBy = reportedBy;
|
|
207
|
+
}
|
|
208
|
+
const attribution = screenApproverAttribution(result.approver);
|
|
209
|
+
if (attribution.defect !== undefined) {
|
|
210
|
+
defects.push(`a tool-gate settlement reported an attribution this engine refuses: ${attribution.defect}; the frame carries no approver`);
|
|
211
|
+
}
|
|
212
|
+
const record = {
|
|
213
|
+
...(settledBy !== undefined ? { settledBy } : {}),
|
|
214
|
+
...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
|
|
215
|
+
};
|
|
216
|
+
return { ...(settledBy !== undefined || attribution.approver !== undefined ? { record } : {}), defects };
|
|
217
|
+
}
|
|
218
|
+
function inheritedAskRuleEvidence(deps) {
|
|
219
|
+
const org = deps.permissionRuleOrg === undefined ? "not_wired" : "not_adjudicated";
|
|
220
|
+
const personal = deps.permissionRuleStore === undefined ? "not_wired" : "not_adjudicated";
|
|
221
|
+
return Object.freeze({ orgRevisionAbsent: org, orgRuleAbsent: org, personalRuleDotsAbsent: personal });
|
|
222
|
+
}
|
|
223
|
+
function orgRevisionEvidenceOf(resolution, onDefect) {
|
|
224
|
+
const reported = resolution.revision;
|
|
225
|
+
if (reported === undefined)
|
|
226
|
+
return {};
|
|
227
|
+
if (typeof reported === "number" && Number.isFinite(reported))
|
|
228
|
+
return { revision: reported };
|
|
229
|
+
onDefect(`the org rule overlay reported revision ${JSON.stringify(reported)} — a snapshot revision is a finite number; ` +
|
|
230
|
+
`the adjudication stands, but the ask carries no revision evidence for this call`);
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
function persistedRuleHitOf(admitting) {
|
|
234
|
+
return admitting === undefined ? undefined : { rule: admitting.rule, dots: admitting.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) };
|
|
235
|
+
}
|
|
195
236
|
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
|
|
196
237
|
const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
|
|
197
238
|
spec = doors.spec;
|
|
@@ -204,7 +245,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
204
245
|
e.code = "resume.session_not_found";
|
|
205
246
|
throw e;
|
|
206
247
|
}
|
|
207
|
-
const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects });
|
|
248
|
+
const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
|
|
208
249
|
const sessionId = acquired.sessionId;
|
|
209
250
|
const hostTaskId = spec.taskId ?? sessionId;
|
|
210
251
|
if (compModel !== undefined) {
|
|
@@ -2778,6 +2819,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2778
2819
|
return {};
|
|
2779
2820
|
return { riskAxes: { ...(irreversible !== undefined ? { irreversible } : {}), ...(egress !== undefined ? { egress } : {}) } };
|
|
2780
2821
|
};
|
|
2822
|
+
const inheritedAskEvidence = inheritedAskRuleEvidence(deps);
|
|
2781
2823
|
const permissionRuleLane = (() => {
|
|
2782
2824
|
const provider = deps.permissionRuleStore;
|
|
2783
2825
|
const localOwnerDeclared = deps.localOwnerRules === true;
|
|
@@ -2817,9 +2859,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2817
2859
|
message: err instanceof Error ? err.message : String(err),
|
|
2818
2860
|
ts: Date.now(),
|
|
2819
2861
|
}));
|
|
2820
|
-
return
|
|
2862
|
+
return { unreadable: true };
|
|
2821
2863
|
}
|
|
2822
|
-
return findAdmittingRule(listed.rules, { tool: req.toolName, command, cwd: root })
|
|
2864
|
+
return persistedRuleHitOf(findAdmittingRule(listed.rules, { tool: req.toolName, command, cwd: root }));
|
|
2823
2865
|
},
|
|
2824
2866
|
};
|
|
2825
2867
|
})();
|
|
@@ -2840,11 +2882,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2840
2882
|
}
|
|
2841
2883
|
if (resolution.status === "unavailable")
|
|
2842
2884
|
return { status: "unavailable", disclosures: resolution.disclosures };
|
|
2885
|
+
const revisionCell = orgRevisionEvidenceOf(resolution, (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }));
|
|
2843
2886
|
const command = req.args?.command;
|
|
2844
2887
|
if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
|
|
2845
|
-
return { status: "available" };
|
|
2888
|
+
return { status: "available", ...revisionCell };
|
|
2846
2889
|
const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
|
|
2847
|
-
return verdict === undefined ? { status: "available" } : { status: "available", verdict };
|
|
2890
|
+
return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
|
|
2848
2891
|
},
|
|
2849
2892
|
};
|
|
2850
2893
|
})();
|
|
@@ -2926,6 +2969,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2926
2969
|
...riskAxesOf(creq.toolName),
|
|
2927
2970
|
...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
2928
2971
|
...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
|
|
2972
|
+
ruleEvidence: inheritedAskEvidence,
|
|
2929
2973
|
}, onAskOf, csignal ?? abortController.signal);
|
|
2930
2974
|
if (rr.action !== "allow")
|
|
2931
2975
|
return rr;
|
|
@@ -3011,6 +3055,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3011
3055
|
...riskAxesOf(creq.toolName),
|
|
3012
3056
|
...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3013
3057
|
...(first.action === "ask" && first.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: first.persistedRuleShadowed } : {}),
|
|
3058
|
+
ruleEvidence: inheritedAskEvidence,
|
|
3014
3059
|
}, pc.onAsk, csignal ?? abortController.signal);
|
|
3015
3060
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
3016
3061
|
if (resolved.action === "deny" && resolved.approverUnavailable === true) {
|
|
@@ -3098,6 +3143,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3098
3143
|
...riskAxesOf(creq.toolName),
|
|
3099
3144
|
...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3100
3145
|
...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
|
|
3146
|
+
ruleEvidence: inheritedAskEvidence,
|
|
3101
3147
|
}, pc.onAsk, csignal ?? abortController.signal);
|
|
3102
3148
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
3103
3149
|
if (resolved.action === "deny" && resolved.approverUnavailable === true) {
|
|
@@ -3239,7 +3285,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3239
3285
|
}
|
|
3240
3286
|
const preToolContexts = new Map();
|
|
3241
3287
|
const blockedToolCalls = new Set();
|
|
3242
|
-
const
|
|
3288
|
+
const approvalSettlement = new Map();
|
|
3243
3289
|
const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
|
|
3244
3290
|
const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
|
|
3245
3291
|
const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
|
|
@@ -3413,6 +3459,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3413
3459
|
...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
|
|
3414
3460
|
...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
|
|
3415
3461
|
...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
|
|
3462
|
+
...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
|
|
3416
3463
|
}, onAsk, abortController.signal);
|
|
3417
3464
|
const waitMs = Math.max(0, now() - t0);
|
|
3418
3465
|
if (resolved.approverUnavailable !== true) {
|
|
@@ -4230,17 +4277,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4230
4277
|
if (blockedTracked && (result.block || result.suspend)) {
|
|
4231
4278
|
blockedToolCalls.add(e.toolCallId);
|
|
4232
4279
|
}
|
|
4233
|
-
const
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
`it is one of "human" / "timeout" / "aborted", and only "human" can be the source of a call that runs; the frame carries no source`), { phase: "config", sessionId });
|
|
4239
|
-
}
|
|
4240
|
-
else {
|
|
4241
|
-
approvalSettledBy.set(e.toolCallId, reported);
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
4280
|
+
const settlement = screenGateSettlement(result, result.block === true);
|
|
4281
|
+
for (const defect of settlement.defects)
|
|
4282
|
+
deps.onError?.(new Error(defect), { phase: "config", sessionId });
|
|
4283
|
+
if (settlement.record !== undefined)
|
|
4284
|
+
approvalSettlement.set(e.toolCallId, settlement.record);
|
|
4244
4285
|
return result.block
|
|
4245
4286
|
? { block: true, reason: result.reason }
|
|
4246
4287
|
: result.updatedInput !== undefined
|
|
@@ -4552,7 +4593,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4552
4593
|
const effectiveReadFaceObserved = carrierReadFace();
|
|
4553
4594
|
const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
|
|
4554
4595
|
const preparedHolder = {};
|
|
4555
|
-
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls,
|
|
4596
|
+
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), 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 } : {}) });
|
|
4556
4597
|
const prepared = buildPrepared();
|
|
4557
4598
|
preparedHolder.current = prepared;
|
|
4558
4599
|
return prepared;
|
|
@@ -80,13 +80,18 @@ declare function toolEndBodyFrom(result: unknown, isError: boolean,
|
|
|
80
80
|
* a parameter and never derived from `result`: a tool's own `details` (which post-tool hooks may
|
|
81
81
|
* also replace) is writable by layers that adjudicate nothing, so reading provenance out of it would
|
|
82
82
|
* let a failing tool claim a person approved it. Omitted ⇒ this call settled no approval. */
|
|
83
|
-
settledBy?: ApprovalSettledBy
|
|
83
|
+
settledBy?: ApprovalSettledBy,
|
|
84
|
+
/** design/252 G-7 — WHOSE settlement, from the same caller and the same channel as `settledBy`, and
|
|
85
|
+
* for the same reason it is a parameter: an attribution read out of a tool's own result would let a
|
|
86
|
+
* tool name the person who approved it. Omitted ⇒ this call's settlement named nobody. */
|
|
87
|
+
approver?: string): {
|
|
84
88
|
output?: unknown;
|
|
85
89
|
truncated?: boolean;
|
|
86
90
|
totalChars?: number;
|
|
87
91
|
structured?: unknown;
|
|
88
92
|
errorCode?: string;
|
|
89
93
|
settledBy?: ApprovalSettledBy;
|
|
94
|
+
approver?: string;
|
|
90
95
|
};
|
|
91
96
|
/**
|
|
92
97
|
* scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
|
|
@@ -2,7 +2,7 @@ import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
|
|
|
2
2
|
import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
|
|
3
3
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
4
4
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
5
|
-
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
5
|
+
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
6
6
|
import { engineVersion } from "../version.js";
|
|
7
7
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
8
8
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
@@ -41,7 +41,7 @@ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntr
|
|
|
41
41
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
42
42
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
43
43
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
44
|
-
import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
|
|
44
|
+
import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
|
|
45
45
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
46
46
|
import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
47
47
|
import { ToolDetachHub } from "../tool-detach.js";
|
|
@@ -68,6 +68,13 @@ function nextHumanInputSeq(key) {
|
|
|
68
68
|
}
|
|
69
69
|
return ++box.n;
|
|
70
70
|
}
|
|
71
|
+
function sameAcceptedSteerInput(a, b) {
|
|
72
|
+
return (a.payload === b.payload &&
|
|
73
|
+
a.trusted === b.trusted &&
|
|
74
|
+
a.actor?.id === b.actor?.id &&
|
|
75
|
+
a.actor?.hostAsserted === b.actor?.hostAsserted &&
|
|
76
|
+
a.actor?.issuer === b.actor?.issuer);
|
|
77
|
+
}
|
|
71
78
|
const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
|
|
72
79
|
const STOP_HOOK_BLOCK_CAP = 8;
|
|
73
80
|
const COMPACTION_REGROWTH_FACTOR = 1.5;
|
|
@@ -130,7 +137,7 @@ function resumeDecisionWasNegative(resume) {
|
|
|
130
137
|
}
|
|
131
138
|
const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
|
|
132
139
|
"NOT executed on resume. If you still need it, issue it again now.";
|
|
133
|
-
function toolEndBodyFrom(result, isError, settledBy) {
|
|
140
|
+
function toolEndBodyFrom(result, isError, settledBy, approver) {
|
|
134
141
|
const o = toolOutputFrom(result);
|
|
135
142
|
const st = structuredFrom(result);
|
|
136
143
|
const det = isError ? result?.details : undefined;
|
|
@@ -142,6 +149,7 @@ function toolEndBodyFrom(result, isError, settledBy) {
|
|
|
142
149
|
...(st !== undefined ? { structured: st } : {}),
|
|
143
150
|
...(typeof code === "string" ? { errorCode: code } : {}),
|
|
144
151
|
...(settledBy !== undefined ? { settledBy } : {}),
|
|
152
|
+
...(approver !== undefined ? { approver } : {}),
|
|
145
153
|
};
|
|
146
154
|
}
|
|
147
155
|
export function reconciledToolEndBody(orphan) {
|
|
@@ -1213,16 +1221,16 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1213
1221
|
prepared.lspDiagnostics.nudge(p);
|
|
1214
1222
|
}
|
|
1215
1223
|
}
|
|
1216
|
-
const
|
|
1217
|
-
if (
|
|
1218
|
-
prepared.
|
|
1224
|
+
const settlement = prepared.approvalSettlement.get(event.toolCallId);
|
|
1225
|
+
if (settlement !== undefined)
|
|
1226
|
+
prepared.approvalSettlement.delete(event.toolCallId);
|
|
1219
1227
|
pushContent({
|
|
1220
1228
|
type: "tool_end",
|
|
1221
1229
|
toolCallId: event.toolCallId,
|
|
1222
1230
|
toolName: event.toolName,
|
|
1223
1231
|
...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
|
|
1224
1232
|
isError: event.isError,
|
|
1225
|
-
...toolEndBodyFrom(event.result, event.isError, settledBy),
|
|
1233
|
+
...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver),
|
|
1226
1234
|
...ident(),
|
|
1227
1235
|
});
|
|
1228
1236
|
announceWorkspaceMove();
|
|
@@ -1444,6 +1452,7 @@ export class Runner {
|
|
|
1444
1452
|
};
|
|
1445
1453
|
let reapHandle;
|
|
1446
1454
|
let steerChain = Promise.resolve();
|
|
1455
|
+
const acceptedSteerInputs = new Map();
|
|
1447
1456
|
const notifyRef = {};
|
|
1448
1457
|
const manualCompactRef = { requested: false, waiters: [] };
|
|
1449
1458
|
const drainManualCompactWaiters = (outcome) => {
|
|
@@ -1565,6 +1574,7 @@ export class Runner {
|
|
|
1565
1574
|
queue.push({ type: "done", result: resultValue });
|
|
1566
1575
|
queue.close();
|
|
1567
1576
|
});
|
|
1577
|
+
void settled.then(() => acceptedSteerInputs.clear(), () => acceptedSteerInputs.clear());
|
|
1568
1578
|
const steeringError = (msg, code = "steering.not_running") => {
|
|
1569
1579
|
const e = new Error(`cannot steer: ${msg}`);
|
|
1570
1580
|
e.code = code;
|
|
@@ -1613,22 +1623,40 @@ export class Runner {
|
|
|
1613
1623
|
return suggestionsDone.catch(() => []);
|
|
1614
1624
|
},
|
|
1615
1625
|
steer: async (text, options) => {
|
|
1616
|
-
|
|
1626
|
+
const trusted = options?.trusted ? true : false;
|
|
1627
|
+
if (trusted && sanitizeUntrustedText(text) !== text) {
|
|
1617
1628
|
throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
|
|
1618
1629
|
}
|
|
1619
|
-
const
|
|
1630
|
+
const inputId = options?.inputId;
|
|
1631
|
+
if (inputId !== undefined) {
|
|
1632
|
+
if (typeof inputId !== "string") {
|
|
1633
|
+
throw steeringError("inputId must be a string when supplied", "steering.invalid_content");
|
|
1634
|
+
}
|
|
1635
|
+
if (inputId === "" || inputId.length > MAX_STEER_INPUT_ID_CHARS) {
|
|
1636
|
+
throw steeringError(`inputId must be a non-empty string of at most ${MAX_STEER_INPUT_ID_CHARS} characters`, "steering.invalid_content");
|
|
1637
|
+
}
|
|
1638
|
+
if (inputId === LEGACY_PENDING_STEER_INPUT_ID) {
|
|
1639
|
+
throw steeringError(`inputId "${LEGACY_PENDING_STEER_INPUT_ID}" is reserved for a pre-queue parked steer and cannot be supplied by a caller`, "steering.invalid_content");
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const actorIn = options?.actor;
|
|
1643
|
+
const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
|
|
1620
1644
|
const projected = projectHumanInput({ text, actor, source: "steer" });
|
|
1621
|
-
const payload =
|
|
1645
|
+
const payload = trusted ? formatHookFeedback(projected) : projected;
|
|
1622
1646
|
const mintsAFrame = payload.trim().length !== 0;
|
|
1623
|
-
const
|
|
1647
|
+
const replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
|
|
1648
|
+
const noteAccepted = (h) => {
|
|
1624
1649
|
if (!mintsAFrame)
|
|
1625
1650
|
return;
|
|
1651
|
+
if (typeof inputId === "string")
|
|
1652
|
+
acceptedSteerInputs.set(inputId, replay);
|
|
1626
1653
|
queue.push({
|
|
1627
1654
|
...buildHumanInputEvent({
|
|
1628
1655
|
carrier: "steer",
|
|
1629
1656
|
source: "steer",
|
|
1630
1657
|
delivery: "queued",
|
|
1631
1658
|
sessionSeq: nextHumanInputSeq(h.harness),
|
|
1659
|
+
...(typeof inputId === "string" ? { inputId } : {}),
|
|
1632
1660
|
...(actor !== undefined ? { actor } : {}),
|
|
1633
1661
|
...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
|
|
1634
1662
|
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
@@ -1645,9 +1673,21 @@ export class Runner {
|
|
|
1645
1673
|
const h = handle ?? (await orTimeout(ready));
|
|
1646
1674
|
if (!h)
|
|
1647
1675
|
throw steeringError("the task is not running");
|
|
1676
|
+
if (typeof inputId === "string") {
|
|
1677
|
+
const prior = acceptedSteerInputs.get(inputId);
|
|
1678
|
+
if (prior !== undefined) {
|
|
1679
|
+
if (resultValue !== undefined || h.loop.ended)
|
|
1680
|
+
throw steeringError("the task is no longer running");
|
|
1681
|
+
if (!sameAcceptedSteerInput(prior, replay)) {
|
|
1682
|
+
throw steeringError("a different steering instruction was already accepted under this inputId — re-issue this one with a fresh inputId " +
|
|
1683
|
+
"(an identical payload would have been an idempotent retry)", "steering.duplicate_input_id");
|
|
1684
|
+
}
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1648
1688
|
try {
|
|
1649
1689
|
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1650
|
-
|
|
1690
|
+
noteAccepted(h);
|
|
1651
1691
|
return;
|
|
1652
1692
|
}
|
|
1653
1693
|
catch (e) {
|
|
@@ -1658,7 +1698,7 @@ export class Runner {
|
|
|
1658
1698
|
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
1659
1699
|
try {
|
|
1660
1700
|
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1661
|
-
|
|
1701
|
+
noteAccepted(h);
|
|
1662
1702
|
return;
|
|
1663
1703
|
}
|
|
1664
1704
|
catch (e2) {
|
|
@@ -3664,6 +3704,15 @@ export class Runner {
|
|
|
3664
3704
|
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" with settledBy "${settledBy}" — a non-human settlement is a fail-closed end (nobody answered), so it cannot be the source of an approval that EXECUTES; ` +
|
|
3665
3705
|
"supply \"human\", or omit the field if the allow came from configuration rather than a person; refusing pre-CAS, the checkpoint stays pending", { field: "settledBy" });
|
|
3666
3706
|
}
|
|
3707
|
+
const approver = decide.approver;
|
|
3708
|
+
const attribution = screenApproverAttribution(approver);
|
|
3709
|
+
if (attribution.defect !== undefined) {
|
|
3710
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries an approver attribution this engine refuses: ${attribution.defect}; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
|
|
3711
|
+
}
|
|
3712
|
+
if (attribution.approver !== undefined && settledBy === undefined) {
|
|
3713
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume names an approver but no settledBy — an attribution says WHOSE settlement it was and needs the word that says what KIND of end it was beside it; ` +
|
|
3714
|
+
`supply settledBy ("human" / "timeout" / "aborted"), or omit the approver; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
|
|
3715
|
+
}
|
|
3667
3716
|
const reason = decide.reason;
|
|
3668
3717
|
assertOutcomeText(reason, "reason");
|
|
3669
3718
|
plainPolicyOutcome = {
|
|
@@ -3675,6 +3724,7 @@ export class Runner {
|
|
|
3675
3724
|
...(reason !== undefined ? { reason } : {}),
|
|
3676
3725
|
...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
|
|
3677
3726
|
...(settledBy !== undefined ? { settledBy } : {}),
|
|
3727
|
+
...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
|
|
3678
3728
|
};
|
|
3679
3729
|
if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
|
|
3680
3730
|
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
@@ -4037,7 +4087,7 @@ export class Runner {
|
|
|
4037
4087
|
const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
|
|
4038
4088
|
const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
|
|
4039
4089
|
emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
|
|
4040
|
-
const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy) });
|
|
4090
|
+
const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy, outcome.approver) });
|
|
4041
4091
|
if (outcome.decision === "deny") {
|
|
4042
4092
|
const defaultDenial = outcome.settledBy === "timeout"
|
|
4043
4093
|
? `No one answered the approval request for the pending tool call "${pendingAction.toolName}" — the approval window elapsed with no answer, so it was not executed.`
|
|
@@ -117,8 +117,8 @@ export type WorkerErrorClass = "budget" | "limit" | "output" | "suspend" | "revi
|
|
|
117
117
|
* are NOT all terminal-errorCode-shaped — see {@link EXACT_CODE_CLASS}'s comment), plus the flat 1.36 codes.
|
|
118
118
|
* The `limits.` namespace splits into the `budget` and `limit` classes by exact code — see
|
|
119
119
|
* {@link EXACT_CODE_CLASS}. Operation-level dotted codes that are surfaced to a *caller* and never become a
|
|
120
|
-
* task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content
|
|
121
|
-
* rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
|
|
120
|
+
* task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`/
|
|
121
|
+
* `steering.duplicate_input_id`, rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
|
|
122
122
|
* reaches `TaskResult.errorCode`, so a caller will never pass them here. A genuinely unmapped code → `"unknown"` (which therefore means
|
|
123
123
|
* "known-but-foreign or no code", e.g. a leaked fs/Node code, NOT "an OUR terminal class we forgot to add").
|
|
124
124
|
*/
|
|
@@ -118,6 +118,34 @@ export type ApprovalSettledBy = "human" | "timeout" | "aborted";
|
|
|
118
118
|
export declare const APPROVAL_SETTLED_BY_VALUES: readonly ApprovalSettledBy[];
|
|
119
119
|
/** True iff `v` is one of the three {@link ApprovalSettledBy} words. */
|
|
120
120
|
export declare function isApprovalSettledBy(v: unknown): v is ApprovalSettledBy;
|
|
121
|
+
/**
|
|
122
|
+
* design/252 G-7 — how long an approver-attribution identifier may be.
|
|
123
|
+
*
|
|
124
|
+
* Sized for the identifiers approval channels actually carry (a login, an email, an opaque account id,
|
|
125
|
+
* a queue name) with room to spare, and bounded at all because this value lands on an operator-plane
|
|
126
|
+
* frame that a deployment may persist: an unbounded field on a record channel is its own denial of a
|
|
127
|
+
* readable record. An over-long value is REFUSED, never truncated — a cut identifier names a different
|
|
128
|
+
* party, or nobody, and either is worse than the honest refusal.
|
|
129
|
+
*/
|
|
130
|
+
export declare const APPROVER_ATTRIBUTION_MAX_CHARS = 256;
|
|
131
|
+
/**
|
|
132
|
+
* design/252 G-7 — screen a deployment-supplied approver identifier before it becomes an observation.
|
|
133
|
+
*
|
|
134
|
+
* THE POSTURE, stated so it is not mistaken for something stronger: core does not authenticate this
|
|
135
|
+
* value, does not compare it to anything, and never reads it back to decide anything. It is a
|
|
136
|
+
* TRANSCRIPTION of what the approval channel said about its own settlement — the channel (a server's
|
|
137
|
+
* approval card, an HMAC-verified callback, an operator console) is where identity is established, and
|
|
138
|
+
* a library that holds no identity surface cannot second-guess it. What core owns is that the value is
|
|
139
|
+
* a value: a string, bounded, free of control bytes, or else loudly refused.
|
|
140
|
+
*
|
|
141
|
+
* Returns `{}` for "nothing supplied" (absent, and an empty string — an id of no characters is the
|
|
142
|
+
* absence of an id, the same truthiness fold the deny-note seat uses), `{ approver }` for a value to
|
|
143
|
+
* carry, or `{ defect }` with the sentence a caller puts in its own refusal.
|
|
144
|
+
*/
|
|
145
|
+
export declare function screenApproverAttribution(v: unknown): {
|
|
146
|
+
approver?: string;
|
|
147
|
+
defect?: string;
|
|
148
|
+
};
|
|
121
149
|
/**
|
|
122
150
|
* A three-state permission decision for a tool call (design/37). Upgrades the old two-state
|
|
123
151
|
* `{allow|deny}`:
|
|
@@ -162,6 +190,7 @@ export type PermissionResult = {
|
|
|
162
190
|
message?: string;
|
|
163
191
|
decisionReason?: DecisionReason;
|
|
164
192
|
settledBy?: Extract<ApprovalSettledBy, "human">;
|
|
193
|
+
approver?: string;
|
|
165
194
|
} | {
|
|
166
195
|
action: "ask";
|
|
167
196
|
updatedInput?: unknown;
|
|
@@ -199,13 +228,86 @@ export type PermissionResult = {
|
|
|
199
228
|
* when ANY folded-away concurrent ask bore it (monotone, tighten-only); it is consumed inside the
|
|
200
229
|
* gate and deliberately NOT copied onto the park/approval-card request. */
|
|
201
230
|
matchedAskRule?: string;
|
|
231
|
+
/** design/252 G-2 (additive): the RULE-PROVENANCE evidence behind this ask — see
|
|
232
|
+
* {@link AskRuleEvidence}. ENGINE-STAMPED inside the gate, once, after the org layer and the
|
|
233
|
+
* persisted-rule lane have both spoken; a policy that self-declares it is overwritten there
|
|
234
|
+
* (the member is a record of what the ENGINE's own governance layers did, so a layer's claim
|
|
235
|
+
* about itself is not evidence). Carried onto the approval request by the gate's own ask mint
|
|
236
|
+
* site. Display/reconciliation metadata, never adjudication input. */
|
|
237
|
+
ruleEvidence?: AskRuleEvidence;
|
|
202
238
|
} | {
|
|
203
239
|
action: "deny";
|
|
204
240
|
updatedInput?: unknown;
|
|
205
241
|
message?: string;
|
|
206
242
|
decisionReason?: DecisionReason;
|
|
207
243
|
settledBy?: ApprovalSettledBy;
|
|
244
|
+
approver?: string;
|
|
208
245
|
};
|
|
246
|
+
/**
|
|
247
|
+
* design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
|
|
248
|
+
*
|
|
249
|
+
* The vocabulary exists because a bare `undefined` reads the same for facts that are opposite: "no
|
|
250
|
+
* governance layer is wired here, so there is nothing to name" and "the layer ran and we could not
|
|
251
|
+
* read what it said" are not the same audit answer, and collapsing them is how an evidence chain
|
|
252
|
+
* comes to be reconstructed as "nothing governed this call". Every member of {@link AskRuleEvidence}
|
|
253
|
+
* therefore ships as a value OR a named absence, never as silence.
|
|
254
|
+
*
|
|
255
|
+
* - `"not_wired"` — the lane does not exist on this leg (an ungoverned deployment, no rule store).
|
|
256
|
+
* The field cannot apply; nothing was lost.
|
|
257
|
+
* - `"not_adjudicated"` — the lane exists but this call never reached it (an exempt tool, a call
|
|
258
|
+
* the rule grammar cannot describe). Applicable in principle, skipped in fact.
|
|
259
|
+
* - `"unavailable"` — the lane was consulted and could not read its source. The evidence is LOST,
|
|
260
|
+
* not absent, and this is the one member of the vocabulary that means an auditor should treat the
|
|
261
|
+
* chain as broken rather than empty.
|
|
262
|
+
* - `"no_match"` — consulted, readable, and nothing spoke for this call. A real negative answer.
|
|
263
|
+
* - `"not_reported"` — the supplying seam answered without the identity (a foreign overlay/store
|
|
264
|
+
* implementation, or one written before the identity was projected). Evidence lost at the seam.
|
|
265
|
+
*/
|
|
266
|
+
export type AskEvidenceAbsence = "not_wired" | "not_adjudicated" | "unavailable" | "no_match" | "not_reported";
|
|
267
|
+
/** The closed set above, for runtime domain checks at the seams that accept a caller-supplied value. */
|
|
268
|
+
export declare const ASK_EVIDENCE_ABSENCE_VALUES: readonly AskEvidenceAbsence[];
|
|
269
|
+
/**
|
|
270
|
+
* design/252 G-2 — the machine-reconcilable provenance of the governance decision behind ONE ask.
|
|
271
|
+
*
|
|
272
|
+
* WHAT THIS IS: a PROJECTION of identity keys the engine's governance layers already hold — the org
|
|
273
|
+
* snapshot's `revision`, the personal rule's add `dot`s — onto the surface a consumer can actually
|
|
274
|
+
* read. It performs no new judgment and changes no verdict; removing it would leave every decision
|
|
275
|
+
* byte-identical. The prose channels ({@link PermissionResult}'s ask `message`,
|
|
276
|
+
* {@link AskRequest.persistedRuleShadowed}) say the same things to a PERSON; those are sanitized,
|
|
277
|
+
* capped, display-shaped strings, and reconciling a decision against a published policy revision by
|
|
278
|
+
* regexing them is not an audit trail. This member is the machine's copy.
|
|
279
|
+
*
|
|
280
|
+
* WHAT THIS IS NOT: an authority channel. Nothing in the engine reads it back to decide anything, and
|
|
281
|
+
* a host that ignores it entirely is governed exactly as before.
|
|
282
|
+
*
|
|
283
|
+
* ABSENCE DISCIPLINE (the reason each member has a `…Absent` twin): see {@link AskEvidenceAbsence}.
|
|
284
|
+
* Exactly one of each pair is present — a member and its absence reason are never both set, and never
|
|
285
|
+
* both missing, on evidence the engine stamped.
|
|
286
|
+
*/
|
|
287
|
+
export interface AskRuleEvidence {
|
|
288
|
+
/** The org snapshot `revision` this call was adjudicated against — the published-policy version an
|
|
289
|
+
* auditor reconciles the decision against. Absent ⇒ {@link orgRevisionAbsent} names why. */
|
|
290
|
+
readonly orgRevision?: number;
|
|
291
|
+
/** Present iff {@link orgRevision} is not. `"unavailable"` here is the load-bearing one: the org
|
|
292
|
+
* layer spoke, its answer was "this deployment cannot see the organization's rules", and the ask
|
|
293
|
+
* in hand is the fail-closed tighten that followed — not an ask any published rule asked for. */
|
|
294
|
+
readonly orgRevisionAbsent?: AskEvidenceAbsence;
|
|
295
|
+
/** The org rule that spoke for this call, verbatim as published (org rules are administrator
|
|
296
|
+
* authored and are relayed unmodified — the prose channel's copy is the same text). Absent ⇒
|
|
297
|
+
* {@link orgRuleAbsent} names why. */
|
|
298
|
+
readonly orgRule?: string;
|
|
299
|
+
/** Present iff {@link orgRule} is not. */
|
|
300
|
+
readonly orgRuleAbsent?: AskEvidenceAbsence;
|
|
301
|
+
/** The add dots of the PERSONAL allow rule that matched this call but did not clear the ask (the
|
|
302
|
+
* #144 shadowed arm). The dots are the rule's durable identity — unlike
|
|
303
|
+
* {@link AskRequest.persistedRuleShadowed}, which is a sanitized, length-capped DISPLAY value and
|
|
304
|
+
* deliberately not an identity channel. A rule is a set of adds (concurrent approvals on one text
|
|
305
|
+
* each redeem their own dot), so this is an array by construction: render/reconcile the entries as
|
|
306
|
+
* data, never re-derive one scalar id by joining them. Absent ⇒ {@link personalRuleDotsAbsent}. */
|
|
307
|
+
readonly personalRuleDots?: readonly import("./permission-rule-model.js").RuleDot[];
|
|
308
|
+
/** Present iff {@link personalRuleDots} is not. */
|
|
309
|
+
readonly personalRuleDotsAbsent?: AskEvidenceAbsence;
|
|
310
|
+
}
|
|
209
311
|
/** The human/model-readable text of a decision. */
|
|
210
312
|
export declare function decisionText(d: PermissionResult): string | undefined;
|
|
211
313
|
/**
|
|
@@ -682,6 +784,15 @@ export interface AskRequest {
|
|
|
682
784
|
* structure: render the entries as data, never re-derive structure by splitting or joining them.
|
|
683
785
|
* The durable park route carries the same value as `RiskDescriptor.probeCause`. */
|
|
684
786
|
readonly probeCause?: import("./checkpoint-store.js").ProbeCause;
|
|
787
|
+
/** design/252 G-2: the RULE-PROVENANCE evidence behind this ask — the org snapshot revision and the
|
|
788
|
+
* matched rules' own identity keys, each present as a value or as a named absence (see
|
|
789
|
+
* {@link AskRuleEvidence}). Present on EVERY ask the engine mints, so a consumer never has to tell a
|
|
790
|
+
* missing field from a field that means something: the gate's own mint site fills it from what its
|
|
791
|
+
* layers did, and the three INHERITED-lane sites — which present an ANCESTOR policy's decision, from
|
|
792
|
+
* upstream of both lanes — fill it with an all-`"not_adjudicated"` record rather than a fabricated
|
|
793
|
+
* revision. Optional on the TYPE only because a deployment may invoke its approver directly.
|
|
794
|
+
* RECONCILIATION metadata, never adjudication input. */
|
|
795
|
+
readonly ruleEvidence?: AskRuleEvidence;
|
|
685
796
|
toolCallId: string;
|
|
686
797
|
/** The (post-rewrite) args the tool would run with. */
|
|
687
798
|
args: unknown;
|
|
@@ -859,12 +970,26 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
859
970
|
* domain the durable leg enforces pre-CAS for the same field), a THROWING read is a fail-closed
|
|
860
971
|
* deny naming the true cause (never a raw rejection out of the gate), and the empty string reads
|
|
861
972
|
* as absent (truthiness, the durable consumer's own read).
|
|
973
|
+
*
|
|
974
|
+
* `approver` (design/252 G-7) — the ATTRIBUTION seat: the identifier the approval channel reports for
|
|
975
|
+
* the party that ended this wait. Core does not authenticate it, compare it, or read it back to decide
|
|
976
|
+
* anything; it transcribes it onto the settlement observation next to `settledBy`, so an audit that can
|
|
977
|
+
* already say "a person ended this wait" can also say which one, without core growing an identity
|
|
978
|
+
* surface it deliberately does not have. Read the two together: `settledBy:"timeout"` with an
|
|
979
|
+
* `approver` names the queue whose window elapsed, NOT someone who refused.
|
|
980
|
+
* Screened, not trusted (see {@link screenApproverAttribution}): a non-string, an over-long value or one
|
|
981
|
+
* carrying control characters is a loud fail-closed refusal at {@link resolveAsk}, the same posture this
|
|
982
|
+
* seam takes for every other out-of-contract value — and the same reason, since a defective attribution
|
|
983
|
+
* that were quietly dropped would leave a settlement looking unattributed rather than misreported. It is
|
|
984
|
+
* carried on the arms where the CALLER settled something (its allow, its human/timeout deny) and never
|
|
985
|
+
* on an end the engine produced (an abort, a throw, an unclonable edit): nobody approved those.
|
|
862
986
|
*/
|
|
863
987
|
export type AskOutcome = boolean | "unavailable" | {
|
|
864
988
|
allow: boolean;
|
|
865
989
|
updatedInput?: unknown;
|
|
866
990
|
settledBy?: Extract<ApprovalSettledBy, "human" | "timeout">;
|
|
867
991
|
reason?: string;
|
|
992
|
+
approver?: string;
|
|
868
993
|
};
|
|
869
994
|
/**
|
|
870
995
|
* ruled 2026-08-04 — forward an approver into a delegated child, stamping every ask it raises with the
|