@stigmer/runner 3.11.0 → 3.11.1-dev.20260812192248
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/README.md +2 -0
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +6 -4
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/emit-event.d.ts +14 -2
- package/dist/activities/emit-event.js +52 -17
- package/dist/activities/emit-event.js.map +1 -1
- package/dist/activities/execute-cursor/error-classifier.d.ts +19 -0
- package/dist/activities/execute-cursor/error-classifier.js +34 -3
- package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +46 -10
- package/dist/activities/execute-cursor/index.js +126 -36
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
- package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
- package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +33 -0
- package/dist/activities/execute-cursor/prompt-builder.js +37 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
- package/dist/activities/execute-cursor/turn-recovery.js +193 -0
- package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
- package/dist/activities/execute-deep-agent/setup.js +16 -7
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
- package/dist/activities/execute-deep-agent/shell-env.js +7 -9
- package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +7 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +17 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +14 -2
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +8 -5
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/activities/run-command.d.ts +5 -2
- package/dist/activities/run-command.js +22 -10
- package/dist/activities/run-command.js.map +1 -1
- package/dist/activities/run-env.d.ts +40 -0
- package/dist/activities/run-env.js +66 -0
- package/dist/activities/run-env.js.map +1 -0
- package/dist/bootstrap.js +5 -0
- package/dist/bootstrap.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +83 -26
- package/dist/client/stigmer-client.js +88 -24
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/encryption/config.d.ts +41 -9
- package/dist/encryption/config.js +63 -23
- package/dist/encryption/config.js.map +1 -1
- package/dist/middleware/index.d.ts +3 -0
- package/dist/middleware/index.js +7 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/path-normalization.d.ts +57 -0
- package/dist/middleware/path-normalization.js +109 -0
- package/dist/middleware/path-normalization.js.map +1 -0
- package/dist/middleware/types.d.ts +8 -0
- package/dist/payload-codecs.d.ts +2 -1
- package/dist/payload-codecs.js +7 -3
- package/dist/payload-codecs.js.map +1 -1
- package/dist/runner-manager.js +17 -3
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +7 -3
- package/dist/runner.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +23 -1
- package/dist/shared/artifact-storage.js +51 -10
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/checkpointer/http-saver.d.ts +4 -20
- package/dist/shared/checkpointer/http-saver.js +15 -57
- package/dist/shared/checkpointer/http-saver.js.map +1 -1
- package/dist/shared/conversation-catchup.d.ts +9 -2
- package/dist/shared/conversation-catchup.js +39 -6
- package/dist/shared/conversation-catchup.js.map +1 -1
- package/dist/shared/grpc-retry.d.ts +5 -1
- package/dist/shared/grpc-retry.js +5 -1
- package/dist/shared/grpc-retry.js.map +1 -1
- package/dist/shared/http-retry.d.ts +51 -9
- package/dist/shared/http-retry.js +60 -9
- package/dist/shared/http-retry.js.map +1 -1
- package/dist/shared/mcp-manager.d.ts +8 -4
- package/dist/shared/mcp-manager.js +30 -4
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
- package/dist/shared/mcp-schema-sanitizer.js +197 -0
- package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
- package/dist/shared/model-client.d.ts +6 -0
- package/dist/shared/model-client.js +14 -3
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/model-pricing-data.d.ts +2 -0
- package/dist/shared/model-pricing-data.js +13 -3
- package/dist/shared/model-pricing-data.js.map +1 -1
- package/dist/shared/model-registry.js +3 -2
- package/dist/shared/model-registry.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +7 -0
- package/dist/shared/plan-mode-permissions.js +7 -0
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/registry-endpoint.d.ts +20 -0
- package/dist/shared/registry-endpoint.js +25 -0
- package/dist/shared/registry-endpoint.js.map +1 -1
- package/dist/shared/runner-credential-keys.d.ts +24 -0
- package/dist/shared/runner-credential-keys.js +47 -0
- package/dist/shared/runner-credential-keys.js.map +1 -0
- package/dist/workflow-engine/resolve.d.ts +19 -1
- package/dist/workflow-engine/resolve.js +37 -2
- package/dist/workflow-engine/resolve.js.map +1 -1
- package/dist/workflows/connect-mcp-server.js +2 -0
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +3 -4
- package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
- package/src/__test-utils__/vitest-global-setup.ts +50 -0
- package/src/__tests__/bootstrap.test.ts +43 -0
- package/src/__tests__/encryption-codec.test.ts +53 -0
- package/src/__tests__/preflight.test.ts +15 -0
- package/src/activities/__tests__/discover-mcp-server.test.ts +39 -1
- package/src/activities/__tests__/error-classifier.test.ts +94 -2
- package/src/activities/__tests__/run-command.test.ts +221 -0
- package/src/activities/discover-mcp-server.ts +16 -2
- package/src/activities/emit-event.ts +65 -21
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +111 -2
- package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
- package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
- package/src/activities/execute-cursor/error-classifier.ts +40 -3
- package/src/activities/execute-cursor/index.ts +154 -38
- package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
- package/src/activities/execute-cursor/prompt-builder.ts +51 -0
- package/src/activities/execute-cursor/turn-recovery.ts +208 -0
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
- package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +22 -17
- package/src/activities/execute-deep-agent/setup.ts +16 -8
- package/src/activities/execute-deep-agent/shell-env.ts +8 -9
- package/src/activities/execute-deep-agent/subagent-transformer.ts +7 -1
- package/src/activities/execute-deep-agent/subagent-wiring.ts +26 -3
- package/src/activities/hydrate-workflow-execution.ts +8 -5
- package/src/activities/run-command.ts +25 -12
- package/src/activities/run-env.ts +79 -0
- package/src/bootstrap.ts +5 -0
- package/src/client/__tests__/stigmer-client.test.ts +104 -5
- package/src/client/stigmer-client.ts +128 -29
- package/src/encryption/config.ts +87 -23
- package/src/middleware/__tests__/path-normalization.test.ts +140 -0
- package/src/middleware/index.ts +8 -0
- package/src/middleware/path-normalization.ts +125 -0
- package/src/middleware/types.ts +11 -0
- package/src/payload-codecs.ts +8 -2
- package/src/runner-manager.ts +21 -3
- package/src/runner.ts +7 -3
- package/src/shared/__tests__/artifact-storage.test.ts +145 -2
- package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
- package/src/shared/__tests__/http-retry.test.ts +5 -4
- package/src/shared/__tests__/mcp-manager.test.ts +53 -13
- package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
- package/src/shared/__tests__/model-client.test.ts +46 -0
- package/src/shared/__tests__/model-pricing.test.ts +62 -2
- package/src/shared/__tests__/model-registry.test.ts +22 -0
- package/src/shared/artifact-storage.ts +75 -10
- package/src/shared/checkpointer/http-saver.ts +15 -60
- package/src/shared/conversation-catchup.ts +39 -6
- package/src/shared/grpc-retry.ts +5 -1
- package/src/shared/http-retry.ts +98 -9
- package/src/shared/mcp-manager.ts +35 -4
- package/src/shared/mcp-schema-sanitizer.ts +224 -0
- package/src/shared/model-client.ts +21 -3
- package/src/shared/model-pricing-data.ts +22 -3
- package/src/shared/model-registry.ts +7 -2
- package/src/shared/plan-mode-permissions.ts +7 -0
- package/src/shared/registry-endpoint.ts +27 -0
- package/src/shared/runner-credential-keys.ts +46 -0
- package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
- package/src/workflow-engine/resolve.ts +48 -2
- package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
- package/src/workflows/connect-mcp-server.ts +2 -0
- package/src/workflows/types.ts +8 -0
- package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
- package/dist/activities/execute-cursor/mcp-config.js +0 -39
- package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
- package/src/activities/execute-cursor/mcp-config.ts +0 -66
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
visionDisclosureLines,
|
|
37
37
|
type NotViewableEntry,
|
|
38
38
|
} from "../../shared/attachment-vision.js";
|
|
39
|
+
import { formatTurnRecoveryText } from "./turn-recovery.js";
|
|
39
40
|
import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
|
|
40
41
|
import {
|
|
41
42
|
buildImplementPlanDirective,
|
|
@@ -338,6 +339,56 @@ export function buildReinvocationPrompt(
|
|
|
338
339
|
return parts.join("\n\n");
|
|
339
340
|
}
|
|
340
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Build the prompt for a fresh agent that replaced a lost one MID-HITL —
|
|
344
|
+
* the stored handle failed to resume when the approval landed, or the
|
|
345
|
+
* resumed handle proved poisoned mid-send (issue #366, both crossings).
|
|
346
|
+
*
|
|
347
|
+
* A resumed agent gets {@link buildReinvocationPrompt} alone because its
|
|
348
|
+
* native conversation carries everything else. A replacement agent's
|
|
349
|
+
* conversation is EMPTY, so the bare decisions prompt would strand it with
|
|
350
|
+
* instructions and no story — and, because recovery repoints the session at
|
|
351
|
+
* the new agent, every later turn would inherit that amnesia. This prompt
|
|
352
|
+
* rebuilds the whole story in chronological-narrative order:
|
|
353
|
+
*
|
|
354
|
+
* 1. the full enhanced prompt (blueprint, standing context, protocol, and
|
|
355
|
+
* the turn's ORIGINAL user message as `<user_request>`),
|
|
356
|
+
* 2. `<turn_recovery>` — the state-loss disclosure plus the recorded
|
|
357
|
+
* transcript of the turn so far (turn-recovery.ts; rendered even with no
|
|
358
|
+
* transcript, since without the disclosure the decisions below would read
|
|
359
|
+
* as reactions to proposals this agent never made),
|
|
360
|
+
* 3. the reinvocation decisions verbatim — already-applied / approved /
|
|
361
|
+
* skipped semantics byte-identical to the resumed path, ending on its
|
|
362
|
+
* "continue the task" directive.
|
|
363
|
+
*
|
|
364
|
+
* Appending past `<user_request>` is the chronology speaking (request →
|
|
365
|
+
* work done → decisions → continue), the same shape as the activity-level
|
|
366
|
+
* structured-output suffix.
|
|
367
|
+
*/
|
|
368
|
+
export function buildHitlRecoveryPrompt(
|
|
369
|
+
options: EnhancedPromptOptions,
|
|
370
|
+
recovery: {
|
|
371
|
+
turnDigest: string | undefined;
|
|
372
|
+
pendingApprovals: PendingApproval[];
|
|
373
|
+
approvalDecisions: Map<string, ApprovalAction>;
|
|
374
|
+
appliedToolCallIds?: ReadonlySet<string>;
|
|
375
|
+
},
|
|
376
|
+
): string {
|
|
377
|
+
return [
|
|
378
|
+
buildEnhancedPrompt(options),
|
|
379
|
+
formatTurnRecoverySection(recovery.turnDigest),
|
|
380
|
+
buildReinvocationPrompt(
|
|
381
|
+
recovery.pendingApprovals,
|
|
382
|
+
recovery.approvalDecisions,
|
|
383
|
+
recovery.appliedToolCallIds,
|
|
384
|
+
),
|
|
385
|
+
].join("\n\n---\n\n");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function formatTurnRecoverySection(digest: string | undefined): string {
|
|
389
|
+
return `<turn_recovery>\n${formatTurnRecoveryText(digest)}\n</turn_recovery>`;
|
|
390
|
+
}
|
|
391
|
+
|
|
341
392
|
/**
|
|
342
393
|
* Render a pending approval as a short human-readable action description for the
|
|
343
394
|
* reinvocation prompt. Prefers the already-resolved approval message; falls back
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-recovery digest: the platform's recorded transcript of the current
|
|
3
|
+
* turn, rendered as prompt context for a fresh agent that replaced one whose
|
|
4
|
+
* conversation state was lost mid-turn (issue #366).
|
|
5
|
+
*
|
|
6
|
+
* A HITL-paused turn can lose its agent two ways — the stored handle fails
|
|
7
|
+
* to resume when the approval lands, or the resumed handle turns out to be
|
|
8
|
+
* poisoned mid-send. Either way the replacement agent starts with an EMPTY
|
|
9
|
+
* conversation, so the approval decisions alone read as instructions with no
|
|
10
|
+
* story: the agent no longer knows what it was doing. This module rebuilds
|
|
11
|
+
* that story from `status.messages` — the persisted execution transcript
|
|
12
|
+
* seeded on every re-invocation — which is the platform's durable record of
|
|
13
|
+
* the turn AND exactly what the user watched happen (introspecting the dead
|
|
14
|
+
* agent's own conversation is best-effort at most; the handle is the thing
|
|
15
|
+
* that failed).
|
|
16
|
+
*
|
|
17
|
+
* Doctrine (mirrors `ChannelRolloverBridgeComposer`, cloud DD-013 — the
|
|
18
|
+
* platform's reference for conversation digests):
|
|
19
|
+
* - Content here, presentation framing separate: {@link composeTurnRecoveryDigest}
|
|
20
|
+
* emits bare `Assistant:` / `Tool:` / `System:` lines;
|
|
21
|
+
* {@link formatTurnRecoveryText} adds the behavioral preamble; the prompt
|
|
22
|
+
* builder wraps the section tag (the context-bridge / conversation-catchup
|
|
23
|
+
* split).
|
|
24
|
+
* - Oldest-first reading order; over budget, the OLDEST lines drop first
|
|
25
|
+
* (recency wins) and the drop is disclosed.
|
|
26
|
+
* - Best-effort by contract: composition never throws — a failed digest
|
|
27
|
+
* degrades to recovery-without-transcript, never a failed recovery.
|
|
28
|
+
*
|
|
29
|
+
* Scope: the CURRENT turn only. Prior turns live in other execution records
|
|
30
|
+
* and are not recoverable runner-side (that is the rollover bridge's domain,
|
|
31
|
+
* composed cloud-side); the enhanced prompt's standing-context sections carry
|
|
32
|
+
* whatever the session already had.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
36
|
+
import { MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
37
|
+
|
|
38
|
+
/** Per-line text budget; longer texts are cut with an ellipsis (DD-013 twin). */
|
|
39
|
+
const MAX_LINE_CHARS = 400;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Args-preview budget inside a tool line — the `describeApproval` precedent
|
|
43
|
+
* (prompt-builder.ts), so tool renderings are bounded the same way on both
|
|
44
|
+
* surfaces that describe them.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_ARGS_CHARS = 200;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whole-digest budget (DD-013 twin). Enforced by dropping the OLDEST lines
|
|
50
|
+
* first — recency wins, matching how a human skims their own recent work
|
|
51
|
+
* before continuing it.
|
|
52
|
+
*/
|
|
53
|
+
const MAX_DIGEST_CHARS = 4000;
|
|
54
|
+
|
|
55
|
+
const ELLIPSIS = "\u2026";
|
|
56
|
+
|
|
57
|
+
/** Disclosed in place of the dropped prefix so the record never lies by omission. */
|
|
58
|
+
const OMISSION_NOTICE = `[${ELLIPSIS} earlier activity in this turn omitted for length]`;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How the transcript is introduced to the model. Establishes the three facts
|
|
62
|
+
* the replacement agent must hold: the work below is its OWN (continue, don't
|
|
63
|
+
* restart), completed actions are DONE (don't redo them), and the workspace —
|
|
64
|
+
* not this summary — is the source of truth for exact current state.
|
|
65
|
+
*/
|
|
66
|
+
const TURN_RECOVERY_PREAMBLE =
|
|
67
|
+
"You had already started working on the user's request above, but the " +
|
|
68
|
+
"session holding that conversation was lost, so you do not remember it. " +
|
|
69
|
+
"Below is the platform's recorded transcript of your progress in this " +
|
|
70
|
+
"turn, oldest first. Treat it as work YOU already did: do not start the " +
|
|
71
|
+
"task over, do not redo actions shown as completed, and check the " +
|
|
72
|
+
"workspace's current state where exact details matter.";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fallback framing when the turn produced no renderable transcript (or
|
|
76
|
+
* composition failed): the state-loss disclosure still matters — without it,
|
|
77
|
+
* the appended approval decisions read as reactions to proposals this agent
|
|
78
|
+
* never made.
|
|
79
|
+
*/
|
|
80
|
+
const TURN_RECOVERY_NO_TRANSCRIPT =
|
|
81
|
+
"You had already started working on the user's request above, but the " +
|
|
82
|
+
"session holding that conversation was lost, so you do not remember it " +
|
|
83
|
+
"and no transcript of your progress is available. Check the workspace's " +
|
|
84
|
+
"current state to see what you already did before continuing.";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Render the turn's persisted transcript as bare digest lines, or undefined
|
|
88
|
+
* when there is nothing worth carrying. Never throws.
|
|
89
|
+
*
|
|
90
|
+
* Human messages are skipped — the turn's user message is already rendered
|
|
91
|
+
* in the enhanced prompt's `<user_request>`, and duplicating it here would
|
|
92
|
+
* dilute the budget. Thinking content is skipped as model-internal (the
|
|
93
|
+
* replacement agent forms its own). System notices are kept: platform
|
|
94
|
+
* messages like budget warnings or degradation notices are turn context the
|
|
95
|
+
* agent acted under.
|
|
96
|
+
*/
|
|
97
|
+
export function composeTurnRecoveryDigest(
|
|
98
|
+
messages: readonly AgentMessage[],
|
|
99
|
+
): string | undefined {
|
|
100
|
+
try {
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
for (const message of messages) {
|
|
103
|
+
lines.push(...messageLines(message));
|
|
104
|
+
}
|
|
105
|
+
if (lines.length === 0) return undefined;
|
|
106
|
+
|
|
107
|
+
// Oldest-first; drop from the FRONT when over budget so the newest work
|
|
108
|
+
// always survives, and disclose the drop (DD-013 enforcement shape). The
|
|
109
|
+
// notice participates in the budget so the result never overshoots.
|
|
110
|
+
let first = 0;
|
|
111
|
+
while (first < lines.length && totalLength(lines, first) > MAX_DIGEST_CHARS) {
|
|
112
|
+
first++;
|
|
113
|
+
}
|
|
114
|
+
if (first >= lines.length) {
|
|
115
|
+
// Degenerate case: even the newest line alone busts the budget (it is
|
|
116
|
+
// already line-truncated, so this would take a pathological budget/line
|
|
117
|
+
// ratio) — keep that one line rather than rendering nothing.
|
|
118
|
+
first = lines.length - 1;
|
|
119
|
+
}
|
|
120
|
+
const kept = lines.slice(first);
|
|
121
|
+
if (first > 0) kept.unshift(OMISSION_NOTICE);
|
|
122
|
+
return kept.join("\n");
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.warn("Turn-recovery digest composition failed — recovering without a transcript:", err);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The framed recovery body (preamble + digest), ready for section wrapping.
|
|
131
|
+
* Accepts an absent digest: the state-loss disclosure is load-bearing even
|
|
132
|
+
* when there is no transcript to show (see {@link TURN_RECOVERY_NO_TRANSCRIPT}).
|
|
133
|
+
*/
|
|
134
|
+
export function formatTurnRecoveryText(digest: string | undefined): string {
|
|
135
|
+
if (digest === undefined || digest.trim() === "") {
|
|
136
|
+
return TURN_RECOVERY_NO_TRANSCRIPT;
|
|
137
|
+
}
|
|
138
|
+
return `${TURN_RECOVERY_PREAMBLE}\n\n${digest.trim()}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The digest lines one transcript message contributes (possibly none). */
|
|
142
|
+
function messageLines(message: AgentMessage): string[] {
|
|
143
|
+
const lines: string[] = [];
|
|
144
|
+
const text = message.content.trim();
|
|
145
|
+
switch (message.type) {
|
|
146
|
+
case MessageType.MESSAGE_AI:
|
|
147
|
+
if (text) lines.push(`Assistant: ${truncate(text, MAX_LINE_CHARS)}`);
|
|
148
|
+
// Tool calls ride AI messages (message.proto contract); render them
|
|
149
|
+
// even when the surrounding text is blank.
|
|
150
|
+
for (const toolCall of message.toolCalls) {
|
|
151
|
+
lines.push(toolCallLine(toolCall));
|
|
152
|
+
}
|
|
153
|
+
return lines;
|
|
154
|
+
case MessageType.MESSAGE_SYSTEM:
|
|
155
|
+
if (text) lines.push(`System: ${truncate(text, MAX_LINE_CHARS)}`);
|
|
156
|
+
return lines;
|
|
157
|
+
default:
|
|
158
|
+
// Human (already in <user_request>), thinking (model-internal), tool
|
|
159
|
+
// results (each ToolCall line already carries its outcome), unknown.
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* One bounded line per tool call: what was attempted and how it ended.
|
|
166
|
+
* Prefers the resolved approval message where one exists — the same
|
|
167
|
+
* human-meaningful description the user approved against
|
|
168
|
+
* (the `describeApproval` convention).
|
|
169
|
+
*/
|
|
170
|
+
function toolCallLine(toolCall: ToolCall): string {
|
|
171
|
+
const action = toolCall.approvalMessage
|
|
172
|
+
? toolCall.approvalMessage
|
|
173
|
+
: toolCall.argsPreview
|
|
174
|
+
? `${toolCall.name}(${truncate(toolCall.argsPreview, MAX_ARGS_CHARS)})`
|
|
175
|
+
: toolCall.name;
|
|
176
|
+
return `Tool: ${truncate(action, MAX_LINE_CHARS)} — ${outcome(toolCall)}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function outcome(toolCall: ToolCall): string {
|
|
180
|
+
switch (toolCall.status) {
|
|
181
|
+
case ToolCallStatus.TOOL_CALL_COMPLETED:
|
|
182
|
+
return "completed";
|
|
183
|
+
case ToolCallStatus.TOOL_CALL_FAILED:
|
|
184
|
+
return toolCall.error
|
|
185
|
+
? `failed: ${truncate(toolCall.error, MAX_ARGS_CHARS)}`
|
|
186
|
+
: "failed";
|
|
187
|
+
case ToolCallStatus.TOOL_CALL_WAITING_APPROVAL:
|
|
188
|
+
return "paused for user approval";
|
|
189
|
+
case ToolCallStatus.TOOL_CALL_SKIPPED:
|
|
190
|
+
return "skipped";
|
|
191
|
+
default:
|
|
192
|
+
// PENDING / RUNNING / unknown: in flight when the session was lost.
|
|
193
|
+
return "interrupted before it finished";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function truncate(text: string, max: number): string {
|
|
198
|
+
return text.length <= max ? text : `${text.slice(0, max)}${ELLIPSIS}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Joined length of lines[first..] plus the omission notice when one would render. */
|
|
202
|
+
function totalLength(lines: readonly string[], first: number): number {
|
|
203
|
+
let total = first > 0 ? OMISSION_NOTICE.length + 1 : 0;
|
|
204
|
+
for (let i = first; i < lines.length; i++) {
|
|
205
|
+
total += lines[i].length + 1;
|
|
206
|
+
}
|
|
207
|
+
return total;
|
|
208
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end proof (real deepagents + LangGraph runtime, no LLM/network) that
|
|
3
|
+
* a plan-mode PARENT graph accepts workspace-relative paths (issue #429).
|
|
4
|
+
*
|
|
5
|
+
* Before the fix, deepagents' permission enforcement canonicalized every
|
|
6
|
+
* filesystem tool-call path BEFORE any rule ran and refused non-absolute
|
|
7
|
+
* shapes, so on a rule-bearing graph a workspace-relative call — reads
|
|
8
|
+
* included — died with `path must be absolute` instead of just working. The
|
|
9
|
+
* path-normalization middleware (middleware/path-normalization.ts) rewrites
|
|
10
|
+
* relative paths to workspace-absolute at our seam, before enforcement sees
|
|
11
|
+
* them.
|
|
12
|
+
*
|
|
13
|
+
* The graph here is composed exactly the way setup.ts composes the parent:
|
|
14
|
+
* the PRODUCTION buildMiddlewareStack (pathNormalization present, the
|
|
15
|
+
* rule-bearing shape) + the CAS capture backend + the PRODUCTION
|
|
16
|
+
* PLAN_MODE_PERMISSIONS constant. These tests are also the empirical proof
|
|
17
|
+
* that langchain's wrapToolCall seam delivers rewritten args to the tool —
|
|
18
|
+
* if it did not, the relative read below could never succeed.
|
|
19
|
+
*
|
|
20
|
+
* The sub-agent twin of this contract is pinned in
|
|
21
|
+
* subagent-plan-mode-permissions.test.ts (issue #255 wiring).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
25
|
+
import { mkdtemp, mkdir, rm, readFile, writeFile, access } from "node:fs/promises";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { HumanMessage, ToolMessage, type BaseMessage } from "@langchain/core/messages";
|
|
29
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
30
|
+
import { createDeepAgent } from "deepagents";
|
|
31
|
+
|
|
32
|
+
import { PLAN_MODE_PERMISSIONS } from "../../../shared/plan-mode-permissions.js";
|
|
33
|
+
import { buildMiddlewareStack } from "../../../middleware/index.js";
|
|
34
|
+
import { createCasCaptureBackend } from "../cas-capture-backend.js";
|
|
35
|
+
import { CasCaptureObserver } from "../cas-capture-observer.js";
|
|
36
|
+
import { ScriptedModel, type ScriptSelector } from "../__test-utils__/scripted-model.js";
|
|
37
|
+
|
|
38
|
+
const SEEDED_CONTENT = "PLAN_MODE_README_TOKEN: hello from the seeded file";
|
|
39
|
+
|
|
40
|
+
describe("plan-mode parent path normalization (issue #429)", () => {
|
|
41
|
+
let root: string;
|
|
42
|
+
let observer: CasCaptureObserver;
|
|
43
|
+
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
root = await mkdtemp(join(tmpdir(), "plan-path-"));
|
|
46
|
+
await mkdir(join(root, "src"), { recursive: true });
|
|
47
|
+
await writeFile(join(root, "src/notes.md"), SEEDED_CONTENT);
|
|
48
|
+
observer = new CasCaptureObserver({ rootDir: root, isIgnored: async () => true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
await rm(root, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build a parent graph the way setup.ts does in plan mode: the production
|
|
57
|
+
* middleware stack with pathNormalization set (derived, like the graph's
|
|
58
|
+
* permissions, from the plan-mode rules), a filesystem-only CAS capture
|
|
59
|
+
* backend (no shellEnv — plan mode clears it), and PLAN_MODE_PERMISSIONS
|
|
60
|
+
* on the graph.
|
|
61
|
+
*/
|
|
62
|
+
async function buildPlanModeParent(script: ScriptSelector) {
|
|
63
|
+
const { middleware } = buildMiddlewareStack({
|
|
64
|
+
approvalGate: null,
|
|
65
|
+
pathNormalization: { rootDir: root },
|
|
66
|
+
});
|
|
67
|
+
const backend = await createCasCaptureBackend({ rootDir: root, observer });
|
|
68
|
+
return createDeepAgent({
|
|
69
|
+
model: new ScriptedModel(script),
|
|
70
|
+
checkpointer: new MemorySaver() as never,
|
|
71
|
+
backend,
|
|
72
|
+
middleware: middleware as never[],
|
|
73
|
+
permissions: PLAN_MODE_PERMISSIONS,
|
|
74
|
+
} as Parameters<typeof createDeepAgent>[0]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toolResultById(messages: BaseMessage[], toolCallId: string): string {
|
|
78
|
+
const match = messages.find(
|
|
79
|
+
(m): m is ToolMessage => m instanceof ToolMessage && m.tool_call_id === toolCallId,
|
|
80
|
+
);
|
|
81
|
+
expect(match, `expected a tool result for call '${toolCallId}'`).toBeDefined();
|
|
82
|
+
return typeof match!.content === "string" ? match!.content : JSON.stringify(match!.content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function invokeOnce(script: ScriptSelector, threadId: string) {
|
|
86
|
+
const agent = await buildPlanModeParent(script);
|
|
87
|
+
return (await agent.invoke(
|
|
88
|
+
{ messages: [new HumanMessage({ content: "go" })] },
|
|
89
|
+
{ configurable: { thread_id: threadId }, recursionLimit: 50 },
|
|
90
|
+
)) as { messages: BaseMessage[] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
it("a prompt-compliant relative read succeeds on the first tool round", async () => {
|
|
94
|
+
const result = await invokeOnce(
|
|
95
|
+
() => ({
|
|
96
|
+
toolCalls: [
|
|
97
|
+
{ name: "read_file", args: { file_path: "src/notes.md" }, id: "c_read_rel" },
|
|
98
|
+
],
|
|
99
|
+
done: "done",
|
|
100
|
+
}),
|
|
101
|
+
"t_read_rel",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const readResult = toolResultById(result.messages, "c_read_rel");
|
|
105
|
+
expect(readResult).toContain("PLAN_MODE_README_TOKEN");
|
|
106
|
+
expect(readResult).not.toMatch(/path must be absolute/i);
|
|
107
|
+
expect(readResult).not.toMatch(/permission denied/i);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("a relative write is refused by the RULES — permission denied, not a path-shape error", async () => {
|
|
111
|
+
const result = await invokeOnce(
|
|
112
|
+
() => ({
|
|
113
|
+
toolCalls: [
|
|
114
|
+
{ name: "write_file", args: { file_path: "src/out.txt", content: "x" }, id: "c_write_rel" },
|
|
115
|
+
],
|
|
116
|
+
done: "done",
|
|
117
|
+
}),
|
|
118
|
+
"t_write_rel",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// The deny-all-writes rule fires on the normalized path — the honest
|
|
122
|
+
// plan-mode refusal the model can reason about, instead of the
|
|
123
|
+
// pre-fix validation error that taught it to abandon relative paths.
|
|
124
|
+
expect(toolResultById(result.messages, "c_write_rel")).toMatch(/permission denied for write/i);
|
|
125
|
+
expect(toolResultById(result.messages, "c_write_rel")).not.toMatch(/path must be absolute/i);
|
|
126
|
+
await expect(access(join(root, "src/out.txt"))).rejects.toThrow();
|
|
127
|
+
expect(observer.before.size).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("an escaping relative read is still refused — normalization grants no new reachability", async () => {
|
|
131
|
+
// Outside the root, reachable only if `..` were naively joined away.
|
|
132
|
+
const result = await invokeOnce(
|
|
133
|
+
() => ({
|
|
134
|
+
toolCalls: [
|
|
135
|
+
{ name: "read_file", args: { file_path: "../escape.txt" }, id: "c_read_escape" },
|
|
136
|
+
],
|
|
137
|
+
done: "done",
|
|
138
|
+
}),
|
|
139
|
+
"t_read_escape",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Left raw by the middleware, refused by upstream validation exactly as
|
|
143
|
+
// before the fix.
|
|
144
|
+
expect(toolResultById(result.messages, "c_read_escape")).toMatch(/must not contain|path must be absolute/i);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("absolute in-workspace paths keep working unchanged", async () => {
|
|
148
|
+
const result = await invokeOnce(
|
|
149
|
+
() => ({
|
|
150
|
+
toolCalls: [
|
|
151
|
+
{ name: "read_file", args: { file_path: join(root, "src/notes.md") }, id: "c_read_abs" },
|
|
152
|
+
],
|
|
153
|
+
done: "done",
|
|
154
|
+
}),
|
|
155
|
+
"t_read_abs",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(toolResultById(result.messages, "c_read_abs")).toContain("PLAN_MODE_README_TOKEN");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("relative ls flows through normalization to list the workspace directory", async () => {
|
|
162
|
+
const result = await invokeOnce(
|
|
163
|
+
() => ({
|
|
164
|
+
toolCalls: [{ name: "ls", args: { path: "src" }, id: "c_ls_rel" }],
|
|
165
|
+
done: "done",
|
|
166
|
+
}),
|
|
167
|
+
"t_ls_rel",
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const lsResult = toolResultById(result.messages, "c_ls_rel");
|
|
171
|
+
expect(lsResult).toContain("notes.md");
|
|
172
|
+
expect(lsResult).not.toMatch(/path must be absolute/i);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { buildShellEnv, SHELL_ENV_DENYLIST } from "../shell-env.js";
|
|
3
|
+
import { RUNNER_CREDENTIAL_ENV_KEYS } from "../../../shared/runner-credential-keys.js";
|
|
3
4
|
|
|
4
5
|
describe("buildShellEnv", () => {
|
|
5
|
-
it("strips runner
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
6
|
+
it("strips every runner credential from the base env", () => {
|
|
7
|
+
// Plant a value for every name in the source-of-truth list, so a key
|
|
8
|
+
// added to runner-credential-keys.ts is covered here automatically.
|
|
9
|
+
const base: NodeJS.ProcessEnv = { PATH: "/usr/bin", HOME: "/home/runner" };
|
|
10
|
+
for (const key of RUNNER_CREDENTIAL_ENV_KEYS) {
|
|
11
|
+
base[key] = `leaked-${key}`;
|
|
12
|
+
}
|
|
13
13
|
|
|
14
14
|
const env = buildShellEnv({}, base);
|
|
15
15
|
|
|
16
16
|
expect(env.PATH).toBe("/usr/bin");
|
|
17
17
|
expect(env.HOME).toBe("/home/runner");
|
|
18
|
-
for (const key of
|
|
19
|
-
expect(env[key]).toBeUndefined();
|
|
18
|
+
for (const key of RUNNER_CREDENTIAL_ENV_KEYS) {
|
|
19
|
+
expect(env[key], `runner credential '${key}' must not reach the shell env`).toBeUndefined();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("denies the full credential list, not just the original three (issue #385)", () => {
|
|
24
|
+
// Pins the five names #385 added. The parameterized test above would
|
|
25
|
+
// pass even if the list regressed to three; this one cannot.
|
|
26
|
+
for (const key of [
|
|
27
|
+
"ANTHROPIC_API_KEY",
|
|
28
|
+
"OPENAI_API_KEY",
|
|
29
|
+
"STIGMER_AUTH_TOKEN",
|
|
30
|
+
"ANTHROPIC_FOUNDRY_API_KEY",
|
|
31
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
32
|
+
]) {
|
|
33
|
+
expect(SHELL_ENV_DENYLIST, `'${key}' must be in the shell denylist`).toContain(key);
|
|
20
34
|
}
|
|
21
35
|
});
|
|
22
36
|
|
|
@@ -83,12 +83,10 @@ describe("plan-mode sub-agent filesystem permissions (issue #255)", () => {
|
|
|
83
83
|
// One turn, three tool calls: both mutation tools must be denied by the
|
|
84
84
|
// permission rules; the read must pass through untouched (the deny rule
|
|
85
85
|
// covers the `write` operation class only).
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// paths with a validation error before any rule or backend runs — a
|
|
91
|
-
// different denial, but still nothing written.
|
|
86
|
+
// Absolute workspace paths — the canonical shape after enforcement
|
|
87
|
+
// canonicalization, and what the single-workspace prompt's prose (the
|
|
88
|
+
// absolute rootDir) steers models toward. The workspace-relative shape
|
|
89
|
+
// is pinned separately below (issue #429).
|
|
92
90
|
const script: ScriptSelector = () => ({
|
|
93
91
|
toolCalls: [
|
|
94
92
|
{ name: "write_file", args: { file_path: join(root, "dist/out.txt"), content: "new file" }, id: "c_write" },
|
|
@@ -143,19 +141,20 @@ describe("plan-mode sub-agent filesystem permissions (issue #255)", () => {
|
|
|
143
141
|
expect(observer.before.size).toBe(1);
|
|
144
142
|
});
|
|
145
143
|
|
|
146
|
-
it("relative-path
|
|
147
|
-
// The
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// relative
|
|
154
|
-
//
|
|
155
|
-
//
|
|
144
|
+
it("relative-path writes are refused by the RULES, and relative reads flow (issue #429)", async () => {
|
|
145
|
+
// The multi-workspace prompt mandates workspace-relative paths, and
|
|
146
|
+
// models routinely choose them in single-workspace sessions too. Before
|
|
147
|
+
// the #429 fix, deepagents' permission canonicalization refused the
|
|
148
|
+
// shape outright — every relative call, reads included, died with
|
|
149
|
+
// "path must be absolute" before any rule ran. compileSubagents now
|
|
150
|
+
// derives a path-normalization shim from the same `permissions` it
|
|
151
|
+
// bakes into the graph, so the relative shape reaches the rules in
|
|
152
|
+
// canonical form: writes get the honest plan-mode denial, reads just
|
|
153
|
+
// work. Read-only-by-construction (issue #255) holds throughout.
|
|
156
154
|
const script: ScriptSelector = () => ({
|
|
157
155
|
toolCalls: [
|
|
158
156
|
{ name: "write_file", args: { file_path: "dist/out.txt", content: "new file" }, id: "c_write_rel" },
|
|
157
|
+
{ name: "read_file", args: { file_path: "notes.md" }, id: "c_read_rel" },
|
|
159
158
|
],
|
|
160
159
|
done: "worker done",
|
|
161
160
|
});
|
|
@@ -166,8 +165,14 @@ describe("plan-mode sub-agent filesystem permissions (issue #255)", () => {
|
|
|
166
165
|
{ configurable: { thread_id: "t3" }, recursionLimit: 50 },
|
|
167
166
|
)) as { messages: BaseMessage[] };
|
|
168
167
|
|
|
169
|
-
|
|
168
|
+
const writeResult = toolResultById(result.messages, "c_write_rel");
|
|
169
|
+
expect(writeResult).toMatch(/permission denied for write/i);
|
|
170
|
+
expect(writeResult).not.toMatch(/path must be absolute/i);
|
|
170
171
|
await expect(access(join(root, "dist/out.txt"))).rejects.toThrow();
|
|
171
172
|
expect(observer.before.size).toBe(0);
|
|
173
|
+
|
|
174
|
+
const readResult = toolResultById(result.messages, "c_read_rel");
|
|
175
|
+
expect(readResult).toContain("PLAN_MODE_README_TOKEN");
|
|
176
|
+
expect(readResult).not.toMatch(/path must be absolute/i);
|
|
172
177
|
});
|
|
173
178
|
});
|
|
@@ -635,17 +635,14 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
635
635
|
// Step 9: Construct the LLM model. Resolution to the provider API id
|
|
636
636
|
// happens inside buildChatModel; modelName stays the registry id for
|
|
637
637
|
// pricing, the native-thinking heuristic, and sub-agent inheritance.
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
Number.parseInt(process.env.STIGMER_LLM_REQUEST_TIMEOUT_MS ?? "", 10);
|
|
642
|
-
const requestTimeoutMs = parsedTimeoutMs > 0 ? parsedTimeoutMs : undefined;
|
|
638
|
+
// The operator's STIGMER_LLM_REQUEST_TIMEOUT_MS bound is applied inside
|
|
639
|
+
// buildChatModel (#468) — the sub-agent modelFactory below silently
|
|
640
|
+
// dropped it when each caller parsed the env itself.
|
|
643
641
|
const { model } = await buildChatModel({
|
|
644
642
|
modelName,
|
|
645
643
|
proxyEndpoint: config.proxyEndpoint ?? undefined,
|
|
646
644
|
stigmerToken: config.stigmerToken ?? undefined,
|
|
647
645
|
headerScope: { executionId },
|
|
648
|
-
timeoutMs: requestTimeoutMs,
|
|
649
646
|
});
|
|
650
647
|
timing.mark("build_model");
|
|
651
648
|
|
|
@@ -655,6 +652,12 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
655
652
|
const execConfig = execution.spec!.executionConfig;
|
|
656
653
|
const isPlanMode = execConfig?.interactionMode === InteractionMode.PLAN;
|
|
657
654
|
const shellEnv = isPlanMode ? undefined : buildShellEnv(envResult.mergedEnvVars);
|
|
655
|
+
// Plan mode's filesystem permission rules, hoisted once: the parent graph,
|
|
656
|
+
// every sub-agent graph, AND the path-normalization middleware (issue #429)
|
|
657
|
+
// all derive from this single value, so a rule-bearing graph can never miss
|
|
658
|
+
// the shim that keeps prompt-compliant relative paths from dying in rule
|
|
659
|
+
// validation (`path must be absolute`).
|
|
660
|
+
const planModePermissions = isPlanMode ? PLAN_MODE_PERMISSIONS : undefined;
|
|
658
661
|
|
|
659
662
|
const toolServerMap = new Map<string, string>();
|
|
660
663
|
if (mcpConnection) {
|
|
@@ -760,6 +763,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
760
763
|
} : null,
|
|
761
764
|
otelSpans: { toolServerMap },
|
|
762
765
|
approvalGate: approvalGateConfig,
|
|
766
|
+
pathNormalization: planModePermissions
|
|
767
|
+
? { rootDir: workspaceBackend.rootDir }
|
|
768
|
+
: null,
|
|
763
769
|
});
|
|
764
770
|
timing.mark("build_middleware");
|
|
765
771
|
|
|
@@ -823,7 +829,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
823
829
|
// Plan mode's deny-all-writes rule, mirrored onto every sub-agent graph
|
|
824
830
|
// (issue #255) — pre-built CompiledSubAgents never inherit the parent's
|
|
825
831
|
// permissions, so read-only-by-construction must be baked in here.
|
|
826
|
-
|
|
832
|
+
// compileSubagents derives each sub-agent's path-normalization shim
|
|
833
|
+
// from this same value (issue #429).
|
|
834
|
+
...(planModePermissions ? { permissions: planModePermissions } : {}),
|
|
827
835
|
});
|
|
828
836
|
timing.mark("compile_subagents");
|
|
829
837
|
}
|
|
@@ -857,7 +865,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
857
865
|
middleware: middleware as any,
|
|
858
866
|
subagents: compiledSubagents ?? undefined,
|
|
859
867
|
...(responseFormat ? { responseFormat } : {}),
|
|
860
|
-
...(
|
|
868
|
+
...(planModePermissions ? { permissions: planModePermissions } : {}),
|
|
861
869
|
} as Parameters<typeof createDeepAgent>[0]);
|
|
862
870
|
|
|
863
871
|
// Step 11: Prepare invocation input and config. The conversation catchup
|
|
@@ -6,15 +6,14 @@
|
|
|
6
6
|
* never once at process start.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
];
|
|
9
|
+
import { RUNNER_CREDENTIAL_ENV_KEYS } from "../../shared/runner-credential-keys.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Runner-internal keys that must never reach agent shell commands: every
|
|
13
|
+
* credential the runner holds for its own outbound calls (issue #385). The
|
|
14
|
+
* names — and the rule for adding one — live in runner-credential-keys.ts.
|
|
15
|
+
*/
|
|
16
|
+
export const SHELL_ENV_DENYLIST: readonly string[] = RUNNER_CREDENTIAL_ENV_KEYS;
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* Build the environment map passed to deepagents' LocalShellBackend.
|