@stigmer/runner 3.12.0 → 3.12.2
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/capture-flow.d.ts +5 -4
- package/dist/activities/execute-cursor/capture-flow.js +5 -4
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/cas-observations.d.ts +10 -4
- package/dist/activities/execute-cursor/cas-observations.js +10 -4
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.js +39 -13
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +8 -0
- package/dist/activities/execute-cursor/skill-resolver.js +77 -26
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +23 -8
- package/dist/activities/execute-deep-agent/attachment-injector.js +104 -105
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +3 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +3 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.js +11 -1
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +15 -3
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.js +16 -9
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/generate-session-subject.d.ts +80 -0
- package/dist/activities/generate-session-subject.js +283 -0
- package/dist/activities/generate-session-subject.js.map +1 -0
- package/dist/client/stigmer-client.d.ts +8 -0
- package/dist/client/stigmer-client.js +10 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +25 -10
- package/dist/middleware/approval-gate.js +33 -14
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/otel-spans.d.ts +2 -1
- package/dist/middleware/otel-spans.js +2 -1
- package/dist/middleware/otel-spans.js.map +1 -1
- package/dist/middleware/path-normalization.d.ts +15 -2
- package/dist/middleware/path-normalization.js +39 -5
- package/dist/middleware/path-normalization.js.map +1 -1
- package/dist/runner-manager.js +3 -1
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +3 -1
- package/dist/runner.js.map +1 -1
- package/dist/shared/attachment-vision.js +9 -0
- package/dist/shared/attachment-vision.js.map +1 -1
- package/dist/shared/mcp-enabled-tools.d.ts +6 -2
- package/dist/shared/mcp-enabled-tools.js +6 -2
- package/dist/shared/mcp-enabled-tools.js.map +1 -1
- package/dist/shared/mcp-manager.js +8 -0
- package/dist/shared/mcp-manager.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +46 -10
- package/dist/shared/plan-mode-permissions.js +56 -12
- package/dist/shared/plan-mode-permissions.js.map +1 -1
- package/dist/shared/zip-extract.d.ts +24 -6
- package/dist/shared/zip-extract.js +31 -90
- package/dist/shared/zip-extract.js.map +1 -1
- package/dist/shared/zip-structure.d.ts +61 -0
- package/dist/shared/zip-structure.js +128 -0
- package/dist/shared/zip-structure.js.map +1 -0
- package/package.json +2 -2
- package/src/__test-utils__/zip-fixtures.ts +206 -0
- package/src/activities/__tests__/generate-session-subject.test.ts +348 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +60 -9
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +166 -49
- package/src/activities/execute-cursor/capture-flow.ts +5 -4
- package/src/activities/execute-cursor/cas-observations.ts +10 -4
- package/src/activities/execute-cursor/hook-script.ts +39 -13
- package/src/activities/execute-cursor/skill-resolver.ts +98 -31
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +165 -126
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +246 -37
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +7 -2
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +35 -4
- package/src/activities/execute-deep-agent/attachment-injector.ts +146 -142
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +3 -1
- package/src/activities/execute-deep-agent/prompt-builder.ts +11 -1
- package/src/activities/execute-deep-agent/setup.ts +15 -3
- package/src/activities/execute-deep-agent/subagent-wiring.ts +16 -9
- package/src/activities/generate-session-subject.ts +370 -0
- package/src/client/stigmer-client.ts +11 -0
- package/src/middleware/__tests__/approval-gate.test.ts +130 -1
- package/src/middleware/__tests__/path-normalization.test.ts +29 -3
- package/src/middleware/approval-gate.ts +58 -24
- package/src/middleware/otel-spans.ts +2 -1
- package/src/middleware/path-normalization.ts +42 -5
- package/src/runner-manager.ts +3 -0
- package/src/runner.ts +3 -0
- package/src/shared/__tests__/attachment-vision.test.ts +4 -0
- package/src/shared/__tests__/mcp-manager.test.ts +7 -2
- package/src/shared/__tests__/plan-mode-permissions.test.ts +58 -0
- package/src/shared/__tests__/zip-extract.test.ts +106 -89
- package/src/shared/attachment-vision.ts +9 -0
- package/src/shared/filereview/__tests__/capture.test.ts +48 -0
- package/src/shared/mcp-enabled-tools.ts +6 -2
- package/src/shared/mcp-manager.ts +8 -0
- package/src/shared/plan-mode-permissions.ts +59 -12
- package/src/shared/zip-extract.ts +35 -117
- package/src/shared/zip-structure.ts +181 -0
|
@@ -88,10 +88,11 @@ export interface ApprovalGateConfig {
|
|
|
88
88
|
* `write`/`delete` whose target path is capturable (git-tracked — see
|
|
89
89
|
* {@link isCapturablePath}) FLOWS instead of interrupting: the edit is reviewed
|
|
90
90
|
* post-hoc as a captured `FileChangeSet`, not gated before it runs. A gitignored
|
|
91
|
-
* path is NOT capturable, so it stays gated
|
|
92
|
-
*
|
|
93
|
-
* allow-branch. `shell` and MCP tools
|
|
94
|
-
* default) keeps the classic
|
|
91
|
+
* path is NOT capturable by the git substrate, so it stays gated unless
|
|
92
|
+
* {@link captureIgnored} routes it into CAS capture — the exact twin of the
|
|
93
|
+
* Cursor hook's `__stigmer_is_gitignored` allow-branch. `shell` and MCP tools
|
|
94
|
+
* are never bypassed by this flag. Off (the default) keeps the classic
|
|
95
|
+
* true-pause gate for every mutation.
|
|
95
96
|
*/
|
|
96
97
|
readonly fileCaptureMode?: boolean;
|
|
97
98
|
/**
|
|
@@ -107,12 +108,12 @@ export interface ApprovalGateConfig {
|
|
|
107
108
|
* instead of the interrupt gate, applying the DD-E secret gate. When off, a
|
|
108
109
|
* gitignored path stays on the interrupt gate exactly as before.
|
|
109
110
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
111
|
+
* True only for gates whose backend a CAS observer wraps: the parent gate
|
|
112
|
+
* always (setup.ts), and — since DD-19 — sub-agent gates too, because
|
|
113
|
+
* `compileSubagents` gives every sub-agent a CAS-observing backend wired to
|
|
114
|
+
* the SAME shared observer and `buildSubAgentMiddleware` then inherits this
|
|
115
|
+
* config verbatim. A gate over an UNOBSERVED backend must keep this false:
|
|
116
|
+
* flowing its gitignored edits would apply unobserved, unreviewable bytes.
|
|
116
117
|
*/
|
|
117
118
|
readonly captureIgnored?: boolean;
|
|
118
119
|
/**
|
|
@@ -122,6 +123,20 @@ export interface ApprovalGateConfig {
|
|
|
122
123
|
* secret; the CONTENT never leaves the workspace). Absent ⇒ nothing recorded.
|
|
123
124
|
*/
|
|
124
125
|
readonly recordBlockedSecret?: (rawPath: string) => void;
|
|
126
|
+
/**
|
|
127
|
+
* Capture a file's pre-delete bytes into the CAS observer so a CAS-owned
|
|
128
|
+
* (gitignored / non-git) DELETE can flow under {@link captureIgnored} and be
|
|
129
|
+
* reviewed post-hoc exactly like a write (issue #303) — the delete twin of the
|
|
130
|
+
* backend's `recordBefore` write observation. A delete is the one mutation the
|
|
131
|
+
* backend cannot observe (deepagents has no backend delete method to wrap), so
|
|
132
|
+
* the gate captures at authorization time instead: the file's bytes are still
|
|
133
|
+
* on disk here, and the turn boundary then reads after=null and authors a
|
|
134
|
+
* `FILE_CHANGE_KIND_DELETE` with the same discard-restores contract.
|
|
135
|
+
*
|
|
136
|
+
* Absent ⇒ deletes keep today's interrupt-gate behavior (fail-closed: a delete
|
|
137
|
+
* we cannot capture cannot be reviewed, so it must be approved up front).
|
|
138
|
+
*/
|
|
139
|
+
readonly captureDeleteBefore?: (rawPath: string) => Promise<void>;
|
|
125
140
|
/**
|
|
126
141
|
* Unattended approval mode (ExecutionConfig.approval_mode = UNATTENDED):
|
|
127
142
|
* the creating surface — a messaging channel, a guest share — has no
|
|
@@ -213,22 +228,39 @@ export function createApprovalGateMiddleware(
|
|
|
213
228
|
emitExecutionReceipt(config, toolCall, serverSlug, category, "auto_approve", "file_capture");
|
|
214
229
|
return await handler(request);
|
|
215
230
|
}
|
|
216
|
-
// Gitignored
|
|
217
|
-
//
|
|
218
|
-
//
|
|
231
|
+
// Gitignored / non-git path: a gate whose backend the shared CAS
|
|
232
|
+
// observer wraps (captureIgnored — the parent gate, and sub-agent
|
|
233
|
+
// gates since DD-19) routes it into CAS capture; any other gate falls
|
|
234
|
+
// through to the interrupt gate below.
|
|
219
235
|
if (config.captureIgnored) {
|
|
220
|
-
if (isSecretLikePath(path)) {
|
|
221
|
-
// DD-E fail-closed: a secret-like gitignored edit is NEVER applied
|
|
222
|
-
// and NEVER captured. Record it so the turn boundary authors a
|
|
223
|
-
// DIFF_UNREVIEWABLE entry (blocking approval); nothing is written.
|
|
224
|
-
config.recordBlockedSecret?.(path);
|
|
225
|
-
return secretBlockToolMessage(toolName, path, toolCall.id);
|
|
226
|
-
}
|
|
227
236
|
if (category === "write") {
|
|
237
|
+
if (isSecretLikePath(path)) {
|
|
238
|
+
// DD-E fail-closed: a secret-like gitignored WRITE is NEVER
|
|
239
|
+
// applied and NEVER captured — its content must not surface
|
|
240
|
+
// anywhere, approval prompt included. Record it so the turn
|
|
241
|
+
// boundary authors a DIFF_UNREVIEWABLE entry (blocking
|
|
242
|
+
// approval); nothing is written.
|
|
243
|
+
config.recordBlockedSecret?.(path);
|
|
244
|
+
return secretBlockToolMessage(toolName, path, toolCall.id);
|
|
245
|
+
}
|
|
228
246
|
// Non-secret gitignored write/edit: flows (apply-then-review). The
|
|
229
247
|
// CAS observer already holds its before-bytes and the turn boundary
|
|
230
|
-
// captures it into CAS.
|
|
231
|
-
|
|
248
|
+
// captures it into CAS.
|
|
249
|
+
emitExecutionReceipt(config, toolCall, serverSlug, category, "auto_approve", "file_capture");
|
|
250
|
+
return await handler(request);
|
|
251
|
+
}
|
|
252
|
+
if (category === "delete" && config.captureDeleteBefore && !isSecretLikePath(path)) {
|
|
253
|
+
// Non-secret CAS-owned delete: capture the before-bytes NOW (the
|
|
254
|
+
// one moment they still exist on disk — no backend delete method
|
|
255
|
+
// to observe them), then flow. The turn boundary reads after=null
|
|
256
|
+
// and authors a reviewable, restorable DELETE entry (issue #303).
|
|
257
|
+
//
|
|
258
|
+
// A SECRET-LIKE delete deliberately falls through to the interrupt
|
|
259
|
+
// gate instead: unlike a write, its args expose no secret content,
|
|
260
|
+
// so a human may safely approve it — but its before-bytes must
|
|
261
|
+
// never enter CAS, so it cannot flow. (This also aligns the twins:
|
|
262
|
+
// the Cursor hook routes secret deletes to its deny-gate.)
|
|
263
|
+
await config.captureDeleteBefore(path);
|
|
232
264
|
emitExecutionReceipt(config, toolCall, serverSlug, category, "auto_approve", "file_capture");
|
|
233
265
|
return await handler(request);
|
|
234
266
|
}
|
|
@@ -244,8 +276,10 @@ export function createApprovalGateMiddleware(
|
|
|
244
276
|
// continues) exactly like the capture-mode secret block — a secret write is
|
|
245
277
|
// never applied or persisted in ANY mode. Placed AFTER the capture block so
|
|
246
278
|
// capture-mode paths stay byte-identical (a capturable write already flowed;
|
|
247
|
-
// a captureIgnored gitignored secret is already blocked).
|
|
248
|
-
//
|
|
279
|
+
// a captureIgnored gitignored secret write is already blocked). A delete
|
|
280
|
+
// carries no content, so a secret-like delete needs no hard-block: it stays
|
|
281
|
+
// on the interrupt gate below, where a human may still approve it (its
|
|
282
|
+
// bytes are simply never captured). recordBlockedSecret is a no-op unless a
|
|
249
283
|
// turn boundary reads it (the git-no-storage case, where it authors a
|
|
250
284
|
// content-less DIFF_UNREVIEWABLE).
|
|
251
285
|
if (!serverSlug && category === "write") {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* OpenTelemetry spans and metrics middleware for deep-agent execution.
|
|
3
3
|
*
|
|
4
4
|
* Creates stigmer.llm.call and stigmer.mcp.tool_call spans with
|
|
5
|
-
* attributes matching the
|
|
5
|
+
* attributes matching the span schema inherited from the retired Go
|
|
6
|
+
* workflow-runner (kept stable so dashboards survived the migration).
|
|
6
7
|
* Also records metrics (histograms + counters) for aggregate dashboards.
|
|
7
8
|
*
|
|
8
9
|
* Graceful no-op when no TracerProvider/MeterProvider is configured.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace-relative path normalization for permission-rule-bearing graphs
|
|
3
|
-
* (
|
|
3
|
+
* (issues #429, #528).
|
|
4
4
|
*
|
|
5
5
|
* deepagents' permission enforcement canonicalizes tool-call paths BEFORE any
|
|
6
6
|
* rule or backend runs, and its validation refuses non-absolute paths — on
|
|
@@ -20,13 +20,26 @@
|
|
|
20
20
|
* resolves relative paths under the workspace root, so act mode keeps a
|
|
21
21
|
* byte-zero delta.
|
|
22
22
|
*
|
|
23
|
+
* It also supplies the workspace root when `ls`/`glob`/`grep` are called
|
|
24
|
+
* with NO path argument (issue #528). Those tools' schema default is "/" —
|
|
25
|
+
* the OS ROOT under the legacy backend — and the default is applied inside
|
|
26
|
+
* the tool, after this seam, so under the workspace read boundary a bare
|
|
27
|
+
* first `ls` would otherwise die with `permission denied for read on /`
|
|
28
|
+
* (the same first-turn degradation class #429 fixed). An EXPLICIT "/" is
|
|
29
|
+
* deliberately NOT rewritten: the model asked for the OS root, and the
|
|
30
|
+
* honest answer under plan mode's read boundary is the rules' denial —
|
|
31
|
+
* silently substituting workspace contents would be an answer to a
|
|
32
|
+
* different question.
|
|
33
|
+
*
|
|
23
34
|
* Invariant — nothing becomes newly reachable: only paths whose resolution
|
|
24
35
|
* stays INSIDE the workspace root are rewritten. Escaping relatives (`../x`)
|
|
25
36
|
* and `~`-carrying paths are left raw so today's validation refusal keeps
|
|
26
37
|
* speaking (a naive join would resolve `..` away and smuggle an out-of-root
|
|
27
38
|
* read past validation), and absolute paths pass through byte-untouched.
|
|
28
39
|
* The middleware converts false errors into correct behavior — never a
|
|
29
|
-
* refusal into an allowance the rules didn't decide.
|
|
40
|
+
* refusal into an allowance the rules didn't decide. (The absent-path
|
|
41
|
+
* injection honors the same line: it narrows the tool's own OS-root default
|
|
42
|
+
* to the workspace, granting nothing the rules would refuse.)
|
|
30
43
|
*
|
|
31
44
|
* Tool matching is by bare built-in name, the house doctrine (an MCP server
|
|
32
45
|
* is not expected to shadow a built-in name — see shared/tool-kind.ts); the
|
|
@@ -41,9 +54,8 @@ import type { StigmerMiddleware } from "./types.js";
|
|
|
41
54
|
|
|
42
55
|
/**
|
|
43
56
|
* The path-bearing argument of each deepagents built-in filesystem tool.
|
|
44
|
-
* `ls`/`glob`/`grep` take a base directory `path
|
|
45
|
-
*
|
|
46
|
-
* Confirmed against the installed deepagents' tool definitions.
|
|
57
|
+
* `ls`/`glob`/`grep` take a base directory `path`; the file tools take
|
|
58
|
+
* `file_path`. Confirmed against the installed deepagents' tool definitions.
|
|
47
59
|
*/
|
|
48
60
|
const PATH_ARG_BY_TOOL: ReadonlyMap<string, string> = new Map([
|
|
49
61
|
["ls", "path"],
|
|
@@ -54,6 +66,15 @@ const PATH_ARG_BY_TOOL: ReadonlyMap<string, string> = new Map([
|
|
|
54
66
|
["edit_file", "file_path"],
|
|
55
67
|
]);
|
|
56
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The tools whose path argument is optional with an OS-root ("/") schema
|
|
71
|
+
* default. When the model omits it, this middleware supplies the workspace
|
|
72
|
+
* root instead (issue #528). The file tools are deliberately excluded: an
|
|
73
|
+
* absent `file_path` is a genuine model error, and the tool's own input
|
|
74
|
+
* validation gives the better message.
|
|
75
|
+
*/
|
|
76
|
+
const DIR_DEFAULTING_TOOLS: ReadonlySet<string> = new Set(["ls", "glob", "grep"]);
|
|
77
|
+
|
|
57
78
|
export interface PathNormalizationConfig {
|
|
58
79
|
/** The workspace root the graph's filesystem backend resolves against. */
|
|
59
80
|
readonly rootDir: string;
|
|
@@ -108,6 +129,22 @@ export function createPathNormalizationMiddleware(
|
|
|
108
129
|
if (!argKey) return handler(request);
|
|
109
130
|
|
|
110
131
|
const raw = request.toolCall.args[argKey];
|
|
132
|
+
|
|
133
|
+
// Absent base directory on ls/glob/grep: the middleware sees the
|
|
134
|
+
// model's raw args, BEFORE the tool's zod parse applies the "/"
|
|
135
|
+
// (OS root) schema default — so the omission must be filled here,
|
|
136
|
+
// where the workspace root is known. An explicit "/" is not this
|
|
137
|
+
// case and flows through to an honest rule denial (header doctrine).
|
|
138
|
+
if (raw == null && DIR_DEFAULTING_TOOLS.has(request.toolCall.name)) {
|
|
139
|
+
return handler({
|
|
140
|
+
...request,
|
|
141
|
+
toolCall: {
|
|
142
|
+
...request.toolCall,
|
|
143
|
+
args: { ...request.toolCall.args, [argKey]: rootDir },
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
111
148
|
if (typeof raw !== "string") return handler(request);
|
|
112
149
|
|
|
113
150
|
const normalized = normalizeWorkspacePathArg(raw, rootDir);
|
package/src/runner-manager.ts
CHANGED
|
@@ -703,6 +703,7 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
|
|
|
703
703
|
{ createCursorActivities },
|
|
704
704
|
{ createDeepAgentActivities },
|
|
705
705
|
{ createEnsureThreadActivities },
|
|
706
|
+
{ createGenerateSessionSubjectActivities },
|
|
706
707
|
{ createClassifyToolApprovalsActivities },
|
|
707
708
|
{ createDiscoverMcpServerActivities },
|
|
708
709
|
{ createEvaluateExpressionsActivities },
|
|
@@ -721,6 +722,7 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
|
|
|
721
722
|
import("./activities/execute-cursor/index.js"),
|
|
722
723
|
import("./activities/execute-deep-agent/index.js"),
|
|
723
724
|
import("./activities/ensure-thread.js"),
|
|
725
|
+
import("./activities/generate-session-subject.js"),
|
|
724
726
|
import("./activities/classify-tool-approvals.js"),
|
|
725
727
|
import("./activities/discover-mcp-server.js"),
|
|
726
728
|
import("./activities/evaluate-expressions.js"),
|
|
@@ -741,6 +743,7 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
|
|
|
741
743
|
...createCursorActivities(config),
|
|
742
744
|
...createDeepAgentActivities(config),
|
|
743
745
|
...createEnsureThreadActivities(),
|
|
746
|
+
...createGenerateSessionSubjectActivities(config),
|
|
744
747
|
...createClassifyToolApprovalsActivities(config),
|
|
745
748
|
...createDiscoverMcpServerActivities(config),
|
|
746
749
|
...createEvaluateExpressionsActivities(),
|
package/src/runner.ts
CHANGED
|
@@ -407,6 +407,7 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
|
|
|
407
407
|
{ createCursorActivities },
|
|
408
408
|
{ createDeepAgentActivities },
|
|
409
409
|
{ createEnsureThreadActivities },
|
|
410
|
+
{ createGenerateSessionSubjectActivities },
|
|
410
411
|
{ createClassifyToolApprovalsActivities },
|
|
411
412
|
{ createDiscoverMcpServerActivities },
|
|
412
413
|
{ createEvaluateExpressionsActivities },
|
|
@@ -425,6 +426,7 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
|
|
|
425
426
|
import("./activities/execute-cursor/index.js"),
|
|
426
427
|
import("./activities/execute-deep-agent/index.js"),
|
|
427
428
|
import("./activities/ensure-thread.js"),
|
|
429
|
+
import("./activities/generate-session-subject.js"),
|
|
428
430
|
import("./activities/classify-tool-approvals.js"),
|
|
429
431
|
import("./activities/discover-mcp-server.js"),
|
|
430
432
|
import("./activities/evaluate-expressions.js"),
|
|
@@ -445,6 +447,7 @@ async function createAllActivities(config: Config): Promise<WorkerActivities> {
|
|
|
445
447
|
...createCursorActivities(config),
|
|
446
448
|
...createDeepAgentActivities(config),
|
|
447
449
|
...createEnsureThreadActivities(),
|
|
450
|
+
...createGenerateSessionSubjectActivities(config),
|
|
448
451
|
...createClassifyToolApprovalsActivities(config),
|
|
449
452
|
...createDiscoverMcpServerActivities(config),
|
|
450
453
|
...createEvaluateExpressionsActivities(),
|
|
@@ -291,6 +291,10 @@ describe("VisionBudget — size and count budgets", () => {
|
|
|
291
291
|
});
|
|
292
292
|
|
|
293
293
|
it("ships the production constants agreed in T04 (raw bytes)", () => {
|
|
294
|
+
// Also the ADVERTISED == ENFORCED drift alarm (stigmer/stigmer#365):
|
|
295
|
+
// the registry document advertises these exact values in its
|
|
296
|
+
// `limits.vision` block, pinned by the cloud codec's own test. If this
|
|
297
|
+
// fails, the budget changed on one side only — update both together.
|
|
294
298
|
expect(MAX_VISION_IMAGE_BYTES).toBe(3 * 1024 * 1024);
|
|
295
299
|
expect(MAX_VISION_TOTAL_BYTES).toBe(4 * 1024 * 1024);
|
|
296
300
|
expect(MAX_VISION_IMAGES).toBe(10);
|
|
@@ -66,7 +66,7 @@ describe("toMcpClientConfig", () => {
|
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
it("maps HTTP servers to streamable_http transport", () => {
|
|
69
|
+
it("maps HTTP servers to streamable_http transport with reconnect enabled", () => {
|
|
70
70
|
const servers = [
|
|
71
71
|
makeServer({
|
|
72
72
|
slug: "github",
|
|
@@ -80,10 +80,14 @@ describe("toMcpClientConfig", () => {
|
|
|
80
80
|
transport: "http",
|
|
81
81
|
url: "https://api.github.com/mcp",
|
|
82
82
|
headers: { Authorization: "Bearer tok" },
|
|
83
|
+
// oss#316: severed remote connections (e.g. the hosted bridge
|
|
84
|
+
// restarting mid-execution) re-initialize instead of stranding the
|
|
85
|
+
// agent on a dead session.
|
|
86
|
+
reconnect: { enabled: true, maxAttempts: 3, delayMs: 1_000 },
|
|
83
87
|
});
|
|
84
88
|
});
|
|
85
89
|
|
|
86
|
-
it("maps SSE servers to streamable_http transport", () => {
|
|
90
|
+
it("maps SSE servers to streamable_http transport with reconnect enabled", () => {
|
|
87
91
|
const servers = [
|
|
88
92
|
makeServer({
|
|
89
93
|
slug: "sse-server",
|
|
@@ -96,6 +100,7 @@ describe("toMcpClientConfig", () => {
|
|
|
96
100
|
transport: "http",
|
|
97
101
|
url: "https://sse.example.com/mcp",
|
|
98
102
|
headers: undefined,
|
|
103
|
+
reconnect: { enabled: true, maxAttempts: 3, delayMs: 1_000 },
|
|
99
104
|
});
|
|
100
105
|
});
|
|
101
106
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit pins for the plan-mode rule builder (issue #528).
|
|
3
|
+
*
|
|
4
|
+
* The rule ORDER and SHAPE are the security boundary: deepagents evaluates
|
|
5
|
+
* first-match-wins with a permissive default, so the workspace read-allow
|
|
6
|
+
* must precede the read-deny, and the deny patterns must be the match-all
|
|
7
|
+
* "/**". These tests pin the built value; the boundary's end-to-end
|
|
8
|
+
* behavior (including matcher semantics for escaped roots and symlinked
|
|
9
|
+
* platform paths) is pinned against the real deepagents runtime in
|
|
10
|
+
* execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from "vitest";
|
|
14
|
+
import {
|
|
15
|
+
buildPlanModePermissions,
|
|
16
|
+
escapeGlobLiteral,
|
|
17
|
+
} from "../plan-mode-permissions.js";
|
|
18
|
+
|
|
19
|
+
describe("buildPlanModePermissions", () => {
|
|
20
|
+
it("builds the policy in enforcement order: workspace read-allow, then read-deny, then write-deny", () => {
|
|
21
|
+
expect(buildPlanModePermissions("/ws/session-1/repo")).toEqual([
|
|
22
|
+
{ operations: ["read"], paths: ["/ws/session-1/repo/**"] },
|
|
23
|
+
{ operations: ["read"], paths: ["/**"], mode: "deny" },
|
|
24
|
+
{ operations: ["write"], paths: ["/**"], mode: "deny" },
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("canonicalizes the root before building the pattern", () => {
|
|
29
|
+
// Enforcement canonicalizes incoming paths (no trailing separator,
|
|
30
|
+
// collapsed slashes) before matching — a pattern built from a raw
|
|
31
|
+
// trailing-slash root would silently match nothing.
|
|
32
|
+
const [allow] = buildPlanModePermissions("/ws//session-1/repo/");
|
|
33
|
+
expect(allow.paths).toEqual(["/ws/session-1/repo/**"]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("glob-escapes the root so special characters match literally", () => {
|
|
37
|
+
const [allow] = buildPlanModePermissions("/Users/x/My (work) [v2]");
|
|
38
|
+
expect(allow.paths).toEqual(["/Users/x/My \\(work\\) \\[v2\\]/**"]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("escapeGlobLiteral", () => {
|
|
43
|
+
it("escapes every micromatch metacharacter", () => {
|
|
44
|
+
expect(escapeGlobLiteral("a*b?c(d)e[f]g{h}i!j+k@l")).toBe(
|
|
45
|
+
"a\\*b\\?c\\(d\\)e\\[f\\]g\\{h\\}i\\!j\\+k\\@l",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("escapes backslashes themselves", () => {
|
|
50
|
+
expect(escapeGlobLiteral("a\\b")).toBe("a\\\\b");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("leaves plain path characters alone", () => {
|
|
54
|
+
expect(escapeGlobLiteral("/ws/session-1/repo.dir")).toBe(
|
|
55
|
+
"/ws/session-1/repo.dir",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,89 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { deflateRawSync } from "node:zlib";
|
|
3
2
|
import { extractZipFileEntries } from "../zip-extract.js";
|
|
4
|
-
|
|
5
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Build a minimal valid ZIP archive from an array of { name, content } entries.
|
|
9
|
-
* Uses stored (method 0) compression for simplicity. Produces local file
|
|
10
|
-
* headers only (no central directory) — sufficient for our parser.
|
|
11
|
-
*/
|
|
12
|
-
function buildStoredZip(files: { name: string; content: string }[]): Uint8Array {
|
|
13
|
-
const parts: Uint8Array[] = [];
|
|
14
|
-
|
|
15
|
-
for (const file of files) {
|
|
16
|
-
const nameBytes = new TextEncoder().encode(file.name);
|
|
17
|
-
const contentBytes = new TextEncoder().encode(file.content);
|
|
18
|
-
const isDir = file.name.endsWith("/");
|
|
19
|
-
|
|
20
|
-
// Local file header: 30 bytes
|
|
21
|
-
const header = new ArrayBuffer(30);
|
|
22
|
-
const view = new DataView(header);
|
|
23
|
-
view.setUint32(0, 0x04034b50, true); // signature
|
|
24
|
-
view.setUint16(4, 20, true); // version needed
|
|
25
|
-
view.setUint16(6, 0, true); // general purpose flags
|
|
26
|
-
view.setUint16(8, 0, true); // compression method (stored)
|
|
27
|
-
view.setUint16(10, 0, true); // last mod time
|
|
28
|
-
view.setUint16(12, 0, true); // last mod date
|
|
29
|
-
view.setUint32(14, 0, true); // crc-32 (unused for our purposes)
|
|
30
|
-
view.setUint32(18, isDir ? 0 : contentBytes.length, true); // compressed size
|
|
31
|
-
view.setUint32(22, isDir ? 0 : contentBytes.length, true); // uncompressed size
|
|
32
|
-
view.setUint16(26, nameBytes.length, true); // file name length
|
|
33
|
-
view.setUint16(28, 0, true); // extra field length
|
|
34
|
-
|
|
35
|
-
parts.push(new Uint8Array(header));
|
|
36
|
-
parts.push(nameBytes);
|
|
37
|
-
if (!isDir) {
|
|
38
|
-
parts.push(contentBytes);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
43
|
-
const result = new Uint8Array(totalLength);
|
|
44
|
-
let offset = 0;
|
|
45
|
-
for (const part of parts) {
|
|
46
|
-
result.set(part, offset);
|
|
47
|
-
offset += part.length;
|
|
48
|
-
}
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Build a ZIP archive with a single deflated (method 8) entry.
|
|
54
|
-
*/
|
|
55
|
-
function buildDeflatedZip(name: string, content: string): Uint8Array {
|
|
56
|
-
const nameBytes = new TextEncoder().encode(name);
|
|
57
|
-
const contentBytes = new TextEncoder().encode(content);
|
|
58
|
-
const compressed = deflateRawSync(contentBytes);
|
|
59
|
-
|
|
60
|
-
const header = new ArrayBuffer(30);
|
|
61
|
-
const view = new DataView(header);
|
|
62
|
-
view.setUint32(0, 0x04034b50, true);
|
|
63
|
-
view.setUint16(4, 20, true);
|
|
64
|
-
view.setUint16(6, 0, true);
|
|
65
|
-
view.setUint16(8, 8, true); // deflated
|
|
66
|
-
view.setUint16(10, 0, true);
|
|
67
|
-
view.setUint16(12, 0, true);
|
|
68
|
-
view.setUint32(14, 0, true);
|
|
69
|
-
view.setUint32(18, compressed.length, true);
|
|
70
|
-
view.setUint32(22, contentBytes.length, true);
|
|
71
|
-
view.setUint16(26, nameBytes.length, true);
|
|
72
|
-
view.setUint16(28, 0, true);
|
|
73
|
-
|
|
74
|
-
const totalLength = 30 + nameBytes.length + compressed.length;
|
|
75
|
-
const result = new Uint8Array(totalLength);
|
|
76
|
-
result.set(new Uint8Array(header), 0);
|
|
77
|
-
result.set(nameBytes, 30);
|
|
78
|
-
result.set(new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.length), 30 + nameBytes.length);
|
|
79
|
-
return result;
|
|
80
|
-
}
|
|
3
|
+
import { buildZip } from "../../__test-utils__/zip-fixtures.js";
|
|
81
4
|
|
|
82
5
|
// ─── extractZipFileEntries ───────────────────────────────────────────────
|
|
83
6
|
|
|
84
7
|
describe("extractZipFileEntries", () => {
|
|
85
8
|
it("extracts stored files from a ZIP archive", async () => {
|
|
86
|
-
const zip =
|
|
9
|
+
const zip = buildZip([
|
|
87
10
|
{ name: "SKILL.md", content: "# My Skill" },
|
|
88
11
|
{ name: "references/schema.md", content: "# Schema\n\nTable definitions." },
|
|
89
12
|
]);
|
|
@@ -96,7 +19,7 @@ describe("extractZipFileEntries", () => {
|
|
|
96
19
|
|
|
97
20
|
it("extracts deflated files", async () => {
|
|
98
21
|
const content = "This content is compressed with deflate.";
|
|
99
|
-
const zip =
|
|
22
|
+
const zip = buildZip([{ name: "notes.txt", content, method: "deflated" }]);
|
|
100
23
|
|
|
101
24
|
const entries = await extractZipFileEntries(zip);
|
|
102
25
|
expect(entries).toHaveLength(1);
|
|
@@ -104,7 +27,7 @@ describe("extractZipFileEntries", () => {
|
|
|
104
27
|
});
|
|
105
28
|
|
|
106
29
|
it("skips directory entries", async () => {
|
|
107
|
-
const zip =
|
|
30
|
+
const zip = buildZip([
|
|
108
31
|
{ name: "references/", content: "" },
|
|
109
32
|
{ name: "references/data.md", content: "data" },
|
|
110
33
|
]);
|
|
@@ -115,7 +38,7 @@ describe("extractZipFileEntries", () => {
|
|
|
115
38
|
});
|
|
116
39
|
|
|
117
40
|
it("excludes files by basename", async () => {
|
|
118
|
-
const zip =
|
|
41
|
+
const zip = buildZip([
|
|
119
42
|
{ name: "SKILL.md", content: "# Skill" },
|
|
120
43
|
{ name: "references/schema.md", content: "# Schema" },
|
|
121
44
|
{ name: "scripts/run.py", content: "print('hi')" },
|
|
@@ -127,7 +50,7 @@ describe("extractZipFileEntries", () => {
|
|
|
127
50
|
});
|
|
128
51
|
|
|
129
52
|
it("excludes files by full path", async () => {
|
|
130
|
-
const zip =
|
|
53
|
+
const zip = buildZip([
|
|
131
54
|
{ name: "a.md", content: "a" },
|
|
132
55
|
{ name: "nested/a.md", content: "nested" },
|
|
133
56
|
]);
|
|
@@ -138,7 +61,7 @@ describe("extractZipFileEntries", () => {
|
|
|
138
61
|
});
|
|
139
62
|
|
|
140
63
|
it("excludes entries matching basename even when nested", async () => {
|
|
141
|
-
const zip =
|
|
64
|
+
const zip = buildZip([
|
|
142
65
|
{ name: "SKILL.md", content: "root" },
|
|
143
66
|
{ name: "sub/SKILL.md", content: "sub" },
|
|
144
67
|
{ name: "data.md", content: "data" },
|
|
@@ -166,7 +89,7 @@ describe("extractZipFileEntries", () => {
|
|
|
166
89
|
});
|
|
167
90
|
|
|
168
91
|
it("handles multiple files with nested directories", async () => {
|
|
169
|
-
const zip =
|
|
92
|
+
const zip = buildZip([
|
|
170
93
|
{ name: "SKILL.md", content: "# Skill" },
|
|
171
94
|
{ name: "references/", content: "" },
|
|
172
95
|
{ name: "references/database-schema.md", content: "# Schema" },
|
|
@@ -184,7 +107,7 @@ describe("extractZipFileEntries", () => {
|
|
|
184
107
|
});
|
|
185
108
|
|
|
186
109
|
it("returns all entries when no exclude option is provided", async () => {
|
|
187
|
-
const zip =
|
|
110
|
+
const zip = buildZip([
|
|
188
111
|
{ name: "a.txt", content: "aaa" },
|
|
189
112
|
{ name: "b.txt", content: "bbb" },
|
|
190
113
|
]);
|
|
@@ -194,11 +117,105 @@ describe("extractZipFileEntries", () => {
|
|
|
194
117
|
});
|
|
195
118
|
|
|
196
119
|
it("returns all entries when exclude list is empty", async () => {
|
|
197
|
-
const zip =
|
|
198
|
-
{ name: "a.txt", content: "aaa" },
|
|
199
|
-
]);
|
|
120
|
+
const zip = buildZip([{ name: "a.txt", content: "aaa" }]);
|
|
200
121
|
|
|
201
122
|
const entries = await extractZipFileEntries(zip, { exclude: [] });
|
|
202
123
|
expect(entries).toHaveLength(1);
|
|
203
124
|
});
|
|
125
|
+
|
|
126
|
+
// ── Streaming entries (issue #450) ─────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
it("extracts Go-default streaming archives (deflated, data descriptors)", async () => {
|
|
129
|
+
const zip = buildZip([
|
|
130
|
+
{ name: "SKILL.md", content: "# Streamed Skill", method: "deflated", streaming: true },
|
|
131
|
+
{ name: "references/notes.md", content: "streamed notes", method: "deflated", streaming: true },
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const entries = await extractZipFileEntries(zip);
|
|
135
|
+
expect(entries).toEqual([
|
|
136
|
+
{ path: "SKILL.md", content: "# Streamed Skill" },
|
|
137
|
+
{ path: "references/notes.md", content: "streamed notes" },
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("extracts a stored streaming entry whose payload embeds the data-descriptor signature", async () => {
|
|
142
|
+
// The four bytes of the descriptor signature (0x08074b50, little-endian
|
|
143
|
+
// "PK\x07\x08") planted mid-content, followed by twelve bytes a
|
|
144
|
+
// descriptor-scanning parser would misread as CRC and sizes. The old
|
|
145
|
+
// local-header walk truncated this entry at the planted signature and
|
|
146
|
+
// desynchronized everything after it — the exact defect of issue #450.
|
|
147
|
+
const poisoned = "before PK\u0007\u0008AAAABBBBCCCC after — full content survives";
|
|
148
|
+
const zip = buildZip([
|
|
149
|
+
{ name: "poison.md", content: poisoned, streaming: true },
|
|
150
|
+
{ name: "after.md", content: "the entry after the poisoned one" },
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const entries = await extractZipFileEntries(zip);
|
|
154
|
+
expect(entries).toEqual([
|
|
155
|
+
{ path: "poison.md", content: poisoned },
|
|
156
|
+
{ path: "after.md", content: "the entry after the poisoned one" },
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("uses central-directory sizes when local header sizes are zeroed", async () => {
|
|
161
|
+
const content = "sizes live only in the central directory";
|
|
162
|
+
const zip = buildZip([{ name: "cd-sizes.txt", content, streaming: true }]);
|
|
163
|
+
|
|
164
|
+
const entries = await extractZipFileEntries(zip);
|
|
165
|
+
expect(entries).toEqual([{ path: "cd-sizes.txt", content }]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── Central directory edge cases ───────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
it("locates the EOCD behind a trailing archive comment", async () => {
|
|
171
|
+
const zip = buildZip([{ name: "a.txt", content: "aaa" }], {
|
|
172
|
+
comment: "release archive — built by tooling",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const entries = await extractZipFileEntries(zip);
|
|
176
|
+
expect(entries).toEqual([{ path: "a.txt", content: "aaa" }]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("is not fooled by EOCD signature bytes inside the archive comment", async () => {
|
|
180
|
+
// "PK\x05\x06" inside the comment is a decoy EOCD; validation must
|
|
181
|
+
// reject it (its "fields" are comment text) and keep scanning backward
|
|
182
|
+
// to the real record.
|
|
183
|
+
const zip = buildZip([{ name: "a.txt", content: "aaa" }], {
|
|
184
|
+
comment: "decoy: PK\u0005\u0006 not a real record",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const entries = await extractZipFileEntries(zip);
|
|
188
|
+
expect(entries).toEqual([{ path: "a.txt", content: "aaa" }]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns empty array when the central directory is missing", async () => {
|
|
192
|
+
// Local headers and payloads only — a download truncated before the
|
|
193
|
+
// archive's index. The parser must not fall back to guessing from
|
|
194
|
+
// local headers (design record 017).
|
|
195
|
+
const zip = buildZip(
|
|
196
|
+
[
|
|
197
|
+
{ name: "a.txt", content: "aaa" },
|
|
198
|
+
{ name: "b.txt", content: "bbb" },
|
|
199
|
+
],
|
|
200
|
+
{ omitCentralDirectory: true },
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const entries = await extractZipFileEntries(zip);
|
|
204
|
+
expect(entries).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("returns empty array for an archive with entries but a truncated tail", async () => {
|
|
208
|
+
const zip = buildZip([{ name: "a.txt", content: "aaa" }]);
|
|
209
|
+
const truncated = zip.subarray(0, zip.length - 10); // clips into the EOCD
|
|
210
|
+
|
|
211
|
+
const entries = await extractZipFileEntries(truncated);
|
|
212
|
+
expect(entries).toEqual([]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("extracts an empty archive (EOCD only) as no entries", async () => {
|
|
216
|
+
const zip = buildZip([]);
|
|
217
|
+
|
|
218
|
+
const entries = await extractZipFileEntries(zip);
|
|
219
|
+
expect(entries).toEqual([]);
|
|
220
|
+
});
|
|
204
221
|
});
|
|
@@ -32,6 +32,15 @@
|
|
|
32
32
|
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
// Budget constants (owner decision, 2026-08-09; project T04)
|
|
35
|
+
//
|
|
36
|
+
// ADVERTISED == ENFORCED (stigmer/stigmer#365): the model registry document
|
|
37
|
+
// advertises these values in its `limits.vision` block (rendered by the
|
|
38
|
+
// cloud's ModelRegistryDocumentCodec, mirrored verbatim by the OSS server)
|
|
39
|
+
// so clients can warn BEFORE an upload this budget would degrade. Both sides
|
|
40
|
+
// pin the literal values in tests — attachment-vision.test.ts here, the
|
|
41
|
+
// codec test there — so changing either side alone fails its own suite.
|
|
42
|
+
// Update both together; deploy the runner first if the budget ever shrinks
|
|
43
|
+
// (never advertise more than the runner accepts).
|
|
35
44
|
// ---------------------------------------------------------------------------
|
|
36
45
|
|
|
37
46
|
/**
|