@sema-agent/core 5.23.0 → 5.25.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 +192 -1
- package/dist/core/checkpoint-store.d.ts +84 -10
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/hooks.d.ts +18 -5
- package/dist/core/hooks.js +28 -4
- package/dist/core/memory-engine/engine.js +18 -3
- package/dist/core/permission-rule-org.d.ts +9 -0
- package/dist/core/permission-rule-org.js +12 -5
- package/dist/core/permission-rule-store.d.ts +25 -14
- package/dist/core/permission-rule-store.js +5 -1
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +40 -9
- package/dist/core/runner/runtask.js +49 -6
- package/dist/core/runner/session-file-state-replay.js +3 -0
- package/dist/core/tool-policy.d.ts +11 -0
- package/dist/core/tool-policy.js +17 -15
- package/dist/core/tool-result-store.d.ts +8 -0
- package/dist/core/tool-result-store.js +77 -3
- package/dist/core/types.d.ts +7 -5
- package/dist/index.d.ts +13 -7
- package/dist/index.js +2 -2
- package/dist/stores/file/adoption/adopt.d.ts +23 -3
- package/dist/stores/file/adoption/adopt.js +4 -8
- package/dist/stores/file/adoption/marker.d.ts +41 -18
- package/dist/stores/file/adoption/marker.js +14 -7
- package/dist/stores/file/permission-rule-store.d.ts +15 -1
- package/dist/stores/file/permission-rule-store.js +4 -1
- package/dist/stores/file/session-policy-store.d.ts +11 -1
- package/dist/stores/file/session-policy-store.js +8 -3
- package/dist/stores/file/task-list-store.d.ts +15 -1
- package/dist/stores/file/task-list-store.js +2 -2
- package/dist/tools/fs/fs-bash.js +7 -4
- package/dist/tools/fs/fs-shared.js +23 -7
- package/dist/tools/monitor.js +3 -3
- package/package.json +1 -1
|
@@ -11,22 +11,29 @@ export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
|
|
|
11
11
|
export function settleOrgVerdictWithin(p, fallback, opts) {
|
|
12
12
|
return new Promise((resolve) => {
|
|
13
13
|
let settled = false;
|
|
14
|
-
const finish = (v) => {
|
|
14
|
+
const finish = (v, cause) => {
|
|
15
15
|
if (settled)
|
|
16
16
|
return;
|
|
17
17
|
settled = true;
|
|
18
18
|
clearTimeout(timer);
|
|
19
19
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
20
|
+
if (cause !== undefined) {
|
|
21
|
+
try {
|
|
22
|
+
opts.onFallback?.(cause);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
20
27
|
resolve(v);
|
|
21
28
|
};
|
|
22
|
-
const onAbort = () => finish(fallback);
|
|
23
|
-
const timer = setTimeout(() => finish(fallback), opts.timeoutMs);
|
|
29
|
+
const onAbort = () => finish(fallback, "aborted");
|
|
30
|
+
const timer = setTimeout(() => finish(fallback, "timeout"), opts.timeoutMs);
|
|
24
31
|
if (opts.signal?.aborted === true) {
|
|
25
|
-
finish(fallback);
|
|
32
|
+
finish(fallback, "aborted");
|
|
26
33
|
return;
|
|
27
34
|
}
|
|
28
35
|
opts.signal?.addEventListener("abort", onAbort);
|
|
29
|
-
p.then(finish, () => finish(fallback));
|
|
36
|
+
p.then((v) => finish(v), () => finish(fallback));
|
|
30
37
|
});
|
|
31
38
|
}
|
|
32
39
|
export const ORG_RULE_DECISION_REASON = "org_rule";
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* design/179 §8 — the persisted allow-rule store seam, its
|
|
2
|
+
* design/179 §8 — the persisted allow-rule store seam, its backend write face, and the removal entry.
|
|
3
3
|
*
|
|
4
4
|
* ## Two faces, deliberately unequal
|
|
5
5
|
*
|
|
6
|
-
* A deployment sees a READ face: `list()`, scoped to one verified principal by a factory.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* A deployment sees a READ face: `list()`, scoped to one verified principal by a factory. The write face
|
|
7
|
+
* is a BACKEND CONTRACT (ruled 2026-08-10: exported so an out-of-repo store twin builds against the same
|
|
8
|
+
* definitions instead of mirroring them), not a host write API — the only way a rule enters the store
|
|
9
|
+
* through the ENGINE is the redemption path (`permission-rule-consent.ts`), which requires an approved
|
|
10
|
+
* durable approval record; that invariant lives in the engine's wiring and is pinned by the
|
|
11
|
+
* writer-caller registry test, not in type visibility.
|
|
9
12
|
* `expectedRev` is concurrency control, not authorization, so "hold a put API and skip the ticket" is not
|
|
10
13
|
* a shape that exists here rather than a rule someone must remember.
|
|
11
14
|
*
|
|
@@ -156,15 +159,19 @@ export interface RuleSyncJoinDelta {
|
|
|
156
159
|
* What a write may say. Authorization discriminates on the DELTA SHAPE, not on a full snapshot: only the
|
|
157
160
|
* add arm can introduce a dot, and the delete arm carries a tombstone and no adds. A backend additionally
|
|
158
161
|
* REFUSES at runtime any delete that would introduce a new add dot — structure and runtime check together,
|
|
159
|
-
* so "pick the delete arm and smuggle an add" is neither expressible nor accepted. The `sync-join` arm
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* not at the
|
|
162
|
+
* so "pick the delete arm and smuggle an add" is neither expressible nor accepted. The `sync-join` arm's
|
|
163
|
+
* safety does not rest on hiding the type (ruled 2026-08-10: the backend contract IS exported for
|
|
164
|
+
* out-of-repo store twins) — it rests on every inbound record inside it passing the single validator
|
|
165
|
+
* again AT THE BACKEND: the fifth door of design/179 §4's validator list closes here, not at the
|
|
166
|
+
* calling layer, and closes identically for every caller.
|
|
163
167
|
*/
|
|
164
168
|
export type RuleWriteDelta = RuleAddDelta | RuleDeleteDelta | RuleSyncJoinDelta;
|
|
165
169
|
/**
|
|
166
|
-
* The
|
|
167
|
-
*
|
|
170
|
+
* The backend write face. Exported as part of the BACKEND CONTRACT (ruled 2026-08-10) so an
|
|
171
|
+
* out-of-repo store implementation hangs the same face the file backend does, instead of mirroring the
|
|
172
|
+
* types. The consent boundary is unchanged by the export: the ENGINE reaches a writer only through the
|
|
173
|
+
* consent protocol's redemption (and the sync client's join) — a property of the engine's wiring —
|
|
174
|
+
* and a deployment always owned its own storage bytes, so type visibility grants nothing new.
|
|
168
175
|
*/
|
|
169
176
|
export interface PermissionRuleWriter {
|
|
170
177
|
/** Mint the next dot for this replica. Counters need only be unique and monotonic, so a dot minted for
|
|
@@ -198,11 +205,11 @@ export interface RawRuleSyncState {
|
|
|
198
205
|
quarantined?: QuarantinedRuleAdd[];
|
|
199
206
|
}
|
|
200
207
|
/**
|
|
201
|
-
* The
|
|
202
|
-
*
|
|
208
|
+
* The handle a writable backend hangs its write face on (exported with the backend contract, ruled 2026-08-10;
|
|
209
|
+
* the consent boundary lives in the engine's wiring, not in this key's visibility).
|
|
203
210
|
*/
|
|
204
211
|
export declare const PERMISSION_RULE_WRITER = "__semaPermissionRuleWriter";
|
|
205
|
-
/** A store that also carries the
|
|
212
|
+
/** A store that also carries the backend write face. */
|
|
206
213
|
export interface WritablePermissionRuleStore extends PermissionRuleStore {
|
|
207
214
|
readonly [PERMISSION_RULE_WRITER]: PermissionRuleWriter;
|
|
208
215
|
}
|
|
@@ -419,7 +426,11 @@ export type RemoveResult =
|
|
|
419
426
|
export declare function removePersistedRule(opts: {
|
|
420
427
|
rule: string;
|
|
421
428
|
scope: RuleScope;
|
|
422
|
-
principal
|
|
429
|
+
/** Whose bucket. A bare string stays the principal shorthand (unchanged callers); the structural
|
|
430
|
+
* {@link RuleOwner} form adds the local-owner bucket (downstream request, 2026-08-10 — design/182 §4.5's
|
|
431
|
+
* `forLocalOwner()` face existed, but removal could not name it, so a local-owner rule was
|
|
432
|
+
* unrevokable through this entry). Same observed-remove/add-wins/stillLive semantics either way. */
|
|
433
|
+
principal: string | RuleOwner;
|
|
423
434
|
provider: PermissionRuleStoreProvider;
|
|
424
435
|
}): Promise<RemoveResult>;
|
|
425
436
|
export declare function errText(err: unknown): string;
|
|
@@ -327,7 +327,11 @@ export async function ruleStoreChecksum(payload) {
|
|
|
327
327
|
}
|
|
328
328
|
const REMOVE_MAX_ATTEMPTS = 8;
|
|
329
329
|
export async function removePersistedRule(opts) {
|
|
330
|
-
const
|
|
330
|
+
const owner = typeof opts.principal === "string" ? { kind: "principal", principal: opts.principal } : opts.principal;
|
|
331
|
+
if (owner.kind === "local-owner" && opts.provider.forLocalOwner === undefined) {
|
|
332
|
+
return { status: "failed", error: "this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be removed through it" };
|
|
333
|
+
}
|
|
334
|
+
const store = owner.kind === "local-owner" ? opts.provider.forLocalOwner() : opts.provider.forPrincipal(owner.principal);
|
|
331
335
|
const writer = writerOf(store);
|
|
332
336
|
if (writer === undefined) {
|
|
333
337
|
return { status: "failed", error: "the resolved permission-rule store has no write face — rules cannot be removed through it" };
|
|
@@ -28,6 +28,8 @@ import { type WiringManifest } from "../wiring-manifest.js";
|
|
|
28
28
|
import type { ActiveWorktreeSession, AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
|
|
29
29
|
import type { NestedUsageAccum, RunnerDeps, TaskEvent, TaskLimits, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
|
|
30
30
|
import type { RepairBundle } from "../../agents/repair-loop.js";
|
|
31
|
+
/** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
|
|
32
|
+
export declare function __resetMaterializeEnvAnnouncements(): void;
|
|
31
33
|
/**
|
|
32
34
|
* design/164 — validate `TaskSpec.limits` at the door and return it unchanged.
|
|
33
35
|
*
|
|
@@ -85,10 +85,14 @@ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-
|
|
|
85
85
|
import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
|
|
86
86
|
import { resolveKey } from "../../tools/fs/safety.js";
|
|
87
87
|
import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
|
|
88
|
-
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
88
|
+
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
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
|
+
const announcedMaterializeEnv = new Set();
|
|
93
|
+
export function __resetMaterializeEnvAnnouncements() {
|
|
94
|
+
announcedMaterializeEnv.clear();
|
|
95
|
+
}
|
|
92
96
|
const RECONCILE_MAX_RETRIES = 3;
|
|
93
97
|
const DEFAULT_MAX_SUSPENDS = 5;
|
|
94
98
|
const TASK_LIMIT_KEY_DICT = {
|
|
@@ -2572,6 +2576,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2572
2576
|
toolsDeltaRef = { pending: [], pendingRemoved: [], pendingReadded: [], pendingFailed: failedMcpServers };
|
|
2573
2577
|
}
|
|
2574
2578
|
const listingRideRef = {};
|
|
2579
|
+
{
|
|
2580
|
+
const raw = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
|
|
2581
|
+
if (raw !== undefined && raw !== "swap" && raw !== "static" && deferred.size === 0) {
|
|
2582
|
+
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.`;
|
|
2583
|
+
if (!announcedMaterializeEnv.has(line)) {
|
|
2584
|
+
announcedMaterializeEnv.add(line);
|
|
2585
|
+
console.warn(line);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2575
2589
|
if (deferred.size > 0) {
|
|
2576
2590
|
if (deferred.has(TOOL_SEARCH_NAME) || tools.some((t) => t.name === TOOL_SEARCH_NAME)) {
|
|
2577
2591
|
const e = new Error(`Tool name "${TOOL_SEARCH_NAME}" is reserved when deferred tools are present.`);
|
|
@@ -2579,12 +2593,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2579
2593
|
throw e;
|
|
2580
2594
|
}
|
|
2581
2595
|
const registry = buildDeferredRegistry(deferred, tools);
|
|
2582
|
-
const
|
|
2583
|
-
|
|
2584
|
-
|
|
2596
|
+
const rawEnvStrategy = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
|
|
2597
|
+
const envStrategyInvalid = rawEnvStrategy !== undefined && rawEnvStrategy !== "swap" && rawEnvStrategy !== "static";
|
|
2598
|
+
if (envStrategyInvalid && spec.toolMaterializeStrategy === undefined) {
|
|
2599
|
+
const e = new Error(`SEMA_TOOL_MATERIALIZE_STRATEGY must be "swap" or "static" (got ${JSON.stringify(rawEnvStrategy)}).`);
|
|
2585
2600
|
e.code = "config.tool_materialize_invalid";
|
|
2586
2601
|
throw e;
|
|
2587
2602
|
}
|
|
2603
|
+
if (envStrategyInvalid) {
|
|
2604
|
+
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.`;
|
|
2605
|
+
if (!announcedMaterializeEnv.has(line)) {
|
|
2606
|
+
announcedMaterializeEnv.add(line);
|
|
2607
|
+
console.warn(line);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
|
|
2588
2611
|
const requestedStrategy = spec.toolMaterializeStrategy ?? envStrategy ?? "swap";
|
|
2589
2612
|
const laneDegrade = requestedStrategy === "static" && spec.deferSelfResolve === false;
|
|
2590
2613
|
const materializeStatic = requestedStrategy === "static" && !laneDegrade;
|
|
@@ -3273,6 +3296,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3273
3296
|
...askSourceIdentity(),
|
|
3274
3297
|
...riskAxesOf(creq.toolName),
|
|
3275
3298
|
...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3299
|
+
...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
|
|
3276
3300
|
}, onAskOf, csignal ?? abortController.signal);
|
|
3277
3301
|
if (rr.action !== "allow")
|
|
3278
3302
|
return rr;
|
|
@@ -3357,6 +3381,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3357
3381
|
...askSourceIdentity(),
|
|
3358
3382
|
...riskAxesOf(creq.toolName),
|
|
3359
3383
|
...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3384
|
+
...(first.action === "ask" && first.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: first.persistedRuleShadowed } : {}),
|
|
3360
3385
|
}, pc.onAsk, csignal ?? abortController.signal);
|
|
3361
3386
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
3362
3387
|
if (resolved.action === "deny" && resolved.approverUnavailable === true) {
|
|
@@ -3443,6 +3468,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3443
3468
|
...askSourceIdentity(),
|
|
3444
3469
|
...riskAxesOf(creq.toolName),
|
|
3445
3470
|
...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3471
|
+
...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
|
|
3446
3472
|
}, pc.onAsk, csignal ?? abortController.signal);
|
|
3447
3473
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
3448
3474
|
if (resolved.action === "deny" && resolved.approverUnavailable === true) {
|
|
@@ -3755,6 +3781,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3755
3781
|
...askSourceIdentity(),
|
|
3756
3782
|
...riskAxesOf(req.toolName),
|
|
3757
3783
|
...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3784
|
+
...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
|
|
3758
3785
|
}, onAsk, abortController.signal);
|
|
3759
3786
|
const waitMs = Math.max(0, now() - t0);
|
|
3760
3787
|
if (resolved.approverUnavailable !== true) {
|
|
@@ -4236,7 +4263,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4236
4263
|
}
|
|
4237
4264
|
};
|
|
4238
4265
|
const suspendAsk = parkLaneArmed && checkpointStore !== undefined
|
|
4239
|
-
? async (req, postHookArgs, safety, liveFaceUnavailable) => {
|
|
4266
|
+
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule) => {
|
|
4240
4267
|
const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
|
|
4241
4268
|
if (syncFirstEligible &&
|
|
4242
4269
|
runtimeCaps?.forceDurableGate !== true &&
|
|
@@ -4341,16 +4368,20 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4341
4368
|
toolName: req.toolName,
|
|
4342
4369
|
args: parkedArgs,
|
|
4343
4370
|
safety,
|
|
4371
|
+
...(shadowedRule !== undefined ? { shadowedRule } : {}),
|
|
4344
4372
|
shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
|
|
4345
4373
|
...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
|
|
4346
4374
|
});
|
|
4347
4375
|
gate =
|
|
4348
|
-
safety !== undefined
|
|
4376
|
+
safety !== undefined || realApproval !== undefined
|
|
4349
4377
|
? {
|
|
4350
4378
|
kind: "irreversible_ask",
|
|
4351
|
-
reason:
|
|
4379
|
+
reason: safety !== undefined
|
|
4380
|
+
? `human approval required before safety-tightened tool "${req.toolName}"`
|
|
4381
|
+
: `real human approval required for tool "${req.toolName}" (non-budgetable: ${realApproval.origin})`,
|
|
4352
4382
|
toolName: req.toolName,
|
|
4353
|
-
safetyAxis: safety,
|
|
4383
|
+
...(safety !== undefined ? { safetyAxis: safety } : {}),
|
|
4384
|
+
...(realApproval !== undefined ? { realApproval } : {}),
|
|
4354
4385
|
riskDescriptor,
|
|
4355
4386
|
}
|
|
4356
4387
|
: durableApproval
|
|
@@ -4373,7 +4404,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4373
4404
|
const mintedAt = Date.now();
|
|
4374
4405
|
cp = {
|
|
4375
4406
|
token,
|
|
4376
|
-
version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4407
|
+
version: realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4377
4408
|
scope,
|
|
4378
4409
|
sessionId,
|
|
4379
4410
|
leafId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
2
2
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
3
|
-
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
3
|
+
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
4
4
|
import { engineVersion } from "../version.js";
|
|
5
5
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
6
6
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
@@ -71,6 +71,7 @@ const STOP_HOOK_BLOCK_CAP = 8;
|
|
|
71
71
|
const COMPACTION_REGROWTH_FACTOR = 1.5;
|
|
72
72
|
const COMPACTION_FREED_EPSILON = 256;
|
|
73
73
|
const BATCH_RESPONSE_MAX_CHARS = 500;
|
|
74
|
+
const ORG_DISCLOSURE_MAX_CHARS = 600;
|
|
74
75
|
function batchResponseDigest(result) {
|
|
75
76
|
const content = result !== null && typeof result === "object" ? result.content : result;
|
|
76
77
|
if (content === undefined || content === null)
|
|
@@ -3702,7 +3703,22 @@ export class Runner {
|
|
|
3702
3703
|
}
|
|
3703
3704
|
}
|
|
3704
3705
|
if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
3705
|
-
throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})
|
|
3706
|
+
throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`, { reason: "version_newer" });
|
|
3707
|
+
}
|
|
3708
|
+
const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
|
|
3709
|
+
const preCasBitWellFormed = preCasGateBit !== undefined &&
|
|
3710
|
+
typeof preCasGateBit === "object" &&
|
|
3711
|
+
(preCasGateBit.origin === "org_rule" ||
|
|
3712
|
+
preCasGateBit.origin === "org_unavailable" ||
|
|
3713
|
+
preCasGateBit.origin === "policy");
|
|
3714
|
+
if (checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? !preCasBitWellFormed : preCasGateBit !== undefined) {
|
|
3715
|
+
throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3716
|
+
? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed non-budgetable realApproval gate bit (origin org_rule/org_unavailable/policy) on an irreversible_ask gate — this row does not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
|
|
3717
|
+
: `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? "real_approval_damaged" : "real_approval_forged" });
|
|
3718
|
+
}
|
|
3719
|
+
if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
|
|
3720
|
+
this.deps.permissionRuleOrg === undefined) {
|
|
3721
|
+
throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
|
|
3706
3722
|
}
|
|
3707
3723
|
const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
|
|
3708
3724
|
if (retiredWalltimeTotal !== undefined) {
|
|
@@ -3714,7 +3730,7 @@ export class Runner {
|
|
|
3714
3730
|
throw new CheckpointError("checkpoint.resume_aborted", "the resume was handed an ALREADY-ABORTED signal — refusing to consume the approval on a leg that cannot run it (the checkpoint stays pending and is resumable with a live signal)");
|
|
3715
3731
|
}
|
|
3716
3732
|
if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
|
|
3717
|
-
throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env");
|
|
3733
|
+
throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env", { reason: "env_factory_missing" });
|
|
3718
3734
|
}
|
|
3719
3735
|
if (cp.state.inheritedGate?.requiresParentConstraint === true) {
|
|
3720
3736
|
const supplied = internals?.inheritedGate?.parentConstraints?.length ?? 0;
|
|
@@ -3730,6 +3746,10 @@ export class Runner {
|
|
|
3730
3746
|
"re-supply would run the resumed leg under a different ancestor chain than it suspended with; " +
|
|
3731
3747
|
"rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
|
|
3732
3748
|
}
|
|
3749
|
+
if (checkpointVersionOf(cp) >= F012_CHECKPOINT_VERSION &&
|
|
3750
|
+
(cp.state.inheritedGate.constraintChain === undefined || cp.state.inheritedGate.constraintDigest === undefined)) {
|
|
3751
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `a v${checkpointVersionOf(cp)} checkpoint that requires parent constraints must carry BOTH the frozen constraintChain and its constraintDigest (they are minted in one write with the version stamp) — this row carries neither or only one; refusing to fall back to the count-only contract on a damaged row (corruption / downgrade guard), the checkpoint stays pending`, { reason: "constraint_chain_missing" });
|
|
3752
|
+
}
|
|
3733
3753
|
const expectedDigest = cp.state.inheritedGate.constraintDigest;
|
|
3734
3754
|
if (expectedDigest !== undefined) {
|
|
3735
3755
|
const persistedChain = cp.state.inheritedGate.constraintChain;
|
|
@@ -4009,18 +4029,41 @@ export class Runner {
|
|
|
4009
4029
|
return;
|
|
4010
4030
|
}
|
|
4011
4031
|
}
|
|
4032
|
+
const gateRealApproval = resume.cp.gate.kind === "irreversible_ask" ? resume.cp.gate.realApproval : undefined;
|
|
4033
|
+
const gateOrgGoverned = gateRealApproval?.origin === "org_rule" || gateRealApproval?.origin === "org_unavailable";
|
|
4034
|
+
if (gateOrgGoverned && prepared.permissionRuleOrg === undefined) {
|
|
4035
|
+
const unwiredDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: its approval was minted under organization governance (${gateRealApproval.origin}), and this worker has no org adjudication wiring — a governed approval may only be redeemed where governance can be enforced. This approval is spent; re-issue the call on an org-wired worker.`);
|
|
4036
|
+
emitEnd(true, { content: unwiredDenial });
|
|
4037
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, unwiredDenial, true));
|
|
4038
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4012
4041
|
if (prepared.permissionRuleOrg !== undefined) {
|
|
4013
4042
|
const orgVerdict = prepared.permissionRuleOrg
|
|
4014
4043
|
.adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId })
|
|
4015
4044
|
.catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
|
|
4016
|
-
|
|
4045
|
+
let orgWaitCancelled = false;
|
|
4046
|
+
const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, {
|
|
4047
|
+
signal: prepared.abortController.signal,
|
|
4048
|
+
timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS,
|
|
4049
|
+
onFallback: (cause) => {
|
|
4050
|
+
orgWaitCancelled = cause === "aborted";
|
|
4051
|
+
},
|
|
4052
|
+
});
|
|
4017
4053
|
const blocked = org.status === "unavailable"
|
|
4018
|
-
?
|
|
4054
|
+
? gateRealApproval?.origin === "org_unavailable"
|
|
4055
|
+
? undefined
|
|
4056
|
+
: orgWaitCancelled
|
|
4057
|
+
? "this deployment is org-governed and the task was cancelled before the organization policy snapshot could be adjudicated"
|
|
4058
|
+
: "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
|
|
4019
4059
|
: org.verdict?.behavior === "deny"
|
|
4020
4060
|
? `an organization policy rule (${org.verdict.rule}) denies it`
|
|
4021
4061
|
: undefined;
|
|
4022
4062
|
if (blocked !== undefined) {
|
|
4023
|
-
const
|
|
4063
|
+
const orgDisclosures = org.status === "unavailable" && org.disclosures.length > 0
|
|
4064
|
+
? ` Governance disclosures: ${inlineUntrusted(org.disclosures.join("; "), ORG_DISCLOSURE_MAX_CHARS)}.`
|
|
4065
|
+
: "";
|
|
4066
|
+
const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}.${orgDisclosures} This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
|
|
4024
4067
|
emitEnd(true, { content: orgDenial });
|
|
4025
4068
|
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
|
|
4026
4069
|
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isAbsolutePathForm } from "../../tools/fs/safety.js";
|
|
2
|
+
import { isOffloadedDetailReplacement } from "../tool-result-store.js";
|
|
2
3
|
const READ_TOOL = "Read";
|
|
3
4
|
const WRITE_TOOL = "Write";
|
|
4
5
|
const RETRACTING_RESULTS = [
|
|
@@ -50,6 +51,8 @@ export function wholeFileRecordsFromTranscript(messages) {
|
|
|
50
51
|
const content = isRead ? wholeFileFromReadCard(rest) : typeof rest.content === "string" ? rest.content : undefined;
|
|
51
52
|
if (content === undefined)
|
|
52
53
|
continue;
|
|
54
|
+
if (isOffloadedDetailReplacement(content))
|
|
55
|
+
continue;
|
|
53
56
|
byPath.set(filePath, { path: filePath, content, at: m.timestamp });
|
|
54
57
|
}
|
|
55
58
|
return [...byPath.values()];
|
|
@@ -167,6 +167,13 @@ export type PermissionResult = {
|
|
|
167
167
|
message?: string;
|
|
168
168
|
decisionReason?: DecisionReason;
|
|
169
169
|
requiresRealApproval?: boolean;
|
|
170
|
+
/** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
|
|
171
|
+
* ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
|
|
172
|
+
* egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
|
|
173
|
+
* the classifier's questions, never a mandated one". Carries the matched rule text so a
|
|
174
|
+
* consumer (approval card, wire frame) can tell the person their rule is alive, just outranked.
|
|
175
|
+
* Absent ⇒ no rule matched, or the ask was cleared normally. */
|
|
176
|
+
persistedRuleShadowed?: string;
|
|
170
177
|
} | {
|
|
171
178
|
action: "deny";
|
|
172
179
|
updatedInput?: unknown;
|
|
@@ -601,6 +608,10 @@ export interface AskDelegationProvenance {
|
|
|
601
608
|
/** The structured context an `onAsk` approver receives for an `ask` decision (design/37). */
|
|
602
609
|
export interface AskRequest {
|
|
603
610
|
toolName: string;
|
|
611
|
+
/** #144: a persisted allow rule MATCHED this call but could not clear the ask (mandated — see
|
|
612
|
+
* {@link PermissionResult}'s ask arm). The matched rule text, so the approval card renders "your
|
|
613
|
+
* rule is alive, just outranked" instead of leaving the person to regex the message prose. */
|
|
614
|
+
persistedRuleShadowed?: string;
|
|
604
615
|
toolCallId: string;
|
|
605
616
|
/** The (post-rewrite) args the tool would run with. */
|
|
606
617
|
args: unknown;
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -122,12 +122,9 @@ export function createAllowDenyPolicy(opts) {
|
|
|
122
122
|
if (entry.startsWith("mcp__")) {
|
|
123
123
|
const segments = entry.slice("mcp__".length).split("__");
|
|
124
124
|
if (segments.some((seg) => seg.length === 0)) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
message: `"${entry}" is a malformed MCP tool name (empty segment) — it can never match any mounted tool. ` +
|
|
129
|
-
`Use \`mcp__<server>\` for every tool of a server, or \`mcp__<server>__<tool>\` for one tool.`,
|
|
130
|
-
});
|
|
125
|
+
const lesson = `a malformed MCP tool name (empty segment) — it can never match any mounted tool. ` +
|
|
126
|
+
`Use \`mcp__<server>\` for every tool of a server, or \`mcp__<server>__<tool>\` for one tool.`;
|
|
127
|
+
invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
|
|
131
128
|
continue;
|
|
132
129
|
}
|
|
133
130
|
}
|
|
@@ -135,14 +132,11 @@ export function createAllowDenyPolicy(opts) {
|
|
|
135
132
|
kept.push(entry);
|
|
136
133
|
continue;
|
|
137
134
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`and route the narrowing through the lane that speaks this grammar: parameter rules via ` +
|
|
144
|
-
`createPermissionRulePolicy, Bash command prefixes via the persisted allow-rule lane.`,
|
|
145
|
-
});
|
|
135
|
+
const lesson = `a rule CONTENT form, not a tool name — a name set matches raw tool names, so this entry ` +
|
|
136
|
+
`can never match any mounted tool (in an allow list it removes the tool entirely). Use the tool NAME here, ` +
|
|
137
|
+
`and route the narrowing through the lane that speaks this grammar: parameter rules via ` +
|
|
138
|
+
`createPermissionRulePolicy, Bash command prefixes via the persisted allow-rule lane.`;
|
|
139
|
+
invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
|
|
146
140
|
}
|
|
147
141
|
return kept;
|
|
148
142
|
};
|
|
@@ -150,8 +144,16 @@ export function createAllowDenyPolicy(opts) {
|
|
|
150
144
|
const screenedDeny = screen(opts.deny, "deny");
|
|
151
145
|
if (invalid.length > 0) {
|
|
152
146
|
if ((opts.onInvalidName ?? "throw") === "throw") {
|
|
147
|
+
const byLesson = new Map();
|
|
148
|
+
for (const i of invalid) {
|
|
149
|
+
const group = byLesson.get(i.lesson) ?? [];
|
|
150
|
+
group.push({ entry: i.entry, list: i.list });
|
|
151
|
+
byLesson.set(i.lesson, group);
|
|
152
|
+
}
|
|
153
153
|
const e = new Error(`createAllowDenyPolicy: ${invalid.length} entr${invalid.length === 1 ? "y is" : "ies are"} not tool name(s):\n` +
|
|
154
|
-
|
|
154
|
+
[...byLesson.entries()]
|
|
155
|
+
.map(([lesson, group]) => ` Each of the following is ${lesson}\n` + group.map((g) => ` [${g.list}] "${g.entry}"`).join("\n"))
|
|
156
|
+
.join("\n"));
|
|
155
157
|
e.code = "config.invalid_tool_name_set";
|
|
156
158
|
e.issues = invalid;
|
|
157
159
|
throw e;
|
|
@@ -240,5 +240,13 @@ export declare function withToolResultOffload(tool: AgentTool, store: ToolResult
|
|
|
240
240
|
* ⇒ byte-identical historic preview. An accessor (not a snapshot) because the wrap happens at
|
|
241
241
|
* prepare time, before the deferred classification exists and before any activation can. */
|
|
242
242
|
reachableTools?: () => ReadonlySet<string> | undefined): AgentTool;
|
|
243
|
+
/** The machine-recognizable start of an offloaded-detail replacement notice. Exported for consumers
|
|
244
|
+
* that treat a `details` string as SEMANTIC INPUT (not display) — e.g. the session-continuation
|
|
245
|
+
* read-state replay — so they can tell "this member was offloaded, the bytes here are a preview"
|
|
246
|
+
* and degrade honestly instead of consuming preview bytes as the real value. */
|
|
247
|
+
export declare const OFFLOADED_DETAIL_NOTICE_PREFIX = "\u2026[offloaded \u2014 ";
|
|
248
|
+
/** True when `s` carries an offloaded-detail replacement notice (the newline-anchored prefix form the
|
|
249
|
+
* walk emits). A semantic consumer treats such a string as NOT the member's real bytes. */
|
|
250
|
+
export declare function isOffloadedDetailReplacement(s: string): boolean;
|
|
243
251
|
/** The injected `read_tool_result` tool: pages through an offloaded result. `effect:"read"` (verifier/reconcile-safe). */
|
|
244
252
|
export declare function createReadToolResultTool(store: ToolResultStore): AgentTool;
|
|
@@ -121,6 +121,8 @@ export function firstPartyOffloadPolicy(toolName) {
|
|
|
121
121
|
switch (toolName) {
|
|
122
122
|
case "Read":
|
|
123
123
|
return { offload: false };
|
|
124
|
+
case "Write":
|
|
125
|
+
return { offload: false };
|
|
124
126
|
case "Bash":
|
|
125
127
|
return { offloadThresholdChars: 30_000 };
|
|
126
128
|
case "Grep":
|
|
@@ -164,21 +166,93 @@ export function withToolResultOffload(tool, store, thresholdChars, sessionId, re
|
|
|
164
166
|
return tool;
|
|
165
167
|
const wrappedExecute = async (toolCallId, params, signal, onUpdate) => {
|
|
166
168
|
const res = await tool.execute(toolCallId, params, signal, onUpdate);
|
|
169
|
+
const offloadedDetails = res.details === undefined ? undefined : await offloadOversizedDetailStrings(res.details, store, thresholdChars, sessionId, toolCallId);
|
|
170
|
+
const withDetails = (r) => offloadedDetails === undefined || offloadedDetails.value === res.details ? r : { ...r, details: offloadedDetails.value };
|
|
167
171
|
if (totalTextChars(res.content) <= thresholdChars)
|
|
168
|
-
return res;
|
|
172
|
+
return withDetails(res);
|
|
169
173
|
const full = res.content
|
|
170
174
|
.filter((b) => b.type === "text")
|
|
171
175
|
.map((b) => b.text)
|
|
172
176
|
.join("\n");
|
|
173
177
|
if (full.length <= PREVIEW_HEAD_CHARS + PREVIEW_TAIL_CHARS)
|
|
174
|
-
return res;
|
|
178
|
+
return withDetails(res);
|
|
175
179
|
const ref = buildToolResultRef(sessionId, toolCallId);
|
|
176
180
|
await store.put(ref, full);
|
|
177
181
|
const images = res.content.filter((b) => b.type !== "text");
|
|
178
|
-
return { ...res, content: [{ type: "text", text: buildPreview(full, ref, undefined, reachableTools?.()) }, ...images] };
|
|
182
|
+
return withDetails({ ...res, content: [{ type: "text", text: buildPreview(full, ref, undefined, reachableTools?.()) }, ...images] });
|
|
179
183
|
};
|
|
180
184
|
return { ...tool, execute: wrappedExecute };
|
|
181
185
|
}
|
|
186
|
+
const OFFLOADED_DETAIL_HEAD_CHARS = 2_000;
|
|
187
|
+
const OFFLOADED_DETAIL_NOTICE_ALLOWANCE_CHARS = 400;
|
|
188
|
+
export const OFFLOADED_DETAIL_NOTICE_PREFIX = "…[offloaded — ";
|
|
189
|
+
export function isOffloadedDetailReplacement(s) {
|
|
190
|
+
return s.includes(`\n${OFFLOADED_DETAIL_NOTICE_PREFIX}`);
|
|
191
|
+
}
|
|
192
|
+
async function offloadOversizedDetailStrings(details, store, thresholdChars, sessionId, toolCallId) {
|
|
193
|
+
const puts = [];
|
|
194
|
+
const onStack = new Set();
|
|
195
|
+
const isPlainObject = (v) => {
|
|
196
|
+
if (typeof v !== "object" || v === null)
|
|
197
|
+
return false;
|
|
198
|
+
const p = Object.getPrototypeOf(v);
|
|
199
|
+
return p === Object.prototype || p === null;
|
|
200
|
+
};
|
|
201
|
+
const replace = (full, path) => {
|
|
202
|
+
const detailRef = buildToolResultRef(sessionId, toolCallId + " " + path);
|
|
203
|
+
puts.push(Promise.resolve(store.put(detailRef, full)));
|
|
204
|
+
return (`${full.slice(0, OFFLOADED_DETAIL_HEAD_CHARS)}\n` +
|
|
205
|
+
`${OFFLOADED_DETAIL_NOTICE_PREFIX}${full.length} chars total; ref "${detailRef}"; the remainder is retained in this run's tool-result store and reads back through the deployment's tool-results face]`);
|
|
206
|
+
};
|
|
207
|
+
const memo = new Map();
|
|
208
|
+
const walk = (v, segs) => {
|
|
209
|
+
if (typeof v === "string")
|
|
210
|
+
return v.length > thresholdChars && v.length > OFFLOADED_DETAIL_HEAD_CHARS + OFFLOADED_DETAIL_NOTICE_ALLOWANCE_CHARS ? replace(v, JSON.stringify(segs)) : v;
|
|
211
|
+
if (Array.isArray(v)) {
|
|
212
|
+
if (onStack.has(v))
|
|
213
|
+
return v;
|
|
214
|
+
const done = memo.get(v);
|
|
215
|
+
if (done !== undefined)
|
|
216
|
+
return done;
|
|
217
|
+
onStack.add(v);
|
|
218
|
+
let changed = false;
|
|
219
|
+
const next = v.map((item, i) => {
|
|
220
|
+
const w = walk(item, [...segs, i]);
|
|
221
|
+
if (w !== item)
|
|
222
|
+
changed = true;
|
|
223
|
+
return w;
|
|
224
|
+
});
|
|
225
|
+
onStack.delete(v);
|
|
226
|
+
const result = changed ? next : v;
|
|
227
|
+
memo.set(v, result);
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
if (isPlainObject(v)) {
|
|
231
|
+
if (onStack.has(v))
|
|
232
|
+
return v;
|
|
233
|
+
const done = memo.get(v);
|
|
234
|
+
if (done !== undefined)
|
|
235
|
+
return done;
|
|
236
|
+
onStack.add(v);
|
|
237
|
+
let changed = false;
|
|
238
|
+
const next = {};
|
|
239
|
+
for (const [k, item] of Object.entries(v)) {
|
|
240
|
+
const w = walk(item, [...segs, k]);
|
|
241
|
+
if (w !== item)
|
|
242
|
+
changed = true;
|
|
243
|
+
Object.defineProperty(next, k, { value: w, enumerable: true, writable: true, configurable: true });
|
|
244
|
+
}
|
|
245
|
+
onStack.delete(v);
|
|
246
|
+
const result = changed ? next : v;
|
|
247
|
+
memo.set(v, result);
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
return v;
|
|
251
|
+
};
|
|
252
|
+
const value = walk(details, []);
|
|
253
|
+
await Promise.all(puts);
|
|
254
|
+
return { value };
|
|
255
|
+
}
|
|
182
256
|
export function createReadToolResultTool(store) {
|
|
183
257
|
return defineTool({
|
|
184
258
|
name: OFFLOAD_TOOL_NAME,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4188,11 +4188,13 @@ export interface RunnerDeps {
|
|
|
4188
4188
|
* they confirmed once is not asked about again.
|
|
4189
4189
|
*
|
|
4190
4190
|
* This is the one seam in this file that LOOSENS, and it is shaped so it can only do so within limits
|
|
4191
|
-
* the engine holds. The provider hands out a READ face anchored to one verified principal
|
|
4192
|
-
* face is
|
|
4193
|
-
*
|
|
4194
|
-
*
|
|
4195
|
-
*
|
|
4191
|
+
* the engine holds. The provider hands out a READ face anchored to one verified principal. The write
|
|
4192
|
+
* face is an exported BACKEND CONTRACT (ruled 2026-08-10: an out-of-repo store twin builds against the
|
|
4193
|
+
* same definitions instead of mirroring them), but the ENGINE reaches it only on the consent lanes —
|
|
4194
|
+
* redemption of a confirmed human decision, the tighten-delete, and the sync join — a wiring invariant
|
|
4195
|
+
* pinned by a registered-caller scan (test/permission-rule-writer-callers); no exported convenience
|
|
4196
|
+
* mints a rule around consent. Rules are consumed post-fold, in the gate's ask branch, and never
|
|
4197
|
+
* resolve an ask carrying `requiresRealApproval` or one a PreToolUse hook raised.
|
|
4196
4198
|
*
|
|
4197
4199
|
* Omitted ⇒ the lane does not exist: no rules are read, no field is added to any ask, and the decision
|
|
4198
4200
|
* path is byte-identical to a build without it. An unauthenticated task (no `principal`) resolves to
|