@sema-agent/core 5.26.0 → 5.28.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 +113 -0
- package/dist/agents/agent-transcript-tool.d.ts +5 -2
- package/dist/agents/agent-transcript-tool.js +2 -1
- package/dist/agents/send-message-tool.d.ts +4 -1
- package/dist/agents/subagent.d.ts +5 -2
- package/dist/core/checkpoint-store.d.ts +7 -2
- package/dist/core/hooks.d.ts +61 -4
- package/dist/core/hooks.js +37 -15
- package/dist/core/memory-engine/engine.d.ts +8 -5
- package/dist/core/memory-engine/engine.js +18 -6
- package/dist/core/memory-engine/file-backend.d.ts +144 -4
- package/dist/core/memory-engine/file-backend.js +304 -36
- package/dist/core/memory-engine/layout.d.ts +31 -2
- package/dist/core/memory-engine/layout.js +132 -8
- package/dist/core/memory-engine/types.d.ts +9 -1
- package/dist/core/memory-vector.d.ts +6 -1
- package/dist/core/memory-vector.js +14 -4
- package/dist/core/memory.js +1 -6
- package/dist/core/permission-rule-consent.d.ts +82 -8
- package/dist/core/permission-rule-consent.js +92 -1
- package/dist/core/permission-rule-model.d.ts +87 -6
- package/dist/core/permission-rule-model.js +79 -0
- package/dist/core/permission-rule-org.d.ts +22 -3
- package/dist/core/permission-rule-org.js +67 -20
- package/dist/core/permission-rule-store.js +2 -2
- package/dist/core/permission-rule-sync.d.ts +15 -1
- package/dist/core/permission-rule-sync.js +89 -47
- package/dist/core/runner/prepare-memory.js +14 -9
- package/dist/core/runner/prepare-task.d.ts +9 -3
- package/dist/core/runner/prepare-task.js +37 -11
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +8 -1
- package/dist/core/task-registry-agent.d.ts +13 -3
- package/dist/core/task-registry-agent.js +51 -21
- package/dist/core/task-registry-monitor.js +1 -1
- package/dist/core/task-registry-shared.d.ts +9 -0
- package/dist/core/task-registry.d.ts +6 -3
- package/dist/core/tool-policy.d.ts +44 -4
- package/dist/core/tool-policy.js +37 -3
- package/dist/core/tool-result-store.d.ts +108 -7
- package/dist/core/tool-result-store.js +95 -15
- package/dist/core/types.d.ts +115 -17
- package/dist/core/types.js +30 -1
- package/dist/engine/loop/types.d.ts +10 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +5 -3
- package/dist/orchestration/workflow.d.ts +9 -6
- package/dist/stores/file/checkpoint-store.d.ts +2 -1
- package/dist/stores/file/index.d.ts +1 -1
- package/dist/stores/file/tool-result-store.d.ts +41 -1
- package/dist/stores/file/tool-result-store.js +107 -19
- package/dist/tools/fs/fs-bash.d.ts +7 -0
- package/dist/tools/fs/fs-shared.d.ts +5 -0
- package/dist/tools/fs/fs-shared.js +11 -7
- package/dist/tools/fs/index.d.ts +6 -0
- package/dist/tools/fs/index.js +2 -0
- package/package.json +1 -1
|
@@ -126,7 +126,12 @@ export async function prepareMemory(input) {
|
|
|
126
126
|
};
|
|
127
127
|
};
|
|
128
128
|
const onEngineIncident = (err) => deps.onError?.(err, { phase: "memory", sessionId });
|
|
129
|
-
const
|
|
129
|
+
const adoptionRestricted = input.memoryPersistenceDeclared === false;
|
|
130
|
+
const retrievalBackend = (b, planeRestricted) => planeRestricted
|
|
131
|
+
? (b.restrictedAdoptionView?.({ audit: false }) ??
|
|
132
|
+
b.retrievalView?.() ??
|
|
133
|
+
b)
|
|
134
|
+
: (b.retrievalView?.() ?? b);
|
|
130
135
|
const planeScopes = (scopes, write) => [...new Set([...scopes, ...(write !== null ? [write] : [])])];
|
|
131
136
|
const pollutedOpts = (engine) => {
|
|
132
137
|
const rec = engine.sessionPollution(sessionId);
|
|
@@ -173,8 +178,8 @@ export async function prepareMemory(input) {
|
|
|
173
178
|
const personal = createPersonalEngine(personalBackendChosen);
|
|
174
179
|
const personalEngine = personal.engine;
|
|
175
180
|
const p = planes;
|
|
176
|
-
const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null);
|
|
177
|
-
const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null);
|
|
181
|
+
const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null, { adoptionRestricted });
|
|
182
|
+
const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null, { adoptionRestricted });
|
|
178
183
|
const writeIsPersonal = p.writePlane === "personal";
|
|
179
184
|
writeEngine = writeIsPersonal ? personalEngine : projectEngine;
|
|
180
185
|
writeHandle = writeIsPersonal ? personalHandle : projectHandle;
|
|
@@ -183,13 +188,13 @@ export async function prepareMemory(input) {
|
|
|
183
188
|
injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted }));
|
|
184
189
|
toolPlanes = [
|
|
185
190
|
{
|
|
186
|
-
backend: retrievalBackend(backend),
|
|
191
|
+
backend: retrievalBackend(backend, adoptionRestricted || p.writePlane !== "project"),
|
|
187
192
|
scopes: planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null),
|
|
188
193
|
recordRetrieved: (ids) => projectEngine.recordRetrieved(ids),
|
|
189
194
|
challengeExclusions: () => projectEngine.readChallengeExclusions(),
|
|
190
195
|
},
|
|
191
196
|
{
|
|
192
|
-
backend: retrievalBackend(personal.backend),
|
|
197
|
+
backend: retrievalBackend(personal.backend, adoptionRestricted || p.writePlane !== "personal"),
|
|
193
198
|
scopes: planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null),
|
|
194
199
|
recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
|
|
195
200
|
challengeExclusions: () => personalEngine.readChallengeExclusions(),
|
|
@@ -216,14 +221,14 @@ export async function prepareMemory(input) {
|
|
|
216
221
|
else if (personalOnly) {
|
|
217
222
|
const personal = createPersonalEngine(choosePersonalBackend());
|
|
218
223
|
const personalEngine = personal.engine;
|
|
219
|
-
const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope);
|
|
224
|
+
const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
|
|
220
225
|
writeEngine = personalEngine;
|
|
221
226
|
writeHandle = handle;
|
|
222
227
|
injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
223
228
|
harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId, ...admitNothingOpts });
|
|
224
229
|
toolPlanes = [
|
|
225
230
|
{
|
|
226
|
-
backend: retrievalBackend(personal.backend),
|
|
231
|
+
backend: retrievalBackend(personal.backend, adoptionRestricted || memorySpec.writeScope === null),
|
|
227
232
|
scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
|
|
228
233
|
recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
|
|
229
234
|
challengeExclusions: () => personalEngine.readChallengeExclusions(),
|
|
@@ -237,14 +242,14 @@ export async function prepareMemory(input) {
|
|
|
237
242
|
controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
|
|
238
243
|
onIncident: onEngineIncident,
|
|
239
244
|
});
|
|
240
|
-
const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope);
|
|
245
|
+
const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
|
|
241
246
|
writeEngine = engine;
|
|
242
247
|
writeHandle = handle;
|
|
243
248
|
injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
244
249
|
harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId, ...admitNothingOpts });
|
|
245
250
|
toolPlanes = [
|
|
246
251
|
{
|
|
247
|
-
backend: retrievalBackend(backend),
|
|
252
|
+
backend: retrievalBackend(backend, adoptionRestricted || memorySpec.writeScope === null),
|
|
248
253
|
scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
|
|
249
254
|
recordRetrieved: (ids) => engine.recordRetrieved(ids),
|
|
250
255
|
challengeExclusions: () => engine.readChallengeExclusions(),
|
|
@@ -354,7 +354,10 @@ export interface Prepared {
|
|
|
354
354
|
* deferred; a caller with no accessor has no deferred family to describe. */
|
|
355
355
|
staticFaceFor?: (name: string) => boolean;
|
|
356
356
|
/**
|
|
357
|
-
* design/138 S1 — the MemoryEngine session
|
|
357
|
+
* design/138 S1 — the MemoryEngine session. Present when `deps.memoryBackend` + `spec.memory.enabled`
|
|
358
|
+
* hold AND the engine mount succeeded: a materialize failure without a `config.memory_*` code is
|
|
359
|
+
* fail-open (reported via `deps.onError`, the task runs memory-less), leaving this absent even though
|
|
360
|
+
* both flags hold.
|
|
358
361
|
* `harvest` is the swallow-guarded boundary hook (task terminal in runtask + the checkpoint mint
|
|
359
362
|
* point in commitSuspendSaga): it runs the FULL gate set (containment/secret/caps/deletion fuse),
|
|
360
363
|
* commits entry patches to the backend, self-heals the derived index, and re-baselines (a second
|
|
@@ -1296,8 +1299,11 @@ export interface RunInternals {
|
|
|
1296
1299
|
* design/99 (nested-subagent live tree) — an OPT-IN, DISPLAY-ONLY event sink a deployment sets on the TOP run to
|
|
1297
1300
|
* receive a subagent's live `task_progress` ticks (which otherwise stay in the child's ISOLATED stream). Threaded
|
|
1298
1301
|
* recursively down the delegation tree (via `ctx.forwardEvent`), so every nested subagent's ticks bubble to the
|
|
1299
|
-
* SAME sink. The Runner forwards
|
|
1300
|
-
*
|
|
1302
|
+
* SAME sink. The Runner's ctx wrapper forwards `task_progress` always; when the run's spec sets
|
|
1303
|
+
* `forwardSubagentEvents: true` it ALSO forwards the child's content events (`text_delta` / `reasoning_delta` /
|
|
1304
|
+
* `tool_start` / `tool_end` — the subagent viewing pane, carrying the same UNTRUSTED-RAW/consumer-must-redact
|
|
1305
|
+
* contract as the main stream's tool events). Either way the child stream is NEVER merged into the parent's
|
|
1306
|
+
* MODEL context (this is purely a render channel). Absent unless the deployment opted in.
|
|
1301
1307
|
*/
|
|
1302
1308
|
onForwardEvent?: (event: TaskEvent) => void;
|
|
1303
1309
|
/**
|
|
@@ -29,7 +29,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
|
|
|
29
29
|
import { policyAskClassOf } from "../ask-class.js";
|
|
30
30
|
import { emitTrace } from "../trace.js";
|
|
31
31
|
import { createSessionRulePolicy } from "./session-rule-policy.js";
|
|
32
|
-
import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, runToolGate } from "../hooks.js";
|
|
32
|
+
import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, persistedRuleMandateOf, runToolGate } from "../hooks.js";
|
|
33
33
|
import { orgRuleVerdictFor } from "../permission-rule-org.js";
|
|
34
34
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
35
35
|
import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
|
|
@@ -89,10 +89,14 @@ import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOI
|
|
|
89
89
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
90
90
|
import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
|
|
91
91
|
import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
|
|
92
|
+
import { deliverEngineNotice } from "../types.js";
|
|
92
93
|
const announcedMaterializeEnv = new Set();
|
|
93
94
|
export function __resetMaterializeEnvAnnouncements() {
|
|
94
95
|
announcedMaterializeEnv.clear();
|
|
95
96
|
}
|
|
97
|
+
function emitMaterializeEnvNotice(onNotice, message, detail) {
|
|
98
|
+
deliverEngineNotice(onNotice, { code: "config.materialize_env_discarded", message, detail });
|
|
99
|
+
}
|
|
96
100
|
const RECONCILE_MAX_RETRIES = 3;
|
|
97
101
|
const DEFAULT_MAX_SUSPENDS = 5;
|
|
98
102
|
const TASK_LIMIT_KEY_DICT = {
|
|
@@ -1659,6 +1663,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1659
1663
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1660
1664
|
...(internals?.onTaskNotification !== undefined ? { taskNotification: internals.onTaskNotification } : {}),
|
|
1661
1665
|
...(internals?.detachHub !== undefined ? { detachHub: internals.detachHub } : {}),
|
|
1666
|
+
...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}),
|
|
1662
1667
|
pdfModelCapabilities: pdfModelCapabilitiesOf(model),
|
|
1663
1668
|
...(deps.hands?.bashReadonlyAllow !== undefined ? { bashReadonlyAllow: deps.hands.bashReadonlyAllow } : {}),
|
|
1664
1669
|
...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
|
|
@@ -2606,7 +2611,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2606
2611
|
const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(raw)} is not "swap" or "static" — inert on this task (no deferred tools), but a deferring task WITHOUT an explicit spec strategy will refuse to prepare under it (an explicit legal spec outranks and discards it, loudly). Fix or unset the flag.`;
|
|
2607
2612
|
if (!announcedMaterializeEnv.has(line)) {
|
|
2608
2613
|
announcedMaterializeEnv.add(line);
|
|
2609
|
-
|
|
2614
|
+
emitMaterializeEnvNotice(deps.onNotice, line, { raw });
|
|
2610
2615
|
}
|
|
2611
2616
|
}
|
|
2612
2617
|
}
|
|
@@ -2628,7 +2633,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2628
2633
|
const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(rawEnvStrategy)} was ignored — not "swap" or "static", and the task spec pins toolMaterializeStrategy=${JSON.stringify(spec.toolMaterializeStrategy)} which outranks it. Fix or unset the env flag.`;
|
|
2629
2634
|
if (!announcedMaterializeEnv.has(line)) {
|
|
2630
2635
|
announcedMaterializeEnv.add(line);
|
|
2631
|
-
|
|
2636
|
+
emitMaterializeEnvNotice(deps.onNotice, line, { raw: rawEnvStrategy, specStrategy: spec.toolMaterializeStrategy });
|
|
2632
2637
|
}
|
|
2633
2638
|
}
|
|
2634
2639
|
const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
|
|
@@ -3260,9 +3265,25 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3260
3265
|
},
|
|
3261
3266
|
};
|
|
3262
3267
|
})();
|
|
3263
|
-
const ruleSuggestionsOf = (toolName, args) => {
|
|
3268
|
+
const ruleSuggestionsOf = (toolName, args, ask) => {
|
|
3264
3269
|
if (permissionRuleLane === undefined || toolName !== PERSISTED_RULE_TOOL)
|
|
3265
3270
|
return {};
|
|
3271
|
+
if ((spec.principal === undefined || spec.principal === "") && deps.localOwnerRules !== true)
|
|
3272
|
+
return {};
|
|
3273
|
+
if (ask?.requiresRealApproval === true ||
|
|
3274
|
+
ask?.persistedRuleShadowed !== undefined ||
|
|
3275
|
+
ask?.decisionReason === "hook" ||
|
|
3276
|
+
ask?.inheritedUnresolved === true ||
|
|
3277
|
+
ask?.ancestorResolved === true) {
|
|
3278
|
+
return {};
|
|
3279
|
+
}
|
|
3280
|
+
if (persistedRuleMandateOf({
|
|
3281
|
+
egress: egressTools.has(toolName),
|
|
3282
|
+
irreversibility: irreversibilityTier.get(toolName),
|
|
3283
|
+
shellGated: shellGatedBash,
|
|
3284
|
+
}) !== undefined) {
|
|
3285
|
+
return {};
|
|
3286
|
+
}
|
|
3266
3287
|
const command = args?.command;
|
|
3267
3288
|
if (typeof command !== "string")
|
|
3268
3289
|
return {};
|
|
@@ -3315,7 +3336,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3315
3336
|
toolName: creq.toolName,
|
|
3316
3337
|
toolCallId: creq.toolCallId,
|
|
3317
3338
|
args: editArgs,
|
|
3318
|
-
...ruleSuggestionsOf(creq.toolName, editArgs),
|
|
3339
|
+
...ruleSuggestionsOf(creq.toolName, editArgs, { ...(re ?? {}), ancestorResolved: true }),
|
|
3319
3340
|
message: re.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
|
|
3320
3341
|
...askSourceIdentity(),
|
|
3321
3342
|
...riskAxesOf(creq.toolName),
|
|
@@ -3400,7 +3421,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3400
3421
|
toolName: creq.toolName,
|
|
3401
3422
|
toolCallId: creq.toolCallId,
|
|
3402
3423
|
args: presentedArgs,
|
|
3403
|
-
...ruleSuggestionsOf(creq.toolName, presentedArgs),
|
|
3424
|
+
...ruleSuggestionsOf(creq.toolName, presentedArgs, { ...(first.action === "ask" ? first : {}), ancestorResolved: true }),
|
|
3404
3425
|
message: first.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
|
|
3405
3426
|
...askSourceIdentity(),
|
|
3406
3427
|
...riskAxesOf(creq.toolName),
|
|
@@ -3487,7 +3508,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3487
3508
|
toolName: creq.toolName,
|
|
3488
3509
|
toolCallId: creq.toolCallId,
|
|
3489
3510
|
args: presentedArgs,
|
|
3490
|
-
...ruleSuggestionsOf(creq.toolName, presentedArgs),
|
|
3511
|
+
...ruleSuggestionsOf(creq.toolName, presentedArgs, { ...(decision.action === "ask" ? decision : {}), ancestorResolved: true }),
|
|
3491
3512
|
message: decision.message ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
|
|
3492
3513
|
...askSourceIdentity(),
|
|
3493
3514
|
...riskAxesOf(creq.toolName),
|
|
@@ -3800,7 +3821,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3800
3821
|
const preview = approvalPreviewOf(req.toolName, req.args);
|
|
3801
3822
|
return preview !== undefined ? { preview } : {};
|
|
3802
3823
|
})(),
|
|
3803
|
-
...ruleSuggestionsOf(req.toolName, req.args),
|
|
3824
|
+
...ruleSuggestionsOf(req.toolName, req.args, decision.action === "ask" ? decision : undefined),
|
|
3804
3825
|
message: decision.message ?? `approval required for "${req.toolName}"`,
|
|
3805
3826
|
...askSourceIdentity(),
|
|
3806
3827
|
...riskAxesOf(req.toolName),
|
|
@@ -4287,7 +4308,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4287
4308
|
}
|
|
4288
4309
|
};
|
|
4289
4310
|
const suspendAsk = parkLaneArmed && checkpointStore !== undefined
|
|
4290
|
-
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule) => {
|
|
4311
|
+
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason) => {
|
|
4291
4312
|
const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
|
|
4292
4313
|
if (syncFirstEligible &&
|
|
4293
4314
|
runtimeCaps?.forceDurableGate !== true &&
|
|
@@ -4442,7 +4463,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4442
4463
|
const preview = approvalPreviewOf(req.toolName, parkedArgs);
|
|
4443
4464
|
return preview !== undefined ? { preview } : {};
|
|
4444
4465
|
})(),
|
|
4445
|
-
...ruleSuggestionsOf(req.toolName, parkedArgs
|
|
4466
|
+
...ruleSuggestionsOf(req.toolName, parkedArgs, {
|
|
4467
|
+
...(realApproval !== undefined ? { requiresRealApproval: true } : {}),
|
|
4468
|
+
...(shadowedRule !== undefined ? { persistedRuleShadowed: shadowedRule } : {}),
|
|
4469
|
+
...(askDecisionReason !== undefined ? { decisionReason: askDecisionReason } : {}),
|
|
4470
|
+
...(inheritedUnavailableAsks.has(req.toolCallId) ? { inheritedUnresolved: true } : {}),
|
|
4471
|
+
}),
|
|
4446
4472
|
boundInputHash: boundInputHashOf(parkedArgs),
|
|
4447
4473
|
batchToolCallIds,
|
|
4448
4474
|
completedCallIds,
|
|
@@ -4710,7 +4736,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4710
4736
|
...(offloadStore
|
|
4711
4737
|
? {
|
|
4712
4738
|
offload: {
|
|
4713
|
-
persist: createOffloadPersist(offloadStore, sessionId),
|
|
4739
|
+
persist: createOffloadPersist(offloadStore, sessionId, deps.onNotice),
|
|
4714
4740
|
},
|
|
4715
4741
|
}
|
|
4716
4742
|
: {}),
|
|
@@ -250,7 +250,14 @@ export declare class Runner {
|
|
|
250
250
|
* env_failed re-resume supplied a decision ≠ the persisted winner), `reopened_concurrently` (the
|
|
251
251
|
* optimistic-concurrency `rev` changed under a concurrent resolve/reopen — re-resume against current
|
|
252
252
|
* state), `unsupported_version` (checkpoint newer than this worker / remote handle with no factory),
|
|
253
|
-
* `already_resolved` (lost the CAS — idempotent no-op)
|
|
253
|
+
* `already_resolved` (lost the CAS — idempotent no-op), `walltime_axis_retired` (pre-CAS: the
|
|
254
|
+
* persisted ledger carries the retired wall-clock budget axis — a worker of the previous release can
|
|
255
|
+
* still finish it), `resume_aborted` (the caller's signal was already aborted at entry, or aborted
|
|
256
|
+
* during the resume — pre-CAS/edit-re-adjudication legs leave the checkpoint pending and resumable,
|
|
257
|
+
* the post-claim leg leaves it consumed; the message says which), `reopen_failed` (an aborted resume
|
|
258
|
+
* needed to reopen the checkpoint and the store refused, or the reopen failed in flight — the message
|
|
259
|
+
* distinguishes terminally-consumed from state-unprovable). Each code's full contract is on
|
|
260
|
+
* {@link CheckpointError.code}.
|
|
254
261
|
*/
|
|
255
262
|
resume(token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig,
|
|
256
263
|
/** RB-48② (1.404): same TRUSTED run-internals seam as {@link resumeStream} — the convenience wrapper
|
|
@@ -131,7 +131,10 @@ const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call t
|
|
|
131
131
|
function toolEndBodyFrom(result, isError, settledBy) {
|
|
132
132
|
const o = toolOutputFrom(result);
|
|
133
133
|
const st = structuredFrom(result);
|
|
134
|
-
const
|
|
134
|
+
const det = isError ? result?.details : undefined;
|
|
135
|
+
const codeRaw = det?.code;
|
|
136
|
+
const kindRaw = det?.errorKind;
|
|
137
|
+
const code = typeof codeRaw === "string" ? codeRaw : typeof kindRaw === "string" ? kindRaw : undefined;
|
|
135
138
|
return {
|
|
136
139
|
...(o !== undefined ? { output: o.output, ...(o.truncated ? { truncated: true } : {}), ...(o.totalChars !== undefined ? { totalChars: o.totalChars } : {}) } : {}),
|
|
137
140
|
...(st !== undefined ? { structured: st } : {}),
|
|
@@ -1412,6 +1415,10 @@ export class Runner {
|
|
|
1412
1415
|
return runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles });
|
|
1413
1416
|
}
|
|
1414
1417
|
runTaskStream(spec, resume, internals) {
|
|
1418
|
+
if (resume !== undefined && (typeof resume !== "object" || resume.outcome === undefined)) {
|
|
1419
|
+
throw new TypeError("runTaskStream: `resume` must be a ResumeRun carrying `outcome` — got a value without one. " +
|
|
1420
|
+
"(Note the parameter order: runTaskStream(spec, resume?, internals?) — internals is the THIRD parameter.)");
|
|
1421
|
+
}
|
|
1415
1422
|
const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor);
|
|
1416
1423
|
const queue = new PushQueue();
|
|
1417
1424
|
const detachHub = new ToolDetachHub();
|
|
@@ -415,9 +415,10 @@ export declare function notFoundRunningAgentsTail(footer: {
|
|
|
415
415
|
* so a structured consumer lost the failure FACT at exactly the moment it could no longer reach the
|
|
416
416
|
* live handle, left to scrape the prose body's `error:` line.
|
|
417
417
|
*
|
|
418
|
-
* The
|
|
419
|
-
*
|
|
420
|
-
*
|
|
418
|
+
* The fact gates live here too (failed ⇒ error/errorCode/retryable/retryAfterMs; `stoppedBy` rides
|
|
419
|
+
* whenever the ROW carries it, on any status — a reaper-settled `failed` row keeps its attribution,
|
|
420
|
+
* see the widening note in the builder) rather than at the call sites: which facts a given row may
|
|
421
|
+
* carry is part of the same contract, and a gate copied per face is the same drift with extra steps. `error` is model/provider-influenceable text, so
|
|
421
422
|
* the fencing + bounding (RB-386②'s posture) happens once, here, for both faces.
|
|
422
423
|
* The parked projection is this builder with fewer facts, not a third literal. */
|
|
423
424
|
export interface AgentPollDetailsInput {
|
|
@@ -470,6 +471,15 @@ export declare function serveDurableAgentRowLane(row: BackgroundAgentRecord): Un
|
|
|
470
471
|
*
|
|
471
472
|
* No store configured (a deployment that never wired a `toolResultStore`) ⇒ returns `clipped` UNCHANGED
|
|
472
473
|
* — the legal degrade design/158 §2.2 calls for, byte-identical to pre-S1 behavior.
|
|
474
|
+
*
|
|
475
|
+
* Backlog #169 — a FAILING `put` (throwing or rejecting; the store contract admits both dialects) is
|
|
476
|
+
* NOT fatal to the poll: the clipped text is still served, with {@link AGENT_SPILL_FAILED_NOTE} in
|
|
477
|
+
* place of the ref disclosure (advertising a ref nobody can read back would be a false promise), and
|
|
478
|
+
* `handle.spillFailed` latches so later polls of the same cycle repeat the honest note instead of
|
|
479
|
+
* retrying the write — see the catch below for why latch-not-retry. Same "store failure is never
|
|
480
|
+
* fatal" posture as the monitor/offload/budget/compaction write sites. A put that settles AFTER a
|
|
481
|
+
* revive (stale cycle) is generation-guarded: it answers its own caller honestly but never writes
|
|
482
|
+
* spill state onto the new cycle (see `mintCycle` below).
|
|
473
483
|
*/
|
|
474
484
|
export declare function spillClippedAgentResult(handle: BackgroundAgentTaskHandle, full: string, clipped: string, store: ToolResultStore | undefined, sessionId: string | undefined): Promise<string>;
|
|
475
485
|
export declare function pollBackgroundAgentLane(handle: BackgroundAgentTaskHandle, deadline?: number, signal?: AbortSignal,
|
|
@@ -981,6 +981,7 @@ export async function reviveBackgroundAgentLane(core, id, access, abort) {
|
|
|
981
981
|
handle.result = undefined;
|
|
982
982
|
handle.resultFull = undefined;
|
|
983
983
|
handle.spillRef = undefined;
|
|
984
|
+
handle.spillFailed = undefined;
|
|
984
985
|
handle.error = undefined;
|
|
985
986
|
handle.errorCode = undefined;
|
|
986
987
|
handle.errorRetryable = undefined;
|
|
@@ -1226,17 +1227,33 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
|
|
|
1226
1227
|
...(row.status === "failed" ? { isError: true } : {}),
|
|
1227
1228
|
};
|
|
1228
1229
|
}
|
|
1230
|
+
const AGENT_SPILL_FAILED_NOTE = "\n\n[full output could not be spilled to the offload store (the write failed or went unconfirmed) — no ref is available to read the dropped middle back.]";
|
|
1229
1231
|
export async function spillClippedAgentResult(handle, full, clipped, store, sessionId) {
|
|
1230
1232
|
if (clipped === full)
|
|
1231
1233
|
return clipped;
|
|
1232
1234
|
if (store === undefined)
|
|
1233
1235
|
return clipped;
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1236
|
+
const disclose = (ref) => `${clipped}\n\n[full output persisted — call ${OFFLOAD_TOOL_NAME} with ref "${ref}" to read it back.]`;
|
|
1237
|
+
if (handle.spillRef !== undefined)
|
|
1238
|
+
return disclose(handle.spillRef);
|
|
1239
|
+
if (handle.spillFailed === true)
|
|
1240
|
+
return `${clipped}${AGENT_SPILL_FAILED_NOTE}`;
|
|
1241
|
+
const mintCycle = handle.reviveCycle ?? 0;
|
|
1242
|
+
const ref = buildToolResultRef(sessionId ?? "no-session", handle.id, `c${mintCycle}`);
|
|
1243
|
+
try {
|
|
1244
|
+
await store.put(ref, full, sessionId === undefined ? undefined : toolResultProvenanceOf(sessionId));
|
|
1245
|
+
}
|
|
1246
|
+
catch {
|
|
1247
|
+
if ((handle.reviveCycle ?? 0) === mintCycle) {
|
|
1248
|
+
if (handle.spillRef !== undefined)
|
|
1249
|
+
return disclose(handle.spillRef);
|
|
1250
|
+
handle.spillFailed = true;
|
|
1251
|
+
}
|
|
1252
|
+
return `${clipped}${AGENT_SPILL_FAILED_NOTE}`;
|
|
1238
1253
|
}
|
|
1239
|
-
|
|
1254
|
+
if ((handle.reviveCycle ?? 0) === mintCycle)
|
|
1255
|
+
handle.spillRef = ref;
|
|
1256
|
+
return disclose(ref);
|
|
1240
1257
|
}
|
|
1241
1258
|
export async function pollBackgroundAgentLane(handle, deadline, signal, oneShot, store, sessionId) {
|
|
1242
1259
|
while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
|
|
@@ -1256,10 +1273,23 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
|
|
|
1256
1273
|
}),
|
|
1257
1274
|
};
|
|
1258
1275
|
}
|
|
1259
|
-
const
|
|
1276
|
+
const snap = {
|
|
1277
|
+
status: handle.status,
|
|
1278
|
+
result: handle.result,
|
|
1279
|
+
resultIsPartial: handle.resultIsPartial,
|
|
1280
|
+
error: handle.error,
|
|
1281
|
+
errorKind: handle.errorKind,
|
|
1282
|
+
errorCode: handle.errorCode,
|
|
1283
|
+
errorRetryable: handle.errorRetryable,
|
|
1284
|
+
errorRetryAfterMs: handle.errorRetryAfterMs,
|
|
1285
|
+
cycleSeq: handle.cycleSeq,
|
|
1286
|
+
stoppedBy: handle.stoppedBy,
|
|
1287
|
+
completionId: handle.completionId,
|
|
1288
|
+
};
|
|
1289
|
+
const fullResult = snap.result ? (handle.resultFull ?? snap.result) : undefined;
|
|
1260
1290
|
const resultText = fullResult !== undefined ? await spillClippedAgentResult(handle, fullResult, clipTaskOutput(fullResult, handle.outputFile), store, sessionId) : undefined;
|
|
1261
|
-
const kindClause =
|
|
1262
|
-
? ` (error_kind: ${
|
|
1291
|
+
const kindClause = snap.status === "failed" && snap.errorKind !== undefined && snap.errorRetryable !== undefined
|
|
1292
|
+
? ` (error_kind: ${snap.errorKind}, retryable: ${snap.errorRetryable}${snap.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${snap.errorRetryAfterMs}` : ""})`
|
|
1263
1293
|
: "";
|
|
1264
1294
|
const body = running
|
|
1265
1295
|
? oneShot === true
|
|
@@ -1267,26 +1297,26 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
|
|
|
1267
1297
|
This is a ONE-SHOT submission — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput({ task_id: "${handle.id}", block: true }). If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget.`
|
|
1268
1298
|
: `status: running
|
|
1269
1299
|
The agent is still working — you will be notified when it completes.`
|
|
1270
|
-
: `status: ${
|
|
1271
|
-
${
|
|
1272
|
-
` : ""}${
|
|
1300
|
+
: `status: ${snap.status}
|
|
1301
|
+
${snap.error ? `error: ${snap.error}${kindClause}
|
|
1302
|
+
` : ""}${snap.result ? `--- result${snap.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
|
|
1273
1303
|
${resultText}` : "(no result text)"}`;
|
|
1274
1304
|
return {
|
|
1275
1305
|
content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
|
|
1276
1306
|
details: buildAgentPollDetails({
|
|
1277
1307
|
taskId: handle.id,
|
|
1278
|
-
status:
|
|
1308
|
+
status: snap.status,
|
|
1279
1309
|
retrievalStatus: retrieval,
|
|
1280
|
-
...(
|
|
1281
|
-
...(
|
|
1282
|
-
...(
|
|
1283
|
-
...(
|
|
1284
|
-
...(
|
|
1285
|
-
...(
|
|
1286
|
-
...(
|
|
1287
|
-
...(
|
|
1310
|
+
...(snap.cycleSeq !== undefined ? { seq: snap.cycleSeq } : {}),
|
|
1311
|
+
...(snap.stoppedBy !== undefined ? { stoppedBy: snap.stoppedBy } : {}),
|
|
1312
|
+
...(snap.error !== undefined ? { error: snap.error } : {}),
|
|
1313
|
+
...(snap.errorCode !== undefined ? { errorCode: snap.errorCode } : {}),
|
|
1314
|
+
...(snap.errorRetryable !== undefined ? { errorRetryable: snap.errorRetryable } : {}),
|
|
1315
|
+
...(snap.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: snap.errorRetryAfterMs } : {}),
|
|
1316
|
+
...(snap.resultIsPartial === true ? { resultIsPartial: true } : {}),
|
|
1317
|
+
...(snap.completionId !== undefined ? { completionId: snap.completionId } : {}),
|
|
1288
1318
|
}),
|
|
1289
|
-
...(
|
|
1319
|
+
...(snap.status === "failed" ? { isError: true } : {}),
|
|
1290
1320
|
};
|
|
1291
1321
|
}
|
|
1292
1322
|
export async function stopBackgroundAgentLane(core, handle) {
|
|
@@ -68,7 +68,7 @@ function spillRolledMonitorChunk(handle, stream, dropped) {
|
|
|
68
68
|
else
|
|
69
69
|
handle.spillErrSegCount = n + 1;
|
|
70
70
|
try {
|
|
71
|
-
const provenance = handle.spillSessionId === undefined ? undefined : toolResultProvenanceOf(handle.spillSessionId
|
|
71
|
+
const provenance = handle.spillSessionId === undefined ? undefined : toolResultProvenanceOf(handle.spillSessionId);
|
|
72
72
|
void Promise.resolve(store.put(ref, dropped, provenance)).catch(() => {
|
|
73
73
|
handle.spillFailed = true;
|
|
74
74
|
});
|
|
@@ -324,6 +324,15 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
|
|
|
324
324
|
* eventual spill lands under a DIFFERENT ref rather than colliding with (and being silently refused
|
|
325
325
|
* by the write-once store under) cycle 1's. */
|
|
326
326
|
spillRef?: string;
|
|
327
|
+
/** Backlog #169 — the spill `put` for the CURRENT revive cycle threw or rejected. One-time latch,
|
|
328
|
+
* scoped to the cycle ({@link import("./task-registry-agent.js").reviveBackgroundAgentLane} clears
|
|
329
|
+
* it in lockstep with `spillRef`): later polls serve the clipped text with an honest ref-free loss
|
|
330
|
+
* note instead of retrying the write — the ref is minted deterministically, so the known permanent
|
|
331
|
+
* failure class (the store's write-once/provenance conflict) would fail identically on every poll,
|
|
332
|
+
* each retry a guaranteed-failure round trip to a durable backend. The monitor lane's
|
|
333
|
+
* `spillFailed` twin (same degrade discipline, different write shape — that one is fire-and-forget,
|
|
334
|
+
* this one is awaited). */
|
|
335
|
+
spillFailed?: true;
|
|
327
336
|
error?: string;
|
|
328
337
|
/** RB-386② ([2090]) — the machine-readable failure code beside `error`, threaded from the settle
|
|
329
338
|
* mint point (subagent.ts computes it ONCE from the child's TaskResult.errorCode taxonomy /
|
|
@@ -33,8 +33,10 @@ export interface RegisterBackgroundBashInput extends TaskAccess {
|
|
|
33
33
|
/** CC 2.1.209 对齐批A A5: the task's OUTPUT FILE — the launch receipt advertises it and the watcher
|
|
34
34
|
* mirrors every polled increment into it (append-only, unbounded — unlike the rolling in-memory
|
|
35
35
|
* spool), so "Read the output file path" is a real alternative to TaskOutput. Terminal
|
|
36
|
-
* task-notifications carry the same path (`output_file`).
|
|
37
|
-
* watcher
|
|
36
|
+
* task-notifications carry the same path (`output_file`). Primarily meaningful with `onTerminal`
|
|
37
|
+
* (only the watcher mirrors increments live); a watcher-less row still gets ONE final best-effort
|
|
38
|
+
* append at TaskStop/run-teardown (the tail drain), so the file is written even without a watcher —
|
|
39
|
+
* there is just no live mirror. Callers create the file (empty) before registering. */
|
|
38
40
|
outputFile?: string;
|
|
39
41
|
}
|
|
40
42
|
export interface TaskPollOptions {
|
|
@@ -369,7 +371,8 @@ export declare class TaskRegistry {
|
|
|
369
371
|
* BEFORE the abort()/status flip — the ordering is load-bearing: the guard below refuses markers on a
|
|
370
372
|
* non-running handle, so a caller that flips first loses its claim and attribution falls back to
|
|
371
373
|
* "system". First-marker-wins: an earlier marker (e.g. a service-wire "user") is never overwritten.
|
|
372
|
-
* Applies to background_agent
|
|
374
|
+
* Applies to background_agent, background_bash AND monitor handles (the markable kinds — the monitor
|
|
375
|
+
* watcher/stop lanes mark and read it too); workflow cancellation is out of scope. */
|
|
373
376
|
markStopSource(id: string, source: StopSource): void;
|
|
374
377
|
/**
|
|
375
378
|
* [1712] / RB-164 — attribute an ENV-LEVEL blanket sweep before it runs.
|
|
@@ -78,7 +78,8 @@ export interface ToolCallRequest {
|
|
|
78
78
|
* - `"org_unavailable"` — an org-governed deployment could not adjudicate against a snapshot, so the
|
|
79
79
|
* whole decision boundary failed closed (see `ORG_UNAVAILABLE_DECISION_REASON`, the single
|
|
80
80
|
* spelling this word is minted from). */
|
|
81
|
-
|
|
81
|
+
declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
|
|
82
|
+
export type DecisionReason = (typeof DECISION_REASONS)[number];
|
|
82
83
|
/**
|
|
83
84
|
* WHO (or what) ENDED an approval — the machine-readable twin of a settlement's human-readable text,
|
|
84
85
|
* so a consumer tells "a person decided this" from "nobody answered" without prose-matching a sentence.
|
|
@@ -336,8 +337,39 @@ export declare function constraintChainDigest(chain: readonly ConstraintChainEnt
|
|
|
336
337
|
* out-of-contract decision object, so it is not a rewrite this layer is willing to report as audited.
|
|
337
338
|
* (A rewrite from an EARLIER, contract-shaped policy still rides out — {@link combinePolicies} adds it
|
|
338
339
|
* on the deny path per the observer contract.)
|
|
340
|
+
*
|
|
341
|
+
* The second arm screens `decisionReason` against the closed set ({@link DecisionReason}): a
|
|
342
|
+
* deployment-authored `ToolPolicy.check` returning a value outside it would otherwise ride silently
|
|
343
|
+
* into every consumer that BRANCHES on the word — provenance-keyed exclusions, audit rows, checkpoint
|
|
344
|
+
* discriminants — each of which would treat the unclassifiable value as "none of the reasons I know",
|
|
345
|
+
* a meaning the producing policy never chose. Same doctrine as the retired-field arm: out-of-contract
|
|
346
|
+
* input to the permission face is refused loudly, not passed through as an accidental tenth reason.
|
|
347
|
+
* An ABSENT `decisionReason` stays legal (it is optional; `undefined` is the typed spelling of
|
|
348
|
+
* absence), and hook results never reach here with a foreign word — the hook seam stamps its own
|
|
349
|
+
* (`hooks.ts`) — so this arm's live producers are exactly the deployment policies the tripwire exists
|
|
350
|
+
* to screen. Unlike the retired-field arm this one reads the property PLAINLY (prototype chain
|
|
351
|
+
* included), because it answers a different question: the retired arm asks whether the AUTHOR wrote
|
|
352
|
+
* the old field (own property = authorship), this arm asks what a CONSUMER would read — and every
|
|
353
|
+
* consumer branches on a plain `d.decisionReason` read, which the prototype can satisfy.
|
|
354
|
+
*
|
|
355
|
+
* SCOPE of the carrier check (ruled after three review rounds converged on the same root): it screens
|
|
356
|
+
* STRUCTURALLY unstable carriers — getters, prototype-supplied values — i.e. shapes an ordinary
|
|
357
|
+
* deployment can write by accident. It does NOT try to defeat a Proxy whose descriptor trap reports a
|
|
358
|
+
* data property while its get trap stays stateful: same-process JavaScript has no trust boundary a
|
|
359
|
+
* function can enforce (an adversary who ships such a Proxy can as easily patch this module), so
|
|
360
|
+
* chasing that shape adds complexity without adding a guarantee. The screen's promise is against
|
|
361
|
+
* drift and accident, not against a hostile co-resident.
|
|
362
|
+
*
|
|
363
|
+
* `reasonIsNonInput` (the HOOK seam's spelling — merged-code scan, 5.28 window): at that seam the
|
|
364
|
+
* field is documented as DISCARDED — every mint point downstream unconditionally re-stamps
|
|
365
|
+
* `decisionReason:"hook"` (fold, deny arm, delegated twin), and the allow path never reads it — so
|
|
366
|
+
* both reason arms are skipped there: refusing a value that cannot travel would fail-close a call
|
|
367
|
+
* over a field with no consumer, which inverted the discard contract the stamp exists to enforce.
|
|
368
|
+
* The retired-`reason` arm still applies (that one is about the MESSAGE channel, which does travel).
|
|
339
369
|
*/
|
|
340
|
-
export declare function refuseOutOfContractDecision(d: PermissionResult
|
|
370
|
+
export declare function refuseOutOfContractDecision(d: PermissionResult, opts?: {
|
|
371
|
+
reasonIsNonInput?: boolean;
|
|
372
|
+
}): PermissionResult;
|
|
341
373
|
/** The raw tool-name lists a name-keyed policy was built from (audit feed, see block note above). */
|
|
342
374
|
export interface ToolPolicyNameSets {
|
|
343
375
|
readonly allow?: readonly string[];
|
|
@@ -632,9 +664,16 @@ export interface AskRequest {
|
|
|
632
664
|
/**
|
|
633
665
|
* design/179 §4 — the persistable allow-rule forms this exact call could be covered by, so a surface can
|
|
634
666
|
* offer "allow, and stop asking me this" with something concrete behind it. Present only when a
|
|
635
|
-
* persisted allow-rule lane is armed AND the call is one the lane can speak for
|
|
667
|
+
* persisted allow-rule lane is armed AND the call is one the lane can speak for (a compound, a
|
|
636
668
|
* redirection or a substitution yields NO suggestion, which is the honest answer rather than an option
|
|
637
|
-
* that would be refused on redemption
|
|
669
|
+
* that would be refused on redemption) AND the ask is one a persisted rule could actually clear — a
|
|
670
|
+
* mandated ask (operator shellGate:"always", the tool's own egress/irreversibility marks, a
|
|
671
|
+
* `requiresRealApproval` demand) and an ask carrying {@link persistedRuleShadowed} offer none.
|
|
672
|
+
*
|
|
673
|
+
* CONTRACT — array order is display order, narrowest first: the EXACT form is always index 0, a
|
|
674
|
+
* broader reviewed PREFIX form (at most one) follows. Basis ≤ 2. Selection indices and redemption
|
|
675
|
+
* tickets are keyed against this order, and the durable park row carries the same array under the
|
|
676
|
+
* same contract.
|
|
638
677
|
*
|
|
639
678
|
* ADVISORY display metadata, never adjudication input, and never a rule by itself: minting one is a
|
|
640
679
|
* separate act that goes through the approval-record protocol, so a surface that ignores this field
|
|
@@ -844,3 +883,4 @@ export type ResolvedAsk = PermissionResult & {
|
|
|
844
883
|
* have ended, and `decisionReason: "mode"` is already the honest word for what produced them.
|
|
845
884
|
*/
|
|
846
885
|
export declare function resolveAsk(req: AskRequest, onAsk: OnAsk | undefined, signal?: AbortSignal): Promise<ResolvedAsk>;
|
|
886
|
+
export {};
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -7,6 +7,8 @@ import { boundInputHashOf } from "./canonical-json.js";
|
|
|
7
7
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
8
8
|
import { parsePermissionRule } from "./permission-rules.js";
|
|
9
9
|
import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
10
|
+
const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
|
|
11
|
+
const DECISION_REASON_SET = new Set(DECISION_REASONS);
|
|
10
12
|
export const APPROVAL_SETTLED_BY_VALUES = ["human", "timeout", "aborted"];
|
|
11
13
|
export function isApprovalSettledBy(v) {
|
|
12
14
|
return typeof v === "string" && APPROVAL_SETTLED_BY_VALUES.includes(v);
|
|
@@ -79,12 +81,44 @@ const ALLOW = { action: "allow" };
|
|
|
79
81
|
const RETIRED_TEXT_FIELD = "reason";
|
|
80
82
|
const RETIRED_TEXT_FIELD_DENY_MESSAGE = `a permission decision carries the retired "${RETIRED_TEXT_FIELD}" field — rename it to "message" (the one text field ` +
|
|
81
83
|
`a decision carries); denied fail-closed rather than executing a decision whose text this layer cannot read`;
|
|
82
|
-
export function refuseOutOfContractDecision(d) {
|
|
84
|
+
export function refuseOutOfContractDecision(d, opts) {
|
|
83
85
|
if (typeof d !== "object" || d === null)
|
|
84
86
|
return d;
|
|
85
|
-
if (
|
|
87
|
+
if (Object.prototype.hasOwnProperty.call(d, RETIRED_TEXT_FIELD)) {
|
|
88
|
+
return { action: "deny", message: RETIRED_TEXT_FIELD_DENY_MESSAGE, decisionReason: "rule" };
|
|
89
|
+
}
|
|
90
|
+
if (opts?.reasonIsNonInput === true)
|
|
91
|
+
return d;
|
|
92
|
+
const dr = d.decisionReason;
|
|
93
|
+
if (dr === undefined)
|
|
86
94
|
return d;
|
|
87
|
-
|
|
95
|
+
if (!DECISION_REASON_SET.has(dr)) {
|
|
96
|
+
let rendered;
|
|
97
|
+
try {
|
|
98
|
+
rendered = JSON.stringify(String(dr).slice(0, 64));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
rendered = `[unprintable ${typeof dr}]`;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
action: "deny",
|
|
105
|
+
message: `a permission decision carries an unrecognized "decisionReason" value (${rendered}) — ` +
|
|
106
|
+
`the closed set is ${DECISION_REASONS.join("/")}; denied fail-closed rather than letting consumers ` +
|
|
107
|
+
`branch on a value this layer cannot classify`,
|
|
108
|
+
decisionReason: "rule",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const carrier = Object.getOwnPropertyDescriptor(d, "decisionReason");
|
|
112
|
+
if (carrier === undefined || !("value" in carrier)) {
|
|
113
|
+
return {
|
|
114
|
+
action: "deny",
|
|
115
|
+
message: `a permission decision supplies "decisionReason" through ${carrier === undefined ? "its prototype" : "an accessor"} — ` +
|
|
116
|
+
`contract fields must be own data properties, so consumers re-reading the field cannot be shown a ` +
|
|
117
|
+
`different value than this screen validated; denied fail-closed`,
|
|
118
|
+
decisionReason: "rule",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return d;
|
|
88
122
|
}
|
|
89
123
|
const DEADLINE_ELAPSED = Symbol("approval.deadline_elapsed");
|
|
90
124
|
function withTimeout(p, ms, onTimeout) {
|