@sema-agent/core 7.24.4 → 7.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 +78 -0
- package/dist/agents/peer-session-drain.d.ts +16 -1
- package/dist/agents/peer-session-drain.js +16 -1
- package/dist/agents/send-message-tool.d.ts +10 -2
- package/dist/agents/send-message-tool.js +18 -7
- package/dist/agents/subagent.js +13 -10
- package/dist/core/background-agent-store.d.ts +3 -3
- package/dist/core/checkpoint-store.d.ts +1 -0
- package/dist/core/delegation-frames.d.ts +5 -0
- package/dist/core/gate-fold.js +85 -0
- package/dist/core/gate-lanes.js +23 -1
- package/dist/core/gate-outcome.d.ts +8 -2
- package/dist/core/gate-outcome.js +3 -1
- package/dist/core/hooks.d.ts +29 -0
- package/dist/core/hooks.js +2 -0
- package/dist/core/mailbox-store.d.ts +5 -0
- package/dist/core/mailbox-store.js +1 -1
- package/dist/core/parent-spec-seat.d.ts +7 -1
- package/dist/core/parent-spec-seat.js +2 -0
- package/dist/core/permission-rule-consent.js +29 -47
- package/dist/core/permission-rule-provider.js +4 -3
- package/dist/core/permission-rule-store.d.ts +143 -7
- package/dist/core/permission-rule-store.js +165 -15
- package/dist/core/runner/contracts.d.ts +36 -2
- package/dist/core/runner/gate-exit.d.ts +14 -0
- package/dist/core/runner/gate-exit.js +4 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +4 -4
- package/dist/core/runner/prepare-delegation-surface.js +2 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
- package/dist/core/runner/prepare-gate-stations.js +7 -4
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
- package/dist/core/runner/prepare-hands-readface.js +25 -10
- package/dist/core/runner/prepare-inherited-gate.d.ts +23 -0
- package/dist/core/runner/prepare-inherited-gate.js +11 -6
- package/dist/core/runner/prepare-policy-chain.d.ts +15 -1
- package/dist/core/runner/prepare-policy-chain.js +14 -4
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -2
- package/dist/core/runner/prepare-suspend-saga.js +4 -3
- package/dist/core/runner/prepare-task.js +10 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -4
- package/dist/core/runner/prepare-wiring-manifest.js +8 -4
- package/dist/core/runner/resume-apply.js +2 -0
- package/dist/core/runner/run-harness-handlers.js +1 -0
- package/dist/core/runner/run-identity-wiring.d.ts +2 -2
- package/dist/core/runner/run-leg.js +26 -0
- package/dist/core/runner/run-notification-lane.d.ts +2 -2
- package/dist/core/runner/run-notification-lane.js +2 -1
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/session-rule-policy.d.ts +3 -4
- package/dist/core/runner/session-rule-policy.js +11 -3
- package/dist/core/session-policy-store.d.ts +27 -3
- package/dist/core/session-policy-store.js +12 -4
- package/dist/core/store-contracts/background-agent-store-contract.js +10 -0
- package/dist/core/store-contracts/peer-directory-contract.d.ts +23 -0
- package/dist/core/store-contracts/peer-directory-contract.js +82 -0
- package/dist/core/task-event.d.ts +6 -0
- package/dist/core/task-notification.d.ts +79 -4
- package/dist/core/task-notification.js +90 -1
- package/dist/core/task-registry-agent.js +1 -2
- package/dist/core/task-registry-monitor.js +2 -1
- package/dist/core/task-registry-shared.d.ts +53 -5
- package/dist/core/task-registry-shared.js +13 -2
- package/dist/core/task-registry.js +8 -5
- package/dist/core/task-spec.d.ts +12 -5
- package/dist/core/tool-catalog-entries.js +1 -0
- package/dist/core/tool-spec.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +2 -2
- package/dist/engine/loop/agent-loop.js +1 -1
- package/dist/engine/loop/types.d.ts +4 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -3
- package/dist/prompts/coordinator.d.ts +2 -1
- package/dist/stores/file/adoption/adopt.js +23 -1
- package/dist/stores/file/permission-rule-store.js +3 -1
- package/dist/stores/file/session-policy-store.d.ts +31 -0
- package/dist/stores/file/session-policy-store.js +87 -7
- package/dist/tools/fs/bash-program-position.d.ts +4 -26
- package/dist/tools/fs/bash-program-position.js +0 -11
- package/dist/tools/fs/bash-readonly-classifier.d.ts +20 -309
- package/dist/tools/fs/bash-readonly-classifier.js +28 -766
- package/dist/tools/fs/fs-bash.d.ts +43 -207
- package/dist/tools/fs/fs-bash.js +20 -341
- package/dist/tools/fs/index.d.ts +12 -13
- package/dist/tools/fs/index.js +3 -4
- package/dist/tools/fs/read-deny.d.ts +25 -0
- package/dist/tools/fs/read-deny.js +19 -0
- package/dist/tools/fs/safety.js +2 -6
- package/dist/tools/fs/shell-read-check.d.ts +99 -0
- package/dist/tools/fs/shell-read-check.js +229 -0
- package/dist/tools/fs/shell-read-establish.d.ts +70 -115
- package/dist/tools/fs/shell-read-establish.js +216 -102
- package/dist/tools/fs/shell-read-judge.d.ts +231 -0
- package/dist/tools/fs/shell-read-judge.js +170 -0
- package/dist/tools/fs/shell-read-plan.d.ts +3 -0
- package/dist/tools/fs/shell-read-plan.js +82 -19
- package/dist/tools/monitor.d.ts +5 -5
- package/dist/tools/monitor.js +3 -5
- package/dist/tools/task-list.d.ts +4 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -3
- package/wire-changes/core@7.25.0.json +671 -0
|
@@ -56,19 +56,19 @@ export function assertDeleteDeltaCarriesNoAdd(delta) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
export function assertWriteDeltaScopeDurable(delta) {
|
|
59
|
-
const scope = delta.kind === "
|
|
59
|
+
const scope = delta.kind === "tighten-delete" ? delta.tombstone?.scope : delta.scope;
|
|
60
60
|
if (scope?.kind === "session") {
|
|
61
|
-
const e = new Error(delta.kind === "
|
|
62
|
-
? "a session-scope
|
|
63
|
-
:
|
|
61
|
+
const e = new Error(delta.kind === "tighten-delete"
|
|
62
|
+
? "a session-scope tombstone cannot enter the persisted store — session rows are not individually deletable and die with their session (design/382 §4.3/§10); refusing the tighten-delete"
|
|
63
|
+
: `a session-scope rule cannot enter the persisted store — its home is the session's own overlay (design/382 §4.3); refusing the ${delta.kind}`);
|
|
64
64
|
e.code = "unsupported.session_scope_store";
|
|
65
65
|
throw e;
|
|
66
66
|
}
|
|
67
67
|
if (!isValidDurableScope(scope)) {
|
|
68
|
-
throw new Error(delta.kind === "
|
|
69
|
-
?
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
throw new Error(delta.kind === "tighten-delete"
|
|
69
|
+
?
|
|
70
|
+
"a tighten-delete must carry a tombstone whose scope is a well-formed durable scope (design/382 §4.3: the face is {global, project-with-a-root}) — refusing the write"
|
|
71
|
+
: `a ${delta.kind} whose scope is not a well-formed durable scope cannot enter the persisted store (design/382 §4.3: the face is {global, project-with-a-root}) — refusing the write`);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
export function isValidConsentScope(scope) {
|
|
@@ -393,6 +393,7 @@ export async function ruleStoreChecksum(payload) {
|
|
|
393
393
|
const { createHash } = await import("node:crypto");
|
|
394
394
|
return `sha256:${createHash("sha256").update(canonicalize(payload), "utf8").digest("hex")}`;
|
|
395
395
|
}
|
|
396
|
+
export const REMOVAL_LIVENESS_WORDS = ["yes", "no", "unknown"];
|
|
396
397
|
const REMOVE_MAX_ATTEMPTS = 8;
|
|
397
398
|
export async function removePersistedRule(opts) {
|
|
398
399
|
const behavior = opts.behavior;
|
|
@@ -435,6 +436,7 @@ export async function removePersistedRule(opts) {
|
|
|
435
436
|
const tombstone = { behavior, rule, scope, removedDots, deletedBy: await writer.nextDot() };
|
|
436
437
|
let expectedRev = snapshot.rev;
|
|
437
438
|
let lastError = "";
|
|
439
|
+
let applyThrew = false;
|
|
438
440
|
for (let attempt = 0; attempt < REMOVE_MAX_ATTEMPTS; attempt++) {
|
|
439
441
|
try {
|
|
440
442
|
const res = await writer.apply({ kind: "tighten-delete", tombstone }, { expectedRev });
|
|
@@ -442,37 +444,181 @@ export async function removePersistedRule(opts) {
|
|
|
442
444
|
return { status: "failed", error: `the store refused the tombstone (${res.refused}) — nothing was removed` };
|
|
443
445
|
}
|
|
444
446
|
if (!("conflict" in res)) {
|
|
445
|
-
let stillLive
|
|
447
|
+
let stillLive;
|
|
446
448
|
try {
|
|
447
449
|
const after = await store.effective();
|
|
448
450
|
if (after.unreadable !== undefined)
|
|
449
451
|
throw new Error(after.unreadable);
|
|
450
|
-
stillLive = after.rules.some((r) => sameRuleIdentity(r, identity) && isRuleLive(r));
|
|
452
|
+
stillLive = after.rules.some((r) => sameRuleIdentity(r, identity) && isRuleLive(r)) ? "yes" : "no";
|
|
451
453
|
}
|
|
452
454
|
catch {
|
|
453
|
-
stillLive =
|
|
455
|
+
stillLive = "unknown";
|
|
454
456
|
}
|
|
455
457
|
return { status: "removed", rev: res.rev, stillLive };
|
|
456
458
|
}
|
|
457
459
|
expectedRev = res.rev;
|
|
458
460
|
}
|
|
459
461
|
catch (err) {
|
|
462
|
+
applyThrew = true;
|
|
460
463
|
lastError = errText(err);
|
|
461
464
|
break;
|
|
462
465
|
}
|
|
463
466
|
}
|
|
464
|
-
const reason =
|
|
467
|
+
const reason = applyThrew
|
|
468
|
+
? lastError !== ""
|
|
469
|
+
? lastError
|
|
470
|
+
: "the permission-rule store threw while applying the tombstone, with no message"
|
|
471
|
+
: `optimistic-concurrency retries exhausted after ${REMOVE_MAX_ATTEMPTS} attempts`;
|
|
465
472
|
try {
|
|
466
473
|
const after = await writer.readRaw();
|
|
467
474
|
const landed = after.tombstones.some((t) => t.deletedBy.actor === tombstone.deletedBy.actor && t.deletedBy.counter === tombstone.deletedBy.counter);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
475
|
+
if (landed) {
|
|
476
|
+
return { status: "removed", rev: after.rev, stillLive: applyTombstones(after.rules, after.tombstones).some((r) => sameRuleIdentity(r, identity) && isRuleLive(r)) ? "yes" : "no" };
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
status: "failed",
|
|
480
|
+
error: applyThrew
|
|
481
|
+
? `${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()`
|
|
482
|
+
: `${reason} (read-back confirms no tombstone was written)`,
|
|
483
|
+
};
|
|
471
484
|
}
|
|
472
485
|
catch (err) {
|
|
473
486
|
return { status: "failed", error: `${reason}; the read-back also failed (${errText(err)}) so this outcome is indeterminate — reconcile with list()` };
|
|
474
487
|
}
|
|
475
488
|
}
|
|
489
|
+
const RULE_ADD_MAX_ATTEMPTS = 8;
|
|
490
|
+
const SESSION_ENDED_REFUSAL = "the session this authorization targets has ended — a session rule dies with its session, and a late or replayed redemption cannot revive it (re-trigger the command in a live session for a fresh card)";
|
|
491
|
+
export async function landRuleAdd(args) {
|
|
492
|
+
switch (args.delta.kind) {
|
|
493
|
+
case "redemption-add":
|
|
494
|
+
case "tighten-add":
|
|
495
|
+
break;
|
|
496
|
+
default: {
|
|
497
|
+
const unreachable = args.delta;
|
|
498
|
+
throw new Error(`landRuleAdd: unhandled add delta ${JSON.stringify(unreachable)}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (let attempt = 0; attempt < RULE_ADD_MAX_ATTEMPTS; attempt++) {
|
|
502
|
+
let rev;
|
|
503
|
+
try {
|
|
504
|
+
const snapshot = await args.store.effective();
|
|
505
|
+
if (snapshot.unreadable !== undefined)
|
|
506
|
+
throw new Error(snapshot.unreadable);
|
|
507
|
+
rev = snapshot.rev;
|
|
508
|
+
const refusal = await args.preflight?.(snapshot);
|
|
509
|
+
if (refusal !== undefined)
|
|
510
|
+
return { landed: false, reason: refusal, committed: "no" };
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
return { landed: false, reason: `could not read the permission-rule store: ${errText(err)}`, committed: "no" };
|
|
514
|
+
}
|
|
515
|
+
let res;
|
|
516
|
+
try {
|
|
517
|
+
res = await args.writer.apply(args.delta, { expectedRev: rev });
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
return { landed: false, reason: `the permission-rule store refused the write: ${errText(err)}`, committed: "unknown" };
|
|
521
|
+
}
|
|
522
|
+
if ("refused" in res)
|
|
523
|
+
return { landed: false, reason: SESSION_ENDED_REFUSAL, committed: "no" };
|
|
524
|
+
if (!("conflict" in res))
|
|
525
|
+
return { landed: true, rev: res.rev };
|
|
526
|
+
}
|
|
527
|
+
return { landed: false, reason: `optimistic-concurrency retries exhausted after ${RULE_ADD_MAX_ATTEMPTS} attempts`, committed: "no" };
|
|
528
|
+
}
|
|
529
|
+
export async function addPersistedRule(opts) {
|
|
530
|
+
const behavior = opts.behavior;
|
|
531
|
+
const ruleText = opts.rule;
|
|
532
|
+
const scope = structuredClone(opts.scope);
|
|
533
|
+
const target = opts.principal;
|
|
534
|
+
if (!isRuleBehavior(behavior)) {
|
|
535
|
+
return { status: "failed", error: `addPersistedRule: behavior ${escapeForDisclosure(behavior)} is not one of ${["deny", "ask", "allow"].join("/")} — refusing to name a rule identity nothing can carry` };
|
|
536
|
+
}
|
|
537
|
+
if (behavior === "allow") {
|
|
538
|
+
return {
|
|
539
|
+
status: "failed",
|
|
540
|
+
error: "addPersistedRule: an allow cannot be written through this entry — a standing approval is a person's yes and takes a consent record (prepare → confirm → redeem); this entry writes the tightening direction (deny/ask) only",
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (!isValidConsentScope(scope)) {
|
|
544
|
+
return { status: "failed", error: `addPersistedRule: ${escapeForDisclosure(JSON.stringify(scope))} is not a valid rule scope — refusing to write a row nothing can read back` };
|
|
545
|
+
}
|
|
546
|
+
const parsed = parseRuleText(ruleText, behavior);
|
|
547
|
+
if ("reject" in parsed)
|
|
548
|
+
return { status: "failed", error: `addPersistedRule: ${parsed.reject.code}: ${parsed.reject.message}` };
|
|
549
|
+
const identity = { behavior, rule: parsed.rule.rule, scope };
|
|
550
|
+
const owner = typeof target === "string" ? { kind: "principal", principal: target } : structuredClone(target);
|
|
551
|
+
const provider = opts.provider;
|
|
552
|
+
if (owner.kind === "local-owner" && provider.forLocalOwner === undefined) {
|
|
553
|
+
return { status: "failed", error: "this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be added through it" };
|
|
554
|
+
}
|
|
555
|
+
const store = owner.kind === "local-owner" ? provider.forLocalOwner() : provider.forPrincipal(owner.principal);
|
|
556
|
+
const writer = writerOf(store);
|
|
557
|
+
if (writer === undefined) {
|
|
558
|
+
return { status: "failed", error: "the resolved permission-rule store has no write face — rules cannot be added through it" };
|
|
559
|
+
}
|
|
560
|
+
const query = scope.kind === "session" ? { sessionId: scope.sessionId } : undefined;
|
|
561
|
+
let snapshot;
|
|
562
|
+
try {
|
|
563
|
+
snapshot = await store.effective(query);
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
return { status: "failed", error: `could not read the permission-rule store: ${errText(err)}` };
|
|
567
|
+
}
|
|
568
|
+
if (snapshot.unreadable !== undefined)
|
|
569
|
+
return { status: "failed", error: `could not read the permission-rule store: ${snapshot.unreadable}` };
|
|
570
|
+
if (snapshot.rules.some((r) => sameRuleIdentity(r, identity) && isRuleLive(r)))
|
|
571
|
+
return { status: "no-op", rev: snapshot.rev };
|
|
572
|
+
let dot;
|
|
573
|
+
try {
|
|
574
|
+
dot = await writer.nextDot();
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
return { status: "failed", error: `could not mint the add's dot: ${errText(err)}` };
|
|
578
|
+
}
|
|
579
|
+
const landing = await landRuleAdd({
|
|
580
|
+
store,
|
|
581
|
+
writer,
|
|
582
|
+
delta: {
|
|
583
|
+
kind: "tighten-add",
|
|
584
|
+
behavior,
|
|
585
|
+
rule: parsed.rule.rule,
|
|
586
|
+
scope,
|
|
587
|
+
tool: parsed.rule.tool,
|
|
588
|
+
match: parsed.rule.match,
|
|
589
|
+
command: parsed.rule.command,
|
|
590
|
+
add: { dot, origin: "user", createdAt: new Date().toISOString() },
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
if (landing.landed)
|
|
594
|
+
return { status: "added", rev: landing.rev, dot };
|
|
595
|
+
if (landing.committed === "no")
|
|
596
|
+
return { status: "failed", error: landing.reason };
|
|
597
|
+
try {
|
|
598
|
+
let rev;
|
|
599
|
+
let rows;
|
|
600
|
+
let quarantinedRows = [];
|
|
601
|
+
if (scope.kind === "session") {
|
|
602
|
+
const view = await store.effective(query);
|
|
603
|
+
if (view.unreadable !== undefined)
|
|
604
|
+
throw new Error(view.unreadable);
|
|
605
|
+
({ rev, rules: rows } = view);
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
const raw = await writer.readRaw();
|
|
609
|
+
({ rev, rules: rows } = raw);
|
|
610
|
+
quarantinedRows = raw.quarantined ?? [];
|
|
611
|
+
}
|
|
612
|
+
const stood = rows.some((r) => sameRuleIdentity(r, identity) && r.adds.some((a) => sameDot(a.dot, dot))) ||
|
|
613
|
+
quarantinedRows.some((q) => sameDot(q.add.dot, dot));
|
|
614
|
+
return stood
|
|
615
|
+
? { status: "added", rev, dot }
|
|
616
|
+
: { status: "failed", error: `${landing.reason}; the store retains no trace of this add, which cannot tell a write that never landed from one whose evidence was collected — the outcome is indeterminate, reconcile with list()` };
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
return { status: "failed", error: `${landing.reason}; the read-back also failed (${errText(err)}) so this outcome is indeterminate — reconcile with list()` };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
476
622
|
export function errText(err) {
|
|
477
623
|
return thrownText(err);
|
|
478
624
|
}
|
|
@@ -525,6 +671,10 @@ export class InMemoryDurableRulePartition {
|
|
|
525
671
|
assertRedemptionNotQuarantined(this.quarantinedRows, delta);
|
|
526
672
|
this.rules = foldDelta(this.rules, delta);
|
|
527
673
|
}
|
|
674
|
+
else if (delta.kind === "tighten-add") {
|
|
675
|
+
assertWriteDeltaScopeDurable(delta);
|
|
676
|
+
this.rules = foldDelta(this.rules, delta);
|
|
677
|
+
}
|
|
528
678
|
else if (delta.kind === "tighten-delete") {
|
|
529
679
|
assertWriteDeltaScopeDurable(delta);
|
|
530
680
|
assertDeleteDeltaCarriesNoAdd(delta);
|
|
@@ -382,6 +382,10 @@ export interface Prepared {
|
|
|
382
382
|
/** The deployment's own caller policy, re-checked on a durable resume ONLY when the approver
|
|
383
383
|
* rewrote the pending call's args (see the composition site for why the edit case is special). */
|
|
384
384
|
basePolicyForResumeEdit?: ToolPolicy;
|
|
385
|
+
/** The leg's shell READ STATION, re-run on a durable RESUME before an approved shell call executes: the parked leg's
|
|
386
|
+
* admission died with that leg, so the resumed bytes are judged (and admitted for their pre-spawn re-establishment)
|
|
387
|
+
* on THIS leg — a deny row found now vetoes, an operator's edit is judged on the `rejudge` seat. */
|
|
388
|
+
shellReadCheck?: import("../../tools/fs/shell-read-check.js").ShellReadCheck;
|
|
385
389
|
/** The ORG adjudication face, re-resolved on a durable RESUME before an approved
|
|
386
390
|
* pending call executes. The resume path bypasses the harness gate by design (a human already
|
|
387
391
|
* adjudicated the checkpointed call), which is exactly where org policy skew is most likely: the
|
|
@@ -437,6 +441,14 @@ export interface Prepared {
|
|
|
437
441
|
* leg always knows its identity.
|
|
438
442
|
*/
|
|
439
443
|
hookIdentity: HookInvocationIdentity;
|
|
444
|
+
/**
|
|
445
|
+
* This session's DURABLE box, present iff this leg mounted the session-box drain (a top-level leg
|
|
446
|
+
* over a cross-process-safe mailbox with a grammatical session id). The notification lane binds it
|
|
447
|
+
* to the park seat ({@link import("../task-notification.js").PendingSessionNotifications.bindSessionBox})
|
|
448
|
+
* so a child's uplink frame parked for this session while it is idle lands in the box the next run
|
|
449
|
+
* drains — one mount judgment, the drain and the park read it alike. Absent ⇒ in-process parking.
|
|
450
|
+
*/
|
|
451
|
+
sessionBox?: import("../task-notification.js").SessionBoxBinding;
|
|
440
452
|
/**
|
|
441
453
|
* The per-invocation TIME BOUND every hook seat of this leg runs under (`Hooks.timeoutMs`, already
|
|
442
454
|
* validated — a garbage value was refused to the default and disclosed ONCE, here, rather than on
|
|
@@ -1252,6 +1264,7 @@ export interface InheritedGate {
|
|
|
1252
1264
|
sessionId: string;
|
|
1253
1265
|
principal?: string;
|
|
1254
1266
|
rev: number;
|
|
1267
|
+
gen?: number;
|
|
1255
1268
|
rules: SessionPermissionRules;
|
|
1256
1269
|
}>;
|
|
1257
1270
|
/** The chain's effective shell-gate doctrine — the child folds it by max-rank with its own spec. */
|
|
@@ -1700,6 +1713,14 @@ export interface RunInternals {
|
|
|
1700
1713
|
* every described child a "teammate" (clamp over-wide, addendum over-composed).
|
|
1701
1714
|
*/
|
|
1702
1715
|
explicitAgentName?: string;
|
|
1716
|
+
/**
|
|
1717
|
+
* The RESOLVED subagent type this child was spawned as (`"general-purpose"` / `"fork"` / a roster
|
|
1718
|
+
* definition's name — the same word the fleet row's `agentType` column carries; never the
|
|
1719
|
+
* description). Read by the hook identity envelope (`identity.agentType`) so a process-level hook
|
|
1720
|
+
* seat can tell WHAT kind of child it is serving without parsing display text. Trusted run-scoped
|
|
1721
|
+
* channel (NOT a {@link TaskSpec} field); absent on a top-level run and on a workflow-lane agent.
|
|
1722
|
+
*/
|
|
1723
|
+
spawnAgentType?: string;
|
|
1703
1724
|
/**
|
|
1704
1725
|
* The SPAWNING run's taskId, threaded at spawn (from the parent's
|
|
1705
1726
|
* `ToolExecuteContext.taskId`) so this child's `task_progress` ticks carry `parentTaskId`. Lets a UI build the
|
|
@@ -1897,7 +1918,20 @@ export interface RunInternals {
|
|
|
1897
1918
|
*/
|
|
1898
1919
|
parentNotify?: (notification: TaskNotificationPayload, opts?: {
|
|
1899
1920
|
priority?: import("../task-notification.js").SystemInjectionPriority;
|
|
1900
|
-
}) =>
|
|
1921
|
+
}) => unknown;
|
|
1922
|
+
/**
|
|
1923
|
+
* The ONE address `SendMessage("main")` names for this child — the session that hosts this child's
|
|
1924
|
+
* lifecycle — paired with {@link parentNotify} at the same mint, three arms:
|
|
1925
|
+
* · a fresh spawn: the SPAWNER's session, whose live injector is `parentNotify`;
|
|
1926
|
+
* · a revival by that tree's ROOT (or any top-level session): the REVIVER's session, its injector;
|
|
1927
|
+
* · a revival by a SIBLING child, or by a host verb with no session: the row's ROOT session, and
|
|
1928
|
+
* NO `parentNotify` — a sibling is a peer the child addresses by name, not its main, and there is
|
|
1929
|
+
* no live callback into the root from either reviver. Such a child's "main" send reads the
|
|
1930
|
+
* honest not-deliverable text; its terminal still reaches the root through the root-anchor arm.
|
|
1931
|
+
* The uplink receipt names this id so the model knows WHICH conversation "main" is. Absent on a
|
|
1932
|
+
* top-level run. Trusted run-scoped channel (never a {@link TaskSpec} field).
|
|
1933
|
+
*/
|
|
1934
|
+
uplinkSessionId?: string;
|
|
1901
1935
|
/**
|
|
1902
1936
|
* The PARENT run's subagent-retain ledger, threaded to a CHILD so its
|
|
1903
1937
|
* SendMessage can continue a RETAINED SIBLING (the sibling's retain entry lives on the parent's
|
|
@@ -1917,7 +1951,7 @@ export interface RunInternals {
|
|
|
1917
1951
|
*/
|
|
1918
1952
|
onNotifyInjectorReady?: (inject: (notification: TaskNotificationPayload, opts?: {
|
|
1919
1953
|
priority?: import("../task-notification.js").SystemInjectionPriority;
|
|
1920
|
-
}) => Promise<"
|
|
1954
|
+
}) => Promise<import("../task-notification.js").NotificationInjectionOutcome>) => void;
|
|
1921
1955
|
/**
|
|
1922
1956
|
* Per-task ISOLATION hint, a TRUSTED run-scoped channel filled ONLY by a core caller
|
|
1923
1957
|
* (the workflow's `ctx.agent` when the SCRIPT passed `{ isolation: "worktree" }` as an OPTION) — NEVER a
|
|
@@ -219,6 +219,15 @@ export interface GatePass {
|
|
|
219
219
|
* that ended as a refusal). The exit mints the outcome from it beside the ledger's `settled`; a refusal
|
|
220
220
|
* that reaches the exit without it is an engine defect and the mint throws. */
|
|
221
221
|
deniedBy: DeniedBy | undefined;
|
|
222
|
+
/** borrowed-mutable — a refusing station's STRUCTURED details (the shell read station's refusal card), written
|
|
223
|
+
* beside `deniedBy` by that station alone; the exit puts it on the gate result for the loop's error result.
|
|
224
|
+
* OPTIONAL (the `probeUnanswered` form): the dispatch that builds the pass has nothing to declare here. */
|
|
225
|
+
denyDetails?: unknown;
|
|
226
|
+
/** borrowed-mutable — the shell read station's containment card behind a standing MANDATED ask (an out-of-root
|
|
227
|
+
* or unestablishable read): when that ask ends as a refusal (a person's no, nobody to ask), the exit puts the
|
|
228
|
+
* card on the gate result, so the refusal's `tool_end` still says WHAT was found (`readonly_out_of_root`) beside
|
|
229
|
+
* WHO refused (`ask_resolution`). Writer: the fold's read station. */
|
|
230
|
+
shellReadDetails?: unknown;
|
|
222
231
|
/** borrowed-mutable — the FORM of the classifier round behind a deny (`unavailable` / `parse_error`), written
|
|
223
232
|
* beside `deniedBy` by the gate's own classifier step (its attestation, read once) or by the fold / the
|
|
224
233
|
* re-check when an inherited station's attested deny arrives through the chain; undefined for a ruling
|
|
@@ -251,6 +260,11 @@ export interface GatePass {
|
|
|
251
260
|
* twin on the answered side, and narrower: the ask stays ordinary for every ANSWERER, and only the allow
|
|
252
261
|
* layer's read-only-command arm stands down. Writer: the fold. Reader: that arm, and nothing else. */
|
|
253
262
|
probeBoundaryDeclined?: true;
|
|
263
|
+
/** borrowed-mutable — the shell read station minted the standing ask's directory-grant offer
|
|
264
|
+
* (`probeReadRootCandidate`). Read by the classify seat's shape probe, which withdraws the offer when its
|
|
265
|
+
* own objection stands beside the station's — the grant answers the station's finding, not the shape's.
|
|
266
|
+
* Writer: the fold's read station. */
|
|
267
|
+
shellReadOffer?: true;
|
|
254
268
|
/** borrowed-mutable — the write-protection judge covered this call and its table MATCHED: the allow
|
|
255
269
|
* layer must not clear the surviving ask, a live decider still may. Writer: the fold's fourth tighten,
|
|
256
270
|
* on BOTH its arms. Reader: the lanes' mandate. */
|
|
@@ -135,6 +135,10 @@ export async function exitGate(pass) {
|
|
|
135
135
|
block: true,
|
|
136
136
|
reason: formatHookFeedback(denyReason, input.reminderMark),
|
|
137
137
|
gate,
|
|
138
|
+
...(() => {
|
|
139
|
+
const details = pass.denyDetails ?? pass.shellReadDetails;
|
|
140
|
+
return details !== undefined ? { details } : {};
|
|
141
|
+
})(),
|
|
138
142
|
preToolContext: pass.preToolContext,
|
|
139
143
|
};
|
|
140
144
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
|
|
2
2
|
import { type PeerLaneRefs } from "../../agents/peer-session-drain.js";
|
|
3
3
|
import type { SubagentRetainLedger } from "../../agents/retain-ledger.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ShellReadAdmission } from "../../tools/fs/shell-read-check.js";
|
|
5
5
|
import { type WorktreeSessionRef } from "../../tools/worktree.js";
|
|
6
6
|
import type { CwdRef } from "../../tools/fs/index.js";
|
|
7
7
|
import { type ToolResultStore } from "../tool-result-store.js";
|
|
@@ -19,7 +19,7 @@ export interface PrepareDelegationSurfaceInput {
|
|
|
19
19
|
deps: Pick<RunnerDeps, "workflowRunStore" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "peerAdmission" | "peerDirectory" | "onNotice" | "onBackgroundChildEvent" | "tracer">;
|
|
20
20
|
/** borrowed-readonly — the trusted spawn-side channel: `parentNotify` (the delegated-child door), the uplink / sibling /
|
|
21
21
|
* identity seats the SendMessage mount forwards, `onTaskNotification`, `onSubagentSpawn`. Never mutated. */
|
|
22
|
-
internals: Pick<RunInternals, "parentNotify" | "parentPeerRef" | "parentRetainLedger" | "explicitAgentName" | "parentTaskId" | "parentSessionId" | "peerSelfRef" | "peerInboundChainRef" | "onTaskNotification" | "onSubagentSpawn"> | undefined;
|
|
22
|
+
internals: Pick<RunInternals, "parentNotify" | "parentPeerRef" | "uplinkSessionId" | "parentRetainLedger" | "explicitAgentName" | "parentTaskId" | "parentSessionId" | "peerSelfRef" | "peerInboundChainRef" | "onTaskNotification" | "onSubagentSpawn"> | undefined;
|
|
23
23
|
/** borrowed-readonly — the resume leg, or undefined: `seed.activeWorktree` seeds the worktree session (checkpoint first). */
|
|
24
24
|
resume: Pick<PrepareResume, "seed"> | undefined;
|
|
25
25
|
/** borrowed-readonly — the acquired session id (the mounts' session axis). */
|
|
@@ -37,8 +37,8 @@ export interface PrepareDelegationSurfaceInput {
|
|
|
37
37
|
runnerSelf: RunnerSelfSeat | undefined;
|
|
38
38
|
/** borrowed-readonly — the background/workflow door's first half: the hands mount decided a real shell with write hands. */
|
|
39
39
|
backgroundTaskToolsActive: boolean;
|
|
40
|
-
/** borrowed-readonly — this run's shell read
|
|
41
|
-
|
|
40
|
+
/** borrowed-readonly — this run's shell read station's pre-spawn ADMISSION (declared at the hands phase), for `Monitor`: the only P11 face that spawns a shell command. */
|
|
41
|
+
shellReadAdmission: ShellReadAdmission | undefined;
|
|
42
42
|
/** borrowed-readonly — the door's second half: the Workflow tool is on the roster. */
|
|
43
43
|
workflowToolsActive: boolean;
|
|
44
44
|
/** borrowed-readonly — the peer lane's mount verdict for this leg. */
|
|
@@ -56,6 +56,7 @@ export function prepareDelegationSurface(input) {
|
|
|
56
56
|
...(internals?.parentNotify !== undefined
|
|
57
57
|
? { uplink: internals.parentNotify, ...(internals.parentPeerRef !== undefined ? { uplinkRecipient: internals.parentPeerRef } : {}) }
|
|
58
58
|
: {}),
|
|
59
|
+
...(internals?.uplinkSessionId !== undefined ? { uplinkSessionId: internals.uplinkSessionId } : {}),
|
|
59
60
|
...(internals?.explicitAgentName !== undefined ? { senderName: internals.explicitAgentName } : {}),
|
|
60
61
|
...(internals?.parentRetainLedger !== undefined ? { siblingRetain: internals.parentRetainLedger } : {}),
|
|
61
62
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
@@ -103,7 +104,7 @@ export function prepareDelegationSurface(input) {
|
|
|
103
104
|
envHandToolNames.add("Monitor");
|
|
104
105
|
roster.mount(firstPartyOffload(createMonitorTool(executionEnv, {
|
|
105
106
|
registry: defaultTaskRegistry,
|
|
106
|
-
...(input.
|
|
107
|
+
...(input.shellReadAdmission !== undefined ? { shellReadAdmission: input.shellReadAdmission } : {}),
|
|
107
108
|
owner: hostTaskId,
|
|
108
109
|
scope: taskScope,
|
|
109
110
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
@@ -31,6 +31,7 @@ import type { ToolPolicy } from "../tool-policy.js";
|
|
|
31
31
|
import { type GateOutcome } from "../gate-outcome.js";
|
|
32
32
|
import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js";
|
|
33
33
|
import type { WriteProtectionCheck } from "../write-protect.js";
|
|
34
|
+
import type { ShellReadCheck } from "../../tools/fs/shell-read-check.js";
|
|
34
35
|
import type { CwdRef } from "../../tools/fs/fs-shared.js";
|
|
35
36
|
import type { AskLane, IrreversibilityTier, ParkAsk, Prepared, ReversibilityProbes } from "./contracts.js";
|
|
36
37
|
import type { StopForDenialLimit } from "./denial-limit-arms.js";
|
|
@@ -153,6 +154,9 @@ export interface PrepareGateStationsInput {
|
|
|
153
154
|
* emptied the table: no judge, and no tighten mounted at all. Carried rather than compiled a second time
|
|
154
155
|
* here — two compiles are two judges, free to disagree about the seats they were handed. */
|
|
155
156
|
writeProtectionCheck: WriteProtectionCheck | undefined;
|
|
157
|
+
/** borrowed-readonly — the leg's shell READ STATION (`ToolGateInput.shellReadCheck`), mounted by the hands phase, or
|
|
158
|
+
* undefined with no real shell: no station, nothing judged. */
|
|
159
|
+
shellReadCheck: ShellReadCheck | undefined;
|
|
156
160
|
/** borrowed-readonly — the per-tool irreversibility tier handed to the gate per call. */
|
|
157
161
|
irreversibilityTier: IrreversibilityTier;
|
|
158
162
|
/** borrowed-readonly — the per-tool reversibility probes handed to the gate per call. */
|
|
@@ -41,7 +41,7 @@ export function maybeHumanRejectionHalt(input) {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
export function prepareGateStations(input) {
|
|
44
|
-
const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, writeProtectionCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash } = input;
|
|
44
|
+
const { askLane, parkAsk, tools, toolRosterDeltas, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, writeProtectionCheck, shellReadCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash } = input;
|
|
45
45
|
if (askLane !== undefined && parkAsk !== undefined) {
|
|
46
46
|
const { adjudicate, resolveAskBound, notifyPermissionDenied, notifyHookError } = askLane;
|
|
47
47
|
const { resolveContentAsk, suspendAsk } = parkAsk;
|
|
@@ -58,7 +58,8 @@ export function prepareGateStations(input) {
|
|
|
58
58
|
irreversibleTools.size > 0 ||
|
|
59
59
|
spec.enablePlanMode === true ||
|
|
60
60
|
complianceDenies.has("web_fetch") ||
|
|
61
|
-
writeProtectionArmed
|
|
61
|
+
writeProtectionArmed ||
|
|
62
|
+
shellReadCheck !== undefined;
|
|
62
63
|
if (toolCallGateArmedRef.armed) {
|
|
63
64
|
harness.on("tool_call", async (e) => {
|
|
64
65
|
blockedToolCalls.delete(e.toolCallId);
|
|
@@ -112,6 +113,7 @@ export function prepareGateStations(input) {
|
|
|
112
113
|
egress: egressTools.has(e.toolName),
|
|
113
114
|
peerMessage: e.toolName === SEND_MESSAGE_TOOL_NAME,
|
|
114
115
|
...(writeProtectionCheck !== undefined ? { writeProtectionCheck } : {}),
|
|
116
|
+
...(shellReadCheck !== undefined ? { shellReadCheck } : {}),
|
|
115
117
|
irreversibility: irreversibilityTier.get(e.toolName),
|
|
116
118
|
reversibilityProbe: reversibilityProbes.get(e.toolName),
|
|
117
119
|
abortSignal: abortController.signal,
|
|
@@ -204,11 +206,12 @@ export function prepareGateStations(input) {
|
|
|
204
206
|
toolName: e.toolName,
|
|
205
207
|
toolCallId: e.toolCallId,
|
|
206
208
|
});
|
|
209
|
+
const details = result.details !== undefined ? { details: result.details } : {};
|
|
207
210
|
if (haltRemaining !== undefined) {
|
|
208
211
|
batchHaltRef.current = { rejectedToolCallId: e.toolCallId };
|
|
209
|
-
return { block: true, reason: result.reason, haltRemaining };
|
|
212
|
+
return { block: true, reason: result.reason, ...details, haltRemaining };
|
|
210
213
|
}
|
|
211
|
-
return { block: true, reason: result.reason };
|
|
214
|
+
return { block: true, reason: result.reason, ...details };
|
|
212
215
|
}
|
|
213
216
|
return result.updatedInput !== undefined ? { updatedInput: result.updatedInput } : undefined;
|
|
214
217
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type BeforeWriteHook, type CwdRef, type ReadDenyMatcher, type ContainmentFace } from "../../tools/fs/index.js";
|
|
2
2
|
import { type ReadFileState } from "../../tools/fs/safety.js";
|
|
3
|
+
import { type ShellReadAdmission, type ShellReadCheck } from "../../tools/fs/shell-read-check.js";
|
|
3
4
|
import type { RunnerDeps, ShellGateDoctrine, TaskSpec } from "../types.js";
|
|
4
5
|
import type { Model } from "../../internal/llm.js";
|
|
5
6
|
import type { AgentHarness, AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
|
|
@@ -266,9 +267,12 @@ export interface PrepareHandsReadFaceResult {
|
|
|
266
267
|
/** owned — real shell + write hands + background-capable env: gates the driver's TaskOutput/
|
|
267
268
|
* TaskStop/Monitor/SendMessage mounts (P11) and the Monitor arm of the shellGate fold here. */
|
|
268
269
|
backgroundTaskToolsActive: boolean;
|
|
269
|
-
/** owned — this run's shell
|
|
270
|
-
*
|
|
271
|
-
|
|
270
|
+
/** owned — this run's shell READ STATION, the gate input (`ToolGateInput.shellReadCheck`): ONE judgment of
|
|
271
|
+
* where a shell command reads, for every shell face this leg mounts, under every doctrine. ⇔ a real shell. */
|
|
272
|
+
shellReadCheck: ShellReadCheck | undefined;
|
|
273
|
+
/** owned — the same station's pre-spawn ADMISSION, published for the P11 face that spawns a shell command
|
|
274
|
+
* and mounts LATER than the band (`Monitor`): the SAME function the band's shell faces got. ⇔ a real shell. */
|
|
275
|
+
shellReadAdmission: ShellReadAdmission | undefined;
|
|
272
276
|
/** write-back (D-3 hold) — the run's RESOLVED read face where hands mount; undefined ⇔ no hands
|
|
273
277
|
* (the hands-less value lives in `handsLessResolvedFace`). The driver assigns this onto its
|
|
274
278
|
* outer `let` for the call-time closures (ctx enrichment / checkpoint mint / envFacts). */
|
|
@@ -3,8 +3,10 @@ import { StubExecutionEnv } from "../stub-env.js";
|
|
|
3
3
|
import { hasBackgroundShell } from "../background-shell.js";
|
|
4
4
|
import { boundaryIsTheSandbox } from "../remote-env.js";
|
|
5
5
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
6
|
-
import {
|
|
6
|
+
import { bashReversibilityProbe, activeReadDenyBuiltinTiers, compileReadDeny, readDenySeat, createHandsToolkit, deploymentFaceClampNotice, pdfModelCapabilitiesOf, inheritSessionReadFileState, resolveContainmentFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, } from "../../tools/fs/index.js";
|
|
7
7
|
import { resolveKey, sha256 } from "../../tools/fs/safety.js";
|
|
8
|
+
import { createShellOverflowSpoolFence } from "../../tools/fs/fs-shared.js";
|
|
9
|
+
import { createShellReadStation } from "../../tools/fs/shell-read-check.js";
|
|
8
10
|
import { normalizeFileText } from "../../tools/fs/encoding.js";
|
|
9
11
|
import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
|
|
10
12
|
import { deliverEngineNotice } from "../types.js";
|
|
@@ -137,7 +139,8 @@ export function resolveHandsLessReadFace(input) {
|
|
|
137
139
|
wsSnapshot: undefined,
|
|
138
140
|
rebaseWsPath: (p) => p,
|
|
139
141
|
backgroundTaskToolsActive: false,
|
|
140
|
-
|
|
142
|
+
shellReadCheck: undefined,
|
|
143
|
+
shellReadAdmission: undefined,
|
|
141
144
|
resolvedReadFace: undefined,
|
|
142
145
|
resolvedWriteFace: undefined,
|
|
143
146
|
readDenyAdditionsNormalized,
|
|
@@ -168,7 +171,7 @@ export async function prepareHandsMount(input) {
|
|
|
168
171
|
let wsSnapshot;
|
|
169
172
|
let rebaseWsPath = (p) => p;
|
|
170
173
|
let backgroundTaskToolsActive = false;
|
|
171
|
-
let
|
|
174
|
+
let shellReadStation;
|
|
172
175
|
let resolvedReadFace;
|
|
173
176
|
let resolvedWriteFace;
|
|
174
177
|
let readDenyAdditionsNormalized = [];
|
|
@@ -337,7 +340,17 @@ export async function prepareHandsMount(input) {
|
|
|
337
340
|
...denySeat,
|
|
338
341
|
...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
|
|
339
342
|
});
|
|
340
|
-
|
|
343
|
+
if (handsIncludeShell) {
|
|
344
|
+
const readonlyBoundary = handsReadOnly
|
|
345
|
+
? { roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical], cwd: rootCanonical, ...denySeat, alsoInside: createShellOverflowSpoolFence(executionEnv) }
|
|
346
|
+
: undefined;
|
|
347
|
+
shellReadStation = createShellReadStation({
|
|
348
|
+
env: executionEnv,
|
|
349
|
+
tools: new Set(handsReadOnly ? ["Bash"] : backgroundTaskToolsActive ? ["Bash", "Monitor"] : ["Bash"]),
|
|
350
|
+
seatOf: () => (handsReadOnly ? "readonly" : effectiveShellGate === "classify" ? "bash_classify" : "bash_off"),
|
|
351
|
+
boundaryOf: () => readonlyBoundary ?? shellReadBoundary(),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
341
354
|
const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
|
|
342
355
|
...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
|
|
343
356
|
...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
|
|
@@ -352,7 +365,7 @@ export async function prepareHandsMount(input) {
|
|
|
352
365
|
...(input.onFileEdited !== undefined ? { onFileEdited: input.onFileEdited } : {}),
|
|
353
366
|
includeShell: handsIncludeShell,
|
|
354
367
|
readOnly: handsReadOnly,
|
|
355
|
-
|
|
368
|
+
...(shellReadStation !== undefined ? { shellReadAdmission: shellReadStation.admit } : {}),
|
|
356
369
|
...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
|
|
357
370
|
taskRegistry: defaultTaskRegistry,
|
|
358
371
|
taskOwner: hostTaskId,
|
|
@@ -395,7 +408,7 @@ export async function prepareHandsMount(input) {
|
|
|
395
408
|
if (!(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
|
|
396
409
|
if (shellGate === "off") {
|
|
397
410
|
deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted — the read boundary is ` +
|
|
398
|
-
`still judged on every command (a deny-listed
|
|
411
|
+
`still judged on every command (a deny-listed read is refused; an out-of-root read asks, and the ask is mandated${boundaryIsTheSandbox(executionEnv) ? "; under an isolated sandbox the paths are judged as written" : "; the paths are judged as the filesystem resolves them, symlinks and patterns included"}), but no ` +
|
|
399
412
|
`residual shell-risk classifier is installed: writes, egress and unknown commands are adjudicated by ` +
|
|
400
413
|
`the ordinary policy/hook chain only. Set spec.shellGate to "classify" or "always" if this deployment ` +
|
|
401
414
|
`expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
|
|
@@ -405,12 +418,13 @@ export async function prepareHandsMount(input) {
|
|
|
405
418
|
const seatOwned = !egressTools.has(name) && tierBefore !== "always" && tierBefore !== "maybe";
|
|
406
419
|
if (seatOwned)
|
|
407
420
|
shellGatedTools.add(name);
|
|
421
|
+
if (shellGate === "off" && tierBefore !== "always")
|
|
422
|
+
return;
|
|
408
423
|
const tier = shellGate === "always" || tierBefore === "always" ? "always" : "maybe";
|
|
409
424
|
irreversibilityTier.set(name, tier);
|
|
410
425
|
irreversibleTools.add(name);
|
|
411
|
-
if (seatOwned && tier === "maybe")
|
|
412
|
-
reversibilityProbes.set(name,
|
|
413
|
-
}
|
|
426
|
+
if (seatOwned && tier === "maybe")
|
|
427
|
+
reversibilityProbes.set(name, bashReversibilityProbe(undefined, shellReadBoundary));
|
|
414
428
|
};
|
|
415
429
|
installShellSeat("Bash");
|
|
416
430
|
if (backgroundTaskToolsActive)
|
|
@@ -425,7 +439,8 @@ export async function prepareHandsMount(input) {
|
|
|
425
439
|
wsSnapshot,
|
|
426
440
|
rebaseWsPath,
|
|
427
441
|
backgroundTaskToolsActive,
|
|
428
|
-
|
|
442
|
+
shellReadCheck: shellReadStation?.check,
|
|
443
|
+
shellReadAdmission: shellReadStation?.admit,
|
|
429
444
|
resolvedReadFace,
|
|
430
445
|
resolvedWriteFace,
|
|
431
446
|
handsLessResolvedWriteFace,
|
|
@@ -208,9 +208,32 @@ export interface PrepareInheritedGateResult {
|
|
|
208
208
|
sessionId: string;
|
|
209
209
|
principal?: string;
|
|
210
210
|
rev: number;
|
|
211
|
+
gen?: number;
|
|
211
212
|
rules: SessionPermissionRules;
|
|
212
213
|
};
|
|
213
214
|
};
|
|
215
|
+
/** borrowed-mutable — the ANCESTOR twin of {@link PrepareInheritedGateResult.ownSessionRulesRef}, keyed by
|
|
216
|
+
* the entry's POSITION in `inheritedAncestorRules`. Writers: the policy chain's ancestor layers, each time
|
|
217
|
+
* one comes to enforce a newer row than it prepared with. Read by {@link ancestorRulesNow}. Position, not
|
|
218
|
+
* `(sessionId, principal)`: the chain may legitimately carry two entries of the same identity with
|
|
219
|
+
* DIFFERENT floors (a leg whose own session is also an ancestor's, a seed and a live channel that both
|
|
220
|
+
* name it), each compiled into its own layer with its own floor — and a by-identity key lets the layer
|
|
221
|
+
* that advanced overwrite the entry of the layer that did not, publishing the weaker floor over the
|
|
222
|
+
* stronger one. A layer upgrades its OWN entry; position is what "its own" means. */
|
|
223
|
+
ancestorSessionRulesRef: {
|
|
224
|
+
current: Map<number, {
|
|
225
|
+
sessionId: string;
|
|
226
|
+
principal?: string;
|
|
227
|
+
rev: number;
|
|
228
|
+
gen?: number;
|
|
229
|
+
rules: SessionPermissionRules;
|
|
230
|
+
}>;
|
|
231
|
+
};
|
|
232
|
+
/** owned — the ancestor rules AS THIS LEG NOW ENFORCES THEM: the inherited entries with the seat above
|
|
233
|
+
* overlaid. The ONE projection, called by the child-chain assembly at spawn and by the checkpoint writer
|
|
234
|
+
* at suspend — two faces of one rule ("what a later reader inherits is what this leg enforces"), which
|
|
235
|
+
* were two implementations while the checkpoint cloned the prepare-time list. */
|
|
236
|
+
ancestorRulesNow: () => InheritedGate["ancestorRules"];
|
|
214
237
|
/** borrowed-mutable — THIS task's own frozen admitted org scopes for its children's chain. Writers: the memory phase's
|
|
215
238
|
* admission door, once per leg. Read by the chain assembly (always emitted, empty = no org plane). */
|
|
216
239
|
memoryAdmittedOrgScopesRef: {
|