@sema-agent/core 5.27.0 → 5.29.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 +97 -0
- package/dist/agents/cascade.d.ts +1 -1
- package/dist/agents/observer.d.ts +3 -3
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/agents/teacher.d.ts +1 -1
- package/dist/agents/verify.d.ts +6 -6
- package/dist/bench/metrics.d.ts +1 -1
- package/dist/brain/retry.d.ts +1 -1
- package/dist/core/arg-summary.d.ts +1 -1
- package/dist/core/ask-class.d.ts +2 -2
- package/dist/core/ask-question.d.ts +1 -1
- package/dist/core/ask-question.js +14 -1
- package/dist/core/background-shell.d.ts +5 -5
- package/dist/core/checkpoint-store.d.ts +14 -14
- package/dist/core/file-snapshot-store.d.ts +1 -1
- package/dist/core/hooks.d.ts +25 -3
- package/dist/core/hooks.js +22 -3
- package/dist/core/human-input-projection.d.ts +2 -2
- package/dist/core/memory-admission.d.ts +2 -2
- package/dist/core/memory-engine/engine.d.ts +1 -1
- package/dist/core/memory-engine/engine.js +2 -4
- package/dist/core/memory-engine/file-backend.d.ts +68 -9
- package/dist/core/memory-engine/file-backend.js +69 -27
- package/dist/core/memory-engine/header-hints.d.ts +1 -1
- package/dist/core/memory-engine/layout.d.ts +32 -3
- package/dist/core/memory-engine/layout.js +132 -8
- package/dist/core/memory-engine/types.d.ts +6 -5
- package/dist/core/memory-recall.d.ts +1 -1
- package/dist/core/memory.d.ts +2 -2
- package/dist/core/oracle-isolation.d.ts +2 -2
- package/dist/core/permission-rule-consent.d.ts +83 -9
- package/dist/core/permission-rule-consent.js +92 -1
- package/dist/core/permission-rule-model.d.ts +18 -2
- package/dist/core/permission-rule-model.js +21 -0
- package/dist/core/permission-rule-org.d.ts +23 -4
- package/dist/core/permission-rule-org.js +67 -20
- package/dist/core/permission-rule-store.d.ts +1 -1
- 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/active-skill-scope.d.ts +1 -1
- package/dist/core/runner/memory-consolidation.d.ts +1 -1
- package/dist/core/runner/prepare-task.d.ts +8 -3
- package/dist/core/runner/prepare-task.js +22 -7
- package/dist/core/runner/runtask.d.ts +1 -1
- package/dist/core/runner/runtask.js +13 -6
- package/dist/core/runner/session-rule-policy.d.ts +2 -2
- package/dist/core/runner/teardown-bounded.d.ts +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +1 -1
- package/dist/core/scheduler.d.ts +4 -4
- package/dist/core/session-policy-store.d.ts +3 -3
- package/dist/core/shared-memory/normalize.d.ts +1 -1
- package/dist/core/skill-tool-specifier.d.ts +1 -1
- package/dist/core/task-notification.d.ts +2 -2
- package/dist/core/task-registry-agent.d.ts +18 -9
- 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 +13 -4
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-policy.d.ts +78 -12
- package/dist/core/tool-policy.js +74 -7
- package/dist/core/tool-result-store.d.ts +109 -8
- package/dist/core/tool-result-store.js +95 -15
- package/dist/core/trace.d.ts +1 -1
- package/dist/core/types.d.ts +92 -22
- package/dist/core/types.js +30 -1
- package/dist/core/untrusted-text.d.ts +6 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/engine/compaction/compaction.d.ts +1 -1
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/types.d.ts +2 -2
- package/dist/engine/llm/types.d.ts +1 -1
- package/dist/engine/loop/types.d.ts +2 -2
- package/dist/engine/session/import-validate.d.ts +1 -1
- package/dist/engine/session/log-digest.d.ts +1 -1
- package/dist/fixtures/index.d.ts +18 -2
- package/dist/fixtures/index.js +11 -0
- package/dist/index.d.ts +2 -2
- package/dist/orchestration/goal.d.ts +1 -1
- package/dist/orchestration/workflow-types.d.ts +5 -4
- package/dist/orchestration/workflow.js +11 -3
- package/dist/prompt-assembly/artifact.d.ts +1 -1
- package/dist/prompts/default.d.ts +1 -1
- package/dist/prompts/default.js +1 -1
- package/dist/scenarios/scenario-registry.d.ts +3 -3
- package/dist/stores/file/tool-result-store.d.ts +43 -3
- package/dist/stores/file/tool-result-store.js +107 -19
- package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
- package/dist/tools/fs/fs-bash.d.ts +8 -1
- package/dist/tools/fs/fs-search-tools.d.ts +1 -1
- package/dist/tools/fs/fs-shared.d.ts +6 -1
- 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/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/scheduler-tools.d.ts +1 -1
- package/dist/tools/task-list.d.ts +1 -1
- package/dist/tools/web.d.ts +3 -3
- package/package.json +1 -1
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* privilege-separation ticket adds device-bound signatures. Deployments should connect knowing which
|
|
26
26
|
* level they are buying.
|
|
27
27
|
*/
|
|
28
|
-
import type { PersistedAllowRule, RuleDot, RuleScope, RuleTombstone } from "./permission-rule-model.js";
|
|
28
|
+
import type { PersistedAllowRule, RuleDot, RuleRejectCode, RuleScope, RuleTombstone } from "./permission-rule-model.js";
|
|
29
29
|
import type { PermissionRuleStoreProvider, RuleOwner, RuleSyncDrop, RuleSyncFrontier, RuleSyncState } from "./permission-rule-store.js";
|
|
30
30
|
import { type TracerHook } from "./trace.js";
|
|
31
31
|
/** The injected HTTP seam — core never bundles a fetch. The deployment owns base URL, auth, TLS and
|
|
@@ -125,6 +125,15 @@ interface ParsedRuleSyncResponse {
|
|
|
125
125
|
dot: RuleDot;
|
|
126
126
|
reason: string;
|
|
127
127
|
}>;
|
|
128
|
+
/** Tombstones whose SHAPE is whole but whose rule TEXT the current validator refuses — the third
|
|
129
|
+
* outcome, kept OUT of `merged.tombstones` so it does not read as malformed. See {@link
|
|
130
|
+
* parseRuleSyncResponse} for why this one does not withhold the round's adds (backlog #176). */
|
|
131
|
+
textRefusedTombstones: Array<{
|
|
132
|
+
rule: string;
|
|
133
|
+
scope: RuleScope;
|
|
134
|
+
dot: RuleDot;
|
|
135
|
+
code: RuleRejectCode;
|
|
136
|
+
}>;
|
|
128
137
|
warnings: string[];
|
|
129
138
|
}
|
|
130
139
|
/**
|
|
@@ -134,6 +143,11 @@ interface ParsedRuleSyncResponse {
|
|
|
134
143
|
* reaches the local disk); an unshapeable tombstone is surfaced as `undefined` so the caller's
|
|
135
144
|
* tighten-only partitioning withholds the round's adds. Unknown keys never flow anywhere — every field
|
|
136
145
|
* below is PICKED off the response, the object itself is discarded.
|
|
146
|
+
*
|
|
147
|
+
* A tombstone has THREE outcomes, not two (backlog #176): sound; malformed (bad shape, or a spelling
|
|
148
|
+
* that parses but is not canonical — the withholding arm); and shape-whole-but-TEXT-refused, reported
|
|
149
|
+
* on {@link ParsedRuleSyncResponse.textRefusedTombstones}. The third is a legacy row a tightened
|
|
150
|
+
* validator no longer admits — permanent, so treating it as the second would stall inbound adds forever.
|
|
137
151
|
*/
|
|
138
152
|
export declare function parseRuleSyncResponse(raw: unknown, expectedPrincipal: string): ParsedRuleSyncResponse;
|
|
139
153
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseAllowRuleText } from "./permission-rule-model.js";
|
|
1
|
+
import { escapeForDisclosure, parseAllowRuleText } from "./permission-rule-model.js";
|
|
2
2
|
import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, ruleSyncVector, sameScope, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
|
|
3
3
|
import { emitTrace } from "./trace.js";
|
|
4
4
|
export const PERMISSION_RULE_SYNC_PATH = "/v1/permission-rules/sync";
|
|
@@ -9,12 +9,13 @@ function refuseLocalOwnerSync() {
|
|
|
9
9
|
throw e;
|
|
10
10
|
}
|
|
11
11
|
const SYNC_MAX_ATTEMPTS = 8;
|
|
12
|
+
const DISCLOSED_REFUSED_TOMBSTONES = 5;
|
|
12
13
|
export async function syncPermissionRules(opts) {
|
|
13
14
|
if (opts.owner !== undefined) {
|
|
14
15
|
if (opts.owner.kind === "local-owner")
|
|
15
16
|
refuseLocalOwnerSync();
|
|
16
17
|
if (opts.owner.principal !== opts.principal) {
|
|
17
|
-
throw new Error(`contradictory identity: principal "${opts.principal}" and owner principal "${opts.owner.principal}" disagree`);
|
|
18
|
+
throw new Error(`contradictory identity: principal "${escapeForDisclosure(opts.principal)}" and owner principal "${escapeForDisclosure(opts.owner.principal)}" disagree`);
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
if (typeof opts.principal !== "string" || opts.principal === "") {
|
|
@@ -83,6 +84,18 @@ export async function syncPermissionRules(opts) {
|
|
|
83
84
|
if (tombstoneWithholding) {
|
|
84
85
|
warnings.push("the response carried at least one malformed tombstone — every inbound add is withheld this round (legal tombstones land; the adds re-arrive once the peer repairs the tombstone: full-state exchange has no missed-forever window)");
|
|
85
86
|
}
|
|
87
|
+
for (const t of response.textRefusedTombstones) {
|
|
88
|
+
dropped.push({ rule: t.rule, scope: t.scope, dot: t.dot, reason: "invalid_rule_text" });
|
|
89
|
+
}
|
|
90
|
+
if (response.textRefusedTombstones.length > 0) {
|
|
91
|
+
const named = response.textRefusedTombstones
|
|
92
|
+
.slice(0, DISCLOSED_REFUSED_TOMBSTONES)
|
|
93
|
+
.map((t) => `"${escapeForDisclosure(t.rule)}" (${t.code})`)
|
|
94
|
+
.join(", ");
|
|
95
|
+
const more = response.textRefusedTombstones.length - DISCLOSED_REFUSED_TOMBSTONES;
|
|
96
|
+
warnings.push(`the response carried ${response.textRefusedTombstones.length} tombstone(s) whose rule text this version's validator refuses — ${named}${more > 0 ? ` and ${more} more` : ""}. ` +
|
|
97
|
+
"They are refused here and never land; this round's inbound adds are NOT withheld for them (an add under the same text is refused by the same validator, so such a tombstone covers nothing that could go live). The peer still holds them — removing them there is what ends this notice");
|
|
98
|
+
}
|
|
86
99
|
const inboundRules = [];
|
|
87
100
|
if (!tombstoneWithholding) {
|
|
88
101
|
for (const r of response.merged.rules) {
|
|
@@ -120,7 +133,7 @@ export async function syncPermissionRules(opts) {
|
|
|
120
133
|
dropped.push({ rule: d.rule, scope: d.scope, dot: d.dot, reason });
|
|
121
134
|
}
|
|
122
135
|
else {
|
|
123
|
-
warnings.push(`the server dropped an inbound-side record (${d.rule}, dot ${d.dot.actor}#${d.dot.counter}): ${d.reason}`);
|
|
136
|
+
warnings.push(`the server dropped an inbound-side record (${escapeForDisclosure(d.rule)}, dot ${escapeForDisclosure(d.dot.actor)}#${d.dot.counter}): ${escapeForDisclosure(d.reason)}`);
|
|
124
137
|
}
|
|
125
138
|
}
|
|
126
139
|
const clean = !tombstoneWithholding;
|
|
@@ -147,7 +160,7 @@ export async function syncPermissionRules(opts) {
|
|
|
147
160
|
resurrected: [],
|
|
148
161
|
dropped,
|
|
149
162
|
rev: current.rev,
|
|
150
|
-
warnings: [...warnings, `the store refused the sync landing: ${errText(err)} — nothing landed, the local state is unchanged`],
|
|
163
|
+
warnings: [...warnings, `the store refused the sync landing: ${escapeForDisclosure(errText(err))} — nothing landed, the local state is unchanged`],
|
|
151
164
|
});
|
|
152
165
|
}
|
|
153
166
|
if ("conflict" in res) {
|
|
@@ -205,9 +218,11 @@ export async function syncPermissionRules(opts) {
|
|
|
205
218
|
warnings: [...warnings, `optimistic-concurrency retries exhausted after ${SYNC_MAX_ATTEMPTS} attempts — nothing landed, the local state is unchanged`],
|
|
206
219
|
});
|
|
207
220
|
}
|
|
208
|
-
function
|
|
221
|
+
function pickDot(v) {
|
|
209
222
|
const d = v;
|
|
210
|
-
|
|
223
|
+
const actor = d?.actor;
|
|
224
|
+
const counter = d?.counter;
|
|
225
|
+
return typeof actor === "string" && actor !== "" && typeof counter === "number" && Number.isFinite(counter) ? { actor, counter } : undefined;
|
|
211
226
|
}
|
|
212
227
|
function pickScope(v) {
|
|
213
228
|
const s = v;
|
|
@@ -219,13 +234,16 @@ function pickScope(v) {
|
|
|
219
234
|
}
|
|
220
235
|
function pickAdd(v) {
|
|
221
236
|
const a = v;
|
|
222
|
-
|
|
237
|
+
const dot = pickDot(a?.dot);
|
|
238
|
+
const origin = a?.origin;
|
|
239
|
+
const createdAt = a?.createdAt;
|
|
240
|
+
if (dot === undefined)
|
|
223
241
|
return undefined;
|
|
224
|
-
if (
|
|
242
|
+
if (origin !== "user" && origin !== "imported-cc" && origin !== "starter")
|
|
225
243
|
return undefined;
|
|
226
|
-
if (typeof
|
|
244
|
+
if (typeof createdAt !== "string")
|
|
227
245
|
return undefined;
|
|
228
|
-
return { dot
|
|
246
|
+
return { dot, origin, createdAt };
|
|
229
247
|
}
|
|
230
248
|
function pickFrontier(v) {
|
|
231
249
|
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
@@ -243,35 +261,44 @@ export function parseRuleSyncResponse(raw, expectedPrincipal) {
|
|
|
243
261
|
throw new Error("the sync response is not an object — protocol error, nothing landed");
|
|
244
262
|
}
|
|
245
263
|
const r = raw;
|
|
246
|
-
|
|
264
|
+
const echoedPrincipal = r.principal;
|
|
265
|
+
if (typeof echoedPrincipal !== "string") {
|
|
247
266
|
throw new Error("the sync response carries no principal echo — protocol error, nothing landed");
|
|
248
267
|
}
|
|
249
|
-
if (
|
|
250
|
-
throw new Error(`the sync response echoes principal "${
|
|
268
|
+
if (echoedPrincipal !== expectedPrincipal) {
|
|
269
|
+
throw new Error(`the sync response echoes principal "${escapeForDisclosure(echoedPrincipal)}" but this round synced "${escapeForDisclosure(expectedPrincipal)}" — the transport is wired to the wrong bucket; refusing the whole round`);
|
|
251
270
|
}
|
|
252
271
|
const merged = r.merged;
|
|
253
|
-
|
|
272
|
+
const mergedRules = merged?.rules;
|
|
273
|
+
const mergedTombstones = merged?.tombstones;
|
|
274
|
+
if (merged === null || typeof merged !== "object" || !Array.isArray(mergedRules) || !Array.isArray(mergedTombstones)) {
|
|
254
275
|
throw new Error("the sync response carries no merged state — protocol error, nothing landed");
|
|
255
276
|
}
|
|
277
|
+
const rawGcFrontier = r.gcFrontier;
|
|
256
278
|
let gcFrontier;
|
|
257
|
-
if (
|
|
258
|
-
gcFrontier = pickFrontier(
|
|
279
|
+
if (rawGcFrontier !== undefined) {
|
|
280
|
+
gcFrontier = pickFrontier(rawGcFrontier);
|
|
259
281
|
if (gcFrontier === undefined) {
|
|
260
282
|
throw new Error("the sync response carries an unparseable gcFrontier — refusing the whole round (a garbled frontier must not drive the fence arm)");
|
|
261
283
|
}
|
|
262
284
|
}
|
|
263
285
|
const warnings = [];
|
|
264
286
|
const rules = [];
|
|
265
|
-
for (const entry of
|
|
287
|
+
for (const entry of mergedRules) {
|
|
266
288
|
const e = entry;
|
|
267
289
|
const scope = pickScope(e?.scope);
|
|
268
|
-
|
|
290
|
+
const eRule = e?.rule;
|
|
291
|
+
const eCommand = e?.command;
|
|
292
|
+
const eTool = e?.tool;
|
|
293
|
+
const eMatch = e?.match;
|
|
294
|
+
const eAdds = e?.adds;
|
|
295
|
+
if (typeof eRule !== "string" || scope === undefined || !Array.isArray(eAdds) || typeof eCommand !== "string" || typeof eTool !== "string" || typeof eMatch !== "string") {
|
|
269
296
|
warnings.push("the response carried an unshapeable rule entry — skipped (it never reaches the local store)");
|
|
270
297
|
continue;
|
|
271
298
|
}
|
|
272
299
|
const adds = [];
|
|
273
300
|
let addDamaged = false;
|
|
274
|
-
for (const a of
|
|
301
|
+
for (const a of eAdds) {
|
|
275
302
|
const picked = pickAdd(a);
|
|
276
303
|
if (picked === undefined) {
|
|
277
304
|
addDamaged = true;
|
|
@@ -280,64 +307,79 @@ export function parseRuleSyncResponse(raw, expectedPrincipal) {
|
|
|
280
307
|
adds.push(picked);
|
|
281
308
|
}
|
|
282
309
|
if (addDamaged)
|
|
283
|
-
warnings.push(`rule "${
|
|
310
|
+
warnings.push(`rule "${escapeForDisclosure(eRule)}" carried at least one unshapeable add — skipped those adds`);
|
|
284
311
|
if (adds.length === 0)
|
|
285
312
|
continue;
|
|
286
313
|
rules.push({
|
|
287
|
-
rule:
|
|
288
|
-
tool:
|
|
289
|
-
match:
|
|
290
|
-
command:
|
|
314
|
+
rule: eRule,
|
|
315
|
+
tool: eTool,
|
|
316
|
+
match: eMatch,
|
|
317
|
+
command: eCommand,
|
|
291
318
|
scope,
|
|
292
319
|
adds,
|
|
293
320
|
});
|
|
294
321
|
}
|
|
295
322
|
const tombstones = [];
|
|
296
|
-
|
|
323
|
+
const textRefusedTombstones = [];
|
|
324
|
+
for (const entry of mergedTombstones) {
|
|
297
325
|
const t = entry;
|
|
298
326
|
const scope = pickScope(t?.scope);
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
327
|
+
const ruleText = t?.rule;
|
|
328
|
+
const deletedBy = pickDot(t?.deletedBy);
|
|
329
|
+
const removedRaw = t?.removedDots;
|
|
330
|
+
const removedDots = [];
|
|
331
|
+
if (Array.isArray(removedRaw)) {
|
|
332
|
+
for (const d of removedRaw) {
|
|
333
|
+
const dot = pickDot(d);
|
|
334
|
+
if (dot === undefined) {
|
|
335
|
+
removedDots.length = 0;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
removedDots.push(dot);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (typeof ruleText !== "string" || scope === undefined || deletedBy === undefined || removedDots.length === 0) {
|
|
309
342
|
tombstones.push(undefined);
|
|
310
343
|
continue;
|
|
311
344
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
scope,
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
345
|
+
const canonicalTomb = parseAllowRuleText(ruleText);
|
|
346
|
+
if ("reject" in canonicalTomb) {
|
|
347
|
+
textRefusedTombstones.push({ rule: ruleText, scope, dot: deletedBy, code: canonicalTomb.reject.code });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (canonicalTomb.rule.rule !== ruleText) {
|
|
351
|
+
tombstones.push(undefined);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
tombstones.push({ rule: ruleText, scope, removedDots, deletedBy });
|
|
318
355
|
}
|
|
319
356
|
const dropped = [];
|
|
320
|
-
|
|
321
|
-
|
|
357
|
+
const rawDropped = r.dropped;
|
|
358
|
+
if (rawDropped !== undefined) {
|
|
359
|
+
if (!Array.isArray(rawDropped)) {
|
|
322
360
|
warnings.push("the response carried a non-array dropped field — ignored");
|
|
323
361
|
}
|
|
324
362
|
else {
|
|
325
|
-
for (const entry of
|
|
363
|
+
for (const entry of rawDropped) {
|
|
326
364
|
const d = entry;
|
|
327
365
|
const scope = pickScope(d?.scope);
|
|
328
|
-
|
|
366
|
+
const dRule = d?.rule;
|
|
367
|
+
const dDot = pickDot(d?.dot);
|
|
368
|
+
const dReason = d?.reason;
|
|
369
|
+
if (typeof dRule !== "string" || scope === undefined || dDot === undefined || typeof dReason !== "string") {
|
|
329
370
|
warnings.push("the response carried an unshapeable dropped entry — ignored (the server's own drop discipline is its contract obligation)");
|
|
330
371
|
continue;
|
|
331
372
|
}
|
|
332
|
-
dropped.push({ rule:
|
|
373
|
+
dropped.push({ rule: dRule, scope, dot: dDot, reason: dReason });
|
|
333
374
|
}
|
|
334
375
|
}
|
|
335
376
|
}
|
|
336
377
|
return {
|
|
337
|
-
principal:
|
|
378
|
+
principal: echoedPrincipal,
|
|
338
379
|
merged: { rules, tombstones },
|
|
339
380
|
...(gcFrontier !== undefined ? { gcFrontier } : {}),
|
|
340
381
|
dropped,
|
|
382
|
+
textRefusedTombstones,
|
|
341
383
|
warnings,
|
|
342
384
|
};
|
|
343
385
|
}
|
|
@@ -34,7 +34,7 @@ export declare class ActiveSkillScope {
|
|
|
34
34
|
* - DENY any tool not in the INTERSECTION of every active frame's `allowTools`.
|
|
35
35
|
* - For a WRITE tool with an `allowPaths` constraint on ANY active frame, DENY a write whose
|
|
36
36
|
* canonicalized target is outside every listed prefix (reuses {@link canonicalizeTarget}, so a
|
|
37
|
-
* symlink can't
|
|
37
|
+
* symlink can't carry across a write outside the allowed paths).
|
|
38
38
|
* - A frame whose manifest could not be resolved (`kind:"unresolved"`) → DENY-ALL (fail-closed: no
|
|
39
39
|
* allowlist means nothing is allowed — NOT the bare task policy).
|
|
40
40
|
* When NO frame is active, the policy returns `allow` for everything (it only ever subtracts), leaving
|
|
@@ -27,7 +27,7 @@ export declare const DEFAULT_CONSOLIDATION_MAX_NOTES = 50;
|
|
|
27
27
|
export declare const DEFAULT_CONSOLIDATION_TIMEOUT_SEC = 30;
|
|
28
28
|
/** Normalize a note body for the EXACT-restatement check in the `<lo` near-dup cheap path. Case- + whitespace-
|
|
29
29
|
* insensitive ONLY (lower-case, collapse whitespace runs, trim); it PRESERVES every symbol/punctuation so it can
|
|
30
|
-
* NOT false-equate semantically-distinct facts. (service [365]
|
|
30
|
+
* NOT false-equate semantically-distinct facts. (service [365] falsification-style: the earlier `[^\p{L}\p{N}]`-stripping
|
|
31
31
|
* normalization made "C#"≡"C", "delta +5"≡"delta -5", "100%"≡"100" — and a textual-CONTAINMENT test on top of it
|
|
32
32
|
* also mis-judged "rate limit 1000" ⊇ "rate limit 100" (word boundary). A fire-on-every-note, no-LLM path must not
|
|
33
33
|
* make a semantic-subset call, so the cheap path now folds ONLY an exact restatement; every real near-dup defers
|
|
@@ -864,7 +864,7 @@ export interface InheritedGate {
|
|
|
864
864
|
* Org-memory admission freeze (ruled 2026-08-05): the spawning chain's FROZEN admitted org-scope
|
|
865
865
|
* set — every org memory scope the parent actually mounted (deployment-origin + admitted request).
|
|
866
866
|
* A delegated child's REQUEST-origin org scopes must be a subset (intersection ≠ request ⇒ the
|
|
867
|
-
* child's prepare refuses, `memory.admission_denied`); deployment-origin scopes
|
|
867
|
+
* child's prepare refuses, `memory.admission_denied`); deployment-origin scopes circumvent the freeze
|
|
868
868
|
* (operator authority is deployment-wide). ALWAYS emitted by chain assembly (empty array = parent
|
|
869
869
|
* mounted no org plane), so an ABSENT field discriminates an older-shape chain — which the child
|
|
870
870
|
* side reads fail-closed as the empty set. Serializable (plain strings): persists on the
|
|
@@ -872,7 +872,7 @@ export interface InheritedGate {
|
|
|
872
872
|
*/
|
|
873
873
|
admittedOrgScopes?: readonly string[];
|
|
874
874
|
/**
|
|
875
|
-
* The MONOTONIC org-governance provenance bit (
|
|
875
|
+
* The MONOTONIC org-governance provenance bit (falsification-style round 5): `true` when any leg of this
|
|
876
876
|
* tree ran with an org-admission surface configured (resolver / deployment scope list) or
|
|
877
877
|
* inherited the bit. It never clears once set (a governed tree stays governed), and it counts as
|
|
878
878
|
* governance evidence at the admission door EVEN when every admitted set en route is empty — a
|
|
@@ -1055,7 +1055,7 @@ export interface RunInternals {
|
|
|
1055
1055
|
* arbitration is decided) and BEFORE the resumed leg starts. The parked-resume caller uses it to
|
|
1056
1056
|
* flip the reserved row `parked→running` (guarded CAS) + adopt the live handle/lane — the ONLY
|
|
1057
1057
|
* legal site for that flip (a pre-consume flip would let TaskStop hit the plain running arm and
|
|
1058
|
-
*
|
|
1058
|
+
* circumvent the checkpoint arbitration entirely). A THROW here aborts the resume (propagates out of
|
|
1059
1059
|
* `resumeStream`): the checkpoint is already consumed, so the caller's compensation
|
|
1060
1060
|
* (`rollbackParkedClaim`) reads `resolved` and settles the honest failed/outcome-unknown terminal
|
|
1061
1061
|
* — never a silent run without an adopted row. Deployment/trusted-caller channel only, mirroring
|
|
@@ -1442,6 +1442,11 @@ export interface ResolvedWorkspace {
|
|
|
1442
1442
|
cwd: string;
|
|
1443
1443
|
/** True only for an accepted `isolation: "worktree"` request (⇒ `cwd` is this agent's own worktree). */
|
|
1444
1444
|
isolated: boolean;
|
|
1445
|
+
/** True when the task's execution env is REMOTE (#197 BGW-7): `cwd` then names a path INSIDE the remote
|
|
1446
|
+
* target (a per-task sandbox on the shape-(d) exemption leg, e.g. `/workspace`), which does not exist —
|
|
1447
|
+
* or names an unrelated directory — on the host. `isolated && !remote` is the only combination under
|
|
1448
|
+
* which `cwd` is a host path a consumer may diff / merge / remove. */
|
|
1449
|
+
remote: boolean;
|
|
1445
1450
|
}
|
|
1446
1451
|
/**
|
|
1447
1452
|
* From the resumed/active transcript, the batch position of `currentId` (design/45 §4.ter): the tool-call
|
|
@@ -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 = {
|
|
@@ -215,6 +219,9 @@ export function resolveModelPromptTraits(model, spec, internals) {
|
|
|
215
219
|
fableMitigations: isFableFamilyModelId(model.id),
|
|
216
220
|
};
|
|
217
221
|
}
|
|
222
|
+
function hasConversationContent(branch) {
|
|
223
|
+
return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
|
|
224
|
+
}
|
|
218
225
|
function isDelegatedNonForkChild(internals) {
|
|
219
226
|
return internals?.isDelegatedChild === true && internals?.insideFork !== true;
|
|
220
227
|
}
|
|
@@ -906,7 +913,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
906
913
|
}
|
|
907
914
|
if (internals?.onWorkspaceResolved !== undefined) {
|
|
908
915
|
try {
|
|
909
|
-
|
|
916
|
+
const workspaceEnv = ownedEnv ?? deps.executionEnv;
|
|
917
|
+
internals.onWorkspaceResolved({
|
|
918
|
+
cwd: taskRootPath,
|
|
919
|
+
isolated: internals.isolation === "worktree",
|
|
920
|
+
remote: workspaceEnv !== undefined && isRemoteExecutionEnv(workspaceEnv),
|
|
921
|
+
});
|
|
910
922
|
}
|
|
911
923
|
catch {
|
|
912
924
|
}
|
|
@@ -1659,6 +1671,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1659
1671
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1660
1672
|
...(internals?.onTaskNotification !== undefined ? { taskNotification: internals.onTaskNotification } : {}),
|
|
1661
1673
|
...(internals?.detachHub !== undefined ? { detachHub: internals.detachHub } : {}),
|
|
1674
|
+
...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}),
|
|
1662
1675
|
pdfModelCapabilities: pdfModelCapabilitiesOf(model),
|
|
1663
1676
|
...(deps.hands?.bashReadonlyAllow !== undefined ? { bashReadonlyAllow: deps.hands.bashReadonlyAllow } : {}),
|
|
1664
1677
|
...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
|
|
@@ -2012,7 +2025,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2012
2025
|
if (spec.sessionId) {
|
|
2013
2026
|
try {
|
|
2014
2027
|
const branch = await session.getBranch();
|
|
2015
|
-
const hasConversation = branch
|
|
2028
|
+
const hasConversation = hasConversationContent(branch);
|
|
2016
2029
|
if (!hasConversation)
|
|
2017
2030
|
projectMemoryPhase = "fresh";
|
|
2018
2031
|
else {
|
|
@@ -2493,7 +2506,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2493
2506
|
}
|
|
2494
2507
|
else if (resume === undefined && internals?.parentTaskId === undefined && internals?.parentSessionId === undefined) {
|
|
2495
2508
|
const candidate = deps.promptSource?.current();
|
|
2496
|
-
if (candidate !== undefined && (await session.getBranch())
|
|
2509
|
+
if (candidate !== undefined && !hasConversationContent(await session.getBranch())) {
|
|
2497
2510
|
centerAdoption = { artifact: candidate.artifact, sourceRevision: candidate.sourceRevision };
|
|
2498
2511
|
centerAdoptionFresh = true;
|
|
2499
2512
|
}
|
|
@@ -2572,7 +2585,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2572
2585
|
const resolved = resolveEpochAgainstBundled(pinned, SEMA_DEFAULT_PACK, epochDeclaredSections);
|
|
2573
2586
|
epochArtifactDigestForSnapshot = resolved.descriptor.artifactDigest;
|
|
2574
2587
|
if (resolved.migrated) {
|
|
2575
|
-
const
|
|
2588
|
+
const priorConversation = pinned === undefined ? hasConversationContent(await session.getBranch()) : true;
|
|
2589
|
+
const activatedBy = pinned === undefined && !priorConversation ? "session_start" : "legacy_migration";
|
|
2576
2590
|
await session.appendPromptEpoch({
|
|
2577
2591
|
...resolved.descriptor,
|
|
2578
2592
|
activatedBy,
|
|
@@ -2606,7 +2620,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2606
2620
|
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
2621
|
if (!announcedMaterializeEnv.has(line)) {
|
|
2608
2622
|
announcedMaterializeEnv.add(line);
|
|
2609
|
-
|
|
2623
|
+
emitMaterializeEnvNotice(deps.onNotice, line, { raw });
|
|
2610
2624
|
}
|
|
2611
2625
|
}
|
|
2612
2626
|
}
|
|
@@ -2628,7 +2642,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2628
2642
|
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
2643
|
if (!announcedMaterializeEnv.has(line)) {
|
|
2630
2644
|
announcedMaterializeEnv.add(line);
|
|
2631
|
-
|
|
2645
|
+
emitMaterializeEnvNotice(deps.onNotice, line, { raw: rawEnvStrategy, specStrategy: spec.toolMaterializeStrategy });
|
|
2632
2646
|
}
|
|
2633
2647
|
}
|
|
2634
2648
|
const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
|
|
@@ -3180,6 +3194,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3180
3194
|
...((internals?.explicitAgentName ?? internals?.agentName) !== undefined
|
|
3181
3195
|
? { sourceAgentName: internals?.explicitAgentName ?? internals?.agentName }
|
|
3182
3196
|
: {}),
|
|
3197
|
+
...(internals?.isDelegatedChild === true ? { isDelegatedChild: true } : {}),
|
|
3183
3198
|
});
|
|
3184
3199
|
const riskAxesOf = (toolName) => {
|
|
3185
3200
|
const tier = irreversibilityTier.get(toolName);
|
|
@@ -4731,7 +4746,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4731
4746
|
...(offloadStore
|
|
4732
4747
|
? {
|
|
4733
4748
|
offload: {
|
|
4734
|
-
persist: createOffloadPersist(offloadStore, sessionId),
|
|
4749
|
+
persist: createOffloadPersist(offloadStore, sessionId, deps.onNotice),
|
|
4735
4750
|
},
|
|
4736
4751
|
}
|
|
4737
4752
|
: {}),
|
|
@@ -300,7 +300,7 @@ export declare class Runner {
|
|
|
300
300
|
* `inheritedManifestScope` and every other RunInternals field) — the library caller is trusted to
|
|
301
301
|
* re-supply the genuine chain; a caller able to invoke resumeStream already holds full Runner
|
|
302
302
|
* authority, so constraint substitution here is outside the enforced threat model (the count check
|
|
303
|
-
* catches shape MISTAKES, not a
|
|
303
|
+
* catches shape MISTAKES, not a untrusted trusted caller). */
|
|
304
304
|
internals?: RunInternals): Promise<TaskStream>;
|
|
305
305
|
/**
|
|
306
306
|
* design/45 resume step: with the harness idle and the branch rewound to the suspension leaf, resolve
|
|
@@ -35,7 +35,7 @@ import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
|
35
35
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
36
36
|
import { cloneObserverInput, formatHookFeedback } from "../hooks.js";
|
|
37
37
|
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
38
|
-
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
38
|
+
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
39
39
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
40
40
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
41
41
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
@@ -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 } : {}),
|
|
@@ -242,7 +245,7 @@ function resumeContinuation(resume) {
|
|
|
242
245
|
if (resume.outcome.gate === "dry_run_review") {
|
|
243
246
|
const verdict = resume.outcome.decision === "approve"
|
|
244
247
|
? "Your predicted change was REVIEWED and APPROVED; it has been applied"
|
|
245
|
-
: `Your predicted change was REVIEWED and REJECTED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason)}` : ""}; it was NOT applied`;
|
|
248
|
+
: `Your predicted change was REVIEWED and REJECTED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}; it was NOT applied`;
|
|
246
249
|
return formatHookFeedback(`You were resumed after a dry-run review. ${verdict}. Before continuing, re-orient from the workspace ` +
|
|
247
250
|
"(run `git status` and review your recent changes) to confirm the current state, then continue the " +
|
|
248
251
|
"remaining work. Do NOT restart the task or re-run work that is already done.");
|
|
@@ -262,7 +265,7 @@ function resumeContinuation(resume) {
|
|
|
262
265
|
"or restart from scratch, just carry out the revised plan.\n\nThe REVISED plan to follow is:\n" +
|
|
263
266
|
delimitUntrusted("revised plan", resume.outcome.editedPlan));
|
|
264
267
|
}
|
|
265
|
-
const why = resume.outcome.reason ? ` Reviewer note: ${delimitUntrusted("reviewer note", resume.outcome.reason)}` : "";
|
|
268
|
+
const why = resume.outcome.reason ? ` Reviewer note: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : "";
|
|
266
269
|
return formatHookFeedback(`Your proposed PLAN was REVIEWED and REJECTED; it was NOT executed.${why} Produce a NEW plan that ` +
|
|
267
270
|
"addresses the concern, then continue. This is a RESUMED task — re-plan from the current state; do " +
|
|
268
271
|
"NOT execute the rejected plan.");
|
|
@@ -273,7 +276,7 @@ function resumeContinuation(resume) {
|
|
|
273
276
|
}
|
|
274
277
|
const decided = resume.outcome.decision === "allow"
|
|
275
278
|
? "was APPROVED and has now been executed — its result is in the tool results above"
|
|
276
|
-
: `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason)}` : ""}`;
|
|
279
|
+
: `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}`;
|
|
277
280
|
const remaining = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !pendingAction.completedCallIds.includes(id)).length;
|
|
278
281
|
const batchNote = remaining > 0
|
|
279
282
|
? ` ${remaining} other tool call(s) from that batch were returned as deferred and not run — re-issue any you still need.`
|
|
@@ -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();
|
|
@@ -3997,7 +4004,7 @@ export class Runner {
|
|
|
3997
4004
|
: outcome.settledBy === "aborted"
|
|
3998
4005
|
? `The approval for the pending tool call "${pendingAction.toolName}" ended without anyone deciding it (it was cancelled or could not be delivered), so it was not executed.`
|
|
3999
4006
|
: `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
|
|
4000
|
-
const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason) : defaultDenial;
|
|
4007
|
+
const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason, REVIEWER_NOTE_MAX_BODY) : defaultDenial;
|
|
4001
4008
|
emitEnd(true, { content: formatHookFeedback(reason) });
|
|
4002
4009
|
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason), true));
|
|
4003
4010
|
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
@@ -54,9 +54,9 @@ export declare function isWithin(root: string, p: string): boolean;
|
|
|
54
54
|
* - `toolAllow` (if set) → deny any tool NOT listed (a narrowing allowlist).
|
|
55
55
|
* - `commandAllow`/`commandDeny` → delegate to {@link createCoarseCommandNamePolicy} (argv[0] names; only
|
|
56
56
|
* speaks about shell tools). allowlist mode denies the un-listed/un-parseable; blocklist mode asks on the
|
|
57
|
-
* un-parseable
|
|
57
|
+
* un-parseable circumvention surface (the gate decides).
|
|
58
58
|
* - `allowDirs` (if set) → a WRITE tool must land inside one of the dirs (resolved via `canonicalizeTarget`,
|
|
59
|
-
* so a symlink can't
|
|
59
|
+
* so a symlink can't carry across a write out); a write-capable tool that cannot be path-confined (e.g. `bash`)
|
|
60
60
|
* is DENIED while `allowDirs` is set (fail-closed, mirrors active-skill-scope MAJOR-2).
|
|
61
61
|
* Returns `allow` when nothing constrains the call. `env`/`rootPath` are the task's real exec env + root, so
|
|
62
62
|
* matching sees the actual filesystem (a remote/E2B task resolves in its container). RB-108: a relative
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* mask the original error") — but a promise that never settles is not a throw, and one wedged
|
|
10
10
|
* adapter turned into: for-await done, `result()` never settles (②型, process alive), or — when the
|
|
11
11
|
* wedge happened before the queue closed — a zero-handle pure-logic hang (the 2.12.0 exit-13 shape).
|
|
12
|
-
* The session lock releases in the run IIFE's finally, so one wedged run also
|
|
12
|
+
* The session lock releases in the run IIFE's finally, so one wedged run also contaminated every later
|
|
13
13
|
* same-session resume.
|
|
14
14
|
*
|
|
15
15
|
* The remedy is the same doctrine as the workflow journal drain (batch-4 F2): a best-effort leg gets
|
|
@@ -223,7 +223,7 @@ export declare function resolveToolSearch(args: ToolSearchArgs, registry: Readon
|
|
|
223
223
|
* direct call (ruled 2026-08-03) activates too and is flagged as an error, so it lands in that same
|
|
224
224
|
* under-approximated class by construction: within the process the in-memory `active` set carries it, and
|
|
225
225
|
* a replay that loses it costs the corrected call — which succeeds, and is itself carrier ②. The one way to
|
|
226
|
-
*
|
|
226
|
+
* self-declare a positive is a deployment `tool_result` hook that rewrites a rejection's error flag to false,
|
|
227
227
|
* which is the deployment editing its own transcript.
|
|
228
228
|
*
|
|
229
229
|
* design/116 W1-2 note: pre-116 checkpoints whose KEYWORD queries activated up to 25 tools re-derive at
|
package/dist/core/scheduler.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare class SchedulerError extends Error {
|
|
|
27
27
|
constructor(code: SchedulerErrorCode, message: string, cause?: unknown | undefined);
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* 🔴 **模型可控面(不可信)**:纯意图,**无任何身份字段** —— 防模型经自排 prompt
|
|
30
|
+
* 🔴 **模型可控面(不可信)**:纯意图,**无任何身份字段** —— 防模型经自排 prompt 权限抬升(design/62 同款红线:身份是
|
|
31
31
|
* Runner-held,非工具参数)。`prompt` = 未来 task 的不可信 `objective`(daemon 起 task 时经完整 prepare-task gate)。
|
|
32
32
|
*/
|
|
33
33
|
export interface ScheduledIntent {
|
|
@@ -84,7 +84,7 @@ export interface ScheduledIntent {
|
|
|
84
84
|
*/
|
|
85
85
|
export interface SchedulerContext {
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* 越界隔离键(cancel/list 只能操作同 scope 排的)。由 Runner 填,且**按被操作 intent 的 lifetime 分叉**
|
|
88
88
|
* (ruled 2026-08-03):`lifetime:"session"` 的意图用会话链(sessionId ?? principal ?? taskId ?? "default");
|
|
89
89
|
* durable 意图用跨会话稳定身份(principal ?? "default")——否则 durable job 会被钉死在排它的那个会话里,
|
|
90
90
|
* 后续会话 cancel/list 一律 `not_found`(任务照常触发却再也删不掉)。backend 侧无需分辨腿别:照旧只按
|
|
@@ -92,7 +92,7 @@ export interface SchedulerContext {
|
|
|
92
92
|
* ⚠️ 部署面:本分叉之前写下的 durable 行仍带旧的会话键,一次性重铸键归 backend 半场,engine 不做迁移。
|
|
93
93
|
*/
|
|
94
94
|
scope: string;
|
|
95
|
-
/** 排程者身份;daemon 触发时原样传给 runTask
|
|
95
|
+
/** 排程者身份;daemon 触发时原样传给 runTask(防权限抬升)。 */
|
|
96
96
|
principal?: string;
|
|
97
97
|
/** 可选:daemon 触发新 task 时复用的 session(chat-continuity;缺省=fresh-session)。⚠️ 复用需 durable sessionStore(design/105 §3.4)。 */
|
|
98
98
|
sessionId?: string;
|
|
@@ -169,7 +169,7 @@ export interface SchedulerCapability {
|
|
|
169
169
|
schedule(intent: ScheduledIntent, ctx: SchedulerContext): Promise<Result<{
|
|
170
170
|
id: ScheduledTaskId;
|
|
171
171
|
}, SchedulerError>>;
|
|
172
|
-
/** 取消一个已排意图(幂等:取消不存在的是 ok no-op)。🔴
|
|
172
|
+
/** 取消一个已排意图(幂等:取消不存在的是 ok no-op)。🔴 越界:`id` MUST 属本 scope;非本 scope 一律 `not_found`,不越界存在性。 */
|
|
173
173
|
cancel(id: ScheduledTaskId, ctx: SchedulerContext): Promise<Result<void, SchedulerError>>;
|
|
174
174
|
/** 列**本 scope** 已排意图(self-discovery + 防重复排)。范围 = `ctx.scope`,非全局。 */
|
|
175
175
|
list(ctx: SchedulerContext): Promise<Result<ScheduledTaskSummary[], SchedulerError>>;
|