@sema-agent/core 7.26.1 → 7.26.3
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/verify.d.ts +16 -6
- package/dist/agents/verify.js +12 -5
- package/dist/core/checkpoint-store.d.ts +13 -10
- package/dist/core/gate-lanes.js +2 -1
- package/dist/core/gate-outcome.d.ts +54 -5
- package/dist/core/gate-outcome.js +17 -0
- package/dist/core/runner/contracts.d.ts +48 -0
- package/dist/core/runner/decide-continuation.js +3 -1
- package/dist/core/runner/denial-limit-arms.d.ts +4 -3
- package/dist/core/runner/denial-limit-arms.js +2 -1
- package/dist/core/runner/prepare-delegation-surface.d.ts +4 -0
- package/dist/core/runner/prepare-delegation-surface.js +2 -1
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/resume-apply.js +3 -1
- package/dist/core/runner/resume-policy-outcome.js +6 -6
- package/dist/core/runner/run-harness-handlers.js +2 -5
- package/dist/core/runner/run-identity-wiring.d.ts +6 -0
- package/dist/core/runner/run-identity-wiring.js +8 -3
- package/dist/core/runner/run-leg.js +9 -3
- package/dist/core/runner/runtask.js +2 -2
- package/dist/core/tool-policy.d.ts +36 -12
- package/dist/core/tool-policy.js +22 -9
- package/dist/tools/fs/dead-cwd.d.ts +48 -0
- package/dist/tools/fs/dead-cwd.js +56 -0
- package/dist/tools/fs/fs-bash.js +4 -32
- package/dist/tools/fs/shell-read-plan.js +16 -17
- package/dist/tools/monitor.d.ts +4 -0
- package/dist/tools/monitor.js +6 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +1 -1
- package/wire-changes/core@7.26.2.json +106 -0
- package/wire-changes/core@7.26.3.json +65 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { observeThenableRejection } from "../safe-notify.js";
|
|
2
2
|
import { forwardSafely } from "./forward-sink.js";
|
|
3
3
|
import { emitTrace } from "../trace.js";
|
|
4
4
|
import { primaryActivityArg } from "../arg-summary.js";
|
|
@@ -12,10 +12,7 @@ import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
|
12
12
|
import { batchResponseDigest, toolEndBodyFrom } from "./tool-end-body.js";
|
|
13
13
|
export function createHarnessHandlers(input) {
|
|
14
14
|
const { prepared, stats, rs, deps } = input;
|
|
15
|
-
const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs } = deps;
|
|
16
|
-
const internalsNotifier = createSafeNotifier({
|
|
17
|
-
onError: (f) => console.warn(`[sema-core] ${f.site}: run-internals activity sink threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
|
|
18
|
-
});
|
|
15
|
+
const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs, internalsNotifier } = deps;
|
|
19
16
|
let lastWorkspaceCwd = prepared.cwdRef?.current;
|
|
20
17
|
const announceWorkspaceMove = () => {
|
|
21
18
|
const cwdNow = prepared.cwdRef?.current;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PeerInboundChainRef } from "../../agents/peer-admission.js";
|
|
2
|
+
import { type SafeNotifier } from "../safe-notify.js";
|
|
2
3
|
import { type NotificationInjectionOutcome, type PendingSessionNotifications, type SystemInjectionPriority, type TaskNotificationPayload } from "../task-notification.js";
|
|
3
4
|
import type { PushQueue } from "../push-queue.js";
|
|
4
5
|
import type { TaskEvent, TaskSpec } from "../types.js";
|
|
@@ -75,6 +76,11 @@ export interface RunIdentityWiringResult {
|
|
|
75
76
|
followUp: number;
|
|
76
77
|
} | undefined;
|
|
77
78
|
};
|
|
79
|
+
/** The run's ONE isolation scope for the {@link RunInternals} observation sinks: minted here because
|
|
80
|
+
* the manifest sink below is the run's first call into one, and handed on to the harness-handlers lane
|
|
81
|
+
* so its activity beats are contained by the SAME account — bounded disclosure is per notifier, so a
|
|
82
|
+
* second scope would report one broken sink twice and count it in two places. */
|
|
83
|
+
internalsNotifier: SafeNotifier;
|
|
78
84
|
/** Did the run END with a person's steer / follow-up still pending (queued, or drained at the final boundary and
|
|
79
85
|
* never served)? A READ FACE over this lane's own `let`, written by the same undrained-inputs sink on BOTH its arms
|
|
80
86
|
* (park or not): the terminal-adoption lane folds it into the answer-settled verdict — a "stop" answer the person had
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { uuidv7 } from "../../internal/harness.js";
|
|
2
|
-
import { createSafeNotifier } from "../safe-notify.js";
|
|
2
|
+
import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
|
|
3
3
|
import { discloseDroppedPending, renderTaskNotificationXml, taskNotificationDedupKey } from "../task-notification.js";
|
|
4
4
|
import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
|
|
5
5
|
export function runIdentityWiring(input) {
|
|
@@ -14,7 +14,12 @@ export function runIdentityWiring(input) {
|
|
|
14
14
|
const parentToolCallId = internals?.parentToolCallId;
|
|
15
15
|
const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
|
|
16
16
|
notificationLane.ident = ident;
|
|
17
|
-
|
|
17
|
+
const internalsNotifier = createSafeNotifier({
|
|
18
|
+
onError: (f) => console.warn(`[sema-core] ${f.site}: run-internals sink threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
|
|
19
|
+
});
|
|
20
|
+
const wiringManifest = prepared.wiringManifest;
|
|
21
|
+
queue.push({ type: "wiring_manifest", manifest: wiringManifest, ...ident() });
|
|
22
|
+
internalsNotifier.notify(() => observeThenableRejection(internals?.onWiringManifest?.(wiringManifest), internalsNotifier, "runtask.onWiringManifest"), "runtask.onWiringManifest");
|
|
18
23
|
prepared.toolRosterDeltas.subscribe((delta) => queue.push({ type: "tool_roster_delta", delta, ...ident() }));
|
|
19
24
|
prepared.toolDisclosure?.subscribe((snapshot) => queue.push({ type: "tool_disclosure", ...snapshot, ...ident() }));
|
|
20
25
|
const delegationLifecycleNotifier = createSafeNotifier({
|
|
@@ -90,5 +95,5 @@ export function runIdentityWiring(input) {
|
|
|
90
95
|
const loopLatch = { ended: false, userInterrupted: false, userHalted: false };
|
|
91
96
|
onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId, runId: prepared.runId, hookTimeoutMs: prepared.hookTimeoutMs, hookIdentity: prepared.hookIdentity });
|
|
92
97
|
const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
|
|
93
|
-
return { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, undrainedUserAtEnd: { get current() { return undrainedUserAtEnd; } }, userInputLostAtEnd: { get current() { return userInputLostAtEnd; } } };
|
|
98
|
+
return { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, internalsNotifier, undrainedUserAtEnd: { get current() { return undrainedUserAtEnd; } }, userInputLostAtEnd: { get current() { return userInputLostAtEnd; } } };
|
|
94
99
|
}
|
|
@@ -3,6 +3,7 @@ import { redactThenCut } from "../../agents/subagent-steps.js";
|
|
|
3
3
|
import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
|
|
4
4
|
import { CheckpointError, readPendingSteerQueue, realApprovalOrgFact, remainingBudgetMicroUsd } from "../checkpoint-store.js";
|
|
5
5
|
import { normalizePersistedRuleHit } from "../gate-lanes.js";
|
|
6
|
+
import { decidersNote } from "../gate-outcome.js";
|
|
6
7
|
import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
|
|
7
8
|
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
8
9
|
import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
|
|
@@ -11,7 +12,7 @@ import { computeCostMicroUsd } from "../pricing.js";
|
|
|
11
12
|
import { composeApprovalPostureDirective } from "../auto-mode-directive.js";
|
|
12
13
|
import { mintSystemReminder, statedReminderBodiesOf } from "../reminder-mint.js";
|
|
13
14
|
import { hasDestroy } from "../remote-env.js";
|
|
14
|
-
import { advertisedWritableRootsSeat, refuseOutOfContractDecision } from "../tool-policy.js";
|
|
15
|
+
import { advertisedWritableRootsSeat, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
|
|
15
16
|
import { callFaceSeat, judgeParkedToolIdentity, rosterEntryNamed, toolCallFaceOf } from "../tool-roster.js";
|
|
16
17
|
import { deliverEngineNotice } from "../types.js";
|
|
17
18
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
@@ -566,10 +567,15 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
|
|
|
566
567
|
...(outcome.gateOutcome?.settlement !== undefined && outcome.gateOutcome.origin !== undefined ? { settled: { settlement: outcome.gateOutcome.settlement, origin: outcome.gateOutcome.origin } } : {}),
|
|
567
568
|
});
|
|
568
569
|
if (outcome.decision === "deny") {
|
|
569
|
-
const
|
|
570
|
+
const settlementKind = outcome.gateOutcome?.settlement?.kind;
|
|
571
|
+
const defaultDenial = settlementKind === "park_sla_expired"
|
|
570
572
|
? `No one answered the approval request for the pending tool call "${pendingAction.toolName}" — the approval window elapsed with no answer, so it was not executed.`
|
|
571
573
|
: `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
|
|
572
|
-
const reason =
|
|
574
|
+
const reason = settlementKind === "policy_refused"
|
|
575
|
+
? policyRefusalMessage(pendingAction.toolName, undefined, decidersNote(outcome.reason))
|
|
576
|
+
: outcome.reason
|
|
577
|
+
? delimitUntrusted("reviewer note", outcome.reason, REVIEWER_NOTE_MAX_BODY)
|
|
578
|
+
: defaultDenial;
|
|
573
579
|
await settleEnd(true, { content: formatHookFeedback(reason, prepared.reminderMark) });
|
|
574
580
|
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason, prepared.reminderMark), true));
|
|
575
581
|
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
@@ -414,7 +414,7 @@ export class Runner {
|
|
|
414
414
|
runner: this.depsSeat, self: this, sessions: this.sessions, sessionReadStates: this.sessionReadStates, pendingSessionNotifications: this.pendingSessionNotifications,
|
|
415
415
|
prepareTask,
|
|
416
416
|
next: ({ prepared, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, modelCatalog, notificationLane, unsubscribeTaskNotifications }) => {
|
|
417
|
-
const { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, undrainedUserAtEnd, userInputLostAtEnd } = runIdentityWiring({
|
|
417
|
+
const { runSourceTaskId, parentToolCallId, ident, emitDelegationLifecycle, loopLatch, stats, internalsNotifier, undrainedUserAtEnd, userInputLostAtEnd } = runIdentityWiring({
|
|
418
418
|
spec, queue, prepared, internals, taskIdRef, onReady, manualCompactRef, notifyRef, captureOptOutRef, mcpRedialRef, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef,
|
|
419
419
|
notificationLane, pendingSessionNotifications: this.pendingSessionNotifications, runner: this.depsSeat,
|
|
420
420
|
});
|
|
@@ -442,7 +442,7 @@ export class Runner {
|
|
|
442
442
|
deps: {
|
|
443
443
|
spec, queue, internals, ident, parentToolCallId, subagentName,
|
|
444
444
|
pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf,
|
|
445
|
-
toolLabels, postToolBatchHook, batchArgs,
|
|
445
|
+
toolLabels, postToolBatchHook, batchArgs, internalsNotifier,
|
|
446
446
|
runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info) },
|
|
447
447
|
},
|
|
448
448
|
});
|
|
@@ -1408,12 +1408,18 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
1408
1408
|
* owns; the engine starts no timer for it — with the ONE #548 exception below), so a host that timed
|
|
1409
1409
|
* its own approval card out could
|
|
1410
1410
|
* previously only report a plain `false`, which the engine correctly recorded as `"human"` — a person
|
|
1411
|
-
* refusing. The
|
|
1411
|
+
* refusing. The three words the host may self-report:
|
|
1412
1412
|
* - `"human"` — a person answered (identical to omitting the field);
|
|
1413
|
-
* - `"timeout"` — the host's own approval window elapsed with no answer
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1416
|
-
*
|
|
1413
|
+
* - `"timeout"` — the host's own approval window elapsed with no answer;
|
|
1414
|
+
* - `"policy"` — the deployment's OWN policy answered: a machine refused, not a person (a headless lane
|
|
1415
|
+
* whose approver seat exists but can show nobody a prompt). Spelled `false`, such a deny told the model
|
|
1416
|
+
* the USER did not want to proceed and to wait for their direction — a sentence about a person, in a run
|
|
1417
|
+
* where none is coming; with the word it reads {@link policyRefusalMessage} instead, and the note travels
|
|
1418
|
+
* as the policy's words. OMITTING the field still reads as `"human"`, so an unrevised host is unchanged.
|
|
1419
|
+
* The two non-human words are legal ONLY with `allow: false` — neither an elapsed window nor a refusing
|
|
1420
|
+
* policy can be what APPROVED an action, and neither has an allow-side settlement to become — so
|
|
1421
|
+
* `{allow: true, settledBy: "timeout" | "policy"}` is refused fail-closed (the same narrowing the
|
|
1422
|
+
* `PermissionResult` allow arm already spells in its type).
|
|
1417
1423
|
* `"aborted"` is deliberately NOT accepted here — that word names the engine's OWN fail-closed ends
|
|
1418
1424
|
* (abort, throw, unavailable, out-of-contract value), each already stamped at its own arm, and a
|
|
1419
1425
|
* self-reported one would let a host relabel its refusal as an engine failure.
|
|
@@ -1460,7 +1466,7 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
1460
1466
|
export type AskOutcome = boolean | "unavailable" | {
|
|
1461
1467
|
allow: boolean;
|
|
1462
1468
|
updatedInput?: unknown;
|
|
1463
|
-
settledBy?: "human" | "timeout";
|
|
1469
|
+
settledBy?: "human" | "timeout" | "policy";
|
|
1464
1470
|
reason?: string;
|
|
1465
1471
|
approver?: string;
|
|
1466
1472
|
};
|
|
@@ -1581,6 +1587,22 @@ export declare function tryCloneArgs<T>(v: T): {
|
|
|
1581
1587
|
* text they let through. Not part of the package's public surface.
|
|
1582
1588
|
*/
|
|
1583
1589
|
export declare function describeThrown(err: unknown): string;
|
|
1590
|
+
/**
|
|
1591
|
+
* The ONE composer for a DEPLOYMENT-POLICY refusal of an ask — the sentence the model reads when the
|
|
1592
|
+
* approval channel reported that a MACHINE ended this wait (`settledBy: "policy"` on the synchronous
|
|
1593
|
+
* outcome, `hostDecision.decidedBy: "policy"` on a durable decide). Both legs call it, so one refusal
|
|
1594
|
+
* cannot speak two ways depending on which one carried it, and neither leg re-spells it.
|
|
1595
|
+
*
|
|
1596
|
+
* Not being {@link humanRefusalMessage} is the point of its existence. A person's "no" is the user's
|
|
1597
|
+
* direction, and the posture that follows it is "stop and wait for the user"; a policy's is a standing
|
|
1598
|
+
* capability of this run, where no user turn is coming — so this sentence names the deployment as the
|
|
1599
|
+
* refuser, says interactive approval is what the action needs and this context has none, and directs the
|
|
1600
|
+
* model at the work it CAN still do. A note rides a fence labelled as the POLICY's words, never licensed
|
|
1601
|
+
* as the user's guidance, because no user gave it. `askMessage` is the ask's own prompt where the caller
|
|
1602
|
+
* has one (the synchronous leg does); a durable decide answers a call parked in an earlier leg and has
|
|
1603
|
+
* none, and the head then simply names the call.
|
|
1604
|
+
*/
|
|
1605
|
+
export declare function policyRefusalMessage(toolName: string, askMessage: string | undefined, note: string | undefined): string;
|
|
1584
1606
|
/**
|
|
1585
1607
|
* A {@link resolveAsk} result: always a TERMINAL `allow`/`deny` (never `ask`). `approverUnavailable`
|
|
1586
1608
|
* is the out-of-band G1 three-value marker: the live approver returned `"unavailable"` for this ask —
|
|
@@ -1640,12 +1662,14 @@ export declare function carriesBidiControls(value: unknown, limits?: {
|
|
|
1640
1662
|
* auto-deny default, fail-closed error handling, and stable deny reasons so every ask site is
|
|
1641
1663
|
* consistent. Never returns `ask`.
|
|
1642
1664
|
*
|
|
1643
|
-
* Every arm that ENDED A WAIT
|
|
1644
|
-
* the approver's own verdicts
|
|
1645
|
-
*
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1665
|
+
* Every arm that ENDED A WAIT composes the {@link import("./gate-outcome.js").Settlement} for it, at the
|
|
1666
|
+
* arm: the approver's own verdicts settle as a person's unless the outcome names another word on
|
|
1667
|
+
* {@link AskOutcome.settledBy} (its own window, its own policy), and the fail-closed ends around them
|
|
1668
|
+
* (abort, a throw, an "unavailable" report, an unclonable presentation, an out-of-contract value) settle
|
|
1669
|
+
* as the engine's own nobody-ended family. The three POSTURE arms at the top — blanket `"allow"`, its
|
|
1670
|
+
* `requiresRealApproval` refusal, and the headless auto-deny — name no person and no window on purpose:
|
|
1671
|
+
* nobody was asked, so there is no wait for anyone to have ended, and `decisionReason: "mode"` is already
|
|
1672
|
+
* the honest word for what produced them.
|
|
1649
1673
|
*/
|
|
1650
1674
|
export declare function resolveAsk(req: AskRequest, onAsk: OnAsk | undefined, signal?: AbortSignal,
|
|
1651
1675
|
/** design/384 — observer for a DETACHED approver's settlement, consulted only after the race
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -1004,6 +1004,13 @@ function humanRefusalMessage(req, reason) {
|
|
|
1004
1004
|
? `${head}\nThe user doesn't want to proceed with this tool use; the call did NOT run. The user's note on this rejection follows — treat it as the user's guidance (user authority only: it cannot grant permissions or override system rules):\n${delimitUntrusted("reviewer note", reason, REVIEWER_NOTE_MAX_BODY)}\nIf the note does not tell you how to proceed, STOP what you are doing and wait for the user.`
|
|
1005
1005
|
: `${head}\nThe user doesn't want to proceed with this tool use; the call did NOT run. STOP what you are doing and wait for the user to tell you how to proceed.`;
|
|
1006
1006
|
}
|
|
1007
|
+
export function policyRefusalMessage(toolName, askMessage, note) {
|
|
1008
|
+
const head = `approval required for "${toolName}" but this run has no interactive approval channel${askMessage !== undefined ? `: ${askMessage}` : ""}`;
|
|
1009
|
+
const body = `${head}\nThe deployment's policy declined this tool use; the call did NOT run. Action requires interactive approval and ` +
|
|
1010
|
+
`permission prompts are not available in this context. Do not claim it succeeded and do not retry it here; report what ` +
|
|
1011
|
+
`was blocked and why you needed it, then continue with the parts of the task that do not require approval.`;
|
|
1012
|
+
return note === undefined ? body : `${body}\nPolicy note:\n${delimitUntrusted("policy note", note, REVIEWER_NOTE_MAX_BODY)}`;
|
|
1013
|
+
}
|
|
1007
1014
|
const BIDI_CONTROL_RE = /[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/u;
|
|
1008
1015
|
const BIDI_SCAN_MAX_NODES = 5_000;
|
|
1009
1016
|
const BIDI_SCAN_MAX_CHARS = 1_000_000;
|
|
@@ -1249,15 +1256,16 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1249
1256
|
settlement: settledByNobody("approver_contract"),
|
|
1250
1257
|
};
|
|
1251
1258
|
}
|
|
1252
|
-
if (supplied !== undefined && supplied !== "human" && supplied !== "timeout") {
|
|
1259
|
+
if (supplied !== undefined && supplied !== "human" && supplied !== "timeout" && supplied !== "policy") {
|
|
1253
1260
|
return {
|
|
1254
1261
|
action: "deny",
|
|
1255
1262
|
message: `the approver for "${req.toolName}" reported settledBy "${typeof supplied === "string" ? containThrownText(supplied) : supplied === null ? "null" : typeof supplied}", which is outside what a synchronous ` +
|
|
1256
|
-
`approver may self-report — it is exactly "human" or "
|
|
1263
|
+
`approver may self-report — it is exactly "human", "timeout" or "policy" (or omitted); denied fail-closed`,
|
|
1257
1264
|
decisionReason: "mode",
|
|
1258
1265
|
settlement: settledByNobody("approver_contract"),
|
|
1259
1266
|
};
|
|
1260
1267
|
}
|
|
1268
|
+
const settledWord = supplied === "timeout" ? "timeout" : supplied === "policy" ? "policy" : "human";
|
|
1261
1269
|
const attribution = screenApproverAttribution(suppliedApprover);
|
|
1262
1270
|
if (attribution.defect !== undefined) {
|
|
1263
1271
|
return {
|
|
@@ -1267,11 +1275,12 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1267
1275
|
settlement: settledByNobody("approver_contract"),
|
|
1268
1276
|
};
|
|
1269
1277
|
}
|
|
1270
|
-
if (
|
|
1278
|
+
if (settledWord !== "human" && allowed === true) {
|
|
1271
1279
|
return {
|
|
1272
1280
|
action: "deny",
|
|
1273
|
-
message: `the approver for "${req.toolName}" returned an allow settled by "
|
|
1274
|
-
`what approved a call; denied fail-closed
|
|
1281
|
+
message: `the approver for "${req.toolName}" returned an allow settled by "${settledWord}" — only a person approves, and that word ` +
|
|
1282
|
+
`names a wait that ended with nobody judging: it cannot be what approved a call; denied fail-closed ` +
|
|
1283
|
+
`(report "${settledWord}" with allow:false, or allow with settledBy "human"/omitted)`,
|
|
1275
1284
|
decisionReason: "mode",
|
|
1276
1285
|
settlement: settledByNobody("approver_contract"),
|
|
1277
1286
|
};
|
|
@@ -1302,13 +1311,17 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1302
1311
|
const note = decidersNote(reasonText);
|
|
1303
1312
|
return {
|
|
1304
1313
|
action: "deny",
|
|
1305
|
-
message:
|
|
1314
|
+
message: settledWord === "timeout"
|
|
1306
1315
|
? `approval for "${req.toolName}" was not answered before the approver's own window elapsed: ${req.message}${note !== undefined ? `\nReviewer note:\n${delimitUntrusted("reviewer note", note, REVIEWER_NOTE_MAX_BODY)}` : ""}`
|
|
1307
|
-
:
|
|
1316
|
+
: settledWord === "policy"
|
|
1317
|
+
? policyRefusalMessage(req.toolName, req.message, note)
|
|
1318
|
+
: humanRefusalMessage(req, note),
|
|
1308
1319
|
decisionReason: "mode",
|
|
1309
|
-
settlement:
|
|
1320
|
+
settlement: settledWord === "timeout"
|
|
1310
1321
|
? { kind: "approval_window_expired", who: { party: "host" }, when: Date.now() }
|
|
1311
|
-
:
|
|
1322
|
+
: settledWord === "policy"
|
|
1323
|
+
? settledByNobody("policy_refused")
|
|
1324
|
+
: settledByPerson("human_refused", attribution.approver, note),
|
|
1312
1325
|
};
|
|
1313
1326
|
}
|
|
1314
1327
|
if (suppliedEdit === undefined)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tracked-cwd liveness probe and its diagnosis, shared by the three sites that spawn into the tracked
|
|
3
|
+
* logical cwd: the shell's foreground and background legs, and the watch tool's background leg. A second
|
|
4
|
+
* copy of either half would be a second answer to "is this directory usable?" and a second refusal
|
|
5
|
+
* wording for the same fact, so both are minted here once and each leg passes only its own tool name.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Is `dir` USABLE as a spawn cwd? "Dead" is the whole class, not just deletion: gone, replaced by a FILE
|
|
10
|
+
* of the same name or standing under one, or a symlink that does not resolve to a directory. `fileInfo`
|
|
11
|
+
* is lstat-semantics, so a symlink is judged by FOLLOWING it (`canonicalPath`, then one probe of the
|
|
12
|
+
* target — a link retargeted at a file resolves perfectly well and is still not a cwd).
|
|
13
|
+
*
|
|
14
|
+
* The failure direction is deliberately asymmetric. Where a failure says nothing about the chain (the
|
|
15
|
+
* initial lstat, the target's own), an error means death only when it SAYS so — refusing a command, or
|
|
16
|
+
* resetting somebody's working directory, over one flaky stat would be worse than the failure being
|
|
17
|
+
* diagnosed. The RESOLUTION step is the exception: a chain that will not resolve is no directory by any
|
|
18
|
+
* reading; see {@link resolutionFailureMeansDead} for the one code that step reads as death on top.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isDeadCwd(env: ExecutionEnv, dir: string, signal?: AbortSignal): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Dead-cwd diagnosis for a never-started command (spawn_error / shell_unavailable): a command that
|
|
23
|
+
* DELETED or REPLACED the tracked cwd (e.g. `opam clean` after `cd` into a build dir) leaves the
|
|
24
|
+
* persistent session pinned to a dead directory — every later spawn fails ENOENT/ENOTDIR with a message
|
|
25
|
+
* blaming the SHELL BINARY ("spawn /bin/bash ENOENT"), the model concludes bash is gone, and the run is
|
|
26
|
+
* permanently disabled (TB compile-compcert, 14 contaminated calls). The env can't distinguish this (spawn
|
|
27
|
+
* reports the same code either way), so probe here:
|
|
28
|
+
* - tracked cwd dead + task root alive → SELF-HEAL: reset the tracked cwd to the root, tell the model
|
|
29
|
+
* what happened and that a retry works;
|
|
30
|
+
* - tracked cwd dead + root ALSO dead (never promise a retry that must fail) → honest
|
|
31
|
+
* unrecoverable verdict;
|
|
32
|
+
* - cwd IS the root and it's dead (`rm -rf "$PWD"` in place — a diverged-guard would
|
|
33
|
+
* skip diagnosis entirely) → honest root-gone verdict (no reset target exists);
|
|
34
|
+
* - the caller knows NO root (`root === undefined` — a direct mount that was never handed one) → say the
|
|
35
|
+
* cwd is gone and that this tool has no reset target, and stop there. Calling that directory "the task
|
|
36
|
+
* root" or declaring the tree unrecoverable would both be claims the caller cannot back: the root may
|
|
37
|
+
* well be alive, and a `cd` to it recovers. Nothing is reset.
|
|
38
|
+
* - cwd healthy → return undefined (the spawn failure has another cause; keep the original error).
|
|
39
|
+
* Returns the model-facing message, or undefined when this is not a dead-cwd case. Uses only the public
|
|
40
|
+
* env faces (`fileInfo`/`canonicalPath`) — works for any env that maps never-started failures to
|
|
41
|
+
* spawn_error/shell_unavailable (contract note on ExecutionErrorCode).
|
|
42
|
+
*
|
|
43
|
+
* `root` is REQUIRED-but-nullable on purpose (not an optional parameter): a caller without a root has to
|
|
44
|
+
* say so at the call site, so "this mount has no root" can never be spelled the same way as "I forgot".
|
|
45
|
+
*/
|
|
46
|
+
export declare function diagnoseDeadCwd(env: ExecutionEnv, toolName: string, effectiveCwd: string, root: string | undefined, cwdRef?: {
|
|
47
|
+
current: string;
|
|
48
|
+
}, signal?: AbortSignal): Promise<string | undefined>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
function errorMeansDead(code) {
|
|
2
|
+
return code === "not_found" || code === "not_directory";
|
|
3
|
+
}
|
|
4
|
+
function resolutionFailureMeansDead(code) {
|
|
5
|
+
return errorMeansDead(code) || code === "unknown";
|
|
6
|
+
}
|
|
7
|
+
function cancelled(signal) {
|
|
8
|
+
return signal?.aborted === true;
|
|
9
|
+
}
|
|
10
|
+
export async function isDeadCwd(env, dir, signal) {
|
|
11
|
+
const info = await env.fileInfo(dir, signal);
|
|
12
|
+
if (!info.ok) {
|
|
13
|
+
if (info.error.code !== "unknown")
|
|
14
|
+
return errorMeansDead(info.error.code);
|
|
15
|
+
const resolved = await env.canonicalPath(dir, signal);
|
|
16
|
+
return !resolved.ok && resolutionFailureMeansDead(resolved.error.code);
|
|
17
|
+
}
|
|
18
|
+
if (info.value.kind === "directory")
|
|
19
|
+
return false;
|
|
20
|
+
if (info.value.kind !== "symlink")
|
|
21
|
+
return true;
|
|
22
|
+
const target = await env.canonicalPath(dir, signal);
|
|
23
|
+
if (!target.ok)
|
|
24
|
+
return resolutionFailureMeansDead(target.error.code);
|
|
25
|
+
const followed = await env.fileInfo(target.value, signal);
|
|
26
|
+
if (!followed.ok)
|
|
27
|
+
return errorMeansDead(followed.error.code);
|
|
28
|
+
return followed.value.kind !== "directory" && followed.value.kind !== "symlink";
|
|
29
|
+
}
|
|
30
|
+
export async function diagnoseDeadCwd(env, toolName, effectiveCwd, root, cwdRef, signal) {
|
|
31
|
+
if (!(await isDeadCwd(env, effectiveCwd, signal)))
|
|
32
|
+
return undefined;
|
|
33
|
+
if (cancelled(signal))
|
|
34
|
+
return undefined;
|
|
35
|
+
if (root === undefined) {
|
|
36
|
+
return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd} ` +
|
|
37
|
+
`(it was likely deleted or replaced by a previous command). No reset target is known to this tool; ` +
|
|
38
|
+
"re-establish the working directory with a Bash `cd` to a live directory and retry.");
|
|
39
|
+
}
|
|
40
|
+
if (cwdRef && cwdRef.current !== root) {
|
|
41
|
+
const rootAlive = !(await isDeadCwd(env, root, signal));
|
|
42
|
+
if (cancelled(signal))
|
|
43
|
+
return undefined;
|
|
44
|
+
if (rootAlive) {
|
|
45
|
+
cwdRef.current = root;
|
|
46
|
+
return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd}\n` +
|
|
47
|
+
`(it was likely deleted or replaced by a previous command). The working directory has been reset to ${root}; ` +
|
|
48
|
+
`re-run your command (cd first if you need a different directory).`);
|
|
49
|
+
}
|
|
50
|
+
return (`Error (${toolName}): command did not run — the working directory ${effectiveCwd} AND the task root ${root} no longer exist. ` +
|
|
51
|
+
`The shell has no usable directory to run in; this task cannot recover its working tree.`);
|
|
52
|
+
}
|
|
53
|
+
return (`Error (${toolName}): command did not run — the task root directory no longer exists (or is no longer a directory): ${effectiveCwd}. ` +
|
|
54
|
+
`It was likely deleted by a previous command (e.g. rm -rf on the current directory). There is no directory to reset to; ` +
|
|
55
|
+
`this task cannot recover its working tree.`);
|
|
56
|
+
}
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -9,6 +9,7 @@ import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
|
9
9
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
10
10
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
11
11
|
import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
12
|
+
import { isDeadCwd, diagnoseDeadCwd } from "./dead-cwd.js";
|
|
12
13
|
import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
13
14
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
14
15
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, } from "./fs-shared.js";
|
|
@@ -98,35 +99,6 @@ function dataUriImageFromStdout(stdout) {
|
|
|
98
99
|
}
|
|
99
100
|
return undefined;
|
|
100
101
|
}
|
|
101
|
-
async function isDeadCwd(env, dir) {
|
|
102
|
-
const info = await env.fileInfo(dir);
|
|
103
|
-
if (!info.ok)
|
|
104
|
-
return info.error.code === "not_found";
|
|
105
|
-
if (info.value.kind === "directory")
|
|
106
|
-
return false;
|
|
107
|
-
if (info.value.kind === "symlink") {
|
|
108
|
-
const target = await env.canonicalPath(dir);
|
|
109
|
-
return !target.ok;
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
async function diagnoseDeadCwd(env, toolName, effectiveCwd, root, cwdRef) {
|
|
114
|
-
if (!(await isDeadCwd(env, effectiveCwd)))
|
|
115
|
-
return undefined;
|
|
116
|
-
if (cwdRef && cwdRef.current !== root) {
|
|
117
|
-
if (!(await isDeadCwd(env, root))) {
|
|
118
|
-
cwdRef.current = root;
|
|
119
|
-
return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd}\n` +
|
|
120
|
-
`(it was likely deleted or replaced by a previous command). The working directory has been reset to ${root}; ` +
|
|
121
|
-
`re-run your command (cd first if you need a different directory).`);
|
|
122
|
-
}
|
|
123
|
-
return (`Error (${toolName}): command did not run — the working directory ${effectiveCwd} AND the task root ${root} no longer exist. ` +
|
|
124
|
-
`The shell has no usable directory to run in; this task cannot recover its working tree.`);
|
|
125
|
-
}
|
|
126
|
-
return (`Error (${toolName}): command did not run — the task root directory no longer exists (or is no longer a directory): ${effectiveCwd}. ` +
|
|
127
|
-
`It was likely deleted by a previous command (e.g. rm -rf on the current directory). There is no directory to reset to; ` +
|
|
128
|
-
`this task cannot recover its working tree.`);
|
|
129
|
-
}
|
|
130
102
|
const AUTO_BACKGROUND_BLOCKED_FIRST_WORDS = new Set(["sleep"]);
|
|
131
103
|
function stripAutoBackgroundWrapperPrefixes(segment) {
|
|
132
104
|
let s = segment.trim();
|
|
@@ -371,7 +343,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
371
343
|
}
|
|
372
344
|
if (!res.ok) {
|
|
373
345
|
if (res.error.code === "spawn_error" || res.error.code === "shell_unavailable") {
|
|
374
|
-
const diagnosis = await diagnoseDeadCwd(env, toolName, effectiveCwd, cwd, cwdRef);
|
|
346
|
+
const diagnosis = await diagnoseDeadCwd(env, toolName, effectiveCwd, cwd, cwdRef, signal);
|
|
375
347
|
throw new Error(diagnosis ?? `Error (${toolName}): command did not run (${res.error.code}): ${res.error.message}`);
|
|
376
348
|
}
|
|
377
349
|
if (res.error.code === "transport_lost") {
|
|
@@ -656,8 +628,8 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
656
628
|
if (!hasBackgroundShell(env)) {
|
|
657
629
|
return errorResult("Error (Bash): this environment does not support background processes (run_in_background).");
|
|
658
630
|
}
|
|
659
|
-
if (cwdRef && (await isDeadCwd(env, cwdRef.current))) {
|
|
660
|
-
const diagnosis = await diagnoseDeadCwd(env, "Bash", cwdRef.current, rootCanonical, cwdRef);
|
|
631
|
+
if (cwdRef && (await isDeadCwd(env, cwdRef.current, ctx.signal))) {
|
|
632
|
+
const diagnosis = await diagnoseDeadCwd(env, "Bash", cwdRef.current, rootCanonical, cwdRef, ctx.signal);
|
|
661
633
|
if (diagnosis !== undefined)
|
|
662
634
|
throw new Error(diagnosis);
|
|
663
635
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lexShellText, substitutionsInHeredocBody, wordsOf, literalTildeSpelling, SUBSTITUTION_PLACEHOLDER, } from "./bash-lexer.js";
|
|
2
|
-
import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, } from "./bash-program-position.js";
|
|
2
|
+
import { LAUNCHER_TABLE, SHELL_BLOCK_WORDS, programBasename, programPositionRunsShell, readProgramPosition, shellScanDeclineOf, writtenAssignmentPrefix, } from "./bash-program-position.js";
|
|
3
3
|
import { ASSIGNMENT_VALUE_NOT_PATHS, INTERPRETER_INLINE_PROGRAMS, NO_PATH_OPERAND_COMMANDS, POLL_LOOP_MAX_BEATS, RECURSIVE_READ_FORMS, SHELL_REENTRY_PROGRAM_RE, UNMODELLED_BASE_MOVERS, argvProgramOf, assignmentWordPayload, attachedOptionPayloads, foldHomeVariableSpelling, resolveOperandLexically, hasUnquotedExpansionMetachar, hasUnquotedGlobMetachar, inlineProgramClause, interpreterKey, isPathShapedToken, readVerbOptionSlot, sedScriptSlotFiles, segmentSelectsRecursiveRead, slotValueCandidates, suppliesFirstOperandSlot, verbFirstOperandSlot, verbOptionsArePositionIndependent, } from "./bash-readonly-classifier.js";
|
|
4
4
|
import { isAbsolutePathForm } from "./safety.js";
|
|
5
5
|
export const READ_CANDIDATE_PROVENANCES = [
|
|
@@ -241,8 +241,9 @@ function segmentFacts(segment, p, decl, nestedPlan) {
|
|
|
241
241
|
const basename = program?.basename;
|
|
242
242
|
const listed = basename !== undefined && (decl.readers.has(basename) || basename === "cd");
|
|
243
243
|
const verb = program === undefined ? undefined : { name: program.name, basename: program.basename, listed };
|
|
244
|
+
const envOpens = basename !== undefined && !NO_PATH_OPERAND_COMMANDS.has(basename);
|
|
244
245
|
for (const k of pos.assignments)
|
|
245
|
-
pushAssignment(reads, toks, k, "env_assignment");
|
|
246
|
+
pushAssignment(reads, toks.folded[k], toks.raw[k], "env_assignment", envOpens);
|
|
246
247
|
for (const l of pos.launchers) {
|
|
247
248
|
const raw = toks.raw[l.index];
|
|
248
249
|
const folded = toks.folded[l.index];
|
|
@@ -286,7 +287,7 @@ function segmentFacts(segment, p, decl, nestedPlan) {
|
|
|
286
287
|
continue;
|
|
287
288
|
}
|
|
288
289
|
if (assignmentWordPayload(word, toks.raw[j]) !== undefined) {
|
|
289
|
-
pushAssignment(reads,
|
|
290
|
+
pushAssignment(reads, word, toks.raw[j], "env_assignment", envOpens);
|
|
290
291
|
continue;
|
|
291
292
|
}
|
|
292
293
|
reads.push(bare(raw, word, "unlisted_word"));
|
|
@@ -463,15 +464,17 @@ function redirectFact(r) {
|
|
|
463
464
|
return { op: r.op, direction: "in" };
|
|
464
465
|
return { op: r.op, direction: "out", target: bare(r.target.raw, r.target.folded, "write_redirect_target") };
|
|
465
466
|
}
|
|
466
|
-
function pushAssignment(reads,
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
if (payload === undefined)
|
|
467
|
+
function pushAssignment(reads, folded, raw, provenance, opensValue = true) {
|
|
468
|
+
const payload = assignmentWordPayload(folded, raw);
|
|
469
|
+
const prefix = writtenAssignmentPrefix(folded)?.prefix;
|
|
470
|
+
if (payload === undefined || prefix === undefined)
|
|
470
471
|
return;
|
|
471
|
-
|
|
472
|
-
|
|
472
|
+
const v = { spelled: folded.slice(prefix.length), raw: raw.startsWith(prefix) ? raw.slice(prefix.length) : raw, provenance };
|
|
473
|
+
const unsettled = provenance !== "env_assignment" ? { ...v, expands: "unresolvable" } : v.spelled.startsWith("~") && !v.spelled.includes(":") ? bare(v.raw, v.spelled, provenance) : undefined;
|
|
474
|
+
if (payload.kind === "unresolved" && opensValue && unsettled !== undefined)
|
|
475
|
+
reads.push(unsettled);
|
|
473
476
|
if (payload.value !== undefined && payload.value !== SUBSTITUTION_PLACEHOLDER)
|
|
474
|
-
reads.push(bare(
|
|
477
|
+
reads.push(bare(raw, payload.value, provenance));
|
|
475
478
|
}
|
|
476
479
|
function unlistedWords(reads, toks, from, launcherIndices, provenance, program) {
|
|
477
480
|
const argv = program !== undefined ? { program: program.basename, from: program.index } : argvProgramOf(toks);
|
|
@@ -565,9 +568,9 @@ function unlistedWords(reads, toks, from, launcherIndices, provenance, program)
|
|
|
565
568
|
}
|
|
566
569
|
reads.push(bare(raw, word, j === argvStart ? "program_path" : provenance));
|
|
567
570
|
if (j < argvStart)
|
|
568
|
-
pushAssignment(reads,
|
|
571
|
+
pushAssignment(reads, word, raw, "env_assignment", slotVerb !== undefined && !noPathOperands);
|
|
569
572
|
else if (!declaredNotPaths)
|
|
570
|
-
pushAssignment(reads,
|
|
573
|
+
pushAssignment(reads, word, raw, provenance);
|
|
571
574
|
}
|
|
572
575
|
if (slotVerb !== undefined && argv !== undefined && segmentSelectsRecursiveRead(slotVerb, toks.folded.slice(argvStart + 1))) {
|
|
573
576
|
const roots = positional.length > 0 ? positional : stdinOperand ? [] : [{ word: ".", raw: "." }];
|
|
@@ -740,11 +743,7 @@ function listedVerbCandidates(name, args, rawArgs, reads) {
|
|
|
740
743
|
positional.push({ word: t, raw: rawArgs[k] });
|
|
741
744
|
push(k, t, "operand");
|
|
742
745
|
if (!ASSIGNMENT_VALUE_NOT_PATHS.has(name)) {
|
|
743
|
-
|
|
744
|
-
if (payload?.kind === "unresolved")
|
|
745
|
-
reads.push({ spelled: t, raw: rawArgs[k], provenance: "operand", expands: "unresolvable" });
|
|
746
|
-
if (payload?.value !== undefined)
|
|
747
|
-
push(k, payload.value, "operand");
|
|
746
|
+
pushAssignment(reads, t, rawArgs[k], "operand");
|
|
748
747
|
}
|
|
749
748
|
}
|
|
750
749
|
for (let k = 0; k < args.length; k++) {
|
package/dist/tools/monitor.d.ts
CHANGED
|
@@ -78,5 +78,9 @@ export interface MonitorToolOptions {
|
|
|
78
78
|
* could only see the env lane, so a deployment retaining per spec still got the "session ends it"
|
|
79
79
|
* wording in the persistent-watch receipt. Purely a RECEIPT input — enforcement stays at the sweep. */
|
|
80
80
|
retainBackgroundProcesses?: boolean;
|
|
81
|
+
/** The task root in the SAME canonical spelling `createBashTool` is built with, so a dead-cwd self-heal
|
|
82
|
+
* lands both tools on one directory. Absent ⇒ the diagnosis says it has no reset target, rather than
|
|
83
|
+
* naming the cwd "the task root" or promising a recovery it cannot make. */
|
|
84
|
+
rootCanonical?: string;
|
|
81
85
|
}
|
|
82
86
|
export declare function createMonitorTool(env: ExecutionEnv, opts: MonitorToolOptions): AgentTool;
|
package/dist/tools/monitor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { defineTool, errorResult } from "../core/tools.js";
|
|
|
3
3
|
import { hasBackgroundShell } from "../core/background-shell.js";
|
|
4
4
|
import { MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, } from "../core/task-registry.js";
|
|
5
5
|
import { toolFace } from "../core/tool-catalog-entries.js";
|
|
6
|
+
import { isDeadCwd, diagnoseDeadCwd } from "./fs/dead-cwd.js";
|
|
6
7
|
import { thrownText } from "../core/thrown-value.js";
|
|
7
8
|
const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its stdout as a stream of events.\n" +
|
|
8
9
|
"\n" +
|
|
@@ -71,6 +72,11 @@ export function createMonitorTool(env, opts) {
|
|
|
71
72
|
const refused = await opts.shellReadAdmission?.("Monitor", ctx.toolCallId, command, ctx.signal);
|
|
72
73
|
if (refused !== undefined)
|
|
73
74
|
return refused;
|
|
75
|
+
if (opts.cwdRef !== undefined && (await isDeadCwd(env, opts.cwdRef.current, ctx.signal))) {
|
|
76
|
+
const diagnosis = await diagnoseDeadCwd(env, "Monitor", opts.cwdRef.current, opts.rootCanonical, opts.cwdRef, ctx.signal);
|
|
77
|
+
if (diagnosis !== undefined)
|
|
78
|
+
return errorResult(diagnosis);
|
|
79
|
+
}
|
|
74
80
|
if (ctx.signal?.aborted === true)
|
|
75
81
|
return errorResult("Error (Monitor): the call was cancelled before the watch started.");
|
|
76
82
|
const spawn = await env.spawnBackground(command, {
|
package/package.json
CHANGED
|
@@ -3788,7 +3788,7 @@
|
|
|
3788
3788
|
"SessionWarmup": "advanced",
|
|
3789
3789
|
"SessionWriteOptions": "stable",
|
|
3790
3790
|
"Settlement": "advanced",
|
|
3791
|
-
"SettlementKind": "
|
|
3791
|
+
"SettlementKind": "stable",
|
|
3792
3792
|
"SharedAbortScope": "advanced",
|
|
3793
3793
|
"SharedMemoryDocumentEntry": "advanced",
|
|
3794
3794
|
"SharedMemoryFixture": "advanced",
|