@sema-agent/core 7.26.0 → 7.26.2
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 +73 -0
- package/dist/agents/verify.d.ts +16 -6
- package/dist/agents/verify.js +12 -5
- package/dist/core/checkpoint-store.d.ts +13 -10
- package/dist/core/gate-lanes.js +2 -1
- package/dist/core/gate-outcome.d.ts +54 -5
- package/dist/core/gate-outcome.js +17 -0
- package/dist/core/permission-rule-provider.js +6 -1
- package/dist/core/permission-rule-store.d.ts +36 -0
- package/dist/core/permission-rule-store.js +119 -19
- package/dist/core/permission-rule-sync.js +14 -5
- package/dist/core/runner/contracts.d.ts +48 -0
- package/dist/core/runner/decide-continuation.js +3 -1
- package/dist/core/runner/denial-limit-arms.d.ts +4 -3
- package/dist/core/runner/denial-limit-arms.js +2 -1
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
- package/dist/core/runner/prepare-inherited-gate.js +2 -1
- package/dist/core/runner/resume-apply.js +3 -1
- package/dist/core/runner/resume-policy-outcome.js +6 -6
- package/dist/core/runner/run-harness-handlers.js +2 -5
- package/dist/core/runner/run-identity-wiring.d.ts +6 -0
- package/dist/core/runner/run-identity-wiring.js +8 -3
- package/dist/core/runner/run-leg.js +9 -3
- package/dist/core/runner/runtask.js +2 -2
- package/dist/core/runner/session-rule-policy.d.ts +1 -1
- package/dist/core/runner/session-rule-policy.js +6 -7
- package/dist/core/session-policy-store.d.ts +18 -0
- package/dist/core/session-policy-store.js +3 -0
- package/dist/core/store-contracts/permission-rule-write-contract.js +16 -3
- package/dist/core/store-contracts/read-back-shape-contract.d.ts +56 -0
- package/dist/core/store-contracts/read-back-shape-contract.js +78 -0
- package/dist/core/tool-policy.d.ts +36 -12
- package/dist/core/tool-policy.js +22 -9
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/stores/file/adoption/adopt.js +4 -10
- package/dist/stores/file/permission-rule-store.d.ts +5 -1
- package/dist/stores/file/permission-rule-store.js +5 -3
- package/dist/stores/file/session-policy-store.d.ts +32 -0
- package/dist/stores/file/session-policy-store.js +72 -50
- package/dist/tools/fs/shell-read-check.d.ts +58 -1
- package/dist/tools/fs/shell-read-check.js +47 -33
- package/dist/tools/fs/shell-read-establish.js +19 -8
- package/dist/tools/fs/shell-read-judge.d.ts +3 -0
- package/dist/tools/fs/shell-read-judge.js +3 -1
- package/dist/tools/fs/shell-read-plan.js +16 -17
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +52 -2
- package/wire-changes/core@7.26.1.json +605 -0
- package/wire-changes/core@7.26.2.json +106 -0
|
@@ -5,6 +5,71 @@ import { thrownText } from "./thrown-value.js";
|
|
|
5
5
|
export function sameRuleOwner(a, b) {
|
|
6
6
|
return a.kind === "principal" ? b.kind === "principal" && a.principal === b.principal : b.kind === "local-owner";
|
|
7
7
|
}
|
|
8
|
+
export function isRuleRevision(rev) {
|
|
9
|
+
return typeof rev === "number" && Number.isSafeInteger(rev) && rev >= 0;
|
|
10
|
+
}
|
|
11
|
+
function outcomeWord(value) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return "no revision at all";
|
|
14
|
+
if (value === null)
|
|
15
|
+
return "a null revision";
|
|
16
|
+
if (typeof value === "number")
|
|
17
|
+
return `the number ${String(value)}, which is not a revision`;
|
|
18
|
+
return `a ${typeof value} where a revision belongs`;
|
|
19
|
+
}
|
|
20
|
+
function isReadableDrop(drop) {
|
|
21
|
+
if (drop === null || typeof drop !== "object")
|
|
22
|
+
return false;
|
|
23
|
+
const dot = drop.dot;
|
|
24
|
+
if (dot === null || typeof dot !== "object")
|
|
25
|
+
return false;
|
|
26
|
+
return typeof dot.actor === "string" && typeof dot.counter === "number" && typeof drop.reason === "string";
|
|
27
|
+
}
|
|
28
|
+
function readSyncReport(sync) {
|
|
29
|
+
if (sync === undefined)
|
|
30
|
+
return {};
|
|
31
|
+
if (sync === null || typeof sync !== "object")
|
|
32
|
+
return { unreadable: `the landing carried a ${sync === null ? "null" : typeof sync} where its sync report belongs` };
|
|
33
|
+
for (const member of ["droppedInbound", "quarantined"]) {
|
|
34
|
+
const value = sync[member];
|
|
35
|
+
if (value === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
if (!Array.isArray(value))
|
|
38
|
+
return { unreadable: `the landing's sync report carried a ${typeof value} as \`${member}\`, which a reader cannot walk` };
|
|
39
|
+
const bad = value.findIndex((row) => !isReadableDrop(row));
|
|
40
|
+
if (bad !== -1)
|
|
41
|
+
return { unreadable: `the landing's sync report carried a \`${member}\` entry (#${String(bad)}) that is not a drop record, so what was dropped cannot be read` };
|
|
42
|
+
}
|
|
43
|
+
return { report: sync };
|
|
44
|
+
}
|
|
45
|
+
export function readRuleWriteOutcome(res) {
|
|
46
|
+
if (res === null || typeof res !== "object") {
|
|
47
|
+
return { kind: "unreadable", why: `the store answered ${res === null ? "null" : `a ${typeof res}`} where a write outcome belongs` };
|
|
48
|
+
}
|
|
49
|
+
const tagged = res;
|
|
50
|
+
const refusal = tagged.refused;
|
|
51
|
+
const conflict = tagged.conflict;
|
|
52
|
+
if (refusal !== undefined && conflict !== undefined) {
|
|
53
|
+
return { kind: "unreadable", why: "the store answered a refusal and a decline at once, and an answer that is two arms is not an answer about either" };
|
|
54
|
+
}
|
|
55
|
+
if (refusal !== undefined) {
|
|
56
|
+
return typeof refusal === "string" && refusal !== ""
|
|
57
|
+
? { kind: "refused", refused: refusal }
|
|
58
|
+
: { kind: "unreadable", why: `the store tagged its answer as a refusal and named it with a ${refusal === null ? "null" : typeof refusal}, so which refusal it is cannot be read` };
|
|
59
|
+
}
|
|
60
|
+
if (conflict !== undefined) {
|
|
61
|
+
if (conflict !== true)
|
|
62
|
+
return { kind: "unreadable", why: `the store tagged its answer as a decline with \`${String(conflict)}\`, which does not say a compare-and-set lost` };
|
|
63
|
+
return isRuleRevision(tagged.rev)
|
|
64
|
+
? { kind: "conflict", rev: tagged.rev }
|
|
65
|
+
: { kind: "unreadable", why: `the store declined the write with ${outcomeWord(tagged.rev)}, so the revision to re-anchor the next attempt to is unknown` };
|
|
66
|
+
}
|
|
67
|
+
if (!isRuleRevision(tagged.rev)) {
|
|
68
|
+
return { kind: "unreadable", why: `the store answered neither a conflict nor a refusal, and carried ${outcomeWord(tagged.rev)} — a landing reports the revision it committed at, so this answer cannot say whether the write landed` };
|
|
69
|
+
}
|
|
70
|
+
const { report, unreadable } = readSyncReport(tagged.sync);
|
|
71
|
+
return { kind: "landed", rev: tagged.rev, ...(report !== undefined ? { sync: report } : {}), ...(unreadable !== undefined ? { reportUnreadable: unreadable } : {}) };
|
|
72
|
+
}
|
|
8
73
|
export function unknownRuleWriteDeltaError(delta) {
|
|
9
74
|
const kind = delta?.kind;
|
|
10
75
|
return new Error(`the permission-rule store cannot apply a write delta of kind ${escapeForDisclosure(typeof kind === "string" ? kind : String(kind))} — the write face folds exactly four named arms (redemption-add, tighten-add, tighten-delete, sync-join), and an unnamed one is refused rather than read as any of them`);
|
|
@@ -405,13 +470,32 @@ function resolveRuleWriter(store) {
|
|
|
405
470
|
w = writerOf(store);
|
|
406
471
|
}
|
|
407
472
|
catch (err) {
|
|
408
|
-
return { unavailable: `the permission-rule store's write face could not be acquired (${errText(err)})
|
|
473
|
+
return { unavailable: `the permission-rule store's write face could not be acquired (${errText(err)})`, committed: "unknown" };
|
|
409
474
|
}
|
|
410
|
-
return w === undefined ? { unavailable: "the resolved permission-rule store has no write face" } : { writer: w };
|
|
475
|
+
return w === undefined ? { unavailable: "the resolved permission-rule store has no write face", committed: "no" } : { writer: w };
|
|
411
476
|
}
|
|
412
477
|
function ruleIdentityStands(rules, identity) {
|
|
413
478
|
return rules.some((r) => sameRuleIdentity(r, identity) && isRuleLive(r));
|
|
414
479
|
}
|
|
480
|
+
async function witnessRuleAdd(store, writer, delta) {
|
|
481
|
+
const identity = { behavior: delta.behavior, rule: delta.rule, scope: delta.scope };
|
|
482
|
+
const dot = delta.add.dot;
|
|
483
|
+
try {
|
|
484
|
+
if (delta.scope.kind === "session") {
|
|
485
|
+
const view = await store.effective({ sessionId: delta.scope.sessionId });
|
|
486
|
+
if (view.unreadable !== undefined)
|
|
487
|
+
return { unreadable: view.unreadable };
|
|
488
|
+
return { seen: view.rules.some((r) => sameRuleIdentity(r, identity) && r.adds.some((a) => sameDot(a.dot, dot))), rev: view.rev };
|
|
489
|
+
}
|
|
490
|
+
const raw = await writer.readRaw();
|
|
491
|
+
const inRows = raw.rules.some((r) => sameRuleIdentity(r, identity) && r.adds.some((a) => sameDot(a.dot, dot)));
|
|
492
|
+
const inQuarantine = (raw.quarantined ?? []).some((q) => sameDot(q.add.dot, dot));
|
|
493
|
+
return { seen: inRows || inQuarantine, rev: raw.rev };
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
return { unreadable: errText(err) };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
415
499
|
async function readRuleLiveness(store, identity) {
|
|
416
500
|
try {
|
|
417
501
|
const after = await store.effective();
|
|
@@ -448,7 +532,7 @@ export async function removePersistedRule(opts) {
|
|
|
448
532
|
const store = owner.kind === "local-owner" ? provider.forLocalOwner() : provider.forPrincipal(owner.principal);
|
|
449
533
|
const face = resolveRuleWriter(store);
|
|
450
534
|
if ("unavailable" in face) {
|
|
451
|
-
return { status: "failed", error: `${face.unavailable} — rules cannot be removed through it`, committed:
|
|
535
|
+
return { status: "failed", error: `${face.unavailable} — rules cannot be removed through it`, committed: face.committed };
|
|
452
536
|
}
|
|
453
537
|
const writer = face.writer;
|
|
454
538
|
let snapshot;
|
|
@@ -474,30 +558,35 @@ export async function removePersistedRule(opts) {
|
|
|
474
558
|
const tombstone = { behavior, rule, scope, removedDots, deletedBy };
|
|
475
559
|
let expectedRev = snapshot.rev;
|
|
476
560
|
let lastError = "";
|
|
477
|
-
let
|
|
561
|
+
let settlement = "no";
|
|
478
562
|
for (let attempt = 0; attempt < REMOVE_MAX_ATTEMPTS; attempt++) {
|
|
479
563
|
try {
|
|
480
|
-
const
|
|
481
|
-
if ("refused"
|
|
482
|
-
return { status: "failed", error: `the store refused the tombstone (${
|
|
564
|
+
const reading = readRuleWriteOutcome(await writer.apply({ kind: "tighten-delete", tombstone }, { expectedRev }));
|
|
565
|
+
if (reading.kind === "refused") {
|
|
566
|
+
return { status: "failed", error: `the store refused the tombstone (${reading.refused}) — nothing was removed`, committed: "no" };
|
|
567
|
+
}
|
|
568
|
+
if (reading.kind === "unreadable") {
|
|
569
|
+
settlement = "unknown";
|
|
570
|
+
lastError = `the permission-rule store answered the write unreadably: ${reading.why}`;
|
|
571
|
+
break;
|
|
483
572
|
}
|
|
484
|
-
if (
|
|
485
|
-
return { status: "removed", rev:
|
|
573
|
+
if (reading.kind === "landed") {
|
|
574
|
+
return { status: "removed", rev: reading.rev, stillLive: await readRuleLiveness(store, identity) };
|
|
486
575
|
}
|
|
487
|
-
expectedRev =
|
|
576
|
+
expectedRev = reading.rev;
|
|
488
577
|
}
|
|
489
578
|
catch (err) {
|
|
490
|
-
|
|
579
|
+
settlement = "unknown";
|
|
491
580
|
lastError = errText(err);
|
|
492
581
|
break;
|
|
493
582
|
}
|
|
494
583
|
}
|
|
495
|
-
const reason =
|
|
584
|
+
const reason = settlement === "unknown"
|
|
496
585
|
? lastError !== ""
|
|
497
586
|
? lastError
|
|
498
587
|
: "the permission-rule store threw while applying the tombstone, with no message"
|
|
499
588
|
: `optimistic-concurrency retries exhausted after ${REMOVE_MAX_ATTEMPTS} attempts`;
|
|
500
|
-
const committed =
|
|
589
|
+
const committed = settlement;
|
|
501
590
|
try {
|
|
502
591
|
const after = await writer.readRaw();
|
|
503
592
|
const landed = after.tombstones.some((t) => t.deletedBy.actor === tombstone.deletedBy.actor && t.deletedBy.counter === tombstone.deletedBy.counter);
|
|
@@ -506,7 +595,7 @@ export async function removePersistedRule(opts) {
|
|
|
506
595
|
}
|
|
507
596
|
return {
|
|
508
597
|
status: "failed",
|
|
509
|
-
error:
|
|
598
|
+
error: settlement === "unknown"
|
|
510
599
|
? `${reason}; the store retains no tombstone for this removal, which cannot tell a write that never landed from one whose evidence was collected — the outcome is indeterminate, reconcile with list()`
|
|
511
600
|
: `${reason} (read-back confirms no tombstone was written)`,
|
|
512
601
|
committed,
|
|
@@ -515,7 +604,7 @@ export async function removePersistedRule(opts) {
|
|
|
515
604
|
catch (err) {
|
|
516
605
|
return {
|
|
517
606
|
status: "failed",
|
|
518
|
-
error:
|
|
607
|
+
error: settlement === "unknown"
|
|
519
608
|
? `${reason}; the read-back also failed (${errText(err)}) so this outcome is indeterminate — reconcile with list()`
|
|
520
609
|
: `${reason}; the read-back also failed (${errText(err)}), but every attempt was answered by the store, so nothing was written`,
|
|
521
610
|
committed,
|
|
@@ -555,10 +644,21 @@ export async function landRuleAdd(args) {
|
|
|
555
644
|
catch (err) {
|
|
556
645
|
return { landed: false, reason: `the permission-rule store refused the write: ${errText(err)}`, committed: "unknown" };
|
|
557
646
|
}
|
|
558
|
-
|
|
647
|
+
const reading = readRuleWriteOutcome(res);
|
|
648
|
+
if (reading.kind === "refused")
|
|
559
649
|
return { landed: false, reason: SESSION_ENDED_REFUSAL, committed: "no" };
|
|
560
|
-
if (
|
|
561
|
-
return { landed:
|
|
650
|
+
if (reading.kind === "unreadable")
|
|
651
|
+
return { landed: false, reason: `the permission-rule store answered the write unreadably: ${reading.why}`, committed: "unknown" };
|
|
652
|
+
if (reading.kind === "landed") {
|
|
653
|
+
const witness = await witnessRuleAdd(args.store, args.writer, args.delta);
|
|
654
|
+
if ("unreadable" in witness) {
|
|
655
|
+
return { landed: false, reason: `the permission-rule store reported a landing at revision ${String(reading.rev)}, and the read-back that would have witnessed it could not be taken (${witness.unreadable})`, committed: "unknown" };
|
|
656
|
+
}
|
|
657
|
+
if (!witness.seen) {
|
|
658
|
+
return { landed: false, reason: `the permission-rule store reported a landing at revision ${String(reading.rev)}, and a read-back of this add's own dot finds no trace of it — a revision says the ANSWER was well formed, not that a row exists, so nothing here can say the write landed`, committed: "unknown" };
|
|
659
|
+
}
|
|
660
|
+
return { landed: true, rev: reading.rev };
|
|
661
|
+
}
|
|
562
662
|
}
|
|
563
663
|
return { landed: false, reason: `optimistic-concurrency retries exhausted after ${RULE_ADD_MAX_ATTEMPTS} attempts`, committed: "no" };
|
|
564
664
|
}
|
|
@@ -592,7 +692,7 @@ export async function addPersistedRule(opts) {
|
|
|
592
692
|
const store = owner.kind === "local-owner" ? provider.forLocalOwner() : provider.forPrincipal(owner.principal);
|
|
593
693
|
const face = resolveRuleWriter(store);
|
|
594
694
|
if ("unavailable" in face) {
|
|
595
|
-
return { status: "failed", error: `${face.unavailable} — rules cannot be added through it`, committed:
|
|
695
|
+
return { status: "failed", error: `${face.unavailable} — rules cannot be added through it`, committed: face.committed };
|
|
596
696
|
}
|
|
597
697
|
const writer = face.writer;
|
|
598
698
|
const query = scope.kind === "session" ? { sessionId: scope.sessionId } : undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { escapeForDisclosure, isRuleBehavior, parseRuleText } from "./permission-rule-model.js";
|
|
2
|
-
import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, ruleSyncVector, sameRuleIdentity, screenRuleSyncState, writerOf, } from "./permission-rule-store.js";
|
|
2
|
+
import { applyTombstones, errText, joinFrontiers, joinRuleStates, normalizePersistedRule, readRuleWriteOutcome, ruleSyncVector, sameRuleIdentity, 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";
|
|
5
5
|
export const LOCAL_OWNER_UNSYNCABLE_CODE = "permission_rules.local_owner_unsyncable";
|
|
@@ -174,11 +174,12 @@ export async function syncPermissionRules(opts) {
|
|
|
174
174
|
warnings: [...warnings, `the store refused the sync landing: ${escapeForDisclosure(errText(err))} — nothing landed, the local state is unchanged`],
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
|
-
|
|
177
|
+
const reading = readRuleWriteOutcome(res);
|
|
178
|
+
if (reading.kind === "conflict") {
|
|
178
179
|
current = await writer.readRaw();
|
|
179
180
|
continue;
|
|
180
181
|
}
|
|
181
|
-
if ("refused"
|
|
182
|
+
if (reading.kind === "refused" || reading.kind === "unreadable") {
|
|
182
183
|
return disclose({
|
|
183
184
|
ok: false,
|
|
184
185
|
pushed: { addDots: raw.rules.reduce((n, r) => n + r.adds.length, 0), tombstones: raw.tombstones.length },
|
|
@@ -186,7 +187,12 @@ export async function syncPermissionRules(opts) {
|
|
|
186
187
|
resurrected: [],
|
|
187
188
|
dropped,
|
|
188
189
|
rev: current.rev,
|
|
189
|
-
warnings: [
|
|
190
|
+
warnings: [
|
|
191
|
+
...warnings,
|
|
192
|
+
reading.kind === "refused"
|
|
193
|
+
? `the store refused the sync landing (${reading.refused}) — nothing landed, the local state is unchanged`
|
|
194
|
+
: `the store answered the sync landing unreadably: ${reading.why} — this round is not reported as landed; re-read before trusting the local state`,
|
|
195
|
+
],
|
|
190
196
|
});
|
|
191
197
|
}
|
|
192
198
|
const landedRaw = await writer.readRaw();
|
|
@@ -211,7 +217,10 @@ export async function syncPermissionRules(opts) {
|
|
|
211
217
|
const tombKeyOf = (t) => JSON.stringify([t.behavior, t.rule, t.scope.kind, t.scope.kind === "project" ? t.scope.root : t.scope.kind === "session" ? t.scope.sessionId : null, dotKeyOf(t.deletedBy)]);
|
|
212
218
|
const preTombs = new Set(current.tombstones.map(tombKeyOf));
|
|
213
219
|
const newTombstones = landedRaw.tombstones.filter((t) => !preTombs.has(tombKeyOf(t))).length;
|
|
214
|
-
const landedReport =
|
|
220
|
+
const landedReport = reading.sync;
|
|
221
|
+
if (reading.reportUnreadable !== undefined) {
|
|
222
|
+
warnings.push(`${reading.reportUnreadable} — the join landed, but what it dropped or quarantined cannot be reported from this round`);
|
|
223
|
+
}
|
|
215
224
|
for (const d of landedReport?.droppedInbound ?? []) {
|
|
216
225
|
if (!dropped.some((x) => x.dot.actor === d.dot.actor && x.dot.counter === d.dot.counter && x.reason === d.reason))
|
|
217
226
|
dropped.push(d);
|
|
@@ -36,6 +36,7 @@ import type { MemoryEngine } from "../memory-engine/engine.js";
|
|
|
36
36
|
import type { ResolvedSkillListingBudget } from "../skill-listing-budget.js";
|
|
37
37
|
import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
|
|
38
38
|
import type { SessionPermissionRules } from "../session-policy-store.js";
|
|
39
|
+
import type { SafeNotifier } from "../safe-notify.js";
|
|
39
40
|
import type { RecoveredOrphan } from "../session-reconcile.js";
|
|
40
41
|
import type { SessionStore, StoredSession } from "../session.js";
|
|
41
42
|
import type { TaskOutcome } from "../task-outcome.js";
|
|
@@ -2063,6 +2064,49 @@ export interface RunInternals {
|
|
|
2063
2064
|
* threads it too) — a resumed leg's restore re-fires the observation with the settled root.
|
|
2064
2065
|
*/
|
|
2065
2066
|
onWorkspaceResolved?: (workspace: ResolvedWorkspace) => void;
|
|
2067
|
+
/**
|
|
2068
|
+
* A TRUSTED run-scoped WIRING-MANIFEST sink, filled ONLY by a core caller and NEVER a {@link
|
|
2069
|
+
* TaskSpec} field — the posture of {@link onActivity} and {@link onWorkspaceResolved} beside it.
|
|
2070
|
+
* Handed the very object this leg's `wiring_manifest` frame carries, at the moment that frame is
|
|
2071
|
+
* pushed: ONCE per prepared leg, after prepare and before the leg's first model or tool
|
|
2072
|
+
* interaction. Rides `internals` on FRESH and RESUMED legs alike, because the mint sits in the
|
|
2073
|
+
* run body both entries share.
|
|
2074
|
+
*
|
|
2075
|
+
* WHY a seat, when the frame already rides the stream: a COMPOSITION entry (the verify gate, the
|
|
2076
|
+
* cascade) drains its inner legs' queues ITSELF, so the host holds no TaskStream for any of them
|
|
2077
|
+
* and an inner leg's manifest dies inside the gate — the hole {@link onStatusEvent} was opened
|
|
2078
|
+
* for, one frame family over. A direct `runTask` caller needs neither seat: its own stream
|
|
2079
|
+
* carries both frames.
|
|
2080
|
+
*
|
|
2081
|
+
* `leg` is the LEG IDENTITY, and the engine mints NONE of it. It is present only where a
|
|
2082
|
+
* composition labelled the leg it dispatched: the verify entries hand each leg a per-dispatch
|
|
2083
|
+
* copy of this object whose sink closes over that leg's identity, so a consumer can tell the
|
|
2084
|
+
* implementation leg from round 2's verifier without inferring it from arrival order (the
|
|
2085
|
+
* ordering is all the sibling sinks have — see the attribution paragraph on `verifyCompleted`).
|
|
2086
|
+
* ABSENT therefore means "nobody labelled this leg", never "implementation": a plain `runTask`,
|
|
2087
|
+
* a workflow agent call and a cascade rung all reach this sink with no identity, and reading one
|
|
2088
|
+
* in would be the engine fabricating a fact it does not hold. NOT the same axis as the manifest's
|
|
2089
|
+
* OWN `leg` field ({@link WiringManifest.leg}, `root` / `child` / `resume`), which is a fact about
|
|
2090
|
+
* how the ENGINE reached this run; this one is a fact about the composition that dispatched it, and
|
|
2091
|
+
* a resumed fix leg carries both at once — `manifest.leg.kind === "resume"` and `leg.kind === "fix"`
|
|
2092
|
+
* are both true of it. `round` is 1-based over the gate's verification rounds: a `verifier` leg
|
|
2093
|
+
* carries the one it verifies, a `fix` leg the one whose findings it answers, and the entry
|
|
2094
|
+
* implementation leg carries 0 — before the first. The axis is relative to the gate INVOCATION,
|
|
2095
|
+
* not to the session's history: a resume entry's work leg is THAT invocation's implementation leg
|
|
2096
|
+
* (`impl`, round 0) even when the checkpoint it resumes was minted inside a previous invocation's
|
|
2097
|
+
* fix round, because the invocation's own round counter (`VerificationResult.rounds`) restarts with
|
|
2098
|
+
* it — one axis for both fields, rather than a leg that reads "fix round 1" inside a result whose
|
|
2099
|
+
* rounds begin at 1 again.
|
|
2100
|
+
*
|
|
2101
|
+
* Observe-only and failure-isolated through the SAME per-run notifier as {@link onActivity} (one
|
|
2102
|
+
* isolation scope for every sink on this object, so a broken sink is counted and disclosed once,
|
|
2103
|
+
* not once per site family): a throw — or an `async` sink returning a rejecting promise — is
|
|
2104
|
+
* contained and never faults the leg whose manifest it was handed.
|
|
2105
|
+
*/
|
|
2106
|
+
onWiringManifest?: (manifest: WiringManifest, leg?: {
|
|
2107
|
+
readonly kind: "impl" | "verifier" | "fix";
|
|
2108
|
+
readonly round: number;
|
|
2109
|
+
}) => void;
|
|
2066
2110
|
}
|
|
2067
2111
|
/**
|
|
2068
2112
|
* The payload of {@link RunInternals.onWorkspaceResolved}: the working root this task actually
|
|
@@ -2541,6 +2585,10 @@ export interface HarnessHandlersDeps {
|
|
|
2541
2585
|
runnerHooks: {
|
|
2542
2586
|
onError: RunnerDeps["onError"];
|
|
2543
2587
|
};
|
|
2588
|
+
/** The run's ONE isolation scope for the {@link RunInternals} observation sinks, minted by the
|
|
2589
|
+
* identity-wiring lane (which calls the first of them) and shared with these handlers, so the
|
|
2590
|
+
* "first failure per site" disclosure is one account for the whole run rather than one per lane. */
|
|
2591
|
+
internalsNotifier: SafeNotifier;
|
|
2544
2592
|
}
|
|
2545
2593
|
/**
|
|
2546
2594
|
* The Runner as the modules it drives and the compositions over it see it — the delegation tools
|
|
@@ -119,7 +119,9 @@ export function resumeContinuation(resume, mark) {
|
|
|
119
119
|
}
|
|
120
120
|
const decided = resume.outcome.decision === "allow"
|
|
121
121
|
? "was APPROVED and has now been executed — its result is in the tool results above"
|
|
122
|
-
:
|
|
122
|
+
: resume.outcome.hostDecision.decidedBy === "policy"
|
|
123
|
+
? "was DENIED by this deployment's own policy, not by a person — the refusal and its reason are in the tool results above"
|
|
124
|
+
: `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}`;
|
|
123
125
|
return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}. ` +
|
|
124
126
|
`This is a RESUMED task — every step before this point is already done and its results are in the ` +
|
|
125
127
|
`conversation above. Do NOT restart the task or re-run any tool you already ran; continue from this ` +
|
|
@@ -168,9 +168,10 @@ export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
|
|
|
168
168
|
}): Promise<InheritedClassifierJudgment<A>>;
|
|
169
169
|
/** The resolver's answer as the settlement reads it. */
|
|
170
170
|
export type FallbackResolution = Pick<ResolvedAsk, "action" | "settlement" | "approverUnavailable">;
|
|
171
|
-
/** A deny nobody
|
|
172
|
-
*
|
|
173
|
-
*
|
|
171
|
+
/** A deny nobody JUDGED, at a wrapper arm — read off the one table over the settlement words
|
|
172
|
+
* ({@link SETTLEMENT_NOBODY_JUDGED}) rather than listed here, so a word added to the set answers this
|
|
173
|
+
* question by getting a row and not by falling out of a disjunction. `approver_unavailable` is NOT
|
|
174
|
+
* headless here — it floats to the main gate, which owns that arm (a park, or the fail-closed refusal). */
|
|
174
175
|
export declare function headlessDenyAtFold(r: FallbackResolution): boolean;
|
|
175
176
|
/** The re-check's reading: the approved-edit helper has NO park route (an approved edit is
|
|
176
177
|
* re-adjudicated in place), so an approver that reports unavailable is as headless as none at all. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { classifierParseFailureDenyMessage, classifierUnavailableDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
|
|
2
2
|
import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js";
|
|
3
3
|
import { attestClassifierDenyCause, attestClassifierRound } from "../tool-policy.js";
|
|
4
|
+
import { SETTLEMENT_NOBODY_JUDGED } from "../gate-outcome.js";
|
|
4
5
|
import { deliverEngineNotice } from "../types.js";
|
|
5
6
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
6
7
|
import { askOriginOf, classifierMayAnswer, probeMandatedAsk } from "../ask-origin.js";
|
|
@@ -109,7 +110,7 @@ export async function judgeInheritedClassifier(opts) {
|
|
|
109
110
|
return { kind: "resolve", ask: minted, fallback, mintedHere: true, origin: askOriginOf(minted, INHERITED_STATION_FACTS) };
|
|
110
111
|
}
|
|
111
112
|
export function headlessDenyAtFold(r) {
|
|
112
|
-
return r.settlement
|
|
113
|
+
return r.settlement !== undefined && SETTLEMENT_NOBODY_JUDGED[r.settlement.kind];
|
|
113
114
|
}
|
|
114
115
|
export function headlessDenyAtRecheck(r) {
|
|
115
116
|
return headlessDenyAtFold(r) || r.settlement?.kind === "approver_unavailable" || r.approverUnavailable === true;
|
|
@@ -41,7 +41,7 @@ import { type HookEnvCapabilities, type Hooks } from "../hooks.js";
|
|
|
41
41
|
import type { LockedPreflight } from "../locked-config.js";
|
|
42
42
|
import type { OwnOrgAdmissionVerdict } from "../memory-admission.js";
|
|
43
43
|
import type { DelegationContentSafety } from "../memory-engine/delegation-provenance.js";
|
|
44
|
-
import type
|
|
44
|
+
import { type SessionPermissionRules } from "../session-policy-store.js";
|
|
45
45
|
import type { SessionStore } from "../session.js";
|
|
46
46
|
import { type OnAsk } from "../tool-policy.js";
|
|
47
47
|
import type { ApproverPosture, RunnerDeps, RuntimeCaps, ShellGateDoctrine, TaskEvent, TaskSpec, ToolExecuteContext } from "../types.js";
|
|
@@ -2,6 +2,7 @@ import { forkGovernanceDenial, resolveDelegationEntryCaps } from "../../agents/s
|
|
|
2
2
|
import { isLiveQuestionFace } from "../ask-question.js";
|
|
3
3
|
import { parentSpecSeatOf } from "../parent-spec-seat.js";
|
|
4
4
|
import { createPreToolUseConstraintPolicy } from "../hooks.js";
|
|
5
|
+
import { continuesLineage } from "../session-policy-store.js";
|
|
5
6
|
import { StubExecutionEnv } from "../stub-env.js";
|
|
6
7
|
import { askApproverIdentity, durableMandateSourceOf } from "../tool-policy.js";
|
|
7
8
|
import { attachRebuiltDenialTrackers, createDenialLimitStop } from "./denial-limit-arms.js";
|
|
@@ -21,7 +22,7 @@ export function prepareInheritedGate(input) {
|
|
|
21
22
|
return [...seedRules];
|
|
22
23
|
const merged = seedRules.map((s) => {
|
|
23
24
|
const live = liveRules.find((l) => l.sessionId === s.sessionId && l.principal === s.principal);
|
|
24
|
-
const sameLineage = live === undefined || live
|
|
25
|
+
const sameLineage = live === undefined || continuesLineage(live, s);
|
|
25
26
|
return live !== undefined && sameLineage && live.rev >= s.rev ? live : s;
|
|
26
27
|
});
|
|
27
28
|
const extras = liveRules.filter((l) => !seedRules.some((s) => s.sessionId === l.sessionId && s.principal === l.principal));
|
|
@@ -176,7 +176,9 @@ export async function resumeApply(input) {
|
|
|
176
176
|
const facts = plainPolicyOutcome.hostDecision;
|
|
177
177
|
const settlement = facts.decidedBy === "sla_timeout"
|
|
178
178
|
? { kind: "park_sla_expired", who: { party: "host", ...(facts.approver !== undefined ? { approver: facts.approver } : {}) }, when: Date.now() }
|
|
179
|
-
:
|
|
179
|
+
: facts.decidedBy === "policy"
|
|
180
|
+
? { kind: "policy_refused", who: { party: "none" }, when: Date.now() }
|
|
181
|
+
: settledByPerson(plainPolicyOutcome.decision === "allow" ? "human_allowed" : "human_refused", facts.approver, plainPolicyOutcome.decision === "deny" ? plainPolicyOutcome.reason : undefined);
|
|
180
182
|
plainPolicyOutcome = { ...plainPolicyOutcome, gateOutcome: mintGateOutcome({ ...(plainPolicyOutcome.decision === "deny" ? { deniedBy: "ask_resolution" } : {}), settled: { settlement, origin: parkedOrigin } }) };
|
|
181
183
|
}
|
|
182
184
|
const outcomeForStore = plainPolicyOutcome ?? plainReviewOutcome ?? plainParkOutcome;
|
|
@@ -5,7 +5,7 @@ import { sanitizeUntrustedText } from "../untrusted-text.js";
|
|
|
5
5
|
import { assertOutcomeText, describeSuppliedValue, pendingContentAskCallId, sameWinner } from "./decide-continuation.js";
|
|
6
6
|
const REFUSED_DECIDE_KEYS = [
|
|
7
7
|
["gateOutcome", "the settlement record is minted by core from the host's decision facts and the row's origin, never supplied"],
|
|
8
|
-
["settledBy", 'a retired key — the host supplies decision FACTS as hostDecision{decidedBy: "person" | "sla_timeout", approver?} and core mints the settlement word from them'],
|
|
8
|
+
["settledBy", 'a retired key — the host supplies decision FACTS as hostDecision{decidedBy: "person" | "sla_timeout" | "policy", approver?} and core mints the settlement word from them'],
|
|
9
9
|
["approver", "a retired key — the attribution rides the facts it belongs to, as hostDecision.approver"],
|
|
10
10
|
];
|
|
11
11
|
export function resumePolicyOutcome(input) {
|
|
@@ -49,14 +49,14 @@ export function resumePolicyOutcome(input) {
|
|
|
49
49
|
}
|
|
50
50
|
const hostDecisionRaw = decide.hostDecision;
|
|
51
51
|
if (typeof hostDecisionRaw !== "object" || hostDecisionRaw === null) {
|
|
52
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries no hostDecision — a policy_ask decide names who decided it ({ decidedBy: "person" | "sla_timeout", approver? }); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
|
|
52
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries no hostDecision — a policy_ask decide names who decided it ({ decidedBy: "person" | "sla_timeout" | "policy", approver? }); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
|
|
53
53
|
}
|
|
54
54
|
const decidedBy = hostDecisionRaw.decidedBy;
|
|
55
|
-
if (decidedBy !== "person" && decidedBy !== "sla_timeout") {
|
|
56
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume hostDecision.decidedBy "${describeSuppliedValue(decidedBy)}" is outside the closed set — it is exactly "person" or "
|
|
55
|
+
if (decidedBy !== "person" && decidedBy !== "sla_timeout" && decidedBy !== "policy") {
|
|
56
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume hostDecision.decidedBy "${describeSuppliedValue(decidedBy)}" is outside the closed set — it is exactly "person", "sla_timeout" or "policy"; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
|
|
57
57
|
}
|
|
58
|
-
if (decidedBy
|
|
59
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" decided by "
|
|
58
|
+
if (decidedBy !== "person" && decision === "allow") {
|
|
59
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" decided by "${decidedBy}" — only a person approves an action that EXECUTES, and that word names a decision nobody made; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
|
|
60
60
|
}
|
|
61
61
|
const attribution = screenApproverAttribution(hostDecisionRaw.approver);
|
|
62
62
|
if (attribution.defect !== undefined) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { observeThenableRejection } from "../safe-notify.js";
|
|
2
2
|
import { forwardSafely } from "./forward-sink.js";
|
|
3
3
|
import { emitTrace } from "../trace.js";
|
|
4
4
|
import { primaryActivityArg } from "../arg-summary.js";
|
|
@@ -12,10 +12,7 @@ import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
|
12
12
|
import { batchResponseDigest, toolEndBodyFrom } from "./tool-end-body.js";
|
|
13
13
|
export function createHarnessHandlers(input) {
|
|
14
14
|
const { prepared, stats, rs, deps } = input;
|
|
15
|
-
const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs } = deps;
|
|
16
|
-
const internalsNotifier = createSafeNotifier({
|
|
17
|
-
onError: (f) => console.warn(`[sema-core] ${f.site}: run-internals activity sink threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
|
|
18
|
-
});
|
|
15
|
+
const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs, internalsNotifier } = deps;
|
|
19
16
|
let lastWorkspaceCwd = prepared.cwdRef?.current;
|
|
20
17
|
const announceWorkspaceMove = () => {
|
|
21
18
|
const cwdNow = prepared.cwdRef?.current;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PeerInboundChainRef } from "../../agents/peer-admission.js";
|
|
2
|
+
import { type SafeNotifier } from "../safe-notify.js";
|
|
2
3
|
import { type NotificationInjectionOutcome, type PendingSessionNotifications, type SystemInjectionPriority, type TaskNotificationPayload } from "../task-notification.js";
|
|
3
4
|
import type { PushQueue } from "../push-queue.js";
|
|
4
5
|
import type { TaskEvent, TaskSpec } from "../types.js";
|
|
@@ -75,6 +76,11 @@ export interface RunIdentityWiringResult {
|
|
|
75
76
|
followUp: number;
|
|
76
77
|
} | undefined;
|
|
77
78
|
};
|
|
79
|
+
/** The run's ONE isolation scope for the {@link RunInternals} observation sinks: minted here because
|
|
80
|
+
* the manifest sink below is the run's first call into one, and handed on to the harness-handlers lane
|
|
81
|
+
* so its activity beats are contained by the SAME account — bounded disclosure is per notifier, so a
|
|
82
|
+
* second scope would report one broken sink twice and count it in two places. */
|
|
83
|
+
internalsNotifier: SafeNotifier;
|
|
78
84
|
/** Did the run END with a person's steer / follow-up still pending (queued, or drained at the final boundary and
|
|
79
85
|
* never served)? A READ FACE over this lane's own `let`, written by the same undrained-inputs sink on BOTH its arms
|
|
80
86
|
* (park or not): the terminal-adoption lane folds it into the answer-settled verdict — a "stop" answer the person had
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { uuidv7 } from "../../internal/harness.js";
|
|
2
|
-
import { createSafeNotifier } from "../safe-notify.js";
|
|
2
|
+
import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
|
|
3
3
|
import { discloseDroppedPending, renderTaskNotificationXml, taskNotificationDedupKey } from "../task-notification.js";
|
|
4
4
|
import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
|
|
5
5
|
export function runIdentityWiring(input) {
|
|
@@ -14,7 +14,12 @@ export function runIdentityWiring(input) {
|
|
|
14
14
|
const parentToolCallId = internals?.parentToolCallId;
|
|
15
15
|
const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
|
|
16
16
|
notificationLane.ident = ident;
|
|
17
|
-
|
|
17
|
+
const internalsNotifier = createSafeNotifier({
|
|
18
|
+
onError: (f) => console.warn(`[sema-core] ${f.site}: run-internals sink threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
|
|
19
|
+
});
|
|
20
|
+
const wiringManifest = prepared.wiringManifest;
|
|
21
|
+
queue.push({ type: "wiring_manifest", manifest: wiringManifest, ...ident() });
|
|
22
|
+
internalsNotifier.notify(() => observeThenableRejection(internals?.onWiringManifest?.(wiringManifest), internalsNotifier, "runtask.onWiringManifest"), "runtask.onWiringManifest");
|
|
18
23
|
prepared.toolRosterDeltas.subscribe((delta) => queue.push({ type: "tool_roster_delta", delta, ...ident() }));
|
|
19
24
|
prepared.toolDisclosure?.subscribe((snapshot) => queue.push({ type: "tool_disclosure", ...snapshot, ...ident() }));
|
|
20
25
|
const delegationLifecycleNotifier = createSafeNotifier({
|
|
@@ -90,5 +95,5 @@ export function runIdentityWiring(input) {
|
|
|
90
95
|
const loopLatch = { ended: false, userInterrupted: false, userHalted: false };
|
|
91
96
|
onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId, runId: prepared.runId, hookTimeoutMs: prepared.hookTimeoutMs, hookIdentity: prepared.hookIdentity });
|
|
92
97
|
const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
|
|
93
|
-
return { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, undrainedUserAtEnd: { get current() { return undrainedUserAtEnd; } }, userInputLostAtEnd: { get current() { return userInputLostAtEnd; } } };
|
|
98
|
+
return { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, internalsNotifier, undrainedUserAtEnd: { get current() { return undrainedUserAtEnd; } }, userInputLostAtEnd: { get current() { return userInputLostAtEnd; } } };
|
|
94
99
|
}
|
|
@@ -3,6 +3,7 @@ import { redactThenCut } from "../../agents/subagent-steps.js";
|
|
|
3
3
|
import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
|
|
4
4
|
import { CheckpointError, readPendingSteerQueue, realApprovalOrgFact, remainingBudgetMicroUsd } from "../checkpoint-store.js";
|
|
5
5
|
import { normalizePersistedRuleHit } from "../gate-lanes.js";
|
|
6
|
+
import { decidersNote } from "../gate-outcome.js";
|
|
6
7
|
import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
|
|
7
8
|
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
8
9
|
import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
|
|
@@ -11,7 +12,7 @@ import { computeCostMicroUsd } from "../pricing.js";
|
|
|
11
12
|
import { composeApprovalPostureDirective } from "../auto-mode-directive.js";
|
|
12
13
|
import { mintSystemReminder, statedReminderBodiesOf } from "../reminder-mint.js";
|
|
13
14
|
import { hasDestroy } from "../remote-env.js";
|
|
14
|
-
import { advertisedWritableRootsSeat, refuseOutOfContractDecision } from "../tool-policy.js";
|
|
15
|
+
import { advertisedWritableRootsSeat, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
|
|
15
16
|
import { callFaceSeat, judgeParkedToolIdentity, rosterEntryNamed, toolCallFaceOf } from "../tool-roster.js";
|
|
16
17
|
import { deliverEngineNotice } from "../types.js";
|
|
17
18
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
@@ -566,10 +567,15 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
|
|
|
566
567
|
...(outcome.gateOutcome?.settlement !== undefined && outcome.gateOutcome.origin !== undefined ? { settled: { settlement: outcome.gateOutcome.settlement, origin: outcome.gateOutcome.origin } } : {}),
|
|
567
568
|
});
|
|
568
569
|
if (outcome.decision === "deny") {
|
|
569
|
-
const
|
|
570
|
+
const settlementKind = outcome.gateOutcome?.settlement?.kind;
|
|
571
|
+
const defaultDenial = settlementKind === "park_sla_expired"
|
|
570
572
|
? `No one answered the approval request for the pending tool call "${pendingAction.toolName}" — the approval window elapsed with no answer, so it was not executed.`
|
|
571
573
|
: `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
|
|
572
|
-
const reason =
|
|
574
|
+
const reason = settlementKind === "policy_refused"
|
|
575
|
+
? policyRefusalMessage(pendingAction.toolName, undefined, decidersNote(outcome.reason))
|
|
576
|
+
: outcome.reason
|
|
577
|
+
? delimitUntrusted("reviewer note", outcome.reason, REVIEWER_NOTE_MAX_BODY)
|
|
578
|
+
: defaultDenial;
|
|
573
579
|
await settleEnd(true, { content: formatHookFeedback(reason, prepared.reminderMark) });
|
|
574
580
|
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason, prepared.reminderMark), true));
|
|
575
581
|
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
@@ -414,7 +414,7 @@ export class Runner {
|
|
|
414
414
|
runner: this.depsSeat, self: this, sessions: this.sessions, sessionReadStates: this.sessionReadStates, pendingSessionNotifications: this.pendingSessionNotifications,
|
|
415
415
|
prepareTask,
|
|
416
416
|
next: ({ prepared, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, modelCatalog, notificationLane, unsubscribeTaskNotifications }) => {
|
|
417
|
-
const { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, undrainedUserAtEnd, userInputLostAtEnd } = runIdentityWiring({
|
|
417
|
+
const { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, internalsNotifier, undrainedUserAtEnd, userInputLostAtEnd } = runIdentityWiring({
|
|
418
418
|
spec, queue, prepared, internals, taskIdRef, onReady, manualCompactRef, notifyRef, captureOptOutRef, mcpRedialRef, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef,
|
|
419
419
|
notificationLane, pendingSessionNotifications: this.pendingSessionNotifications, runner: this.depsSeat,
|
|
420
420
|
});
|
|
@@ -442,7 +442,7 @@ export class Runner {
|
|
|
442
442
|
deps: {
|
|
443
443
|
spec, queue, internals, ident, parentToolCallId, subagentName,
|
|
444
444
|
pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf,
|
|
445
|
-
toolLabels, postToolBatchHook, batchArgs,
|
|
445
|
+
toolLabels, postToolBatchHook, batchArgs, internalsNotifier,
|
|
446
446
|
runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info) },
|
|
447
447
|
},
|
|
448
448
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
2
2
|
import type { ToolEffect } from "../types.js";
|
|
3
3
|
import { type NamedToolPolicy } from "../tool-policy.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type SessionPermissionRules, type SessionPolicyStore, type StoredSessionRules } from "../session-policy-store.js";
|
|
5
5
|
/** Tool names whose write target is the `file_path`/`path` arg ({@link fileArgPath}).
|
|
6
6
|
*
|
|
7
7
|
* REF-B3 — the ONE definition of that narrower set. Production consumer today:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canonicalizeTarget, isWinFormPath, pathFamilyOf, writeTargetPath } from "../../tools/fs/safety.js";
|
|
2
2
|
import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
|
|
3
|
+
import { continuesLineage } from "../session-policy-store.js";
|
|
3
4
|
import { thrownText } from "../thrown-value.js";
|
|
4
5
|
import { ruleDenialSentence } from "../rule-denial-sentence.js";
|
|
5
6
|
import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
|
|
@@ -61,13 +62,11 @@ export function createLiveSessionRules(store, floor, hooks) {
|
|
|
61
62
|
return enforced;
|
|
62
63
|
}
|
|
63
64
|
if (enforced.rev > 0) {
|
|
64
|
-
if (live
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return enforced;
|
|
68
|
-
}
|
|
65
|
+
if (!continuesLineage(live, enforced)) {
|
|
66
|
+
announce(`session ${JSON.stringify(floor.sessionId)} has a rules row of a new lineage (generation ${live.gen}, rev ${live.rev}), which does not continue the row this leg enforces (generation ${enforced.gen})`);
|
|
67
|
+
return enforced;
|
|
69
68
|
}
|
|
70
|
-
|
|
69
|
+
if (vanished && live.gen === undefined) {
|
|
71
70
|
announce(`session ${JSON.stringify(floor.sessionId)} has a new rules row at rev ${live.rev} after the row this leg enforced was deleted, and its backend carries no lineage to continue it by`);
|
|
72
71
|
return enforced;
|
|
73
72
|
}
|
|
@@ -89,7 +88,7 @@ export function createLiveSessionRules(store, floor, hooks) {
|
|
|
89
88
|
if (handedRead !== undefined) {
|
|
90
89
|
handed.delete(toolCallId);
|
|
91
90
|
const claimed = await handedRead;
|
|
92
|
-
if (claimed.rev >= enforced.rev)
|
|
91
|
+
if (continuesLineage(claimed, enforced) && claimed.rev >= enforced.rev)
|
|
93
92
|
return claimed;
|
|
94
93
|
}
|
|
95
94
|
const p = read(signal);
|