@sema-agent/core 7.23.4 → 7.23.5
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 +56 -0
- package/dist/agents/list-agents-tool.d.ts +5 -1
- package/dist/agents/list-agents-tool.js +14 -2
- package/dist/brain/anthropic.js +3 -2
- package/dist/brain/circuit-breaker.js +3 -3
- package/dist/brain/degrading.js +3 -5
- package/dist/brain/errors.d.ts +35 -18
- package/dist/brain/errors.js +49 -31
- package/dist/brain/failover.js +2 -3
- package/dist/brain/open-responses.js +6 -5
- package/dist/brain/openai.js +6 -5
- package/dist/brain/route-adjudicator.d.ts +26 -4
- package/dist/brain/route-adjudicator.js +26 -0
- package/dist/brain/stream-engine.js +8 -2
- package/dist/core/checkpoint-store.d.ts +4 -6
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/permission-rule-model.js +8 -5
- package/dist/core/runner/prepare-delegation-surface.d.ts +3 -0
- package/dist/core/runner/prepare-delegation-surface.js +1 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -1
- package/dist/core/runner/prepare-hands-readface.js +4 -0
- package/dist/core/runner/prepare-task.js +2 -2
- package/dist/core/runner/run-leg.js +2 -13
- package/dist/core/thrown-value.d.ts +30 -0
- package/dist/core/thrown-value.js +23 -1
- package/dist/core/tool-catalog-entries.js +1 -1
- package/dist/core/tool-model-gate.js +3 -15
- package/dist/core/tools.js +7 -7
- package/dist/core/untrusted-text.d.ts +6 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/core/workflow-run-store-contract.js +13 -0
- package/dist/core/workflow-run-store.d.ts +15 -0
- package/dist/core/workflow-run-store.js +1 -0
- package/dist/engine/loop/agent-loop.js +10 -24
- package/dist/tools/fs/bash-program-position.d.ts +23 -0
- package/dist/tools/fs/bash-program-position.js +11 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +30 -7
- package/dist/tools/fs/bash-readonly-classifier.js +31 -10
- package/dist/tools/fs/fs-bash.d.ts +30 -1
- package/dist/tools/fs/fs-bash.js +123 -54
- package/dist/tools/fs/read-deny.d.ts +14 -0
- package/dist/tools/fs/read-deny.js +5 -0
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/safety.js +2 -1
- package/dist/tools/monitor.d.ts +5 -0
- package/dist/tools/monitor.js +7 -0
- package/package.json +1 -1
- package/wire-changes/core@7.23.5.json +133 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
2
2
|
import { STREAM_TORN_WARNING } from "./stream-shared.js";
|
|
3
3
|
import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
|
|
4
|
-
import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
|
|
4
|
+
import { abortLikeName, BrainError, classifyConnectFailure, classifyFetchThrow, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
|
|
5
|
+
import { stripUrlCredentials } from "./route-adjudicator.js";
|
|
5
6
|
import { classifyInputTooLong } from "./input-too-long.js";
|
|
6
7
|
import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
|
|
7
8
|
import { bindBrainStatusEmitter, emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
8
9
|
import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
|
|
9
10
|
import { thrownText } from "../core/thrown-value.js";
|
|
11
|
+
import { redactSecrets } from "../core/untrusted-egress.js";
|
|
10
12
|
const DEFAULT_MAX_RETRIES = 10;
|
|
11
13
|
const MAX_RETRIES_ENV_CEILING = 15;
|
|
12
14
|
const MAX_RETRIES_ENV = "SEMA_MAX_RETRIES";
|
|
@@ -215,7 +217,7 @@ function emptyAssistant(model) {
|
|
|
215
217
|
};
|
|
216
218
|
}
|
|
217
219
|
function isAbortError(err) {
|
|
218
|
-
return
|
|
220
|
+
return abortLikeName(err) !== undefined;
|
|
219
221
|
}
|
|
220
222
|
function sleep(ms, signal) {
|
|
221
223
|
return new Promise((resolve) => {
|
|
@@ -338,6 +340,10 @@ export function runStreamingBrain(args) {
|
|
|
338
340
|
cc.dispose();
|
|
339
341
|
throw e;
|
|
340
342
|
}
|
|
343
|
+
else if (classifyFetchThrow(e) === "request_unbuildable") {
|
|
344
|
+
cc.dispose();
|
|
345
|
+
throw new BrainError("invalid_request", `${httpLabel} request could not be built: ${redactSecrets(stripUrlCredentials(thrownText(e)))}`);
|
|
346
|
+
}
|
|
341
347
|
else {
|
|
342
348
|
netErr = e;
|
|
343
349
|
}
|
|
@@ -248,12 +248,10 @@ export interface ProbeCause {
|
|
|
248
248
|
* account of an approval's reach is worse than none. */
|
|
249
249
|
readonly further?: ProbeCauseOperands;
|
|
250
250
|
}
|
|
251
|
-
/** The bloat ceiling on {@link RiskDescriptor.probeReason} —
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
|
|
255
|
-
* boundary cannot cap the same value at two different lengths. */
|
|
256
|
-
export declare const PROBE_REASON_MAX = 200;
|
|
251
|
+
/** The bloat ceiling on {@link RiskDescriptor.probeReason} — re-exported from the leaf neutralizer module
|
|
252
|
+
* for the same reason its `PROBE_CAUSE_PATH_MAX` sibling is homed there (a value import from tools/fs
|
|
253
|
+
* back into this module closes an ESM cycle), so mint, gate and persist read ONE cap. */
|
|
254
|
+
export { PROBE_REASON_MAX } from "./untrusted-text.js";
|
|
257
255
|
/**
|
|
258
256
|
* The ONE validator for a {@link ProbeCause} arriving from a probe.
|
|
259
257
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { uuidv7 } from "../internal/harness.js";
|
|
3
|
-
import { PROBE_CAUSE_PATH_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
3
|
+
import { PROBE_CAUSE_PATH_MAX, PROBE_REASON_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
4
4
|
import { carriesBidiControls } from "./tool-policy.js";
|
|
5
5
|
import { readDenialLimitFallback } from "./auto-mode.js";
|
|
6
6
|
import { isAskOrigin, isRuleStoreUnreadable } from "./ask-origin.js";
|
|
@@ -14,7 +14,7 @@ export function mintCheckpointToken() {
|
|
|
14
14
|
export function mintCheckpointId() {
|
|
15
15
|
return `cp_${randomUUID()}`;
|
|
16
16
|
}
|
|
17
|
-
export
|
|
17
|
+
export { PROBE_REASON_MAX } from "./untrusted-text.js";
|
|
18
18
|
const PROBE_CAUSE_MAX_SHOWN = 8;
|
|
19
19
|
export function normalizeProbeCause(value) {
|
|
20
20
|
if (!isPlainRecord(value))
|
|
@@ -144,9 +144,12 @@ function foldSpacing(s) {
|
|
|
144
144
|
function reject(code, message) {
|
|
145
145
|
return { reject: { code, message } };
|
|
146
146
|
}
|
|
147
|
-
const RULE_LANE_FLOOR = {
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
const RULE_LANE_FLOOR = {
|
|
148
|
+
pathPrefixedNameIsText: true,
|
|
149
|
+
quotedOperatorsAreText: true,
|
|
150
|
+
};
|
|
151
|
+
const MATCH_READING = { terminator: "keep", redirection: "reject" };
|
|
152
|
+
const OFFER_READING = { terminator: "keep", redirection: "tolerate" };
|
|
150
153
|
function commandBasename(name) {
|
|
151
154
|
return name.slice(name.lastIndexOf("/") + 1);
|
|
152
155
|
}
|
|
@@ -160,7 +163,7 @@ function ruleLaneShapeOf(command, reading) {
|
|
|
160
163
|
names.push(undefined);
|
|
161
164
|
continue;
|
|
162
165
|
}
|
|
163
|
-
const floor = parseLeadingCommandName(segment,
|
|
166
|
+
const floor = parseLeadingCommandName(segment, RULE_LANE_FLOOR);
|
|
164
167
|
if ("reject" in floor) {
|
|
165
168
|
return {
|
|
166
169
|
reject: split.segments.length > 1
|
|
@@ -561,7 +564,7 @@ function admitsUnder(rule, command, reading) {
|
|
|
561
564
|
if (rule.match === "exact")
|
|
562
565
|
return folded === rule.command;
|
|
563
566
|
const bodyShape = ruleLaneShapeOf(rule.command, reading);
|
|
564
|
-
if ("reject" in bodyShape)
|
|
567
|
+
if ("reject" in bodyShape || foldSpacing(rule.command) === undefined)
|
|
565
568
|
return false;
|
|
566
569
|
if (shape.segments.length !== bodyShape.segments.length)
|
|
567
570
|
return false;
|
|
@@ -1,6 +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 { BashReadonlyRootBoundary } from "../../tools/fs/bash-readonly-classifier.js";
|
|
4
5
|
import { type WorktreeSessionRef } from "../../tools/worktree.js";
|
|
5
6
|
import type { CwdRef } from "../../tools/fs/index.js";
|
|
6
7
|
import { type ToolResultStore } from "../tool-result-store.js";
|
|
@@ -36,6 +37,8 @@ export interface PrepareDelegationSurfaceInput {
|
|
|
36
37
|
runnerSelf: RunnerSelfSeat | undefined;
|
|
37
38
|
/** borrowed-readonly — the background/workflow door's first half: the hands mount decided a real shell with write hands. */
|
|
38
39
|
backgroundTaskToolsActive: boolean;
|
|
40
|
+
/** borrowed-readonly — this run's shell read boundary (declared at the hands phase), for `Monitor`: the only P11 face that spawns a shell command. */
|
|
41
|
+
shellReadBoundarySeat: (() => BashReadonlyRootBoundary | undefined) | undefined;
|
|
39
42
|
/** borrowed-readonly — the door's second half: the Workflow tool is on the roster. */
|
|
40
43
|
workflowToolsActive: boolean;
|
|
41
44
|
/** borrowed-readonly — the peer lane's mount verdict for this leg. */
|
|
@@ -103,6 +103,7 @@ export function prepareDelegationSurface(input) {
|
|
|
103
103
|
envHandToolNames.add("Monitor");
|
|
104
104
|
roster.mount(firstPartyOffload(createMonitorTool(executionEnv, {
|
|
105
105
|
registry: defaultTaskRegistry,
|
|
106
|
+
...(input.shellReadBoundarySeat !== undefined ? { readBoundary: input.shellReadBoundarySeat } : {}),
|
|
106
107
|
owner: hostTaskId,
|
|
107
108
|
scope: taskScope,
|
|
108
109
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BeforeWriteHook, type CwdRef, type ReadDenyMatcher, type ContainmentFace } from "../../tools/fs/index.js";
|
|
1
|
+
import { type BashReadonlyRootBoundary, type BeforeWriteHook, type CwdRef, type ReadDenyMatcher, type ContainmentFace } from "../../tools/fs/index.js";
|
|
2
2
|
import { type ReadFileState } from "../../tools/fs/safety.js";
|
|
3
3
|
import type { RunnerDeps, ShellGateDoctrine, TaskSpec } from "../types.js";
|
|
4
4
|
import type { Model } from "../../internal/llm.js";
|
|
@@ -266,6 +266,9 @@ export interface PrepareHandsReadFaceResult {
|
|
|
266
266
|
/** owned — real shell + write hands + background-capable env: gates the driver's TaskOutput/
|
|
267
267
|
* TaskStop/Monitor/SendMessage mounts (P11) and the Monitor arm of the shellGate fold here. */
|
|
268
268
|
backgroundTaskToolsActive: boolean;
|
|
269
|
+
/** owned — this run's shell read BOUNDARY, published for a P11 face that spawns a shell command and
|
|
270
|
+
* mounts LATER than the band (`Monitor`): the SAME closure the band's shell faces got. ⇔ hands. */
|
|
271
|
+
shellReadBoundarySeat: (() => BashReadonlyRootBoundary | undefined) | undefined;
|
|
269
272
|
/** write-back (D-3 hold) — the run's RESOLVED read face where hands mount; undefined ⇔ no hands
|
|
270
273
|
* (the hands-less value lives in `handsLessResolvedFace`). The driver assigns this onto its
|
|
271
274
|
* outer `let` for the call-time closures (ctx enrichment / checkpoint mint / envFacts). */
|
|
@@ -137,6 +137,7 @@ export function resolveHandsLessReadFace(input) {
|
|
|
137
137
|
wsSnapshot: undefined,
|
|
138
138
|
rebaseWsPath: (p) => p,
|
|
139
139
|
backgroundTaskToolsActive: false,
|
|
140
|
+
shellReadBoundarySeat: undefined,
|
|
140
141
|
resolvedReadFace: undefined,
|
|
141
142
|
resolvedWriteFace: undefined,
|
|
142
143
|
readDenyAdditionsNormalized,
|
|
@@ -167,6 +168,7 @@ export async function prepareHandsMount(input) {
|
|
|
167
168
|
let wsSnapshot;
|
|
168
169
|
let rebaseWsPath = (p) => p;
|
|
169
170
|
let backgroundTaskToolsActive = false;
|
|
171
|
+
let shellReadBoundarySeat;
|
|
170
172
|
let resolvedReadFace;
|
|
171
173
|
let resolvedWriteFace;
|
|
172
174
|
let readDenyAdditionsNormalized = [];
|
|
@@ -335,6 +337,7 @@ export async function prepareHandsMount(input) {
|
|
|
335
337
|
...denySeat,
|
|
336
338
|
...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
|
|
337
339
|
});
|
|
340
|
+
shellReadBoundarySeat = shellReadBoundary;
|
|
338
341
|
const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
|
|
339
342
|
...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
|
|
340
343
|
...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
|
|
@@ -422,6 +425,7 @@ export async function prepareHandsMount(input) {
|
|
|
422
425
|
wsSnapshot,
|
|
423
426
|
rebaseWsPath,
|
|
424
427
|
backgroundTaskToolsActive,
|
|
428
|
+
shellReadBoundarySeat,
|
|
425
429
|
resolvedReadFace,
|
|
426
430
|
resolvedWriteFace,
|
|
427
431
|
handsLessResolvedWriteFace,
|
|
@@ -513,14 +513,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
513
513
|
}
|
|
514
514
|
const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, taskRootCanonical, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGate, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), firstPartyOffload, roster, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts, ...(trackFileEdit !== undefined ? { trackFileEdit } : {}), onFileEdited: noteFileEdited, harnessRef, toolProgressRef, ...(restoredFilePaths.length > 0 ? { restoredFilePaths } : {}) };
|
|
515
515
|
const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
|
|
516
|
-
const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
|
|
516
|
+
const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, shellReadBoundarySeat, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames, sealReadStateSeat } = handsReadFace;
|
|
517
517
|
resolvedReadFace = handsReadFace.resolvedReadFace;
|
|
518
518
|
resolvedWriteFace = handsReadFace.resolvedWriteFace;
|
|
519
519
|
readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
|
|
520
520
|
handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
|
|
521
521
|
handsLessResolvedWriteFace = handsReadFace.handsLessResolvedWriteFace;
|
|
522
522
|
shellGatedTools = handsReadFace.shellGatedTools;
|
|
523
|
-
const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
|
|
523
|
+
const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, shellReadBoundarySeat, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
|
|
524
524
|
const { liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, checkpointStore, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
|
|
525
525
|
if (spec.handsReadOnly !== true) {
|
|
526
526
|
const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
|
|
@@ -29,7 +29,7 @@ import { toolEndBodyFrom, toolResultMsg } from "./tool-end-body.js";
|
|
|
29
29
|
import { announceListingTruncation } from "../skill-listing-budget.js";
|
|
30
30
|
import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingFrame, renderOrphanedBackgroundTasks, renderSkillsListingFrame, stampWriteAnchor } from "./turn-attachments.js";
|
|
31
31
|
import { flushOwedAnnouncedSnapshot } from "./announce-once-ledger.js";
|
|
32
|
-
import { asThrownError, thrownText } from "../thrown-value.js";
|
|
32
|
+
import { asThrownError, thrownMarksOf, thrownText } from "../thrown-value.js";
|
|
33
33
|
export async function runLeg(input) {
|
|
34
34
|
const { spec, queue, prepared, resume, rs, isDelegatedChild, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner, next } = input;
|
|
35
35
|
let final;
|
|
@@ -693,18 +693,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
|
|
|
693
693
|
}
|
|
694
694
|
catch (err) {
|
|
695
695
|
const execError = `Error: ${thrownText(err)}`;
|
|
696
|
-
const marks = (
|
|
697
|
-
if (err === null || typeof err !== "object")
|
|
698
|
-
return undefined;
|
|
699
|
-
const src = err;
|
|
700
|
-
let d;
|
|
701
|
-
if (src.details !== null && typeof src.details === "object" && !Array.isArray(src.details)) {
|
|
702
|
-
d = { ...src.details };
|
|
703
|
-
}
|
|
704
|
-
if (typeof src.errorKind === "string")
|
|
705
|
-
d = { ...(d ?? {}), errorKind: src.errorKind };
|
|
706
|
-
return d;
|
|
707
|
-
})();
|
|
696
|
+
const marks = thrownMarksOf(err);
|
|
708
697
|
await settleEnd(true, { content: execError, ...(marks !== undefined ? { details: marks } : {}) });
|
|
709
698
|
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, execError, true));
|
|
710
699
|
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this an Error, including one minted in ANOTHER REALM?
|
|
3
|
+
*
|
|
4
|
+
* `instanceof` compares against THIS realm's `Error.prototype`, so a genuine `Error` thrown by a script the
|
|
5
|
+
* engine ran inside `vm.createContext` — a workflow script is exactly that — fails the test and would be
|
|
6
|
+
* described as an anonymous object, losing the message that is the entire point of reporting it. The brand
|
|
7
|
+
* check is the realm-independent one: an object with the `[[ErrorData]]` internal slot answers `[object Error]`
|
|
8
|
+
* no matter where it was made, and a proxy forwards it. A plain object can forge the tag through
|
|
9
|
+
* `Symbol.toStringTag`, which costs nothing — the only thing this decides is whether we look for a `message`,
|
|
10
|
+
* and a forger that supplies a string message has told us what it wanted rendered anyway.
|
|
11
|
+
*
|
|
12
|
+
* Guarded, because both operations are attacker-reachable: `instanceof` walks a prototype chain a revoked
|
|
13
|
+
* proxy traps, and the tag is a getter that can throw.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isErrorLike(value: unknown): boolean;
|
|
1
16
|
/**
|
|
2
17
|
* Turning a value that was THROWN at us into text — the ONE rule, for the whole engine.
|
|
3
18
|
*
|
|
@@ -77,3 +92,18 @@ export declare function asThrownError(err: unknown): Error;
|
|
|
77
92
|
* to describing the value, which {@link thrownText} already does totally.
|
|
78
93
|
*/
|
|
79
94
|
export declare function readMember(value: unknown, key: string): unknown;
|
|
95
|
+
/** The PLURAL of {@link readMember}: read a value's own enumerable members, once each, into an inert
|
|
96
|
+
* snapshot — for sites that want a COPY of a whole bag an untrusted value handed over (a thrown
|
|
97
|
+
* error's `details` mark, on its way onto a tool result). The idiom it replaces is `{ ...value }`,
|
|
98
|
+
* a second way of running the value's code: the spread asks for its own keys and evaluates every
|
|
99
|
+
* getter behind them, so one hostile member took the copy down with it, and a revoked proxy traps the
|
|
100
|
+
* enumeration itself (even `Array.isArray` on one throws). Non-objects, arrays (spreading one yields
|
|
101
|
+
* index keys) and anything whose keys cannot be enumerated answer `undefined` — "there is no bag
|
|
102
|
+
* here"; a member that refuses to be read comes back `undefined` in a PRESENT key. */
|
|
103
|
+
export declare function readOwnMembers(value: unknown): Record<string, unknown> | undefined;
|
|
104
|
+
/** The MACHINE-READABLE marks a thrown value carries (`details`, with a string `errorKind` folded in) —
|
|
105
|
+
* the one extractor for both legs that lift them onto a tool result. The live leg and the resume leg
|
|
106
|
+
* each had their own copy, and the resume copy still spread the bag and called `Array.isArray` on it,
|
|
107
|
+
* so a hostile `details` failed the whole leg where the live one had stopped losing anything but the
|
|
108
|
+
* mark. `undefined` = nothing to carry; a caller that must always present a bag folds that itself. */
|
|
109
|
+
export declare function thrownMarksOf(err: unknown): Record<string, unknown> | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describeRejectedValue } from "./tool-search-setting.js";
|
|
2
|
-
function isErrorLike(value) {
|
|
2
|
+
export function isErrorLike(value) {
|
|
3
3
|
try {
|
|
4
4
|
if (value instanceof Error)
|
|
5
5
|
return true;
|
|
@@ -54,3 +54,25 @@ export function readMember(value, key) {
|
|
|
54
54
|
return undefined;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
export function readOwnMembers(value) {
|
|
58
|
+
let keys;
|
|
59
|
+
try {
|
|
60
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
61
|
+
return undefined;
|
|
62
|
+
keys = Object.keys(value);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const key of keys)
|
|
69
|
+
out[key] = readMember(value, key);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
export function thrownMarksOf(err) {
|
|
73
|
+
const details = readOwnMembers(readMember(err, "details"));
|
|
74
|
+
const errorKind = readMember(err, "errorKind");
|
|
75
|
+
if (typeof errorKind !== "string")
|
|
76
|
+
return details;
|
|
77
|
+
return { ...(details ?? {}), errorKind };
|
|
78
|
+
}
|
|
@@ -108,7 +108,7 @@ export const TOOL_CATALOG_ENTRIES = [
|
|
|
108
108
|
{ id: "task-stop-hands", name: "TaskStop", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"], cards: [], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
|
|
109
109
|
{ id: "task-output", name: "TaskOutput", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-output"], face: { family: "delegate", effect: "read", contract: TASK_OUTPUT_CONTRACT } },
|
|
110
110
|
{ id: "task-stop", name: "TaskStop", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-stop"], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
|
|
111
|
-
{ id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
|
|
111
|
+
{ id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
|
|
112
112
|
{ id: "send-message", name: "SendMessage", source: "builtin", definedIn: "src/agents/send-message-tool.ts", mountedBy: ["delegation", "hostInternals", "peerLane"], cards: ["send-message"], face: { family: "protocol", effect: "write", contract: c("core.send_message@1") } },
|
|
113
113
|
{ id: "agent-transcript", name: "AgentTranscript", source: "builtin", definedIn: "src/agents/agent-transcript-tool.ts", mountedBy: ["delegation"], cards: ["agent-transcript"], face: { family: "delegate", effect: "read", contract: c("core.agent_transcript@1") } },
|
|
114
114
|
{ id: "list-agents", name: "ListAgents", source: "builtin", definedIn: "src/agents/list-agents-tool.ts", mountedBy: ["peerLane"], cards: ["list-agents"], face: { family: "protocol", effect: "read", aliases: ["ListPeers"], contract: c("core.list_agents@1") } },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { thrownText } from "./thrown-value.js";
|
|
1
|
+
import { readMember, thrownText } from "./thrown-value.js";
|
|
2
2
|
export function modelIdTail(id) {
|
|
3
3
|
return id.toLowerCase().split("/").pop() ?? "";
|
|
4
4
|
}
|
|
@@ -207,22 +207,10 @@ export function applyToolModelGate(input) {
|
|
|
207
207
|
merged = input.depsSeat === false ? undefined : mergeToolModelGateClasses(input.depsSeat);
|
|
208
208
|
}
|
|
209
209
|
catch (e) {
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
code = e?.code;
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
code = undefined;
|
|
216
|
-
}
|
|
210
|
+
const code = readMember(e, "code");
|
|
217
211
|
if (code !== undefined)
|
|
218
212
|
throw e;
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
msg = thrownText(e);
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
msg = "[unreportable throw]";
|
|
225
|
-
}
|
|
213
|
+
const msg = thrownText(e);
|
|
226
214
|
throw gateConfigError(`RunnerDeps.toolModelGate could not be read (${msg}) — a config seat whose reads throw is a bad value, refused rather than passed.`);
|
|
227
215
|
}
|
|
228
216
|
const raw = input.envRaw;
|
package/dist/core/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Value } from "typebox/value";
|
|
2
2
|
import { formatToolError, formatZodValidationError } from "./tool-errors.js";
|
|
3
|
+
import { readMember } from "./thrown-value.js";
|
|
3
4
|
import { attachToolContract } from "../prompt-assembly/tool-catalog.js";
|
|
4
5
|
function normalizeContent(ret) {
|
|
5
6
|
if (typeof ret === "string") {
|
|
@@ -115,13 +116,12 @@ export function defineTool(spec, options) {
|
|
|
115
116
|
}
|
|
116
117
|
catch (err) {
|
|
117
118
|
const wrapped = new Error(formatToolError(err));
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
119
|
+
const marks = readMember(err, "details");
|
|
120
|
+
if (marks !== undefined)
|
|
121
|
+
wrapped.details = marks;
|
|
122
|
+
const errorKind = readMember(err, "errorKind");
|
|
123
|
+
if (typeof errorKind === "string")
|
|
124
|
+
wrapped.errorKind = errorKind;
|
|
125
125
|
throw wrapped;
|
|
126
126
|
}
|
|
127
127
|
const { content, details, terminate, isError } = normalizeContent(ret);
|
|
@@ -405,6 +405,12 @@ export declare function inlineUntrusted(text: string, maxLen?: number): string;
|
|
|
405
405
|
* checkpoint-store closed an ESM evaluation cycle (checkpoint-store → tool-policy → tools/fs →
|
|
406
406
|
* checkpoint-store, a latent TDZ). Both already import this module; one cap, no cycle. */
|
|
407
407
|
export declare const PROBE_CAUSE_PATH_MAX = 200;
|
|
408
|
+
/** The bloat ceiling on a probe's one-sentence account (`RiskDescriptor.probeReason` /
|
|
409
|
+
* `AskRequest.probeReason`); the neutralizer is the INJECTION ceiling, this is only the LENGTH one. Homed
|
|
410
|
+
* here for the same reason as the constant above: three readers — the shell probe that MINTS the sentence
|
|
411
|
+
* (tools/fs) and must fit every load-bearing part inside this bound, the gate, the persist boundary — and
|
|
412
|
+
* a value import from tools/fs into checkpoint-store closes an ESM cycle. */
|
|
413
|
+
export declare const PROBE_REASON_MAX = 200;
|
|
408
414
|
/** The one body bound every "reviewer note" relay passes to {@link delimitUntrusted} — the decider's
|
|
409
415
|
* free text attached to a deny (sync `AskOutcome.reason` and the durable `ResumeOutcome` `reason`
|
|
410
416
|
* legs alike). A note is steering, not payload: unbounded it can flood the transcript/context the
|
|
@@ -601,6 +601,7 @@ function sanitizeLabel(label) {
|
|
|
601
601
|
return inlineUntrusted(label, LABEL_MAX);
|
|
602
602
|
}
|
|
603
603
|
export const PROBE_CAUSE_PATH_MAX = 200;
|
|
604
|
+
export const PROBE_REASON_MAX = 200;
|
|
604
605
|
export const REVIEWER_NOTE_MAX_BODY = 2048;
|
|
605
606
|
export function delimitUntrusted(label, text, maxBody) {
|
|
606
607
|
return delimitUntrustedWithClip(label, text, maxBody).text;
|
|
@@ -118,6 +118,19 @@ export async function workflowRunStoreContract(make, runAssertion = defaultSeque
|
|
|
118
118
|
byId.get("d-over").budgetOvershoot.spentTokens = 7;
|
|
119
119
|
assert.equal((await store.get("d-over")).budgetOvershoot.spentTokens, 1_200);
|
|
120
120
|
});
|
|
121
|
+
run("the failure CODE reaches the LIST row verbatim, and a run without one keeps the key ABSENT", async () => {
|
|
122
|
+
const store = make();
|
|
123
|
+
const coded = createWorkflowRun({ id: "f-coded", status: "failed", createdAt: 5_000, error: "the body gave up", errorCode: "workflow.agent_blocked" });
|
|
124
|
+
const codeless = createWorkflowRun({ id: "f-codeless", status: "failed", createdAt: 6_000, error: "the body gave up" });
|
|
125
|
+
const ok = createWorkflowRun({ id: "f-ok", status: "completed", createdAt: 7_000 });
|
|
126
|
+
for (const r of [coded, codeless, ok])
|
|
127
|
+
await store.put(r.id, r);
|
|
128
|
+
const byId = new Map((await store.listByScope("tenant-a")).map((s) => [s.id, s]));
|
|
129
|
+
assert.equal(byId.get("f-coded").errorCode, "workflow.agent_blocked");
|
|
130
|
+
assert.equal("errorCode" in byId.get("f-codeless"), false, "a failed run with no code ⇒ the key is OMITTED, not an `unknown` stand-in");
|
|
131
|
+
assert.equal("errorCode" in byId.get("f-ok"), false, "a completed run never carries one ⇒ the key is OMITTED");
|
|
132
|
+
assert.deepEqual(byId.get("f-coded"), summarizeWorkflowRun(coded));
|
|
133
|
+
});
|
|
121
134
|
run("the TOTAL-TIMEOUT disclosure reaches the LIST row: timeoutInterruption projects (de-aliased), and a run without it keeps the key ABSENT", async () => {
|
|
122
135
|
const store = make();
|
|
123
136
|
const seat = { timeoutMs: 600_000, agentsCompleted: 3, agentsFailed: 1, agentsInFlight: 2, approvalWaitedMs: 540_000 };
|
|
@@ -46,6 +46,21 @@ export interface WorkflowRunSummary {
|
|
|
46
46
|
description?: string;
|
|
47
47
|
/** Lifecycle status ({@link WorkflowRun.status}): running / completed / failed. */
|
|
48
48
|
status: WorkflowRunStatus;
|
|
49
|
+
/** The MACHINE code of the failure that ended the run ({@link WorkflowRun.errorCode}) — the same word,
|
|
50
|
+
* in the same vocabulary, that the full run carries; never re-derived and never a second taxonomy.
|
|
51
|
+
* The fourth member of the same family as {@link agentFailures} / {@link budgetOvershoot} /
|
|
52
|
+
* {@link timeoutInterruption}: those three qualify a row that LOOKS clean, and this one says WHY a
|
|
53
|
+
* `failed` row failed. Nothing else on this projection can answer that — {@link status} is `failed`
|
|
54
|
+
* for a refused spawn, a blown budget, a stalled run and a body that simply threw alike, so a list
|
|
55
|
+
* face without this seat must `get` the full run per failed row to say anything at all, which is the
|
|
56
|
+
* N+1 this projection exists to avoid.
|
|
57
|
+
*
|
|
58
|
+
* PRESENT EXACTLY WHEN THE RUN CARRIES ONE, absent otherwise — an absent key reads "no code on the
|
|
59
|
+
* record", never "the run succeeded" and never a `workflow.unknown` stand-in. A `completed` run never
|
|
60
|
+
* carries one (per {@link WorkflowRun.errorCode}), and a `failed` run whose body threw a codeless
|
|
61
|
+
* error with no failed child on the record carries none either. Free of any own attribution rule: the
|
|
62
|
+
* attribution (thrown code → first failed agent → absent) happens once, where the run is written. */
|
|
63
|
+
errorCode?: string;
|
|
49
64
|
/** Count of agent-runs that ended failed ({@link WorkflowRun.agentFailures}) — present only when > 0.
|
|
50
65
|
* Lets a list view flag a "completed, with failures" run without an N+1 `get` of the full run. */
|
|
51
66
|
agentFailures?: number;
|
|
@@ -8,6 +8,7 @@ export function summarizeWorkflowRun(run) {
|
|
|
8
8
|
...(run.name !== undefined ? { name: run.name } : {}),
|
|
9
9
|
...(run.description !== undefined ? { description: run.description } : {}),
|
|
10
10
|
status: run.status,
|
|
11
|
+
...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
|
|
11
12
|
...(run.agentFailures !== undefined ? { agentFailures: run.agentFailures } : {}),
|
|
12
13
|
...(run.budgetOvershoot !== undefined ? { budgetOvershoot: { ...run.budgetOvershoot } } : {}),
|
|
13
14
|
...(run.timeoutInterruption !== undefined ? { timeoutInterruption: { ...run.timeoutInterruption } } : {}),
|
|
@@ -2,7 +2,7 @@ import { findToolByName, validateToolArguments } from "../llm/index.js";
|
|
|
2
2
|
import { truncateError } from "../../core/tool-errors.js";
|
|
3
3
|
import { INTERRUPTED_BY_USER_FOR_TOOL_USE_MARKER, INTERRUPTED_BY_USER_MARKER, } from "../../core/session-reconcile.js";
|
|
4
4
|
import { resolveAgentCoreStreamFn } from "./runtime-deps.js";
|
|
5
|
-
import { thrownText } from "../../core/thrown-value.js";
|
|
5
|
+
import { isErrorLike, readMember, thrownMarksOf, thrownText } from "../../core/thrown-value.js";
|
|
6
6
|
function appendTextDeltaToAssistantMessage(message, contentIndex, delta) {
|
|
7
7
|
const content = [...message.content];
|
|
8
8
|
const currentContent = content[contentIndex];
|
|
@@ -1537,33 +1537,19 @@ function safeAbortDetails(fn) {
|
|
|
1537
1537
|
}
|
|
1538
1538
|
}
|
|
1539
1539
|
function isAbortSemanticsError(error) {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
cursor = next;
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
catch {
|
|
1552
|
-
return false;
|
|
1540
|
+
let cursor = error;
|
|
1541
|
+
for (let depth = 0; depth < 8 && isErrorLike(cursor); depth++) {
|
|
1542
|
+
if (readMember(cursor, "name") === "AbortError")
|
|
1543
|
+
return true;
|
|
1544
|
+
const next = readMember(cursor, "cause");
|
|
1545
|
+
if (next === cursor)
|
|
1546
|
+
break;
|
|
1547
|
+
cursor = next;
|
|
1553
1548
|
}
|
|
1554
1549
|
return false;
|
|
1555
1550
|
}
|
|
1556
1551
|
function createErrorToolResult(message, source) {
|
|
1557
|
-
|
|
1558
|
-
if (source !== null && typeof source === "object") {
|
|
1559
|
-
const marks = source;
|
|
1560
|
-
if (marks.details !== null && typeof marks.details === "object" && !Array.isArray(marks.details)) {
|
|
1561
|
-
details = { ...marks.details };
|
|
1562
|
-
}
|
|
1563
|
-
if (typeof marks.errorKind === "string") {
|
|
1564
|
-
details.errorKind = marks.errorKind;
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1552
|
+
const details = thrownMarksOf(source) ?? {};
|
|
1567
1553
|
return {
|
|
1568
1554
|
content: [{ type: "text", text: truncateError(message) }],
|
|
1569
1555
|
details,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* does (an option skipped, a chain member missed) is how one face used to vouch for a program another refused.
|
|
10
10
|
*/
|
|
11
11
|
import type { SegmentTokens } from "./bash-lexer.js";
|
|
12
|
+
import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
|
|
12
13
|
/** The verdict every unresolvable / out-of-root / unbounded finding ends on. The findings differ in what
|
|
13
14
|
* they found; the consequence is one consequence, and a copy that drifts reads as a second rule. */
|
|
14
15
|
export declare const NOT_AUTO_ALLOWED = "\u2014 not auto-allowed";
|
|
@@ -170,6 +171,28 @@ export declare function argvReentryUnjudged(pos: ProgramPosition, acceptedTarget
|
|
|
170
171
|
*/
|
|
171
172
|
export declare const SHELL_SCAN_DECLINES: readonly ["unreadable_program_position", "stream_fed_operands"];
|
|
172
173
|
export type ShellScanDecline = (typeof SHELL_SCAN_DECLINES)[number];
|
|
174
|
+
/** The two SEAT TABLES over that set — ROW PER MEMBER behind a compile-time fence, registered in
|
|
175
|
+
* `docs/CLOSED-SETS.md` so `gate:closed-sets` is their runtime twin. The ARGUMENT for the rows is at the
|
|
176
|
+
* seats that read them (`fs-bash.ts`), which DERIVE the `Set` shape their readers take. Rows and not two
|
|
177
|
+
* sets because a derived set beside an empty literal made a third decline SILENTLY asymmetric, in the
|
|
178
|
+
* widening direction; a member with no row now reds `tsc` at both fences with the word spelled out. */
|
|
179
|
+
export declare const BOUNDARY_SEAT_READS_DECLINE: {
|
|
180
|
+
readonly unreadable_program_position: true;
|
|
181
|
+
readonly stream_fed_operands: true;
|
|
182
|
+
};
|
|
183
|
+
/** The fence over the read-boundary seat's table: `never` while every decline has a row. */
|
|
184
|
+
export type BoundarySeatTableCoversEveryDecline = AssertAllKeysHandled<Exclude<ShellScanDecline, keyof typeof BOUNDARY_SEAT_READS_DECLINE>>;
|
|
185
|
+
/** The classify doctrine's table — every row `false` BY THE ARGUMENT (stated at the seat), not by omission. */
|
|
186
|
+
export declare const CLASSIFY_SEAT_READS_DECLINE: {
|
|
187
|
+
readonly unreadable_program_position: false;
|
|
188
|
+
readonly stream_fed_operands: false;
|
|
189
|
+
};
|
|
190
|
+
/** The fence over the classify seat's table. */
|
|
191
|
+
export type ClassifySeatTableCoversEveryDecline = AssertAllKeysHandled<Exclude<ShellScanDecline, keyof typeof CLASSIFY_SEAT_READS_DECLINE>>;
|
|
192
|
+
/** A seat's table in the shape its readers take — the members it answers YES for. DERIVED, so the ROWS are
|
|
193
|
+
* the single source and a set can never say something its table does not. The parameter is TOTAL, not
|
|
194
|
+
* `Partial`: that stops a third seat being written `seatDeclineEvidence({})`, one level up from the fences. */
|
|
195
|
+
export declare function seatDeclineEvidence(table: Record<ShellScanDecline, boolean>): ReadonlySet<ShellScanDecline>;
|
|
173
196
|
/** Does a SHELL stand where this segment's program would be — as the program itself (`sh -c`), or as a launcher
|
|
174
197
|
* ROW handing its operands to one (`watch "…"`)? ONE predicate for the re-entry class, so the two spellings
|
|
175
198
|
* cannot be answered differently; which names are shells is the caller's declaration. Not a decline. */
|
|
@@ -159,6 +159,17 @@ export function argvReentryUnjudged(pos, acceptedTargets) {
|
|
|
159
159
|
return runs === undefined || !acceptedTargets.has(runs);
|
|
160
160
|
}
|
|
161
161
|
export const SHELL_SCAN_DECLINES = ["unreadable_program_position", "stream_fed_operands"];
|
|
162
|
+
export const BOUNDARY_SEAT_READS_DECLINE = {
|
|
163
|
+
unreadable_program_position: true,
|
|
164
|
+
stream_fed_operands: true,
|
|
165
|
+
};
|
|
166
|
+
export const CLASSIFY_SEAT_READS_DECLINE = {
|
|
167
|
+
unreadable_program_position: false,
|
|
168
|
+
stream_fed_operands: false,
|
|
169
|
+
};
|
|
170
|
+
export function seatDeclineEvidence(table) {
|
|
171
|
+
return new Set(SHELL_SCAN_DECLINES.filter((decline) => table[decline] === true));
|
|
172
|
+
}
|
|
162
173
|
export function programPositionRunsShell(pos, shellProgram) {
|
|
163
174
|
if (pos.refusal !== undefined)
|
|
164
175
|
return pos.refusal.kind === "control_syntax_at_program_position" && pos.refusal.runsShell === true;
|
|
@@ -76,11 +76,13 @@ export interface LeadingCommandNameOptions {
|
|
|
76
76
|
* Read a shell operator that sits inside a QUOTED region as ordinary argument TEXT rather than as an
|
|
77
77
|
* operator, so `grep -E "a|b" f.log` parses as the single simple command it is.
|
|
78
78
|
*
|
|
79
|
-
* OPT-IN, and
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
79
|
+
* OPT-IN, and the two callers that set it share one premise: the whole command has ALREADY been refused
|
|
80
|
+
* for backslashes / an expanding `$` / backticks / subshells / line breaks, so they are inside
|
|
81
|
+
* {@link quoteMask}'s exactness envelope — the compound classify face
|
|
82
|
+
* ({@link classifyCompoundReadonlyDetailed}) and the persisted permission-RULE lane (`RULE_LANE_FLOOR`),
|
|
83
|
+
* whose segments come out of that same gate ({@link splitShellCompoundSegments}). The `bash_readonly`
|
|
84
|
+
* DECLARATION face does NOT set it: its contract is "a single simple command with no operator characters
|
|
85
|
+
* at all", and it runs on raw tool input with no whole-string gate in front of it.
|
|
84
86
|
*
|
|
85
87
|
* Fail-closed on unbalanced quoting: with a region left open, the mask is not trusted and every
|
|
86
88
|
* operator character counts, exactly as without this option.
|
|
@@ -617,8 +619,29 @@ export interface OutOfRootReadGateVerdict {
|
|
|
617
619
|
readonly paths: readonly string[];
|
|
618
620
|
/** Did the deployment's DENY judge speak about an operand this command names? The other half of the same
|
|
619
621
|
* question, over the very same readers, in a second pass WITH the deny judge — which is what lets the
|
|
620
|
-
* containment answer above stay deny-free and keep its `paths` contract. Absent ⇔ it said nothing.
|
|
621
|
-
|
|
622
|
+
* containment answer above stay deny-free and keep its `paths` contract. Absent ⇔ it said nothing.
|
|
623
|
+
*
|
|
624
|
+
* It carries the FINDING, not a boolean: this is the arm a command the grammar gate refused WHOLE
|
|
625
|
+
* (`cat .env > out.txt`, a subshell, a substitution) reaches, and there the compound verdict carries no
|
|
626
|
+
* deny fields at all — so a caller disclosing the hit has nowhere else to read the path and the pattern
|
|
627
|
+
* from, and said nothing. The second pass already computed both; throwing them away was the whole bug. */
|
|
628
|
+
readonly denied?: {
|
|
629
|
+
readonly path: string;
|
|
630
|
+
readonly pattern: string;
|
|
631
|
+
};
|
|
632
|
+
/** The reader operands this pass RESOLVED and placed inside the roots, plus the expansions it parsed but
|
|
633
|
+
* cannot enumerate — the candidates an execute-time recheck must canonicalize.
|
|
634
|
+
*
|
|
635
|
+
* Published because this pass reads SHAPES the compound walk hands nothing out for: a launcher whose
|
|
636
|
+
* program it cannot name (`env -- cat f`, `sudo -u root cat f`), a stream-fed one (`… | xargs cat f`).
|
|
637
|
+
* There the compound verdict carries no candidates at all, so the recheck had nothing to resolve and an
|
|
638
|
+
* in-root symlink to a deny-listed target RAN after an approval — while the same read spelled without
|
|
639
|
+
* the launcher was refused. Same operands, same judge, one more pass allowed to hand them over.
|
|
640
|
+
*
|
|
641
|
+
* NOT the out-of-root ones: those ride {@link paths}, they are what the boundary RAISED, and a leg that
|
|
642
|
+
* re-refused them would be a second adjudicator of a decision already made. */
|
|
643
|
+
readonly checked: readonly string[];
|
|
644
|
+
readonly undecided: readonly string[];
|
|
622
645
|
}
|
|
623
646
|
/** Test seam for the lockstep pin that the launcher table (the ONE launcher truth, in the program-position
|
|
624
647
|
* reader's module) stays a SUBSET of the rule lane's curated `BARE_INTERPRETER_NAMES`. Exported for that one
|