@sema-agent/core 7.24.4 → 7.25.1
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 +100 -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/arg-summary.d.ts +0 -7
- package/dist/core/arg-summary.js +2 -1
- 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-policy.js +30 -23
- 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-lexer.d.ts +13 -0
- package/dist/tools/fs/bash-lexer.js +61 -0
- package/dist/tools/fs/bash-program-position.d.ts +36 -29
- package/dist/tools/fs/bash-program-position.js +33 -13
- package/dist/tools/fs/bash-readonly-classifier.d.ts +26 -313
- package/dist/tools/fs/bash-readonly-classifier.js +81 -782
- 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 +88 -27
- 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
- package/wire-changes/core@7.25.1.json +64 -0
package/dist/core/gate-fold.js
CHANGED
|
@@ -48,6 +48,24 @@ export async function runGateFold(pass) {
|
|
|
48
48
|
...(pass.hookAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
+
if ((pass.decision.action === "allow" || pass.decision.action === "ask") && input.shellReadCheck !== undefined) {
|
|
52
|
+
const station = input.shellReadCheck;
|
|
53
|
+
const judged = station.judge(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, input.trackedCwd?.(), pass.callSignal ?? input.abortSignal, { toolCallId });
|
|
54
|
+
if (judged !== null) {
|
|
55
|
+
let verdict;
|
|
56
|
+
try {
|
|
57
|
+
verdict = await withGateTimeout(judged, boundedAwaitDeadlineMs(), pass.callSignal ?? input.abortSignal, "shellReadStation");
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const kind = gateWaitFailureOf(err);
|
|
61
|
+
if (kind !== "aborted" && input.abortSignal?.aborted !== true) {
|
|
62
|
+
traceHookCrash(input, new Error(`shell read station ${kind === "timeout" ? "timed out" : "threw"} for tool "${toolName}" (toolCallId ${toolCallId}) — where this call reads could not be established: ${thrownText(err)}`), notifier);
|
|
63
|
+
}
|
|
64
|
+
verdict = station.unanswered(toolName, kind === "timeout" ? "the read station timed out" : "the read station could not answer");
|
|
65
|
+
}
|
|
66
|
+
applyShellReadVerdict(pass, verdict, toolName, input.shellGated === true);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
51
69
|
if (input.egress && pass.decision.action === "allow") {
|
|
52
70
|
pass.decision = {
|
|
53
71
|
action: "ask",
|
|
@@ -59,6 +77,14 @@ export async function runGateFold(pass) {
|
|
|
59
77
|
const alreadyAsking = pass.decision.action === "ask";
|
|
60
78
|
if ((pass.decision.action === "allow" || alreadyAsking) && (input.irreversibility === "always" || input.irreversibility === "maybe")) {
|
|
61
79
|
let tighten = !alreadyAsking;
|
|
80
|
+
const withdrawStationOffer = () => {
|
|
81
|
+
if (alreadyAsking && pass.shellReadOffer === true && pass.decision.action === "ask" && pass.decision.probeReadRootCandidate !== undefined) {
|
|
82
|
+
const { probeReadRootCandidate: _withdrawn, ...kept } = pass.decision;
|
|
83
|
+
pass.decision = kept;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
if (input.irreversibility === "always" || !input.reversibilityProbe)
|
|
87
|
+
withdrawStationOffer();
|
|
62
88
|
let probeReason;
|
|
63
89
|
let probeCause;
|
|
64
90
|
let probeMandated;
|
|
@@ -80,11 +106,13 @@ export async function runGateFold(pass) {
|
|
|
80
106
|
if (verdict?.boundaryDeclined === true)
|
|
81
107
|
pass.probeBoundaryDeclined = true;
|
|
82
108
|
probeReadRootCandidate = normalizeReadRootCandidate(verdict?.readRootCandidate);
|
|
109
|
+
withdrawStationOffer();
|
|
83
110
|
}
|
|
84
111
|
}
|
|
85
112
|
catch (err) {
|
|
86
113
|
reversible = false;
|
|
87
114
|
pass.probeUnanswered = true;
|
|
115
|
+
withdrawStationOffer();
|
|
88
116
|
if (input.abortSignal?.aborted !== true)
|
|
89
117
|
traceHookCrash(input, err, notifier);
|
|
90
118
|
}
|
|
@@ -140,3 +168,60 @@ export async function runGateFold(pass) {
|
|
|
140
168
|
}
|
|
141
169
|
return await pass.afterFold(pass);
|
|
142
170
|
}
|
|
171
|
+
function shellReadAskMessage(toolName, finding) {
|
|
172
|
+
return finding === "outside_roots"
|
|
173
|
+
? `tool "${toolName}" reads outside the directories allowed for this session — explicit approval required`
|
|
174
|
+
: `tool "${toolName}" reads where the session's read boundary could not establish what is read — explicit approval required`;
|
|
175
|
+
}
|
|
176
|
+
function applyShellReadVerdict(pass, verdict, toolName, shellGated) {
|
|
177
|
+
const account = verdict.sentence !== undefined && verdict.sentence.trim().length > 0 ? inlineUntrusted(verdict.sentence, PROBE_REASON_MAX) : undefined;
|
|
178
|
+
switch (verdict.disposition) {
|
|
179
|
+
case "vouch":
|
|
180
|
+
return;
|
|
181
|
+
case "refuse":
|
|
182
|
+
pass.decision = { action: "deny", message: verdict.sentence ?? `tool "${toolName}" reads what the session's read boundary refuses` };
|
|
183
|
+
pass.deniedBy = "read_boundary";
|
|
184
|
+
if (verdict.details !== undefined)
|
|
185
|
+
pass.denyDetails = verdict.details;
|
|
186
|
+
return;
|
|
187
|
+
case "mandated": {
|
|
188
|
+
if (verdict.details !== undefined)
|
|
189
|
+
pass.shellReadDetails = verdict.details;
|
|
190
|
+
if (pass.decision.action !== "allow") {
|
|
191
|
+
if (pass.decision.action === "ask")
|
|
192
|
+
pass.decision = { ...pass.decision, probeMandated: true };
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const candidate = normalizeReadRootCandidate(verdict.readRootCandidate);
|
|
196
|
+
pass.decision = {
|
|
197
|
+
action: "ask",
|
|
198
|
+
message: shellReadAskMessage(toolName, verdict.finding),
|
|
199
|
+
decisionReason: "safety",
|
|
200
|
+
probeMandated: true,
|
|
201
|
+
...(account !== undefined ? { probeReason: account } : {}),
|
|
202
|
+
...(candidate !== undefined ? { probeReadRootCandidate: candidate } : {}),
|
|
203
|
+
};
|
|
204
|
+
if (candidate !== undefined)
|
|
205
|
+
pass.shellReadOffer = true;
|
|
206
|
+
pass.tightenedBy = shellGated ? "shell_gate" : "safety";
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
case "ordinary": {
|
|
210
|
+
if (pass.decision.action === "allow") {
|
|
211
|
+
pass.decision = {
|
|
212
|
+
action: "ask",
|
|
213
|
+
message: shellReadAskMessage(toolName, verdict.finding),
|
|
214
|
+
decisionReason: "safety",
|
|
215
|
+
...(account !== undefined ? { probeReason: account } : {}),
|
|
216
|
+
};
|
|
217
|
+
pass.tightenedBy = shellGated ? "shell_gate" : "safety";
|
|
218
|
+
}
|
|
219
|
+
pass.probeBoundaryDeclined = true;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
default: {
|
|
223
|
+
const exhaustive = verdict.disposition;
|
|
224
|
+
return exhaustive;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
package/dist/core/gate-lanes.js
CHANGED
|
@@ -11,7 +11,7 @@ import { catalogRuleFaceOf } from "./tool-registry.js";
|
|
|
11
11
|
import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
|
|
12
12
|
import { exitGate } from "./runner/gate-exit.js";
|
|
13
13
|
import { traceHookCrash } from "./gate-crash-trace.js";
|
|
14
|
-
import { createGateDeadline, awaitWriteProtectionJudgment } from "./gate-bounded-await.js";
|
|
14
|
+
import { createGateDeadline, awaitWriteProtectionJudgment, withGateTimeout } from "./gate-bounded-await.js";
|
|
15
15
|
import { thrownText } from "./thrown-value.js";
|
|
16
16
|
function isPlainOwnRecord(x) {
|
|
17
17
|
try {
|
|
@@ -691,6 +691,28 @@ export async function runGateLanes(pass) {
|
|
|
691
691
|
pass.deniedBy = "write_protection";
|
|
692
692
|
}
|
|
693
693
|
}
|
|
694
|
+
if (editDenied === undefined && input.shellReadCheck !== undefined) {
|
|
695
|
+
const station = input.shellReadCheck;
|
|
696
|
+
const judged = station.judge(toolName, editArgs, input.trackedCwd?.(), callSignal ?? input.abortSignal, { seat: "rejudge", toolCallId });
|
|
697
|
+
if (judged !== null) {
|
|
698
|
+
let verdict;
|
|
699
|
+
try {
|
|
700
|
+
verdict = await withGateTimeout(judged, boundedAwaitDeadlineMs(), callSignal ?? input.abortSignal, "shellReadStation");
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
verdict = station.unanswered(toolName, "the read station could not answer");
|
|
704
|
+
}
|
|
705
|
+
if (verdict.disposition !== "vouch") {
|
|
706
|
+
editDenied = {
|
|
707
|
+
action: "deny",
|
|
708
|
+
message: verdict.sentence ?? `the approved edit for "${toolName}" reads where the read boundary does not vouch — denied fail-closed; re-submit the command as a fresh call and answer its card`,
|
|
709
|
+
};
|
|
710
|
+
pass.deniedBy = "read_boundary";
|
|
711
|
+
if (verdict.details !== undefined)
|
|
712
|
+
pass.denyDetails = verdict.details;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
694
716
|
pass.decision = editDenied ?? { ...pass.decision, updatedInput: editArgs };
|
|
695
717
|
}
|
|
696
718
|
else if (pass.decision.action === "allow" && pass.decision.updatedInput === undefined && resolved.presentedInput !== undefined) {
|
|
@@ -154,8 +154,11 @@ export type SettlementRefusalTableCoversEveryKind = AssertAllKeysHandled<Exclude
|
|
|
154
154
|
* path that no approval covers.
|
|
155
155
|
* - `ask_resolution` — the ask's own settlement is the refusal (a person said no, a window elapsed,
|
|
156
156
|
* nobody could be asked…); the detail is on {@link GateOutcome.settlement}.
|
|
157
|
+
* - `read_boundary` — the shell read station refused: a deny-listed read (refused on every seat, with no
|
|
158
|
+
* ask), a read the read-only shell face may not make, or a rewritten / re-established command whose reads
|
|
159
|
+
* no longer are what a person approved.
|
|
157
160
|
*/
|
|
158
|
-
export declare const DENIED_BY_VALUES: readonly ["policy", "hook", "org", "persisted_rule", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution"];
|
|
161
|
+
export declare const DENIED_BY_VALUES: readonly ["policy", "hook", "org", "persisted_rule", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution", "read_boundary"];
|
|
159
162
|
export type DeniedBy = (typeof DENIED_BY_VALUES)[number];
|
|
160
163
|
/** Membership test for {@link DeniedBy}. */
|
|
161
164
|
export declare function isDeniedBy(v: unknown): v is DeniedBy;
|
|
@@ -165,7 +168,8 @@ export declare function isDeniedBy(v: unknown): v is DeniedBy;
|
|
|
165
168
|
* classifier, plan mode and compliance run before any ask exists, and `ask_resolution` IS the
|
|
166
169
|
* settlement, not a veto of it. `persisted_rule` = a person's own standing DENY row (their settings'
|
|
167
170
|
* deny list, imported) — the personal-store sibling of `org`, and like it re-judged on an edited
|
|
168
|
-
* command.
|
|
171
|
+
* command. `read_boundary` vetoes too: the rewrite re-judge and the pre-spawn re-establishment run after
|
|
172
|
+
* the person's yes, on bytes or targets the person never saw. */
|
|
169
173
|
export declare const DENIED_BY_MAY_VETO: {
|
|
170
174
|
readonly policy: true;
|
|
171
175
|
readonly hook: true;
|
|
@@ -176,6 +180,7 @@ export declare const DENIED_BY_MAY_VETO: {
|
|
|
176
180
|
readonly compliance: false;
|
|
177
181
|
readonly write_protection: true;
|
|
178
182
|
readonly ask_resolution: false;
|
|
183
|
+
readonly read_boundary: true;
|
|
179
184
|
};
|
|
180
185
|
/** Compile-time fence: `never` while every deny layer has a veto row. */
|
|
181
186
|
export type VetoTableCoversEveryDeniedBy = AssertAllKeysHandled<Exclude<DeniedBy, keyof typeof DENIED_BY_MAY_VETO>>;
|
|
@@ -209,6 +214,7 @@ export declare const DENIED_BY_MAY_CARRY_CAUSE: {
|
|
|
209
214
|
readonly compliance: false;
|
|
210
215
|
readonly write_protection: false;
|
|
211
216
|
readonly ask_resolution: false;
|
|
217
|
+
readonly read_boundary: false;
|
|
212
218
|
};
|
|
213
219
|
/** Compile-time fence: `never` while every deny layer has a cause row. */
|
|
214
220
|
export type CauseTableCoversEveryDeniedBy = AssertAllKeysHandled<Exclude<DeniedBy, keyof typeof DENIED_BY_MAY_CARRY_CAUSE>>;
|
|
@@ -39,7 +39,7 @@ export const SETTLEMENT_IS_REFUSAL = {
|
|
|
39
39
|
blanket_allow_refused: true,
|
|
40
40
|
task_aborted: true,
|
|
41
41
|
};
|
|
42
|
-
export const DENIED_BY_VALUES = ["policy", "hook", "org", "persisted_rule", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution"];
|
|
42
|
+
export const DENIED_BY_VALUES = ["policy", "hook", "org", "persisted_rule", "classifier", "plan_mode", "compliance", "write_protection", "ask_resolution", "read_boundary"];
|
|
43
43
|
const DENIED_BY_SET = new Set(DENIED_BY_VALUES);
|
|
44
44
|
export function isDeniedBy(v) {
|
|
45
45
|
return DENIED_BY_SET.has(v);
|
|
@@ -54,6 +54,7 @@ export const DENIED_BY_MAY_VETO = {
|
|
|
54
54
|
compliance: false,
|
|
55
55
|
write_protection: true,
|
|
56
56
|
ask_resolution: false,
|
|
57
|
+
read_boundary: true,
|
|
57
58
|
};
|
|
58
59
|
export const CLASSIFIER_DENY_CAUSES = ["unavailable", "parse_error"];
|
|
59
60
|
const CLASSIFIER_DENY_CAUSE_SET = new Set(CLASSIFIER_DENY_CAUSES);
|
|
@@ -70,6 +71,7 @@ export const DENIED_BY_MAY_CARRY_CAUSE = {
|
|
|
70
71
|
compliance: false,
|
|
71
72
|
write_protection: false,
|
|
72
73
|
ask_resolution: false,
|
|
74
|
+
read_boundary: false,
|
|
73
75
|
};
|
|
74
76
|
export function screenGateOutcome(g) {
|
|
75
77
|
const defects = [];
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -732,6 +732,16 @@ export interface HookInvocationIdentity {
|
|
|
732
732
|
* UNTRUSTED display text (model-chosen) — already single-line-bounded at mint (same
|
|
733
733
|
* `inlineUntrusted` treatment as the `task_progress` name lane); never an identity key. */
|
|
734
734
|
readonly agentName?: string;
|
|
735
|
+
/** The EXPLICIT `Agent({name})` spawn identity, when the spawn carried one — sema's teammate key
|
|
736
|
+
* (validated at spawn: `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`, never `main`). The same string the
|
|
737
|
+
* child's roster row, its uplink `from` label and its `task_progress.spawnName` carry, so a hook
|
|
738
|
+
* seat binds "which teammate" by string equality. ABSENT = an unnamed delegation (not a
|
|
739
|
+
* teammate); {@link agentName} above stays the DISPLAY label and is never an identity key. */
|
|
740
|
+
readonly spawnName?: string;
|
|
741
|
+
/** The RESOLVED subagent type the leg was spawned as (`"general-purpose"` / `"fork"` / a roster
|
|
742
|
+
* definition's name — the fleet row's `agentType` word, never the description). Absent on a
|
|
743
|
+
* top-level run and on a leg whose spawner threaded no type (a workflow-lane agent). */
|
|
744
|
+
readonly agentType?: string;
|
|
735
745
|
/** The spawning Agent tool call's own id, when this leg runs under one — the delegation-chain
|
|
736
746
|
* position anchor ({@link import("./types.js").TaskEvent}'s `parentToolCallId` twin). Absent on
|
|
737
747
|
* a top-level run and on a directly-started workflow child (no launching tool call — no id is
|
|
@@ -919,6 +929,11 @@ export interface ToolGateResult {
|
|
|
919
929
|
gate?: GateOutcome;
|
|
920
930
|
/** Rewritten args to execute with (re-validated by the loop); omitted when nothing rewrote. */
|
|
921
931
|
updatedInput?: unknown;
|
|
932
|
+
/** A blocking refusal's structured details, when the refusing station minted any (the shell read station's
|
|
933
|
+
* `read_path_denied` / `readonly_out_of_root` cards): the loop attaches them to the error result it mints
|
|
934
|
+
* from `reason`, so the refusal reaches `tool_end.structured` / `errorCode` exactly as the tool's own
|
|
935
|
+
* refusal of the same finding did. Absent on every other block. */
|
|
936
|
+
details?: unknown;
|
|
922
937
|
/**
|
|
923
938
|
* design/45: the gate routed a policy `ask` to a **durable suspension** (no synchronous approver).
|
|
924
939
|
* `suspendAsk` already persisted the checkpoint and aborted the run; the caller records this so the
|
|
@@ -1362,6 +1377,20 @@ export interface ToolGateInput {
|
|
|
1362
1377
|
* point there), a promise for a covered write. Absent ⇒ no table (`[]`) and the decision path is byte-identical.
|
|
1363
1378
|
*/
|
|
1364
1379
|
writeProtectionCheck?: import("./write-protect.js").WriteProtectionCheck;
|
|
1380
|
+
/**
|
|
1381
|
+
* The shell READ STATION — ENGINE-BUILT by the hands phase (`createShellReadStation`,
|
|
1382
|
+
* `src/tools/fs/shell-read-check.ts`) for the shell-running tools of this leg, never a deployment callback.
|
|
1383
|
+
* Judges the FINAL args of a shell call: ONE parse of the command into its read candidates, each
|
|
1384
|
+
* established against the execution environment (symlinks resolved, patterns expanded, traversal roots
|
|
1385
|
+
* enumerated, within a bound) and judged by the leg's read deny seat and its containment roots. A
|
|
1386
|
+
* POLICY-class member of the fold: a deny-listed read is REFUSED (`deniedBy: "read_boundary"`, composing
|
|
1387
|
+
* under `deny > ask > allow`), an out-of-root or unestablishable read tightens a surviving allow to a
|
|
1388
|
+
* MANDATED ask (or marks an existing ask mandated), a read the station declined to judge tightens to an
|
|
1389
|
+
* ordinary ask on the classify seat and a mandated one on the off seat, and a vouched call is untouched.
|
|
1390
|
+
* `null` SYNCHRONOUSLY for a call it does not cover (no suspension point there), a promise for a shell
|
|
1391
|
+
* call. Absent ⇒ no station (no shell mounted) and the decision path is byte-identical.
|
|
1392
|
+
*/
|
|
1393
|
+
shellReadCheck?: import("../tools/fs/shell-read-check.js").ShellReadCheck;
|
|
1365
1394
|
/**
|
|
1366
1395
|
* design/143 批2 (A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
|
|
1367
1396
|
* small-model policy CLASSIFIER before any human/durable resolution:
|
package/dist/core/hooks.js
CHANGED
|
@@ -167,6 +167,8 @@ export function mintHookInvocationIdentity(facts) {
|
|
|
167
167
|
isDelegatedChild: facts.isDelegatedChild,
|
|
168
168
|
...(facts.insideFork === true ? { insideFork: true } : {}),
|
|
169
169
|
...(facts.agentName !== undefined ? { agentName: inlineUntrusted(facts.agentName.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}),
|
|
170
|
+
...(facts.spawnName !== undefined ? { spawnName: inlineUntrusted(facts.spawnName.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}),
|
|
171
|
+
...(facts.agentType !== undefined ? { agentType: inlineUntrusted(facts.agentType.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}),
|
|
170
172
|
...(facts.parentToolCallId !== undefined ? { parentToolCallId: facts.parentToolCallId } : {}),
|
|
171
173
|
}));
|
|
172
174
|
}
|
|
@@ -25,6 +25,11 @@ export interface MailboxMessage {
|
|
|
25
25
|
export interface MailboxPeerMeta {
|
|
26
26
|
/** The sending session's id (reply routing). */
|
|
27
27
|
fromSession?: string;
|
|
28
|
+
/** The delegation-tree ROOT the sender belongs to, stamped by the park seat of a session that
|
|
29
|
+
* received the frame through its own child uplink — never by the sender. A recipient whose
|
|
30
|
+
* session IS this root reads the record as a teammate's words (same tree = same admission as the
|
|
31
|
+
* live uplink: the admission gate runs, the cross-session parity hold does not). */
|
|
32
|
+
fromRoot?: string;
|
|
28
33
|
/** The sender's permission-mode class at send time — the drain judges parity against the RECEIVER. */
|
|
29
34
|
fromMode?: MailboxPeerFromMode;
|
|
30
35
|
/** The admission gate's sender key (rate/dedup axis). */
|
|
@@ -4,7 +4,7 @@ export const MAILBOX_PEER_RECORD_KINDS = ["peer_message", "idle_notice", "delive
|
|
|
4
4
|
export const MAILBOX_PEER_NOTICE_STATES = ["held", "denied", "expired", "delivered", "refused", "dropped", "idle", "exited", "unavailable"];
|
|
5
5
|
export const MAILBOX_INVALID_PEER_META_CODE = "mailbox.invalid_peer_meta";
|
|
6
6
|
export const MAILBOX_CROSS_PROCESS_UNSAFE_CODE = "mailbox.cross_process_unsafe";
|
|
7
|
-
const PEER_META_STRING_KEYS = ["fromSession", "senderKey", "fromScope", "gateReceiptId"];
|
|
7
|
+
const PEER_META_STRING_KEYS = ["fromSession", "fromRoot", "senderKey", "fromScope", "gateReceiptId"];
|
|
8
8
|
export function readMailboxPeerMeta(raw) {
|
|
9
9
|
if (raw === undefined)
|
|
10
10
|
return undefined;
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* The checkpoint seat (`./checkpoint-seat.ts`) is the same shape for one field; this seat carries the rest.
|
|
10
10
|
*/
|
|
11
11
|
import type { OnQuestion } from "./ask-question.js";
|
|
12
|
+
import type { Hooks } from "./hooks.js";
|
|
12
13
|
import type { ToolExecuteContext } from "./tool-spec.js";
|
|
13
14
|
import type { TaskSpec } from "./types.js";
|
|
14
15
|
/** The fields of a spec the seat carries — one `Pick`, shared by the mint's input and the apply's target. */
|
|
15
|
-
export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "memory">;
|
|
16
|
+
export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "clientContext" | "onQuestion" | "hooks" | "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "memoryPersistenceCapable" | "memory">;
|
|
16
17
|
/**
|
|
17
18
|
* The seat a run hands its children. Every member is the parent's own value, frozen at mint (arrays copied and
|
|
18
19
|
* frozen; the facts object copied). Members present iff the parent carries them — an absent member is "nothing to
|
|
@@ -22,6 +23,10 @@ export type ParentSpecSeatFields = Pick<TaskSpec, "principal" | "oneShot" | "cli
|
|
|
22
23
|
* · `clientContext` — the user's TZ/email facts (the child's environment block localizes the same way).
|
|
23
24
|
* · `onQuestion` — the parent's FROZEN question face (`spec.onQuestion ?? deps.onQuestion`, the value the parent's
|
|
24
25
|
* own AskUserQuestion resolves at): the child's question tool MOUNTS on it, so without it the child has no tool.
|
|
26
|
+
* · `hooks` — the parent's SPEC-level hook seams (`spec.hooks`, the request-level lifecycle hooks a host mounts from
|
|
27
|
+
* the user's settings). A deployment's `RunnerDeps.hooks` already serve every leg; the spec-level seams did not,
|
|
28
|
+
* so a `stop` seat a user wired ran for the leader and never for its teammates (CC runs `SubagentStop` from the
|
|
29
|
+
* same settings). Same law as `onQuestion`: a child's own value wins, the seat fills an unset one.
|
|
25
30
|
* · `additionalDirectories` / `additionalReadDirectories` — the fs-fence widening (deployment property).
|
|
26
31
|
* · `envFacts` — the sandbox facts (profile / egress / scratchpad).
|
|
27
32
|
* · `memoryPersistenceCapable` — the deployment's DECLARED persistence statement: `false` is a REMOVAL (floor),
|
|
@@ -34,6 +39,7 @@ export interface ParentSpecSeat {
|
|
|
34
39
|
readonly oneShot?: true;
|
|
35
40
|
readonly clientContext?: TaskSpec["clientContext"];
|
|
36
41
|
readonly onQuestion?: OnQuestion;
|
|
42
|
+
readonly hooks?: Hooks;
|
|
37
43
|
readonly additionalDirectories?: readonly string[];
|
|
38
44
|
readonly additionalReadDirectories?: readonly string[];
|
|
39
45
|
readonly envFacts?: TaskSpec["envFacts"];
|
|
@@ -4,6 +4,7 @@ export function parentSpecSeatOf(spec, frozenOnQuestion) {
|
|
|
4
4
|
...(spec.oneShot === true ? { oneShot: true } : {}),
|
|
5
5
|
...(spec.clientContext !== undefined ? { clientContext: spec.clientContext } : {}),
|
|
6
6
|
...(frozenOnQuestion !== undefined ? { onQuestion: frozenOnQuestion } : {}),
|
|
7
|
+
...(spec.hooks !== undefined ? { hooks: spec.hooks } : {}),
|
|
7
8
|
...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
|
|
8
9
|
...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
|
|
9
10
|
...(spec.envFacts !== undefined ? { envFacts: Object.freeze({ ...spec.envFacts }) } : {}),
|
|
@@ -20,6 +21,7 @@ export function applyParentSpecSeat(child, seat) {
|
|
|
20
21
|
...fill("principal", seat.principal),
|
|
21
22
|
...fill("clientContext", seat.clientContext),
|
|
22
23
|
...fill("onQuestion", seat.onQuestion),
|
|
24
|
+
...fill("hooks", seat.hooks),
|
|
23
25
|
...fill("additionalDirectories", (seat.additionalDirectories !== undefined ? [...seat.additionalDirectories] : undefined)),
|
|
24
26
|
...fill("additionalReadDirectories", (seat.additionalReadDirectories !== undefined ? [...seat.additionalReadDirectories] : undefined)),
|
|
25
27
|
...fill("envFacts", (seat.envFacts !== undefined ? { ...seat.envFacts } : undefined)),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, segmentCoverageOf, suggestRulesForCommand, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
|
|
3
3
|
import { effectiveOrThrow } from "./permission-rule-provider.js";
|
|
4
|
-
import { errText, isValidConsentScope, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
4
|
+
import { errText, isValidConsentScope, landRuleAdd, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
5
5
|
export class InMemoryRuleApprovalRecordStore {
|
|
6
6
|
rows = new Map();
|
|
7
7
|
async get(id) {
|
|
@@ -600,7 +600,6 @@ export async function redeemRuleTicket(opts) {
|
|
|
600
600
|
}
|
|
601
601
|
return await applyRedemption({ deps: opts.deps, owner: caller, candidate, parsedRule: parsedRule.rule, dot, recordId: rec.id, kind: rec.kind, replay: known !== undefined, ...(siblingsOf !== undefined ? { siblingsOf } : {}) });
|
|
602
602
|
}
|
|
603
|
-
const REDEEM_MAX_ATTEMPTS = 8;
|
|
604
603
|
async function tighteningSiblingsStand(rec, offer, index, store, durableSnapshot) {
|
|
605
604
|
for (const member of offerMembersOf(offer)) {
|
|
606
605
|
if (member === index)
|
|
@@ -630,52 +629,35 @@ async function applyRedemption(args) {
|
|
|
630
629
|
if (writer === undefined)
|
|
631
630
|
return { status: "refused", reason: "the resolved permission-rule store has no write face" };
|
|
632
631
|
const origin = originOfRecordKind(args.kind);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
632
|
+
const siblings = args.siblingsOf;
|
|
633
|
+
const landing = await landRuleAdd({
|
|
634
|
+
store,
|
|
635
|
+
writer,
|
|
636
|
+
delta: {
|
|
637
|
+
kind: "redemption-add",
|
|
638
|
+
behavior: args.parsedRule.behavior,
|
|
639
|
+
rule: args.parsedRule.rule,
|
|
640
|
+
scope: args.candidate.scope,
|
|
641
|
+
tool: args.parsedRule.tool,
|
|
642
|
+
match: args.parsedRule.match,
|
|
643
|
+
command: args.parsedRule.command,
|
|
644
|
+
add: { dot: args.dot, origin, createdAt: nowIso(args.deps) },
|
|
645
|
+
redemption: {
|
|
646
|
+
recordId: args.recordId,
|
|
647
|
+
...(args.owner.kind === "principal" ? { principal: args.owner.principal } : { owner: args.owner }),
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
...(siblings !== undefined
|
|
651
|
+
? {
|
|
652
|
+
preflight: async (snapshot) => (await tighteningSiblingsStand(siblings.rec, siblings.offer, siblings.index, store, snapshot))
|
|
653
|
+
? undefined
|
|
654
|
+
: "withheld: this allow belongs to a batch whose deny/ask members do not all stand in the store — an allow must not stand wider than the list that was confirmed; redeem the batch (redeemRuleBatch) so the tightening members land first, or draw a fresh card",
|
|
643
655
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
try {
|
|
650
|
-
res = await writer.apply({
|
|
651
|
-
kind: "redemption-add",
|
|
652
|
-
behavior: args.parsedRule.behavior,
|
|
653
|
-
rule: args.parsedRule.rule,
|
|
654
|
-
scope: args.candidate.scope,
|
|
655
|
-
tool: args.parsedRule.tool,
|
|
656
|
-
match: args.parsedRule.match,
|
|
657
|
-
command: args.parsedRule.command,
|
|
658
|
-
add: { dot: args.dot, origin, createdAt: nowIso(args.deps) },
|
|
659
|
-
redemption: {
|
|
660
|
-
recordId: args.recordId,
|
|
661
|
-
...(args.owner.kind === "principal" ? { principal: args.owner.principal } : { owner: args.owner }),
|
|
662
|
-
},
|
|
663
|
-
}, { expectedRev: rev });
|
|
664
|
-
}
|
|
665
|
-
catch (err) {
|
|
666
|
-
return { status: "refused", reason: `the permission-rule store refused the write: ${errText(err)}` };
|
|
667
|
-
}
|
|
668
|
-
if ("refused" in res) {
|
|
669
|
-
return {
|
|
670
|
-
status: "refused",
|
|
671
|
-
reason: "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)",
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
if (!("conflict" in res)) {
|
|
675
|
-
return { status: "redeemed", behavior: args.parsedRule.behavior, rule: args.parsedRule.rule, scope: args.candidate.scope, dot: args.dot, rev: res.rev, alreadyRedeemed: args.replay };
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
return { status: "refused", reason: `optimistic-concurrency retries exhausted after ${REDEEM_MAX_ATTEMPTS} attempts` };
|
|
656
|
+
: {}),
|
|
657
|
+
});
|
|
658
|
+
if (!landing.landed)
|
|
659
|
+
return { status: "refused", reason: landing.reason };
|
|
660
|
+
return { status: "redeemed", behavior: args.parsedRule.behavior, rule: args.parsedRule.rule, scope: args.candidate.scope, dot: args.dot, rev: landing.rev, alreadyRedeemed: args.replay };
|
|
679
661
|
}
|
|
680
662
|
function originOfRecordKind(kind) {
|
|
681
663
|
return kind === "import" ? "imported-cc" : kind === "starter" ? "starter" : "user";
|
|
@@ -111,11 +111,12 @@ function routingWriter(durable, session) {
|
|
|
111
111
|
nextDot: () => durable.nextDot(),
|
|
112
112
|
readRaw: () => durable.readRaw(),
|
|
113
113
|
apply: async (delta, opts) => {
|
|
114
|
-
const
|
|
115
|
-
|
|
114
|
+
const isAdd = delta.kind === "redemption-add" || delta.kind === "tighten-add";
|
|
115
|
+
const scope = isAdd ? structuredClone(delta.scope) : undefined;
|
|
116
|
+
if (!isAdd || scope?.kind !== "session")
|
|
116
117
|
return durable.apply(delta, opts);
|
|
117
118
|
if (session === undefined) {
|
|
118
|
-
const e = new Error(
|
|
119
|
+
const e = new Error(`a session-scope rule has nowhere to land — this deployment configured no session rule partition (createPermissionRuleStoreProvider({ session })); refusing the ${delta.kind}`);
|
|
119
120
|
e.code = "unsupported.session_scope_store";
|
|
120
121
|
throw e;
|
|
121
122
|
}
|