@stigmer/runner 3.12.4 → 3.12.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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
- package/dist/activities/execute-cursor/index.d.ts +10 -1
- package/dist/activities/execute-cursor/index.js +47 -29
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
- package/dist/activities/execute-cursor/prompt-builder.js +11 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
- package/dist/activities/execute-cursor/skill-resolver.js +5 -108
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +57 -5
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +17 -16
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/middleware/path-normalization.d.ts +49 -49
- package/dist/middleware/path-normalization.js +73 -84
- package/dist/middleware/path-normalization.js.map +1 -1
- package/dist/middleware/types.d.ts +3 -3
- package/dist/runner-manager.d.ts +1 -1
- package/dist/runner-manager.js +22 -15
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +11 -0
- package/dist/runner.js.map +1 -1
- package/dist/shared/args-preview.d.ts +8 -0
- package/dist/shared/args-preview.js +14 -3
- package/dist/shared/args-preview.js.map +1 -1
- package/dist/shared/artifact-storage.d.ts +11 -1
- package/dist/shared/artifact-storage.js +10 -1
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/declared-preferences.d.ts +47 -0
- package/dist/shared/declared-preferences.js +64 -0
- package/dist/shared/declared-preferences.js.map +1 -0
- package/dist/shared/mcp-enabled-tools.d.ts +2 -3
- package/dist/shared/mcp-enabled-tools.js +2 -3
- package/dist/shared/mcp-enabled-tools.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +20 -15
- package/dist/shared/mcp-resolver.js +11 -12
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/placeholder-resolver.d.ts +9 -2
- package/dist/shared/placeholder-resolver.js +9 -2
- package/dist/shared/placeholder-resolver.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +26 -46
- package/dist/shared/plan-mode-permissions.js +27 -55
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/skill-mount.d.ts +89 -0
- package/dist/shared/skill-mount.js +142 -0
- package/dist/shared/skill-mount.js.map +1 -0
- package/dist/shared/skill-writer.d.ts +28 -26
- package/dist/shared/skill-writer.js +79 -102
- package/dist/shared/skill-writer.js.map +1 -1
- package/dist/shared/worker-shutdown.d.ts +67 -0
- package/dist/shared/worker-shutdown.js +79 -0
- package/dist/shared/worker-shutdown.js.map +1 -0
- package/dist/shared/workspace/types.d.ts +3 -2
- package/dist/workflow-engine/loader.js +38 -1
- package/dist/workflow-engine/loader.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.js +23 -2
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/package.json +15 -11
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
- package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
- package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
- package/src/activities/execute-cursor/index.ts +63 -35
- package/src/activities/execute-cursor/prompt-builder.ts +29 -2
- package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
- package/src/activities/execute-cursor/skill-resolver.ts +10 -137
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
- package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
- package/src/activities/execute-deep-agent/index.ts +57 -5
- package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
- package/src/activities/execute-deep-agent/setup.ts +21 -19
- package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
- package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
- package/src/index.ts +1 -1
- package/src/middleware/__tests__/path-normalization.test.ts +32 -32
- package/src/middleware/path-normalization.ts +78 -90
- package/src/middleware/types.ts +3 -3
- package/src/runner-manager.ts +26 -16
- package/src/runner.ts +14 -0
- package/src/shared/__tests__/artifact-storage.test.ts +28 -0
- package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
- package/src/shared/__tests__/declared-preferences.test.ts +109 -0
- package/src/shared/__tests__/foundry-seam.test.ts +5 -5
- package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
- package/src/shared/__tests__/skill-mount.test.ts +238 -0
- package/src/shared/__tests__/skill-writer.test.ts +151 -128
- package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
- package/src/shared/__tests__/vertex-seam.test.ts +5 -5
- package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
- package/src/shared/args-preview.ts +17 -3
- package/src/shared/artifact-storage.ts +20 -1
- package/src/shared/declared-preferences.ts +84 -0
- package/src/shared/mcp-enabled-tools.ts +2 -3
- package/src/shared/mcp-resolver.ts +20 -20
- package/src/shared/placeholder-resolver.ts +9 -2
- package/src/shared/plan-mode-permissions.ts +27 -58
- package/src/shared/skill-mount.ts +179 -0
- package/src/shared/skill-writer.ts +96 -130
- package/src/shared/worker-shutdown.ts +99 -0
- package/src/shared/workspace/types.ts +3 -2
- package/src/workflow-engine/__tests__/loader.test.ts +51 -0
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
- package/src/workflow-engine/loader.ts +46 -1
- package/src/workflow-engine/tasks/human-input.ts +33 -5
- package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
- package/dist/activities/execute-cursor/connect-backfill.js +0 -27
- package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
- package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
- package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
- package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
- package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
- package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
- package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
- package/src/activities/execute-cursor/connect-backfill.ts +0 -52
- package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
- package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
|
@@ -124,6 +124,81 @@ describe("CasCaptureShellBackend (shell + CAS adapter)", () => {
|
|
|
124
124
|
});
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The issue #754 regression pins: the backends are virtual-rooted, so a
|
|
129
|
+
* leading-"/" tool path resolves INSIDE the workspace — never onto the host
|
|
130
|
+
* filesystem — and the CAS observer sees it under the same key the
|
|
131
|
+
* turn-boundary capture and InlinePublisher resolve. Before the fix, an
|
|
132
|
+
* absolute `write_file("/tmp/x")` landed in the REAL /tmp: outside the
|
|
133
|
+
* workspace, outside CAS capture, outside the file_review ledger — an
|
|
134
|
+
* auto-approved, unreviewed host write.
|
|
135
|
+
*/
|
|
136
|
+
describe("virtual-root confinement (issue #754)", () => {
|
|
137
|
+
let root: string;
|
|
138
|
+
|
|
139
|
+
beforeEach(async () => {
|
|
140
|
+
root = await mkdtemp(join(tmpdir(), "cas-virtual-"));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
afterEach(async () => {
|
|
144
|
+
await rm(root, { recursive: true, force: true });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function makeBackend(): { backend: CasCaptureFilesystemBackend; observer: CasCaptureObserver } {
|
|
148
|
+
// Non-git shape: the observer owns EVERY touched path (setup.ts wires
|
|
149
|
+
// isIgnored to always-true when there is no git work tree).
|
|
150
|
+
const observer = new CasCaptureObserver({ rootDir: root, isIgnored: async () => true });
|
|
151
|
+
const backend = new CasCaptureFilesystemBackend(
|
|
152
|
+
{ rootDir: root, virtualMode: true },
|
|
153
|
+
{ observer },
|
|
154
|
+
);
|
|
155
|
+
return { backend, observer };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
it("an absolute write resolves inside the workspace, never onto the host", async () => {
|
|
159
|
+
const { backend, observer } = makeBackend();
|
|
160
|
+
const hostPath = `/tmp/stigmer-754-escape-${Date.now()}.txt`;
|
|
161
|
+
|
|
162
|
+
await backend.write(hostPath, "confined");
|
|
163
|
+
|
|
164
|
+
// Landed under the workspace root, keyed workspace-relative for capture…
|
|
165
|
+
expect(await readFile(join(root, hostPath.slice(1)), "utf8")).toBe("confined");
|
|
166
|
+
expect(observer.before.get(hostPath.slice(1))).toBeNull();
|
|
167
|
+
// …and the REAL host path was never touched.
|
|
168
|
+
await expect(readFile(hostPath, "utf8")).rejects.toThrow();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("reads resolve inside the workspace — '/etc/hosts' is the workspace's, not the host's", async () => {
|
|
172
|
+
const { backend } = makeBackend();
|
|
173
|
+
|
|
174
|
+
const result = await backend.read("/etc/hosts");
|
|
175
|
+
|
|
176
|
+
// The workspace has no etc/hosts: the honest not-found, never host bytes.
|
|
177
|
+
expect(JSON.stringify(result)).toMatch(/not found|no such file/i);
|
|
178
|
+
expect(JSON.stringify(result)).not.toContain("localhost");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("traversal is rejected at resolution", async () => {
|
|
182
|
+
const { backend } = makeBackend();
|
|
183
|
+
|
|
184
|
+
const result = await backend.write("../escape.txt", "nope");
|
|
185
|
+
|
|
186
|
+
expect((result as { error?: string }).error).toMatch(/traversal/i);
|
|
187
|
+
await expect(readFile(join(root, "..", "escape.txt"), "utf8")).rejects.toThrow();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("the shell variant is confined identically", async () => {
|
|
191
|
+
const observer = new CasCaptureObserver({ rootDir: root, isIgnored: async () => true });
|
|
192
|
+
const backend = await createCasCaptureBackend({ rootDir: root, observer, shellEnv: {} });
|
|
193
|
+
const hostPath = `/tmp/stigmer-754-shell-escape-${Date.now()}.txt`;
|
|
194
|
+
|
|
195
|
+
await backend.write(hostPath, "confined");
|
|
196
|
+
|
|
197
|
+
expect(await readFile(join(root, hostPath.slice(1)), "utf8")).toBe("confined");
|
|
198
|
+
await expect(readFile(hostPath, "utf8")).rejects.toThrow();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
127
202
|
describe("plan-mode filesystem backend", () => {
|
|
128
203
|
it("is not sandbox-capable", async () => {
|
|
129
204
|
const root = await mkdtemp(join(tmpdir(), "cas-plan-"));
|
|
@@ -53,6 +53,7 @@ vi.mock("@temporalio/activity", () => ({
|
|
|
53
53
|
// on it, which a bare `{ aborted: false }` stub cannot satisfy.
|
|
54
54
|
cancellationSignal: new AbortController().signal,
|
|
55
55
|
heartbeat: vi.fn(),
|
|
56
|
+
info: { taskQueue: "test-queue" },
|
|
56
57
|
}),
|
|
57
58
|
},
|
|
58
59
|
CancelledFailure: class CancelledFailure extends Error {},
|
|
@@ -46,6 +46,7 @@ vi.mock("@temporalio/activity", () => ({
|
|
|
46
46
|
// on it, which a bare `{ aborted: false }` stub cannot satisfy.
|
|
47
47
|
cancellationSignal: new AbortController().signal,
|
|
48
48
|
heartbeat: vi.fn(),
|
|
49
|
+
info: { taskQueue: "test-queue" },
|
|
49
50
|
}),
|
|
50
51
|
},
|
|
51
52
|
CancelledFailure: class CancelledFailure extends Error {},
|
|
@@ -12,6 +12,7 @@ vi.mock("@temporalio/activity", () => ({
|
|
|
12
12
|
// on it, which a bare `{ aborted: false }` stub cannot satisfy.
|
|
13
13
|
cancellationSignal: new AbortController().signal,
|
|
14
14
|
heartbeat: vi.fn(),
|
|
15
|
+
info: { taskQueue: "test-queue" },
|
|
15
16
|
}),
|
|
16
17
|
},
|
|
17
18
|
CancelledFailure: class CancelledFailure extends Error {},
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* End-to-end proof (real deepagents + LangGraph runtime, no LLM/network) that
|
|
3
3
|
* a plan-mode PARENT graph accepts workspace-relative paths (issue #429) and
|
|
4
|
-
*
|
|
4
|
+
* confines every read to the workspace (issues #528, #754).
|
|
5
5
|
*
|
|
6
6
|
* Before the #429 fix, deepagents' permission enforcement canonicalized every
|
|
7
7
|
* filesystem tool-call path BEFORE any rule ran and refused non-absolute
|
|
8
8
|
* shapes, so on a rule-bearing graph a workspace-relative call — reads
|
|
9
9
|
* included — died with `path must be absolute` instead of just working. The
|
|
10
10
|
* path-normalization middleware (middleware/path-normalization.ts) rewrites
|
|
11
|
-
* relative paths to
|
|
12
|
-
* them. #528
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `.stigmer` symlink keeps platform-dir reads in-root
|
|
11
|
+
* relative paths to VIRTUAL-absolute at our seam, before enforcement sees
|
|
12
|
+
* them. The read boundary #528 built out of rules is structural since #754:
|
|
13
|
+
* the backend is virtual-rooted, so every expressible path resolves inside
|
|
14
|
+
* the workspace — an out-of-root name is simply nonexistent, and the
|
|
15
|
+
* `.stigmer` symlink keeps platform-dir reads addressable in-root.
|
|
16
16
|
*
|
|
17
17
|
* The graph here is composed exactly the way setup.ts composes the parent:
|
|
18
|
-
* the PRODUCTION buildMiddlewareStack (pathNormalization present
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* the PRODUCTION buildMiddlewareStack (pathNormalization present on every
|
|
19
|
+
* graph) + the CAS capture backend + the PRODUCTION buildPlanModePermissions
|
|
20
|
+
* rules. These tests are also the empirical proof that langchain's
|
|
21
|
+
* wrapToolCall seam delivers rewritten args to the tool — if it did not, the
|
|
22
|
+
* relative read below could never succeed.
|
|
23
23
|
*
|
|
24
24
|
* The sub-agent twin of this contract is pinned in
|
|
25
25
|
* subagent-plan-mode-permissions.test.ts (issue #255 wiring).
|
|
@@ -65,7 +65,7 @@ async function buildPlanModeParent(
|
|
|
65
65
|
checkpointer: new MemorySaver() as never,
|
|
66
66
|
backend,
|
|
67
67
|
middleware: middleware as never[],
|
|
68
|
-
permissions: buildPlanModePermissions(
|
|
68
|
+
permissions: buildPlanModePermissions(),
|
|
69
69
|
} as Parameters<typeof createDeepAgent>[0]);
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -222,7 +222,11 @@ describe("plan-mode workspace read boundary (issue #528)", () => {
|
|
|
222
222
|
)) as { messages: BaseMessage[] };
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
it("
|
|
225
|
+
it("an out-of-root absolute read cannot reach the bytes — the name resolves in-workspace and finds nothing", async () => {
|
|
226
|
+
// Pre-#754 this was a rule-based denial; the boundary is structural now:
|
|
227
|
+
// the virtual root makes the outside path unaddressable, so the honest
|
|
228
|
+
// answer is not-found. The security property — the bytes never cross —
|
|
229
|
+
// is what this test pins.
|
|
226
230
|
const result = await invokeOnce(
|
|
227
231
|
() => ({
|
|
228
232
|
toolCalls: [
|
|
@@ -234,7 +238,7 @@ describe("plan-mode workspace read boundary (issue #528)", () => {
|
|
|
234
238
|
);
|
|
235
239
|
|
|
236
240
|
const readResult = toolResultById(result.messages, "c_read_out");
|
|
237
|
-
expect(readResult).toMatch(/
|
|
241
|
+
expect(readResult).toMatch(/not found|no such file/i);
|
|
238
242
|
expect(readResult).not.toContain("OUT_OF_ROOT_SECRET_TOKEN");
|
|
239
243
|
});
|
|
240
244
|
|
|
@@ -269,9 +273,9 @@ describe("plan-mode workspace read boundary (issue #528)", () => {
|
|
|
269
273
|
});
|
|
270
274
|
|
|
271
275
|
it("a bare ls (no path argument) lists the workspace, not the OS root", async () => {
|
|
272
|
-
// The tool's schema default is "/" — the
|
|
273
|
-
//
|
|
274
|
-
//
|
|
276
|
+
// The tool's schema default is "/" — which under the virtual root IS the
|
|
277
|
+
// workspace root (pre-#754 it was the OS root and the middleware had to
|
|
278
|
+
// fill the omission), so the model's first listing just works.
|
|
275
279
|
const result = await invokeOnce(
|
|
276
280
|
() => ({
|
|
277
281
|
toolCalls: [{ name: "ls", args: {}, id: "c_ls_bare" }],
|
|
@@ -287,7 +291,8 @@ describe("plan-mode workspace read boundary (issue #528)", () => {
|
|
|
287
291
|
|
|
288
292
|
it("a bare grep (no path argument) searches the workspace, not the whole filesystem", async () => {
|
|
289
293
|
// Pre-#528, a bare grep recursively scanned the ENTIRE OS filesystem
|
|
290
|
-
// (schema default "/" + the legacy backend's literal pass-through)
|
|
294
|
+
// (schema default "/" + the legacy backend's literal pass-through);
|
|
295
|
+
// under the virtual root the default itself means the workspace.
|
|
291
296
|
const result = await invokeOnce(
|
|
292
297
|
() => ({
|
|
293
298
|
toolCalls: [
|
|
@@ -303,7 +308,11 @@ describe("plan-mode workspace read boundary (issue #528)", () => {
|
|
|
303
308
|
expect(grepResult).not.toMatch(/permission denied/i);
|
|
304
309
|
});
|
|
305
310
|
|
|
306
|
-
it("an explicit ls of '/'
|
|
311
|
+
it("an explicit ls of '/' lists the workspace root — '/' MEANS the workspace in the virtual dialect", async () => {
|
|
312
|
+
// Deliberate reversal of the pre-#754 pin ("denied honestly"): back then
|
|
313
|
+
// "/" named the OS root, so denial was the honest answer. Under the
|
|
314
|
+
// virtual root "/" IS the workspace root — the model asked for the
|
|
315
|
+
// workspace and gets it.
|
|
307
316
|
const result = await invokeOnce(
|
|
308
317
|
() => ({
|
|
309
318
|
toolCalls: [{ name: "ls", args: { path: "/" }, id: "c_ls_slash" }],
|
|
@@ -312,9 +321,9 @@ describe("plan-mode workspace read boundary (issue #528)", () => {
|
|
|
312
321
|
"t_ls_slash",
|
|
313
322
|
);
|
|
314
323
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
);
|
|
324
|
+
const lsResult = toolResultById(result.messages, "c_ls_slash");
|
|
325
|
+
expect(lsResult).toContain("src");
|
|
326
|
+
expect(lsResult).not.toMatch(/permission denied/i);
|
|
318
327
|
});
|
|
319
328
|
|
|
320
329
|
it("writes stay denied everywhere — the read-allow rule admits reads only", async () => {
|
|
@@ -333,12 +342,12 @@ describe("plan-mode workspace read boundary (issue #528)", () => {
|
|
|
333
342
|
});
|
|
334
343
|
});
|
|
335
344
|
|
|
336
|
-
describe("plan-mode
|
|
345
|
+
describe("plan-mode boundary with a glob-special workspace root (issues #528/#754)", () => {
|
|
337
346
|
// Desktop localPath workspaces use the user's real project directory AS
|
|
338
|
-
// the root — names like "My (work) [v2]" are legal there.
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
347
|
+
// the root — names like "My (work) [v2]" are legal there. Pre-#754 the
|
|
348
|
+
// read-allow rule embedded the root as a glob and needed escapeGlobLiteral
|
|
349
|
+
// to survive such names; the rules no longer embed the root at all, so
|
|
350
|
+
// this suite now pins that special-character roots just work.
|
|
342
351
|
let base: string;
|
|
343
352
|
let root: string;
|
|
344
353
|
let observer: CasCaptureObserver;
|
|
@@ -363,7 +372,7 @@ describe("plan-mode read boundary with a glob-special workspace root (issue #528
|
|
|
363
372
|
)) as { messages: BaseMessage[] };
|
|
364
373
|
}
|
|
365
374
|
|
|
366
|
-
it("in-root reads work, absolute and relative alike;
|
|
375
|
+
it("in-root reads work, absolute and relative alike; host files stay unreachable", async () => {
|
|
367
376
|
const result = await invokeOnce(
|
|
368
377
|
() => ({
|
|
369
378
|
toolCalls: [
|
|
@@ -378,6 +387,10 @@ describe("plan-mode read boundary with a glob-special workspace root (issue #528
|
|
|
378
387
|
|
|
379
388
|
expect(toolResultById(result.messages, "c_abs")).toContain("PLAN_MODE_README_TOKEN");
|
|
380
389
|
expect(toolResultById(result.messages, "c_rel")).toContain("PLAN_MODE_README_TOKEN");
|
|
381
|
-
|
|
390
|
+
// "/etc/hosts" names the WORKSPACE's etc/hosts, which doesn't exist —
|
|
391
|
+
// the host file is structurally unaddressable.
|
|
392
|
+
const outResult = toolResultById(result.messages, "c_out");
|
|
393
|
+
expect(outResult).toMatch(/not found|no such file/i);
|
|
394
|
+
expect(outResult).not.toContain("localhost");
|
|
382
395
|
});
|
|
383
396
|
});
|
|
@@ -411,6 +411,58 @@ describe("buildEnhancedSystemPrompt", () => {
|
|
|
411
411
|
});
|
|
412
412
|
});
|
|
413
413
|
|
|
414
|
+
describe("declared preferences", () => {
|
|
415
|
+
const base = {
|
|
416
|
+
instructions: "Test",
|
|
417
|
+
provisionResults: [],
|
|
418
|
+
containerRoot: "",
|
|
419
|
+
skillsPromptSection: "",
|
|
420
|
+
workspaceFileRefs: [],
|
|
421
|
+
workspaceRoot: "",
|
|
422
|
+
injectedFiles: [],
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
it("appends the preferences with per-scope attribution (every-turn injection)", () => {
|
|
426
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
427
|
+
...base,
|
|
428
|
+
declaredPreferences: {
|
|
429
|
+
orgContext: "We deploy to us-east-1.",
|
|
430
|
+
userContext: "Keep answers terse.",
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(prompt).toContain("## Declared preferences");
|
|
435
|
+
expect(prompt).toContain("Declared by the organization:\nWe deploy to us-east-1.");
|
|
436
|
+
expect(prompt).toContain("Declared by the user:\nKeep answers terse.");
|
|
437
|
+
expect(prompt).toContain("Do not repeat them back");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("omits the section when the execution carries no preferences", () => {
|
|
441
|
+
const prompt = buildEnhancedSystemPrompt(base);
|
|
442
|
+
|
|
443
|
+
expect(prompt).not.toContain("## Declared preferences");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("places platform-declared preferences after the sender, before the embedder's session context (DD-002 D3)", () => {
|
|
447
|
+
const prompt = buildEnhancedSystemPrompt({
|
|
448
|
+
...base,
|
|
449
|
+
senderIdentity: { value: "15550001111", kind: "whatsapp_phone" },
|
|
450
|
+
declaredPreferences: { orgContext: "We deploy to us-east-1." },
|
|
451
|
+
sessionContext: "Role: platform admin",
|
|
452
|
+
contextBridge: "User: hi\nAssistant: hello",
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const sender = prompt.indexOf("## Conversation sender");
|
|
456
|
+
const preferences = prompt.indexOf("## Declared preferences");
|
|
457
|
+
const context = prompt.indexOf("## Session context");
|
|
458
|
+
const bridge = prompt.indexOf("## Previous conversation context");
|
|
459
|
+
expect(sender).toBeGreaterThan(-1);
|
|
460
|
+
expect(preferences).toBeGreaterThan(sender);
|
|
461
|
+
expect(context).toBeGreaterThan(preferences);
|
|
462
|
+
expect(bridge).toBeGreaterThan(context);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
414
466
|
describe("plan mode", () => {
|
|
415
467
|
const base = {
|
|
416
468
|
instructions: "Test",
|
|
@@ -55,6 +55,7 @@ vi.mock("@temporalio/activity", () => ({
|
|
|
55
55
|
// on it, which a bare `{ aborted: false }` stub cannot satisfy.
|
|
56
56
|
cancellationSignal: new AbortController().signal,
|
|
57
57
|
heartbeat: vi.fn(),
|
|
58
|
+
info: { taskQueue: "test-queue" },
|
|
58
59
|
}),
|
|
59
60
|
},
|
|
60
61
|
CancelledFailure: class CancelledFailure extends Error {},
|
|
@@ -64,7 +64,7 @@ describe("plan-mode sub-agent filesystem permissions (issue #255)", () => {
|
|
|
64
64
|
workspaceRootDir: root,
|
|
65
65
|
casObserver: observer,
|
|
66
66
|
modelFactory: async () => new ScriptedModel(script),
|
|
67
|
-
...(planMode ? { permissions: buildPlanModePermissions(
|
|
67
|
+
...(planMode ? { permissions: buildPlanModePermissions() } : {}),
|
|
68
68
|
},
|
|
69
69
|
);
|
|
70
70
|
expect(compiled).toHaveLength(1);
|
|
@@ -176,12 +176,14 @@ describe("plan-mode sub-agent filesystem permissions (issue #255)", () => {
|
|
|
176
176
|
expect(readResult).not.toMatch(/path must be absolute/i);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
it("
|
|
180
|
-
// The
|
|
181
|
-
//
|
|
182
|
-
// parent-only fix would leave
|
|
183
|
-
//
|
|
184
|
-
//
|
|
179
|
+
it("confines sub-agent reads to the workspace; the bare-ls default lists it (issues #528/#754)", async () => {
|
|
180
|
+
// The boundary must hold on SUB-AGENT graphs too — they carry their own
|
|
181
|
+
// rules (issue #255), their own normalization shim (#429), and their own
|
|
182
|
+
// virtual-rooted backend (#754), so a parent-only fix would leave
|
|
183
|
+
// sub-agents reading anywhere. One turn, three calls: a host-file read
|
|
184
|
+
// resolves in-workspace and finds nothing, an in-root read flows, a bare
|
|
185
|
+
// ls (schema default "/", the workspace root in the virtual dialect)
|
|
186
|
+
// lists the workspace.
|
|
185
187
|
const script: ScriptSelector = () => ({
|
|
186
188
|
toolCalls: [
|
|
187
189
|
{ name: "read_file", args: { file_path: "/etc/hosts" }, id: "c_read_out" },
|
|
@@ -198,7 +200,8 @@ describe("plan-mode sub-agent filesystem permissions (issue #255)", () => {
|
|
|
198
200
|
)) as { messages: BaseMessage[] };
|
|
199
201
|
|
|
200
202
|
const outResult = toolResultById(result.messages, "c_read_out");
|
|
201
|
-
expect(outResult).toMatch(/
|
|
203
|
+
expect(outResult).toMatch(/not found|no such file/i);
|
|
204
|
+
expect(outResult).not.toContain("localhost");
|
|
202
205
|
|
|
203
206
|
expect(toolResultById(result.messages, "c_read_in")).toContain("PLAN_MODE_README_TOKEN");
|
|
204
207
|
|
|
@@ -18,9 +18,19 @@
|
|
|
18
18
|
export interface ApprovalCaptureResult {
|
|
19
19
|
/** Sanitized JSON args preview, omitted when there are no args to show. */
|
|
20
20
|
readonly argsPreview?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The secret-redacted args object, for stamping `ToolCall.args` on the
|
|
23
|
+
* interrupt-placeholder row. Without it the placeholder carried ONLY the
|
|
24
|
+
* preview string, so every args-driven UI read — the row header's
|
|
25
|
+
* filename-first path above all — rendered nothing for a pending gate
|
|
26
|
+
* (issue #754). Redacted (never raw): the placeholder must not widen the
|
|
27
|
+
* exposure the preview sanitizer bounds.
|
|
28
|
+
*/
|
|
29
|
+
readonly args?: Record<string, unknown>;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
import { sanitizeArgsPreview } from "./status-builder-shared.js";
|
|
33
|
+
import { redactSensitiveArgs } from "../../shared/args-preview.js";
|
|
24
34
|
|
|
25
35
|
/**
|
|
26
36
|
* Correlate a gated `tool_call_id` to its arguments by scanning graph-state
|
|
@@ -76,5 +86,5 @@ export function captureApprovalArtifacts(opts: {
|
|
|
76
86
|
if (!args || Object.keys(args).length === 0) return {};
|
|
77
87
|
|
|
78
88
|
const argsPreview = sanitizeArgsPreview(args) || undefined;
|
|
79
|
-
return { argsPreview };
|
|
89
|
+
return { argsPreview, args: redactSensitiveArgs(args) };
|
|
80
90
|
}
|
|
@@ -25,8 +25,25 @@
|
|
|
25
25
|
* records its before-bytes at authorization time instead (`captureDeleteBefore`,
|
|
26
26
|
* issue #303). A delete via shell (`rm`) stays on the approval gate as always.
|
|
27
27
|
*
|
|
28
|
+
* VIRTUAL ROOT — THE ONE PATH DIALECT (issue #754)
|
|
29
|
+
* ------------------------------------------------
|
|
30
|
+
* Every backend here is constructed with `virtualMode: true`: a leading "/"
|
|
31
|
+
* denotes the WORKSPACE ROOT, traversal (`..`, `~`) is rejected, and every
|
|
32
|
+
* resolution stays inside `rootDir` by construction. This is the same dialect
|
|
33
|
+
* the rest of the harness already speaks — the system prompt's path-resolution
|
|
34
|
+
* directive, `resolveWorkspacePath(..., virtualRoot=true)` in the CAS observer
|
|
35
|
+
* and turn-boundary capture, `InlinePublisher.normalizePath`, and the approval
|
|
36
|
+
* gate's capturability checks. Before this flag, deepagents' legacy default
|
|
37
|
+
* passed absolute paths through to the REAL filesystem: a `write_file("/tmp/x")`
|
|
38
|
+
* escaped the session workspace onto the host AND escaped review entirely
|
|
39
|
+
* (auto-approved as capture-mode flow, but the observer/boundary/publisher all
|
|
40
|
+
* looked inside the workspace and found nothing). Read-side, the same legacy
|
|
41
|
+
* pass-through was why plan mode needed rule-based read fencing at all. Do not
|
|
42
|
+
* remove this flag: workspace confinement is structural, not policy.
|
|
43
|
+
*
|
|
28
44
|
* @since File-Change HITL Redesign (Phase 3 — CAS deep-agent wiring); sub-agent
|
|
29
|
-
* gitignored capture parity (Session 26, DD-19); shell restore (issue #248)
|
|
45
|
+
* gitignored capture parity (Session 26, DD-19); shell restore (issue #248);
|
|
46
|
+
* virtual-root confinement (issue #754)
|
|
30
47
|
*/
|
|
31
48
|
|
|
32
49
|
import { FilesystemBackend, LocalShellBackend } from "deepagents";
|
|
@@ -111,12 +128,12 @@ export async function createCasCaptureBackend(
|
|
|
111
128
|
|
|
112
129
|
if (shellEnv !== undefined) {
|
|
113
130
|
const backend = new CasCaptureShellBackend(
|
|
114
|
-
{ rootDir, env: shellEnv },
|
|
131
|
+
{ rootDir, virtualMode: true, env: shellEnv },
|
|
115
132
|
{ observer },
|
|
116
133
|
);
|
|
117
134
|
await backend.initialize();
|
|
118
135
|
return backend;
|
|
119
136
|
}
|
|
120
137
|
|
|
121
|
-
return new CasCaptureFilesystemBackend({ rootDir }, { observer });
|
|
138
|
+
return new CasCaptureFilesystemBackend({ rootDir, virtualMode: true }, { observer });
|
|
122
139
|
}
|
|
@@ -78,10 +78,33 @@ import { stampFlowedFileEditRows, stampFlowedSubAgentFileEditRows } from "./stam
|
|
|
78
78
|
import { deriveTurnCommandProvenance } from "./command-provenance.js";
|
|
79
79
|
import { describeExecutionError } from "../../shared/model-error.js";
|
|
80
80
|
import { inferProvider, type LlmProvider } from "../../shared/llm-proxy.js";
|
|
81
|
+
import { getShutdownSignalForQueue } from "../../shared/worker-shutdown.js";
|
|
81
82
|
|
|
82
83
|
/** The harness id stamped on the deep-agent's file-review ledger events. */
|
|
83
84
|
const DEEP_AGENT_HARNESS_ID = "deep-agent";
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* The worker-shutdown terminal status (#776). A shutdown is not a pause: the
|
|
88
|
+
* execution will not resume on this worker, so persisting PAUSED would strand
|
|
89
|
+
* the user with a lie. The copy is byte-identical to the Cursor harness's
|
|
90
|
+
* worker-shutdown branch so every downstream status.error consumer (the Go and
|
|
91
|
+
* Java workflow fallbacks, the cloud channel decision table) keys on ONE shape.
|
|
92
|
+
*/
|
|
93
|
+
function buildWorkerShutdownStatus(): AgentExecutionStatus {
|
|
94
|
+
return create(AgentExecutionStatusSchema, {
|
|
95
|
+
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
96
|
+
error: "Execution interrupted: runner worker was shut down. Retry or resume.",
|
|
97
|
+
completedAt: utcTimestamp(),
|
|
98
|
+
messages: [
|
|
99
|
+
create(AgentMessageSchema, {
|
|
100
|
+
type: MessageType.MESSAGE_SYSTEM,
|
|
101
|
+
content: "Execution interrupted: the runner worker was shut down while the agent was still running. You can retry or resume.",
|
|
102
|
+
timestamp: utcTimestamp(),
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
85
108
|
/**
|
|
86
109
|
* Best-effort provider inference for error-message wording. inferProvider
|
|
87
110
|
* throws on unrecognized names; an error path must never throw over a label.
|
|
@@ -115,6 +138,11 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
115
138
|
): Promise<unknown> => {
|
|
116
139
|
const { executionId, threadId, turnSeq } = normalizeActivityInput(arg0, arg1);
|
|
117
140
|
activityStarted();
|
|
141
|
+
// Worker-shutdown classification channel (#776): the pause paths below
|
|
142
|
+
// consult this signal to distinguish "my worker is draining" (SIGTERM,
|
|
143
|
+
// desktop quit) from "the orchestrator cancelled me" (a real user
|
|
144
|
+
// pause). See shared/worker-shutdown.ts for the ownership contract.
|
|
145
|
+
const shutdownSignal = getShutdownSignalForQueue(Context.current().info.taskQueue);
|
|
118
146
|
let setup: SetupResult | null = null;
|
|
119
147
|
// Exclusive turn lock on the workspace working tree — held across the
|
|
120
148
|
// entire tree-mutating window (decision reconcile, agent writes,
|
|
@@ -567,6 +595,13 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
567
595
|
|
|
568
596
|
if (result.terminalStatus) {
|
|
569
597
|
if (initialStatus.phase === ExecutionPhase.EXECUTION_PAUSED) {
|
|
598
|
+
// The stream loop marks PAUSED for ANY platform cancellation; a
|
|
599
|
+
// worker shutdown is not a pause and must not persist as one (#776).
|
|
600
|
+
if (shutdownSignal?.aborted) {
|
|
601
|
+
console.log(`[ExecuteDeepAgent] Cancelled (worker shutdown) for execution ${executionId}: events=${result.eventsProcessed}`);
|
|
602
|
+
await persistStatus(client, executionId, buildWorkerShutdownStatus(), { offload: statusOffload }).catch(() => {});
|
|
603
|
+
throw new CancelledFailure("Activity cancelled (worker shutdown, not user pause)");
|
|
604
|
+
}
|
|
570
605
|
await persistStatus(client, executionId, initialStatus, { offload: statusOffload });
|
|
571
606
|
console.log(`[ExecuteDeepAgent] Paused for execution ${executionId}: events=${result.eventsProcessed}`);
|
|
572
607
|
throw new CancelledFailure("Activity paused by orchestrator");
|
|
@@ -617,15 +652,20 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
617
652
|
policyEngineVersion: intr.policySource ? POLICY_ENGINE_VERSION : "",
|
|
618
653
|
});
|
|
619
654
|
|
|
620
|
-
// Capture a sanitized args preview
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
|
|
655
|
+
// Capture a sanitized args preview AND the redacted args object
|
|
656
|
+
// while the graph is paused, so the approval UI renders the
|
|
657
|
+
// proposed change before the tool runs — and the row header can
|
|
658
|
+
// extract its filename-first path from `args` like every other
|
|
659
|
+
// row (issue #754: a placeholder without args rendered a
|
|
660
|
+
// pathless "Write" header). Args are correlated from the
|
|
661
|
+
// AI-message tool call in graph state (the single source of
|
|
662
|
+
// truth).
|
|
663
|
+
const { argsPreview, args } = captureApprovalArtifacts({
|
|
625
664
|
toolCallId: intr.toolCallId,
|
|
626
665
|
messages: aiMessages,
|
|
627
666
|
});
|
|
628
667
|
if (argsPreview) toolCall.argsPreview = argsPreview;
|
|
668
|
+
if (args) toolCall.args = args as typeof toolCall.args;
|
|
629
669
|
|
|
630
670
|
aiMsg.toolCalls.push(toolCall);
|
|
631
671
|
}
|
|
@@ -749,6 +789,13 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
749
789
|
|
|
750
790
|
} catch (err: unknown) {
|
|
751
791
|
if (err instanceof CancelledFailure) {
|
|
792
|
+
// Worker shutdown is infrastructure failure, not pause — the caught
|
|
793
|
+
// CancelledFailure is itself the interruption evidence (#776).
|
|
794
|
+
if (shutdownSignal?.aborted) {
|
|
795
|
+
console.log(`[ExecuteDeepAgent] Cancelled (worker shutdown) for execution ${executionId}`);
|
|
796
|
+
await persistStatus(client, executionId, buildWorkerShutdownStatus()).catch(() => {});
|
|
797
|
+
throw err;
|
|
798
|
+
}
|
|
752
799
|
console.log(`[ExecuteDeepAgent] Cancelled (pause) for execution ${executionId}`);
|
|
753
800
|
const pausedStatus = create(AgentExecutionStatusSchema, {
|
|
754
801
|
phase: ExecutionPhase.EXECUTION_PAUSED,
|
|
@@ -758,6 +805,11 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
758
805
|
}
|
|
759
806
|
|
|
760
807
|
if (Context.current().cancellationSignal.aborted) {
|
|
808
|
+
if (shutdownSignal?.aborted) {
|
|
809
|
+
console.log(`[ExecuteDeepAgent] Error during worker-shutdown cancellation for ${executionId}: ${err}`);
|
|
810
|
+
await persistStatus(client, executionId, buildWorkerShutdownStatus()).catch(() => {});
|
|
811
|
+
throw new CancelledFailure("Activity cancelled (worker shutdown, not user pause)");
|
|
812
|
+
}
|
|
761
813
|
console.log(`[ExecuteDeepAgent] Error during cancellation for ${executionId}, treating as pause: ${err}`);
|
|
762
814
|
const pausedStatus = create(AgentExecutionStatusSchema, {
|
|
763
815
|
phase: ExecutionPhase.EXECUTION_PAUSED,
|
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
type SenderIdentity,
|
|
18
18
|
} from "../../shared/sender-identity.js";
|
|
19
19
|
import { formatSessionContextText } from "../../shared/session-context.js";
|
|
20
|
+
import {
|
|
21
|
+
formatDeclaredPreferencesText,
|
|
22
|
+
type DeclaredPreferencesContent,
|
|
23
|
+
} from "../../shared/declared-preferences.js";
|
|
20
24
|
import {
|
|
21
25
|
visionDisclosureLines,
|
|
22
26
|
type NotViewableEntry,
|
|
@@ -166,6 +170,14 @@ export interface PromptBuilderInput {
|
|
|
166
170
|
* context is standing session state, like skills.
|
|
167
171
|
*/
|
|
168
172
|
sessionContext?: string;
|
|
173
|
+
/**
|
|
174
|
+
* Platform-declared standing preferences (stigmer/stigmer#293): the org's
|
|
175
|
+
* and user's standing context, server-snapshotted onto the execution
|
|
176
|
+
* spec's `declared_preferences` at create. Injected on EVERY turn like
|
|
177
|
+
* the bridge — the native system prompt is rebuilt per invocation, so an
|
|
178
|
+
* edited preference reaches the very next turn.
|
|
179
|
+
*/
|
|
180
|
+
declaredPreferences?: DeclaredPreferencesContent;
|
|
169
181
|
}
|
|
170
182
|
|
|
171
183
|
// The prompt renders the injector's own result type — a local structural twin
|
|
@@ -229,6 +241,16 @@ export function buildEnhancedSystemPrompt(input: PromptBuilderInput): string {
|
|
|
229
241
|
formatSenderIdentityText(input.senderIdentity);
|
|
230
242
|
}
|
|
231
243
|
|
|
244
|
+
// Platform-declared standing facts precede embedder-supplied context
|
|
245
|
+
// (DD-002 D3): both are standing background, but the declared preferences
|
|
246
|
+
// are platform-authored while session context is the embedder's overlay —
|
|
247
|
+
// the more specific overlay reads later and naturally refines.
|
|
248
|
+
if (input.declaredPreferences) {
|
|
249
|
+
prompt +=
|
|
250
|
+
"\n\n## Declared preferences\n\n" +
|
|
251
|
+
formatDeclaredPreferencesText(input.declaredPreferences);
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
// Standing facts about the user (session context) come before the
|
|
233
255
|
// carried conversation (bridge): the bridge may refer back to them.
|
|
234
256
|
if (input.sessionContext) {
|