@stigmer/runner 3.11.1-dev.20260812192248 → 3.12.1
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/attachment-resolver.d.ts +11 -0
- package/dist/activities/execute-cursor/attachment-resolver.js +8 -0
- package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
- package/dist/activities/execute-cursor/blueprint-resolver.js +0 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +6 -2
- package/dist/activities/execute-cursor/index.js +16 -32
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +15 -11
- package/dist/activities/execute-cursor/prompt-builder.js +20 -10
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +34 -8
- package/dist/activities/execute-deep-agent/attachment-injector.js +111 -106
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +0 -15
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -2
- package/dist/activities/execute-deep-agent/mcp-gate.js +0 -1
- package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -8
- package/dist/activities/execute-deep-agent/prompt-builder.js +25 -8
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +0 -10
- package/dist/activities/execute-deep-agent/setup.js +13 -45
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/config.d.ts +6 -5
- package/dist/config.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/shared/artifact-storage.d.ts +11 -0
- package/dist/shared/artifact-storage.js +2 -0
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/attachment-download-urls.d.ts +62 -0
- package/dist/shared/attachment-download-urls.js +79 -0
- package/dist/shared/attachment-download-urls.js.map +1 -0
- package/dist/shared/channel-attachment.d.ts +1 -1
- package/dist/shared/channel-attachment.js +1 -1
- package/dist/shared/conversation-attachment.d.ts +2 -3
- package/dist/shared/conversation-attachment.js +2 -3
- package/dist/shared/conversation-attachment.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-transport-guard.d.ts +2 -2
- package/dist/shared/mcp-transport-guard.js +2 -2
- 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/synthesized-attachment.d.ts +4 -4
- package/dist/shared/synthesized-attachment.js +4 -4
- 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__/fake-artifact-storage.ts +10 -1
- package/src/__test-utils__/zip-fixtures.ts +206 -0
- package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +63 -3
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +62 -29
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -42
- package/src/activities/execute-cursor/attachment-resolver.ts +24 -0
- package/src/activities/execute-cursor/blueprint-resolver.ts +1 -10
- package/src/activities/execute-cursor/index.ts +22 -37
- package/src/activities/execute-cursor/prompt-builder.ts +38 -22
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +270 -126
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -2
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +2 -7
- 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 +78 -2
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +35 -4
- package/src/activities/execute-deep-agent/attachment-injector.ts +166 -143
- package/src/activities/execute-deep-agent/index.ts +0 -19
- package/src/activities/execute-deep-agent/mcp-gate.ts +0 -3
- package/src/activities/execute-deep-agent/prompt-builder.ts +38 -16
- package/src/activities/execute-deep-agent/setup.ts +13 -68
- package/src/config.ts +6 -5
- package/src/middleware/__tests__/path-normalization.test.ts +29 -3
- package/src/middleware/path-normalization.ts +42 -5
- package/src/shared/__tests__/artifact-storage.test.ts +9 -0
- package/src/shared/__tests__/attachment-download-urls.test.ts +70 -0
- package/src/shared/__tests__/channel-attachment.test.ts +2 -2
- package/src/shared/__tests__/plan-mode-permissions.test.ts +58 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +19 -41
- package/src/shared/__tests__/zip-extract.test.ts +106 -89
- package/src/shared/artifact-storage.ts +11 -0
- package/src/shared/attachment-download-urls.ts +98 -0
- package/src/shared/channel-attachment.ts +1 -1
- package/src/shared/conversation-attachment.ts +2 -3
- package/src/shared/mcp-enabled-tools.ts +6 -2
- package/src/shared/mcp-transport-guard.ts +2 -2
- package/src/shared/plan-mode-permissions.ts +59 -12
- package/src/shared/synthesized-attachment.ts +4 -4
- package/src/shared/zip-extract.ts +35 -117
- package/src/shared/zip-structure.ts +181 -0
- package/dist/shared/datastore-attachment.d.ts +0 -99
- package/dist/shared/datastore-attachment.js +0 -184
- package/dist/shared/datastore-attachment.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +0 -104
- package/src/shared/__tests__/datastore-attachment.test.ts +0 -293
- package/src/shared/datastore-attachment.ts +0 -209
|
@@ -36,12 +36,6 @@ import { connectMcpServers, type McpConnectionResult } from "../../shared/mcp-ma
|
|
|
36
36
|
import { mergeMcpServerUsages, resolveMcpServers } from "../../shared/mcp-resolver.js";
|
|
37
37
|
import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
|
|
38
38
|
import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
|
|
39
|
-
import {
|
|
40
|
-
DATASTORE_ATTACHMENT_SLUG,
|
|
41
|
-
formatDatastoresSection,
|
|
42
|
-
missingRecordTools,
|
|
43
|
-
synthesizeDatastoreAttachment,
|
|
44
|
-
} from "../../shared/datastore-attachment.js";
|
|
45
39
|
import {
|
|
46
40
|
discoverChannelMessaging,
|
|
47
41
|
formatChannelTemplatesSection,
|
|
@@ -58,7 +52,7 @@ import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
|
|
|
58
52
|
import type { WorkspaceBackend, ProvisionResult } from "../../shared/workspace/types.js";
|
|
59
53
|
import { createCasCaptureBackend } from "./cas-capture-backend.js";
|
|
60
54
|
import { buildShellEnv } from "./shell-env.js";
|
|
61
|
-
import {
|
|
55
|
+
import { buildPlanModePermissions } from "../../shared/plan-mode-permissions.js";
|
|
62
56
|
import { CasCaptureObserver } from "./cas-capture-observer.js";
|
|
63
57
|
import { isGitWorkTree, isPathCapturable } from "../../shared/filereview/git-substrate.js";
|
|
64
58
|
import { deriveCaptureMode } from "../../shared/filereview/capture.js";
|
|
@@ -129,16 +123,6 @@ export interface SetupResult {
|
|
|
129
123
|
readonly session: Session;
|
|
130
124
|
readonly workspaceBackend: WorkspaceBackend;
|
|
131
125
|
readonly mcpConnection: McpConnectionResult | null;
|
|
132
|
-
/**
|
|
133
|
-
* Record tools declared via datastore_usages but absent from the connected
|
|
134
|
-
* stigmer-records roster this turn (issue #325). Empty = healthy (also when
|
|
135
|
-
* the agent uses no datastores). Non-empty selects the degraded prompt
|
|
136
|
-
* section in Step 8, and the activity surfaces it to operators as a
|
|
137
|
-
* MESSAGE_SYSTEM row — the vision-facts pattern: setup computes the honest
|
|
138
|
-
* struct, the caller discloses. Recomputed every turn, so a healed outage
|
|
139
|
-
* stops disclosing on the next turn.
|
|
140
|
-
*/
|
|
141
|
-
readonly datastoreToolsMissing: readonly string[];
|
|
142
126
|
readonly mergedEnvVars: Record<string, string>;
|
|
143
127
|
readonly secretKeys: ReadonlySet<string>;
|
|
144
128
|
readonly modelName: string;
|
|
@@ -369,7 +353,6 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
369
353
|
agent.spec!.mcpServerUsages || [],
|
|
370
354
|
session.spec!.mcpServerUsages || [],
|
|
371
355
|
);
|
|
372
|
-
const datastoreUsages = agent.spec!.datastoreUsages || [];
|
|
373
356
|
|
|
374
357
|
// The synthesized attachments' credential story (DD-006 D4): the
|
|
375
358
|
// exchanged token authenticates the discovery reads per-call (the
|
|
@@ -410,7 +393,6 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
410
393
|
let resolvedMcpServers: Awaited<ReturnType<typeof resolveMcpServers>> | null = null;
|
|
411
394
|
if (shouldConnectMcp({
|
|
412
395
|
mcpServerUsageCount: mcpServerUsages.length,
|
|
413
|
-
datastoreUsageCount: datastoreUsages.length,
|
|
414
396
|
channelMessagingCount: channelMessaging.length,
|
|
415
397
|
conversationChannelId,
|
|
416
398
|
})) {
|
|
@@ -444,25 +426,9 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
444
426
|
envResult.secretKeys,
|
|
445
427
|
);
|
|
446
428
|
|
|
447
|
-
// The
|
|
448
|
-
// backfill so the destructiveHint tightener can never gate
|
|
449
|
-
// approval maps keep it approval-free by construction
|
|
450
|
-
// shared/datastore-attachment.ts).
|
|
451
|
-
if (datastoreUsages.length > 0) {
|
|
452
|
-
const attachment = synthesizeDatastoreAttachment(datastoreUsages, {
|
|
453
|
-
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
454
|
-
credential: attachmentCredential,
|
|
455
|
-
backendEndpoint: config.stigmerBackendEndpoint,
|
|
456
|
-
});
|
|
457
|
-
if (attachment) {
|
|
458
|
-
backfilledServers = injectSynthesizedAttachment(
|
|
459
|
-
backfilledServers, attachment, "datastore records",
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// The channel messaging attachment (DD-006 D7/D8) — the records
|
|
465
|
-
// attachment's twin, injected under the same after-backfill rule.
|
|
429
|
+
// The channel messaging attachment (DD-006 D7/D8) — injected AFTER
|
|
430
|
+
// resolve + backfill so the destructiveHint tightener can never gate
|
|
431
|
+
// it; empty approval maps keep it approval-free by construction.
|
|
466
432
|
if (channelMessaging.length > 0) {
|
|
467
433
|
const attachment = synthesizeChannelAttachment(channelMessaging, {
|
|
468
434
|
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
@@ -477,8 +443,8 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
477
443
|
}
|
|
478
444
|
|
|
479
445
|
// The conversation participation attachment (DD-008 D-c) — the
|
|
480
|
-
//
|
|
481
|
-
// answers undefined with no bridge endpoint by design (see
|
|
446
|
+
// channel attachment's sibling, same after-backfill rule. HTTP-only:
|
|
447
|
+
// synthesize answers undefined with no bridge endpoint by design (see
|
|
482
448
|
// shared/conversation-attachment.ts).
|
|
483
449
|
const conversationAttachment = synthesizeConversationAttachment(conversationChannelId, {
|
|
484
450
|
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
@@ -585,37 +551,12 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
585
551
|
}
|
|
586
552
|
timing.mark("inject_attachments");
|
|
587
553
|
|
|
588
|
-
// Reconcile the declared datastores against the record tools actually
|
|
589
|
-
// connected (issue #325): the records roster always serves all five
|
|
590
|
-
// tools, so any absence means the store is degraded, and the prompt
|
|
591
|
-
// must say so instead of promising tools the agent does not have. The
|
|
592
|
-
// connected roster is post-connect truth — when usages exist, MCP
|
|
593
|
-
// connect always ran above (a thrown connect already failed the run).
|
|
594
|
-
const datastoreToolsMissing = datastoreUsages.length > 0 && mcpConnection
|
|
595
|
-
? missingRecordTools(
|
|
596
|
-
(mcpConnection.serverToolMap[DATASTORE_ATTACHMENT_SLUG] ?? [])
|
|
597
|
-
.map((t) => t.name),
|
|
598
|
-
)
|
|
599
|
-
: [];
|
|
600
|
-
if (datastoreToolsMissing.length > 0) {
|
|
601
|
-
console.warn(
|
|
602
|
-
`[setup] Datastore record tools degraded: declared ` +
|
|
603
|
-
`${datastoreUsages.length} datastore usage(s) but the ` +
|
|
604
|
-
`'${DATASTORE_ATTACHMENT_SLUG}' roster is missing ` +
|
|
605
|
-
`[${datastoreToolsMissing.join(", ")}] — the prompt discloses the ` +
|
|
606
|
-
`outage instead of the available-datastores section.`,
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
554
|
// Step 8: Build enhanced system prompt
|
|
611
555
|
const systemPrompt = buildEnhancedSystemPrompt({
|
|
612
556
|
instructions,
|
|
613
557
|
provisionResults,
|
|
614
558
|
containerRoot: workspaceBackend.rootDir,
|
|
615
559
|
skillsPromptSection,
|
|
616
|
-
datastoresPromptSection: datastoreUsages.length > 0
|
|
617
|
-
? formatDatastoresSection(datastoreUsages, datastoreToolsMissing)
|
|
618
|
-
: undefined,
|
|
619
560
|
// "" (nothing sendable) threads as undefined: the tool alone still
|
|
620
561
|
// serves text sends inside a 24-hour window (DD-006 D6).
|
|
621
562
|
channelTemplatesPromptSection: channelMessaging.length > 0
|
|
@@ -625,6 +566,7 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
625
566
|
workspaceRoot: workspaceBackend.rootDir,
|
|
626
567
|
injectedFiles,
|
|
627
568
|
vision: visionPromptInfo,
|
|
569
|
+
downloadUrlKind: artifactStorage?.downloadUrlKind,
|
|
628
570
|
interactionMode: execution.spec!.executionConfig?.interactionMode,
|
|
629
571
|
buildFromPlan: execution.spec!.executionConfig?.buildFromPlan,
|
|
630
572
|
contextBridge: readContextBridge(session.spec!.metadata),
|
|
@@ -656,8 +598,12 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
656
598
|
// every sub-agent graph, AND the path-normalization middleware (issue #429)
|
|
657
599
|
// all derive from this single value, so a rule-bearing graph can never miss
|
|
658
600
|
// the shim that keeps prompt-compliant relative paths from dying in rule
|
|
659
|
-
// validation (`path must be absolute`).
|
|
660
|
-
|
|
601
|
+
// validation (`path must be absolute`). Built from the same rootDir the
|
|
602
|
+
// backends and the shim resolve against, so the workspace read boundary
|
|
603
|
+
// (issue #528) and the paths it must admit agree by construction.
|
|
604
|
+
const planModePermissions = isPlanMode
|
|
605
|
+
? buildPlanModePermissions(workspaceBackend.rootDir)
|
|
606
|
+
: undefined;
|
|
661
607
|
|
|
662
608
|
const toolServerMap = new Map<string, string>();
|
|
663
609
|
if (mcpConnection) {
|
|
@@ -936,7 +882,6 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
936
882
|
session,
|
|
937
883
|
workspaceBackend,
|
|
938
884
|
mcpConnection,
|
|
939
|
-
datastoreToolsMissing,
|
|
940
885
|
mergedEnvVars: envResult.mergedEnvVars,
|
|
941
886
|
secretKeys: envResult.secretKeys,
|
|
942
887
|
modelName,
|
package/src/config.ts
CHANGED
|
@@ -61,11 +61,12 @@ export interface Config {
|
|
|
61
61
|
readonly stigmerBackendEndpoint: string;
|
|
62
62
|
readonly stigmerToken: string | null;
|
|
63
63
|
/**
|
|
64
|
-
* The MCP bridge endpoint for the runner-synthesized
|
|
65
|
-
*
|
|
66
|
-
* https://mcp.stigmer.ai). Null
|
|
67
|
-
* spawned `stigmer mcp-server` stdio
|
|
68
|
-
* See shared/
|
|
64
|
+
* The MCP bridge endpoint for the runner-synthesized attachments —
|
|
65
|
+
* channel messaging and conversation participation
|
|
66
|
+
* (STIGMER_MCP_BRIDGE_ENDPOINT, e.g. https://mcp.stigmer.ai). Null
|
|
67
|
+
* selects the OSS/local shape: a spawned `stigmer mcp-server` stdio
|
|
68
|
+
* child against the local backend. See shared/channel-attachment.ts
|
|
69
|
+
* and shared/conversation-attachment.ts.
|
|
69
70
|
*/
|
|
70
71
|
readonly mcpBridgeEndpoint: string | null;
|
|
71
72
|
/**
|
|
@@ -104,9 +104,35 @@ describe("createPathNormalizationMiddleware", () => {
|
|
|
104
104
|
expect(args.file_path).toBe("/etc/hosts");
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
it("
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
it("supplies the workspace root when the base path is omitted on ls/glob/grep", async () => {
|
|
108
|
+
// Deliberate reversal of the original #429 pin ("leaves an omitted base
|
|
109
|
+
// path alone"): the tools' schema default is "/" — the OS ROOT under the
|
|
110
|
+
// legacy backend — applied inside the tool AFTER this seam, so under the
|
|
111
|
+
// #528 workspace read boundary an untouched omission would deny the bare
|
|
112
|
+
// first listing. The middleware fills the omission with the root it
|
|
113
|
+
// already knows.
|
|
114
|
+
for (const tool of ["ls", "glob", "grep"]) {
|
|
115
|
+
const args = await argsSeenByHandler(tool, { pattern: "TOKEN" });
|
|
116
|
+
expect(args.path).toBe(ROOT);
|
|
117
|
+
expect(args.pattern).toBe("TOKEN");
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does not fill an omitted file_path on the file tools", async () => {
|
|
122
|
+
// An absent file_path is a genuine model error — the tool's own input
|
|
123
|
+
// validation gives the better message than any path this seam could invent.
|
|
124
|
+
const args = await argsSeenByHandler("read_file", {});
|
|
125
|
+
expect(args.file_path).toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("leaves an explicit '/' untouched on the dir tools — the rules answer it honestly", async () => {
|
|
129
|
+
// The model asked for the OS root; under plan mode's read boundary the
|
|
130
|
+
// honest answer is the rules' denial, not silently substituted workspace
|
|
131
|
+
// contents (the #429 never-convert-a-refusal-into-an-allowance doctrine).
|
|
132
|
+
for (const tool of ["ls", "glob", "grep"]) {
|
|
133
|
+
const args = await argsSeenByHandler(tool, { path: "/" });
|
|
134
|
+
expect(args.path).toBe("/");
|
|
135
|
+
}
|
|
110
136
|
});
|
|
111
137
|
|
|
112
138
|
it("never touches tools outside the built-in filesystem set", async () => {
|
|
@@ -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);
|
|
@@ -49,6 +49,10 @@ describe("LocalArtifactStorage", () => {
|
|
|
49
49
|
expect(url).toBe("http://localhost:7235/artifacts/file.txt");
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
it("self-describes its URLs as local-serve (loopback reach, no expiry — issue #532)", () => {
|
|
53
|
+
expect(storage.downloadUrlKind).toBe("local-serve");
|
|
54
|
+
});
|
|
55
|
+
|
|
52
56
|
it("returns true for existing keys", async () => {
|
|
53
57
|
const key = "artifacts/exec-1/file.bin";
|
|
54
58
|
await storage.upload(key, Buffer.from("data"));
|
|
@@ -167,6 +171,11 @@ describe("ProxyArtifactStorage", () => {
|
|
|
167
171
|
globalThis.fetch = originalFetch;
|
|
168
172
|
});
|
|
169
173
|
|
|
174
|
+
it("self-describes its URLs as presigned (time-limited, remotely fetchable — issue #532)", () => {
|
|
175
|
+
const s = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
176
|
+
expect(s.downloadUrlKind).toBe("presigned");
|
|
177
|
+
});
|
|
178
|
+
|
|
170
179
|
it("uploads via presigned URL flow", async () => {
|
|
171
180
|
const calls: { url: string; method: string; body?: string }[] = [];
|
|
172
181
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the attachment download-URL hand-off policy (issue #532):
|
|
3
|
+
* the branch-independent mint rule, the non-fatal degrade, and the
|
|
4
|
+
* per-kind disclosure wording both harnesses embed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
mintAttachmentDownloadUrl,
|
|
10
|
+
downloadUrlDisclosureLine,
|
|
11
|
+
} from "../attachment-download-urls.js";
|
|
12
|
+
import { makeInMemoryArtifactStorage } from "../../__test-utils__/fake-artifact-storage.js";
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.restoreAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("mintAttachmentDownloadUrl", () => {
|
|
19
|
+
it("mints a URL for a storage key", async () => {
|
|
20
|
+
const { storage } = makeInMemoryArtifactStorage();
|
|
21
|
+
|
|
22
|
+
const url = await mintAttachmentDownloadUrl(storage, "attachments/01A/lease.pdf", "lease.pdf");
|
|
23
|
+
|
|
24
|
+
expect(url).toBe("mem://attachments/01A/lease.pdf");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns undefined when the attachment has no storage key", async () => {
|
|
28
|
+
const { storage } = makeInMemoryArtifactStorage();
|
|
29
|
+
|
|
30
|
+
const url = await mintAttachmentDownloadUrl(storage, "", "local.csv");
|
|
31
|
+
|
|
32
|
+
expect(url).toBeUndefined();
|
|
33
|
+
expect(storage.getDownloadUrl).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns undefined when no storage is available", async () => {
|
|
37
|
+
const url = await mintAttachmentDownloadUrl(undefined, "attachments/01A/lease.pdf", "lease.pdf");
|
|
38
|
+
|
|
39
|
+
expect(url).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("degrades to undefined on a mint failure and logs the degrade (never throws)", async () => {
|
|
43
|
+
const { storage } = makeInMemoryArtifactStorage();
|
|
44
|
+
storage.getDownloadUrl.mockRejectedValueOnce(new Error("presign endpoint unreachable"));
|
|
45
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
46
|
+
|
|
47
|
+
const url = await mintAttachmentDownloadUrl(storage, "attachments/01A/lease.pdf", "lease.pdf");
|
|
48
|
+
|
|
49
|
+
expect(url).toBeUndefined();
|
|
50
|
+
expect(warn).toHaveBeenCalledOnce();
|
|
51
|
+
expect(warn.mock.calls[0][0]).toContain("lease.pdf");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("downloadUrlDisclosureLine", () => {
|
|
56
|
+
it("promises time-limited single-object access for presigned URLs", () => {
|
|
57
|
+
const line = downloadUrlDisclosureLine("presigned");
|
|
58
|
+
|
|
59
|
+
expect(line).toContain("time-limited");
|
|
60
|
+
expect(line).toContain("single file");
|
|
61
|
+
expect(line).not.toContain("this machine");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("is honest about local-serve reach — same machine only, no expiry claim", () => {
|
|
65
|
+
const line = downloadUrlDisclosureLine("local-serve");
|
|
66
|
+
|
|
67
|
+
expect(line).toContain("reachable only from this machine");
|
|
68
|
+
expect(line).not.toContain("time-limited");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The channel messaging attachment (proactive-messaging DD-006 D7/D8):
|
|
3
3
|
* discovery with the never-throw failure posture, both connection
|
|
4
|
-
* shapes, the structural approval-freedom
|
|
5
|
-
*
|
|
4
|
+
* shapes, the structural approval-freedom of synthesized attachments,
|
|
5
|
+
* and the prompt section's filter/order/cap rules
|
|
6
6
|
* (DD-006 D6). The route is the cross-repo string, guarded here and in
|
|
7
7
|
* the mcp-server integration test (the TOOL_CALL_LIMIT precedent); the
|
|
8
8
|
* slug and roster are runner-internal and guarded here alone.
|
|
@@ -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,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Composition of
|
|
3
|
-
* injectSynthesizedAttachment — the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Composition of the synthesized attachments through
|
|
3
|
+
* injectSynthesizedAttachment — chained the way both harness call sites
|
|
4
|
+
* do (channels, then conversation, each after resolve + backfill).
|
|
5
|
+
* Per-slug independence is the property that makes another attachment
|
|
6
|
+
* safe to add: replacing one reserved slug must never disturb its
|
|
7
|
+
* siblings.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it, vi } from "vitest";
|
|
11
11
|
import type { MessagingChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
12
|
-
import { create } from "@bufbuild/protobuf";
|
|
13
|
-
import { DatastoreUsageSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
14
|
-
import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
15
12
|
|
|
16
13
|
import {
|
|
17
14
|
CHANNEL_ATTACHMENT_SLUG,
|
|
@@ -21,10 +18,6 @@ import {
|
|
|
21
18
|
CONVERSATION_ATTACHMENT_SLUG,
|
|
22
19
|
synthesizeConversationAttachment,
|
|
23
20
|
} from "../conversation-attachment.js";
|
|
24
|
-
import {
|
|
25
|
-
DATASTORE_ATTACHMENT_SLUG,
|
|
26
|
-
synthesizeDatastoreAttachment,
|
|
27
|
-
} from "../datastore-attachment.js";
|
|
28
21
|
import { injectSynthesizedAttachment } from "../synthesized-attachment.js";
|
|
29
22
|
import type { ResolvedMcpServer } from "../mcp-resolver.js";
|
|
30
23
|
|
|
@@ -44,47 +37,34 @@ const userServer: ResolvedMcpServer = {
|
|
|
44
37
|
discoveredCapabilitiesEmpty: false,
|
|
45
38
|
};
|
|
46
39
|
|
|
47
|
-
function
|
|
48
|
-
const datastore = synthesizeDatastoreAttachment(
|
|
49
|
-
[create(DatastoreUsageSchema, {
|
|
50
|
-
datastoreRef: create(ApiResourceReferenceSchema, { slug: "clinic" }),
|
|
51
|
-
})],
|
|
52
|
-
options,
|
|
53
|
-
)!;
|
|
40
|
+
function bothAttachments(): ResolvedMcpServer[] {
|
|
54
41
|
const channels = synthesizeChannelAttachment(
|
|
55
42
|
[{ channel: { channel: "isc-whatsapp", provider: "whatsapp" } as MessagingChannel, templates: [] }],
|
|
56
43
|
options,
|
|
57
44
|
)!;
|
|
58
45
|
const conversation = synthesizeConversationAttachment("agch_1", options)!;
|
|
59
46
|
|
|
60
|
-
// The harness order at both call sites:
|
|
61
|
-
let servers = injectSynthesizedAttachment([userServer],
|
|
62
|
-
servers = injectSynthesizedAttachment(servers, channels, "channel messaging");
|
|
47
|
+
// The harness order at both call sites: channels, then conversation.
|
|
48
|
+
let servers = injectSynthesizedAttachment([userServer], channels, "channel messaging");
|
|
63
49
|
return injectSynthesizedAttachment(servers, conversation, "conversation participation");
|
|
64
50
|
}
|
|
65
51
|
|
|
66
|
-
describe("
|
|
67
|
-
it("composes
|
|
68
|
-
expect(
|
|
52
|
+
describe("synthesized attachments in one chain", () => {
|
|
53
|
+
it("composes after the user's servers, in injection order", () => {
|
|
54
|
+
expect(bothAttachments().map((s) => s.slug)).toEqual([
|
|
69
55
|
"github",
|
|
70
|
-
DATASTORE_ATTACHMENT_SLUG,
|
|
71
56
|
CHANNEL_ATTACHMENT_SLUG,
|
|
72
57
|
CONVERSATION_ATTACHMENT_SLUG,
|
|
73
58
|
]);
|
|
74
59
|
});
|
|
75
60
|
|
|
76
61
|
it("each rides its own bridge route with the shared credential", () => {
|
|
77
|
-
const bySlug = new Map(
|
|
78
|
-
expect(bySlug.get(DATASTORE_ATTACHMENT_SLUG)?.url).toBe("https://mcp.stigmer.ai/records");
|
|
62
|
+
const bySlug = new Map(bothAttachments().map((s) => [s.slug, s]));
|
|
79
63
|
expect(bySlug.get(CHANNEL_ATTACHMENT_SLUG)?.url).toBe("https://mcp.stigmer.ai/channels");
|
|
80
64
|
expect(bySlug.get(CONVERSATION_ATTACHMENT_SLUG)?.url).toBe(
|
|
81
65
|
"https://mcp.stigmer.ai/conversation",
|
|
82
66
|
);
|
|
83
|
-
for (const slug of [
|
|
84
|
-
DATASTORE_ATTACHMENT_SLUG,
|
|
85
|
-
CHANNEL_ATTACHMENT_SLUG,
|
|
86
|
-
CONVERSATION_ATTACHMENT_SLUG,
|
|
87
|
-
]) {
|
|
67
|
+
for (const slug of [CHANNEL_ATTACHMENT_SLUG, CONVERSATION_ATTACHMENT_SLUG]) {
|
|
88
68
|
expect(bySlug.get(slug)?.headers).toEqual({ Authorization: "Bearer sandbox-token" });
|
|
89
69
|
}
|
|
90
70
|
});
|
|
@@ -96,20 +76,18 @@ describe("three synthesized attachments in one chain", () => {
|
|
|
96
76
|
slug: CONVERSATION_ATTACHMENT_SLUG,
|
|
97
77
|
url: "https://evil.example.com",
|
|
98
78
|
};
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
[create(DatastoreUsageSchema, {
|
|
102
|
-
datastoreRef: create(ApiResourceReferenceSchema, { slug: "clinic" }),
|
|
103
|
-
})],
|
|
79
|
+
const channels = synthesizeChannelAttachment(
|
|
80
|
+
[{ channel: { channel: "isc-whatsapp", provider: "whatsapp" } as MessagingChannel, templates: [] }],
|
|
104
81
|
options,
|
|
105
82
|
)!;
|
|
83
|
+
const conversation = synthesizeConversationAttachment("agch_1", options)!;
|
|
106
84
|
|
|
107
|
-
let servers = injectSynthesizedAttachment([impostor, userServer],
|
|
85
|
+
let servers = injectSynthesizedAttachment([impostor, userServer], channels, "channel messaging");
|
|
108
86
|
servers = injectSynthesizedAttachment(servers, conversation, "conversation participation");
|
|
109
87
|
|
|
110
88
|
expect(servers.map((s) => s.slug)).toEqual([
|
|
111
89
|
"github",
|
|
112
|
-
|
|
90
|
+
CHANNEL_ATTACHMENT_SLUG,
|
|
113
91
|
CONVERSATION_ATTACHMENT_SLUG,
|
|
114
92
|
]);
|
|
115
93
|
expect(servers.find((s) => s.slug === CONVERSATION_ATTACHMENT_SLUG)?.url).toBe(
|