@stigmer/runner 3.40.1 → 3.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/activities/call-agent.js +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -1
- package/dist/activities/discover-mcp-server.js +13 -29
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
- package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
- package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
- package/dist/activities/execute-cursor/approval-policy.js +11 -35
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/boundary-rows.js +4 -4
- package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
- package/dist/activities/execute-cursor/translator.d.ts +12 -17
- package/dist/activities/execute-cursor/translator.js +13 -30
- package/dist/activities/execute-cursor/translator.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -2
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
- package/dist/activities/execute-deep-agent/platform-route.js +87 -19
- package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
- package/dist/activities/execute-deep-agent/translator.js +5 -4
- package/dist/activities/execute-deep-agent/translator.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
- package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.js +7 -7
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
- package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
- package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
- package/dist/activities/execute-deep-agent/turn.js +15 -6
- package/dist/activities/execute-deep-agent/turn.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +2 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/exit-backstop.d.ts +64 -0
- package/dist/exit-backstop.js +79 -0
- package/dist/exit-backstop.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +12 -5
- package/dist/harness/terminal-table.js +13 -13
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/harness/transcript/builder.d.ts +21 -0
- package/dist/harness/transcript/builder.js +53 -31
- package/dist/harness/transcript/builder.js.map +1 -1
- package/dist/harness/transcript/events.d.ts +22 -26
- package/dist/harness/transcript/events.js +6 -5
- package/dist/harness/transcript/events.js.map +1 -1
- package/dist/harness/turn-context.d.ts +3 -1
- package/dist/harness/turn-context.js +5 -3
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +10 -4
- package/dist/main.js.map +1 -1
- package/dist/middleware/cost-advisory.d.ts +6 -3
- package/dist/middleware/cost-advisory.js +6 -3
- package/dist/middleware/cost-advisory.js.map +1 -1
- package/dist/middleware/execution-budget.d.ts +22 -7
- package/dist/middleware/execution-budget.js +57 -35
- package/dist/middleware/execution-budget.js.map +1 -1
- package/dist/middleware/index.d.ts +10 -7
- package/dist/middleware/index.js +10 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/types.d.ts +6 -1
- package/dist/runner-manager.js +3 -2
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +25 -0
- package/dist/runner.js +41 -14
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +23 -13
- package/dist/shared/approval-policy.js +47 -14
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +4 -5
- package/dist/shared/caller-identity.js +4 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +3 -2
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +2 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +10 -1
- package/dist/shared/mcp-resolver.js +11 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/memory-attachment.js +3 -2
- package/dist/shared/memory-attachment.js.map +1 -1
- package/dist/shared/model-error.d.ts +25 -3
- package/dist/shared/model-error.js +78 -10
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/platform-server-address.d.ts +71 -0
- package/dist/shared/platform-server-address.js +103 -0
- package/dist/shared/platform-server-address.js.map +1 -0
- package/dist/shared/synthesized-attachment.d.ts +0 -5
- package/dist/shared/synthesized-attachment.js +0 -12
- package/dist/shared/synthesized-attachment.js.map +1 -1
- package/dist/shared/tool-rounds.d.ts +51 -23
- package/dist/shared/tool-rounds.js +67 -24
- package/dist/shared/tool-rounds.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +15 -0
- package/dist/shared/workspace/platform-dir.js +18 -1
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +21 -14
- package/dist/shared/workspace/stigmer-link.js +91 -29
- package/dist/shared/workspace/stigmer-link.js.map +1 -1
- package/dist/tools/url-guard.d.ts +5 -1
- package/dist/tools/url-guard.js +9 -2
- package/dist/tools/url-guard.js.map +1 -1
- package/dist/worker.d.ts +11 -2
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
- package/dist/workflow-engine/tasks/human-input.js +79 -37
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +14 -4
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/engine-core.d.ts +25 -0
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +3 -0
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +9 -12
- package/dist/workflows/human-input-orchestrator.js +12 -44
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +1 -0
- package/src/__test-utils__/execution-record-fixture.ts +2 -1
- package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
- package/src/__test-utils__/test-gate.ts +8 -0
- package/src/__tests__/exit-backstop.test.ts +91 -0
- package/src/__tests__/golden-e2e.test.ts +80 -62
- package/src/__tests__/history-encryption-e2e.test.ts +51 -27
- package/src/__tests__/runner.test.ts +60 -2
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
- package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
- package/src/activities/call-agent.ts +1 -1
- package/src/activities/discover-mcp-server.ts +27 -37
- package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
- package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
- package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
- package/src/activities/execute-cursor/approval-policy.ts +11 -36
- package/src/activities/execute-cursor/boundary-rows.ts +3 -3
- package/src/activities/execute-cursor/translator.ts +15 -32
- package/src/activities/execute-cursor/turn-stream.ts +4 -1
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
- package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
- package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
- package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
- package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
- package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
- package/src/activities/execute-deep-agent/platform-route.ts +109 -20
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
- package/src/activities/execute-deep-agent/translator.ts +5 -4
- package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
- package/src/activities/execute-deep-agent/turn-setup.ts +7 -7
- package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
- package/src/activities/execute-deep-agent/turn.ts +16 -7
- package/src/activities/workflow-event-activities.ts +2 -0
- package/src/config.ts +11 -0
- package/src/exit-backstop.ts +97 -0
- package/src/harness/README.md +5 -5
- package/src/harness/terminal-table.ts +13 -13
- package/src/harness/transcript/__tests__/builder.test.ts +93 -21
- package/src/harness/transcript/builder.ts +53 -30
- package/src/harness/transcript/events.ts +22 -27
- package/src/harness/turn-context.ts +5 -3
- package/src/main.ts +9 -5
- package/src/middleware/__tests__/execution-budget.test.ts +181 -103
- package/src/middleware/cost-advisory.ts +6 -3
- package/src/middleware/execution-budget.ts +60 -34
- package/src/middleware/index.ts +10 -7
- package/src/middleware/types.ts +6 -1
- package/src/runner-manager.ts +3 -2
- package/src/runner.ts +57 -15
- package/src/shared/__tests__/approval-policy.test.ts +41 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/connect-backfill.test.ts +15 -11
- package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
- package/src/shared/__tests__/model-error.test.ts +71 -3
- package/src/shared/__tests__/platform-server-address.test.ts +122 -0
- package/src/shared/__tests__/tool-rounds.test.ts +64 -14
- package/src/shared/approval-policy.ts +57 -14
- package/src/shared/caller-identity.ts +4 -5
- package/src/shared/channel-attachment.ts +2 -2
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-resolver.ts +11 -1
- package/src/shared/memory-attachment.ts +2 -2
- package/src/shared/model-error.ts +87 -9
- package/src/shared/platform-server-address.ts +123 -0
- package/src/shared/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- package/src/shared/workspace/stigmer-link.ts +99 -29
- package/src/tools/__tests__/url-guard.test.ts +19 -1
- package/src/tools/url-guard.ts +9 -1
- package/src/worker.ts +12 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
- package/src/workflow-engine/__tests__/loader.test.ts +1 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
- package/src/workflow-engine/tasks/human-input.ts +92 -42
- package/src/workflow-engine/types.ts +16 -4
- package/src/workflows/call-agent-orchestrator.ts +7 -0
- package/src/workflows/engine-core.ts +18 -0
- package/src/workflows/execute-from-execution.ts +2 -1
- package/src/workflows/human-input-orchestrator.ts +15 -56
|
@@ -1,35 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* max_tool_rounds resolution and the budget's stop signal.
|
|
3
|
+
*
|
|
4
|
+
* Pins the proto contract's arithmetic (0/unset = unlimited, the 10–1000
|
|
5
|
+
* clamp and its warnings), the backstop that must never bind before the
|
|
6
|
+
* round count, and the classifier the native turn uses to read a thrown
|
|
7
|
+
* error as the `tool_call_limit` outcome. The classifier cases use
|
|
8
|
+
* LangChain's and LangGraph's own error classes, so an engine upgrade that
|
|
9
|
+
* changes how a middleware's throw reaches the turn fails here, not in a
|
|
10
|
+
* production run that would otherwise read the stop as a failure.
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
import { describe, expect, it, vi, afterEach } from "vitest";
|
|
14
|
+
import { MiddlewareError } from "langchain";
|
|
15
|
+
import { GraphRecursionError } from "@langchain/langgraph";
|
|
2
16
|
import {
|
|
3
17
|
MAX_TOOL_ROUNDS,
|
|
4
18
|
MIN_TOOL_ROUNDS,
|
|
5
|
-
|
|
6
|
-
|
|
19
|
+
SUPER_STEPS_BACKSTOP_PER_ROUND,
|
|
20
|
+
ToolRoundLimitError,
|
|
21
|
+
backstopRecursionLimit,
|
|
22
|
+
isToolCallBudgetStop,
|
|
23
|
+
resolveToolRoundLimit,
|
|
7
24
|
} from "../tool-rounds.js";
|
|
8
25
|
|
|
9
|
-
describe("
|
|
26
|
+
describe("resolveToolRoundLimit", () => {
|
|
10
27
|
afterEach(() => {
|
|
11
28
|
vi.restoreAllMocks();
|
|
12
29
|
});
|
|
13
30
|
|
|
14
31
|
it("returns null for unset (proto contract: 0 = unlimited)", () => {
|
|
15
|
-
expect(
|
|
16
|
-
expect(
|
|
32
|
+
expect(resolveToolRoundLimit(undefined)).toBeNull();
|
|
33
|
+
expect(resolveToolRoundLimit(0)).toBeNull();
|
|
17
34
|
});
|
|
18
35
|
|
|
19
36
|
it("returns null for negative values (treated as unset, never a tiny limit)", () => {
|
|
20
|
-
expect(
|
|
37
|
+
expect(resolveToolRoundLimit(-5)).toBeNull();
|
|
21
38
|
});
|
|
22
39
|
|
|
23
|
-
it("
|
|
24
|
-
expect(
|
|
25
|
-
expect(
|
|
26
|
-
expect(
|
|
40
|
+
it("returns an in-range value as the round count itself", () => {
|
|
41
|
+
expect(resolveToolRoundLimit(10)).toBe(10);
|
|
42
|
+
expect(resolveToolRoundLimit(100)).toBe(100);
|
|
43
|
+
expect(resolveToolRoundLimit(MAX_TOOL_ROUNDS)).toBe(MAX_TOOL_ROUNDS);
|
|
27
44
|
});
|
|
28
45
|
|
|
29
46
|
it("clamps below-range values up to the minimum with a warning", () => {
|
|
30
47
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
31
48
|
|
|
32
|
-
expect(
|
|
49
|
+
expect(resolveToolRoundLimit(3)).toBe(MIN_TOOL_ROUNDS);
|
|
33
50
|
|
|
34
51
|
expect(warn).toHaveBeenCalledOnce();
|
|
35
52
|
expect(warn.mock.calls[0][0]).toContain("max_tool_rounds=3");
|
|
@@ -39,7 +56,7 @@ describe("resolveRecursionLimit", () => {
|
|
|
39
56
|
it("clamps above-range values down to the maximum with a warning", () => {
|
|
40
57
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
41
58
|
|
|
42
|
-
expect(
|
|
59
|
+
expect(resolveToolRoundLimit(5000)).toBe(MAX_TOOL_ROUNDS);
|
|
43
60
|
|
|
44
61
|
expect(warn).toHaveBeenCalledOnce();
|
|
45
62
|
expect(warn.mock.calls[0][0]).toContain("max_tool_rounds=5000");
|
|
@@ -49,9 +66,42 @@ describe("resolveRecursionLimit", () => {
|
|
|
49
66
|
it("does not warn for in-range values", () => {
|
|
50
67
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
resolveToolRoundLimit(MIN_TOOL_ROUNDS);
|
|
70
|
+
resolveToolRoundLimit(MAX_TOOL_ROUNDS);
|
|
54
71
|
|
|
55
72
|
expect(warn).not.toHaveBeenCalled();
|
|
56
73
|
});
|
|
57
74
|
});
|
|
75
|
+
|
|
76
|
+
describe("backstopRecursionLimit", () => {
|
|
77
|
+
it("allows the backstop's super-steps per round, far above what one round costs", () => {
|
|
78
|
+
expect(backstopRecursionLimit(MIN_TOOL_ROUNDS)).toBe(MIN_TOOL_ROUNDS * SUPER_STEPS_BACKSTOP_PER_ROUND);
|
|
79
|
+
expect(backstopRecursionLimit(MAX_TOOL_ROUNDS)).toBe(MAX_TOOL_ROUNDS * SUPER_STEPS_BACKSTOP_PER_ROUND);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("isToolCallBudgetStop", () => {
|
|
84
|
+
it("reads the round count's own error as the budget's stop", () => {
|
|
85
|
+
expect(isToolCallBudgetStop(new ToolRoundLimitError(10))).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("reads it through LangChain's MiddlewareError wrapping, once per wrapModelCall layer", () => {
|
|
89
|
+
let wrapped: unknown = new ToolRoundLimitError(10);
|
|
90
|
+
for (const layer of ["ExecutionBudgetMiddleware", "todoListMiddleware", "SummarizationMiddleware"]) {
|
|
91
|
+
wrapped = MiddlewareError.wrap(wrapped, layer);
|
|
92
|
+
}
|
|
93
|
+
expect(wrapped).toBeInstanceOf(MiddlewareError);
|
|
94
|
+
expect(isToolCallBudgetStop(wrapped)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("reads the backstop's recursion error as the same stop", () => {
|
|
98
|
+
expect(isToolCallBudgetStop(new GraphRecursionError("Recursion limit of 200 reached without hitting a stop condition."))).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does not read any other failure as the budget's stop, wrapped or not", () => {
|
|
102
|
+
expect(isToolCallBudgetStop(new Error("upstream 529"))).toBe(false);
|
|
103
|
+
expect(isToolCallBudgetStop(MiddlewareError.wrap(new Error("upstream 529"), "ExecutionBudgetMiddleware"))).toBe(false);
|
|
104
|
+
expect(isToolCallBudgetStop("Recursion limit")).toBe(false);
|
|
105
|
+
expect(isToolCallBudgetStop(undefined)).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -20,6 +20,7 @@ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexe
|
|
|
20
20
|
import { ApprovalAction, ApprovalMode, ApprovalPolicySource } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
21
21
|
import { SENSITIVE_ARG_KEYS } from "./args-preview.js";
|
|
22
22
|
import { toolApprovalCategory, type ToolApprovalCategory } from "./tool-kind.js";
|
|
23
|
+
import { extractFilePath } from "./file-tools.js";
|
|
23
24
|
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -480,18 +481,15 @@ export function resolveApprovalMessage(
|
|
|
480
481
|
* THE approval-message template per mutating built-in category — the one
|
|
481
482
|
* table both harnesses word their approval cards from (since #1097).
|
|
482
483
|
* Keyed by category (not raw tool name) so every alias of a mutation renders
|
|
483
|
-
* one message
|
|
484
|
-
* (`
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* Cursor boundary resolves a denied call's message through the same path.
|
|
484
|
+
* one message. It is read only through {@link resolveBuiltInApprovalMessage},
|
|
485
|
+
* which the native gate (`resolveToolApproval`), the Cursor translator's `gate`
|
|
486
|
+
* and the Cursor boundary's denied-call message all call, so the three can
|
|
487
|
+
* never word a card differently.
|
|
488
488
|
*
|
|
489
489
|
* Until then the Cursor harness carried a table of its own and the two differed
|
|
490
490
|
* by a word — `shell` read "Run command" there and "Execute command" here.
|
|
491
491
|
* Unified on the plainer word: a user reads "Run command" and knows
|
|
492
|
-
* what is being asked.
|
|
493
|
-
* `write` resolves `{{args.path}}`, while deepagents' file tools send
|
|
494
|
-
* `file_path`, so a gated native write reads "Write file: <unknown>".
|
|
492
|
+
* what is being asked.
|
|
495
493
|
*/
|
|
496
494
|
export const CATEGORY_APPROVAL_MESSAGE: Record<ToolApprovalCategory, string> = {
|
|
497
495
|
write: "Write file: {{args.path}}",
|
|
@@ -499,6 +497,50 @@ export const CATEGORY_APPROVAL_MESSAGE: Record<ToolApprovalCategory, string> = {
|
|
|
499
497
|
shell: "Run command: {{args.command}}",
|
|
500
498
|
};
|
|
501
499
|
|
|
500
|
+
/**
|
|
501
|
+
* THE approval-card message for a built-in tool of either harness taxonomy, or
|
|
502
|
+
* `undefined` when the tool is not a gated built-in.
|
|
503
|
+
*
|
|
504
|
+
* The `{{args.path}}` a file card reads is filled from `extractFilePath`
|
|
505
|
+
* (`file-tools.ts`), the one home of "which argument names the file" across
|
|
506
|
+
* both taxonomies: deepagents' file tools and the Cursor hook send
|
|
507
|
+
* `file_path`, the Cursor stream sends `path`. Resolving the raw args instead
|
|
508
|
+
* rendered a gated native write as "Write file: <unknown>" (#1112). The
|
|
509
|
+
* template keeps its one placeholder, so the table stays harness-neutral.
|
|
510
|
+
*/
|
|
511
|
+
export function resolveBuiltInApprovalMessage(
|
|
512
|
+
toolName: string,
|
|
513
|
+
args: Record<string, unknown>,
|
|
514
|
+
): string | undefined {
|
|
515
|
+
const category = toolApprovalCategory(toolName);
|
|
516
|
+
return category ? resolveCategoryApprovalMessage(category, toolName, args) : undefined;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function resolveCategoryApprovalMessage(
|
|
520
|
+
category: ToolApprovalCategory,
|
|
521
|
+
toolName: string,
|
|
522
|
+
args: Record<string, unknown>,
|
|
523
|
+
): string {
|
|
524
|
+
return resolveApprovalMessage(CATEGORY_APPROVAL_MESSAGE[category], toolName, categoryTemplateArgs(category, args));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The args a category's template resolves against: a file card reads the file from wherever its taxonomy names it. */
|
|
528
|
+
function categoryTemplateArgs(category: ToolApprovalCategory, args: Record<string, unknown>): Record<string, unknown> {
|
|
529
|
+
switch (category) {
|
|
530
|
+
case "write":
|
|
531
|
+
case "delete": {
|
|
532
|
+
const path = extractFilePath(args);
|
|
533
|
+
return path === null ? args : { ...args, path };
|
|
534
|
+
}
|
|
535
|
+
case "shell":
|
|
536
|
+
return args;
|
|
537
|
+
default: {
|
|
538
|
+
const exhaustive: never = category;
|
|
539
|
+
throw new Error(`categoryTemplateArgs: unknown approval category ${String(exhaustive)}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
502
544
|
/** What the gate decides for one call: whether it waits, the message the card shows, and which layer said so. */
|
|
503
545
|
export interface ApprovalRequirement {
|
|
504
546
|
readonly requiresApproval: boolean;
|
|
@@ -508,11 +550,12 @@ export interface ApprovalRequirement {
|
|
|
508
550
|
}
|
|
509
551
|
|
|
510
552
|
/**
|
|
511
|
-
* THE approval decision for one tool call — the
|
|
512
|
-
* (`middleware/approval-gate.ts`) interrupts on
|
|
513
|
-
*
|
|
514
|
-
* about whether a call waits (since #1097; until
|
|
515
|
-
* builder carried its own weaker copy that gated MCP tools
|
|
553
|
+
* THE approval decision for one tool call — the function the native gate
|
|
554
|
+
* (`middleware/approval-gate.ts`) interrupts on. A held call reaches the
|
|
555
|
+
* transcript only through that interrupt (`approval_proposed`), so the row can
|
|
556
|
+
* never disagree with the gate about whether a call waits (since #1097; until
|
|
557
|
+
* then the transcript builder carried its own weaker copy that gated MCP tools
|
|
558
|
+
* only). Its
|
|
516
559
|
* read-side twin is {@link resolveApprovalProvenance}, which answers "which
|
|
517
560
|
* layer governs this call" for EVERY call, gated or not.
|
|
518
561
|
*
|
|
@@ -561,7 +604,7 @@ export function resolveToolApproval(
|
|
|
561
604
|
}
|
|
562
605
|
return {
|
|
563
606
|
requiresApproval: true,
|
|
564
|
-
message:
|
|
607
|
+
message: resolveCategoryApprovalMessage(category, toolName, args),
|
|
565
608
|
source: "builtin_category",
|
|
566
609
|
};
|
|
567
610
|
}
|
|
@@ -139,9 +139,8 @@ export function resolveCallerIdentity(
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* Return a NEW env map with the reserved caller-identity keys set —
|
|
142
|
-
* platform values are authoritative over same-named user entries (
|
|
143
|
-
*
|
|
144
|
-
* impersonate a caller).
|
|
142
|
+
* platform values are authoritative over same-named user entries (a user
|
|
143
|
+
* env var must never be able to impersonate a caller).
|
|
145
144
|
*
|
|
146
145
|
* Call this on the env map handed to MCP resolution ONLY — never on the
|
|
147
146
|
* map that reaches agent subprocess environments. Per-server opt-in is
|
|
@@ -174,8 +173,8 @@ export function injectCallerIdentityEnv(
|
|
|
174
173
|
* Discovery-context injection: the connect workflow resolves the same
|
|
175
174
|
* header templates with no session, so every declared reserved key gets
|
|
176
175
|
* the anonymous sentinel — otherwise a caller-identity-templating server
|
|
177
|
-
* can never be discovered. Gated on the server's declared keys,
|
|
178
|
-
*
|
|
176
|
+
* can never be discovered. Gated on the server's declared keys, as every
|
|
177
|
+
* platform key is (platform-server-address.ts).
|
|
179
178
|
*/
|
|
180
179
|
export function injectAnonymousCallerIdentityForDiscovery(
|
|
181
180
|
declaredEnvKeys: ReadonlySet<string>,
|
|
@@ -45,8 +45,8 @@ import type {
|
|
|
45
45
|
} from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
46
46
|
import type { StigmerClient } from "../client/stigmer-client.js";
|
|
47
47
|
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
48
|
+
import { grpcTarget, SERVER_ADDRESS_ENV_KEY } from "./platform-server-address.js";
|
|
48
49
|
import {
|
|
49
|
-
grpcTarget,
|
|
50
50
|
stdioCredentialEnv,
|
|
51
51
|
type SynthesizedAttachmentOptions,
|
|
52
52
|
} from "./synthesized-attachment.js";
|
|
@@ -157,7 +157,7 @@ export function synthesizeChannelAttachment(
|
|
|
157
157
|
args: ["mcp-server"],
|
|
158
158
|
env: {
|
|
159
159
|
STIGMER_MCP_ROSTER: "channels",
|
|
160
|
-
|
|
160
|
+
[SERVER_ADDRESS_ENV_KEY]: grpcTarget(options.backendEndpoint),
|
|
161
161
|
...stdioCredentialEnv(options.credential),
|
|
162
162
|
},
|
|
163
163
|
};
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
22
22
|
import { resolveMcpServers } from "./mcp-resolver.js";
|
|
23
23
|
import type { McpTransportPosture } from "./mcp-transport-guard.js";
|
|
24
|
+
import type { PlatformEndpoints } from "./platform-server-address.js";
|
|
24
25
|
import { withTimeout } from "./with-timeout.js";
|
|
25
26
|
import type { StigmerClient } from "../client/stigmer-client.js";
|
|
26
27
|
import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
@@ -67,6 +68,7 @@ export async function backfillMcpServersIfNeeded(
|
|
|
67
68
|
envVars: Record<string, string>,
|
|
68
69
|
org: string,
|
|
69
70
|
transportPosture: McpTransportPosture,
|
|
71
|
+
platformEndpoints: PlatformEndpoints,
|
|
70
72
|
onHeartbeat?: () => void,
|
|
71
73
|
secretKeys?: ReadonlySet<string>,
|
|
72
74
|
): Promise<ResolvedMcpServer[]> {
|
|
@@ -131,7 +133,9 @@ export async function backfillMcpServersIfNeeded(
|
|
|
131
133
|
|
|
132
134
|
// Same posture as the initial resolution: every server here already
|
|
133
135
|
// passed the transport guard once, so re-resolving cannot newly reject.
|
|
134
|
-
const refreshed = await resolveMcpServers(
|
|
136
|
+
const refreshed = await resolveMcpServers(
|
|
137
|
+
client, usages, envVars, transportPosture, platformEndpoints,
|
|
138
|
+
);
|
|
135
139
|
return refreshed.resolvedServers;
|
|
136
140
|
}
|
|
137
141
|
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* toCursorMcpConfig (execute-cursor/cursor-mcp-config.ts) for the Cursor
|
|
13
13
|
* SDK — so a behavioral change here (like the transport guard) lands in
|
|
14
14
|
* both execution paths by construction.
|
|
15
|
+
*
|
|
16
|
+
* The one platform key filled here is STIGMER_SERVER_ADDRESS
|
|
17
|
+
* (platform-server-address.ts), before the declared-key filter so the filter
|
|
18
|
+
* sees the key present; discovery applies the same fill itself because it
|
|
19
|
+
* resolves a single server without a usage.
|
|
15
20
|
*/
|
|
16
21
|
|
|
17
22
|
import type { McpServerUsage, ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
30
35
|
PlaceholderResolutionError,
|
|
31
36
|
} from "./placeholder-resolver.js";
|
|
32
37
|
import { effectiveEnabledTools } from "./mcp-enabled-tools.js";
|
|
38
|
+
import { fillPlatformServerAddress, type PlatformEndpoints } from "./platform-server-address.js";
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
41
|
* Harness-agnostic intermediate representation of a resolved MCP server.
|
|
@@ -86,12 +92,16 @@ export interface McpResolutionResult {
|
|
|
86
92
|
* resolveMcpTransportPosture(config.mode)). A stdio server under a
|
|
87
93
|
* forbidding posture throws {@link McpTransportError} and fails the
|
|
88
94
|
* whole resolution — never degraded to a skipped server.
|
|
95
|
+
* @param platformEndpoints The runner's endpoints a missing
|
|
96
|
+
* STIGMER_SERVER_ADDRESS is filled from (the runner Config satisfies
|
|
97
|
+
* it). Required so no resolution path can forget the fill.
|
|
89
98
|
*/
|
|
90
99
|
export async function resolveMcpServers(
|
|
91
100
|
client: StigmerClient,
|
|
92
101
|
usages: McpServerUsage[],
|
|
93
102
|
envVars: Record<string, string>,
|
|
94
103
|
transportPosture: McpTransportPosture,
|
|
104
|
+
platformEndpoints: PlatformEndpoints,
|
|
95
105
|
): Promise<McpResolutionResult> {
|
|
96
106
|
const resolved: ResolvedMcpServer[] = [];
|
|
97
107
|
|
|
@@ -103,7 +113,7 @@ export async function resolveMcpServers(
|
|
|
103
113
|
const mcpServer = await client.getMcpServerByReference(ref);
|
|
104
114
|
const serverEnv = filterEnvToDeclaredKeys(
|
|
105
115
|
mcpServer.spec?.env,
|
|
106
|
-
envVars,
|
|
116
|
+
fillPlatformServerAddress(mcpServer, envVars, platformEndpoints),
|
|
107
117
|
ref.slug,
|
|
108
118
|
);
|
|
109
119
|
const server = mcpServerToResolved(
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
|
|
52
52
|
import type { RecalledMemories } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
53
53
|
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
54
|
+
import { grpcTarget, SERVER_ADDRESS_ENV_KEY } from "./platform-server-address.js";
|
|
54
55
|
import {
|
|
55
|
-
grpcTarget,
|
|
56
56
|
stdioCredentialEnv,
|
|
57
57
|
type SynthesizedAttachmentOptions,
|
|
58
58
|
} from "./synthesized-attachment.js";
|
|
@@ -159,7 +159,7 @@ export function synthesizeMemoryAttachment(
|
|
|
159
159
|
args: ["mcp-server"],
|
|
160
160
|
env: {
|
|
161
161
|
STIGMER_MCP_ROSTER: "memory",
|
|
162
|
-
|
|
162
|
+
[SERVER_ADDRESS_ENV_KEY]: grpcTarget(options.backendEndpoint),
|
|
163
163
|
...stdioCredentialEnv(options.credential),
|
|
164
164
|
...nonEmptyEntries([
|
|
165
165
|
[MEMORY_ORG_ENV, context.org],
|
|
@@ -19,6 +19,14 @@
|
|
|
19
19
|
* (BYO-key) mode, attributes billing errors to the user's own account
|
|
20
20
|
* with actionable wording.
|
|
21
21
|
*
|
|
22
|
+
* 3. On Stigmer Cloud an organization may bring its own provider key, which
|
|
23
|
+
* the proxy injects upstream in place of the platform's. A failure on
|
|
24
|
+
* that key is the organization's, not the platform's, yet it reaches the
|
|
25
|
+
* runner in proxy mode. The proxy keeps the provider's status and message
|
|
26
|
+
* and marks the message with ORGANIZATION_PROVIDER_KEY_SENTINEL; this
|
|
27
|
+
* module words a marked failure as the organization's key, never as a
|
|
28
|
+
* platform fault.
|
|
29
|
+
*
|
|
22
30
|
* Classification order is load-bearing: the sentinel check must precede
|
|
23
31
|
* status mapping, otherwise the proxy's 503 would classify as a retryable
|
|
24
32
|
* 5xx and waste Temporal retries against a dead platform account.
|
|
@@ -43,6 +51,14 @@ import {
|
|
|
43
51
|
*/
|
|
44
52
|
export const PLATFORM_CAPACITY_SENTINEL = "STIGMER_PLATFORM_MODEL_CAPACITY";
|
|
45
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Machine-readable code the cloud proxy embeds in a provider failure it
|
|
56
|
+
* relays from a call served on the organization's own provider key. The
|
|
57
|
+
* message text carries it, as it carries PLATFORM_CAPACITY_SENTINEL.
|
|
58
|
+
* Duplicated in stigmer-cloud's LLM proxy lane; change both or neither.
|
|
59
|
+
*/
|
|
60
|
+
export const ORGANIZATION_PROVIDER_KEY_SENTINEL = "STIGMER_ORGANIZATION_PROVIDER_KEY";
|
|
61
|
+
|
|
46
62
|
export interface ClassifiedModelError {
|
|
47
63
|
/** Stable machine-readable code (doubles as the Temporal failure type). */
|
|
48
64
|
readonly code: string;
|
|
@@ -72,17 +88,22 @@ export interface ModelErrorContext {
|
|
|
72
88
|
|
|
73
89
|
/**
|
|
74
90
|
* Walk the `cause` chain to the root error. LangChain's MiddlewareError (and
|
|
75
|
-
* anything else that chains causes) preserves the original SDK error there
|
|
76
|
-
*
|
|
91
|
+
* anything else that chains causes) preserves the original SDK error there,
|
|
92
|
+
* and the root is the one link carrying the provider's `.status`.
|
|
93
|
+
*
|
|
94
|
+
* The chain is as long as the agent's middleware stack: LangChain wraps a
|
|
95
|
+
* model call's failure once per middleware that implements `wrapModelCall`
|
|
96
|
+
* (`MiddlewareError.wrap` in its AgentNode), and a native turn stacks
|
|
97
|
+
* deepagents' own middleware, the to-do list and Stigmer's, well past ten
|
|
98
|
+
* layers. So the walk has no depth bound; a cause cycle ends it instead, at
|
|
99
|
+
* the first error seen twice.
|
|
77
100
|
*/
|
|
78
101
|
export function unwrapModelError(err: unknown): unknown {
|
|
102
|
+
const seen = new Set<unknown>([err]);
|
|
79
103
|
let current = err;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} else {
|
|
84
|
-
return current;
|
|
85
|
-
}
|
|
104
|
+
while (current instanceof Error && current.cause instanceof Error && !seen.has(current.cause)) {
|
|
105
|
+
current = current.cause;
|
|
106
|
+
seen.add(current);
|
|
86
107
|
}
|
|
87
108
|
return current;
|
|
88
109
|
}
|
|
@@ -99,10 +120,21 @@ export function unwrapModelError(err: unknown): unknown {
|
|
|
99
120
|
*/
|
|
100
121
|
export function classifyModelCallError(
|
|
101
122
|
err: unknown,
|
|
102
|
-
|
|
123
|
+
callerCtx: ModelErrorContext,
|
|
103
124
|
): ClassifiedModelError | undefined {
|
|
104
125
|
const root = unwrapModelError(err);
|
|
105
126
|
const message = root instanceof Error ? root.message : String(root);
|
|
127
|
+
|
|
128
|
+
// 0. Organization-key sentinel — before everything that reads proxyMode:
|
|
129
|
+
// the call went through the proxy but on the organization's own key,
|
|
130
|
+
// so its failures are the organization's. The key-specific failures
|
|
131
|
+
// get key-specific wording; the rest classify as direct mode would.
|
|
132
|
+
const organizationKey = message.includes(ORGANIZATION_PROVIDER_KEY_SENTINEL);
|
|
133
|
+
if (organizationKey) {
|
|
134
|
+
const classified = classifyOrganizationKeyError(root, message, callerCtx);
|
|
135
|
+
if (classified !== undefined) return classified;
|
|
136
|
+
}
|
|
137
|
+
const ctx = organizationKey ? { ...callerCtx, proxyMode: false } : callerCtx;
|
|
106
138
|
const backend = resolveDirectBackend(ctx);
|
|
107
139
|
|
|
108
140
|
// 1. Platform sentinel — before status mapping (see module doc).
|
|
@@ -235,6 +267,52 @@ export function classifyModelCallError(
|
|
|
235
267
|
return undefined;
|
|
236
268
|
}
|
|
237
269
|
|
|
270
|
+
/**
|
|
271
|
+
* The failures an organization's own provider key causes, worded for the
|
|
272
|
+
* organization's admin: out of credit or quota at the provider, the key
|
|
273
|
+
* rejected, or the key's account not permitted the model. Undefined for
|
|
274
|
+
* every other failure, which classifies as direct mode would.
|
|
275
|
+
*/
|
|
276
|
+
function classifyOrganizationKeyError(
|
|
277
|
+
root: unknown,
|
|
278
|
+
message: string,
|
|
279
|
+
ctx: ModelErrorContext,
|
|
280
|
+
): ClassifiedModelError | undefined {
|
|
281
|
+
const provider = providerSubject(ctx);
|
|
282
|
+
if (isProviderBillingMessage(message)) {
|
|
283
|
+
return {
|
|
284
|
+
code: "LLM_PROVIDER_BILLING",
|
|
285
|
+
retryable: false,
|
|
286
|
+
message:
|
|
287
|
+
`Your organization's own ${providerLabel(ctx)} key is out of credits or over quota. ` +
|
|
288
|
+
`Add credits to that provider account, or update the key in Settings → Provider keys. ` +
|
|
289
|
+
`Provider message: ${message}`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const status = typeof (root as { status?: unknown }).status === "number"
|
|
293
|
+
? (root as { status: number }).status
|
|
294
|
+
: undefined;
|
|
295
|
+
if (status === 401) {
|
|
296
|
+
return {
|
|
297
|
+
code: "LLM_AUTHENTICATION_ERROR",
|
|
298
|
+
retryable: false,
|
|
299
|
+
message:
|
|
300
|
+
`${provider} rejected your organization's own key (authentication, HTTP 401) for ${modelLabel(ctx)}. ` +
|
|
301
|
+
`Update the key in Settings → Provider keys.`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (status === 403) {
|
|
305
|
+
return {
|
|
306
|
+
code: "LLM_PERMISSION_DENIED",
|
|
307
|
+
retryable: false,
|
|
308
|
+
message:
|
|
309
|
+
`${provider} denied your organization's own key (authorization, HTTP 403) for ${modelLabel(ctx)}. ` +
|
|
310
|
+
`Check that the key's account may use this model, or update the key in Settings → Provider keys.`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
238
316
|
/**
|
|
239
317
|
* Retryability policy (unchanged from the original call-llm classifier):
|
|
240
318
|
* - 4xx (except 429): nonRetryable — client/config errors won't self-heal
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Stigmer server's address for user-defined MCP servers — the reserved
|
|
3
|
+
* STIGMER_SERVER_ADDRESS key, and the one dial-target rule every writer of
|
|
4
|
+
* that key shares (stigmer/stigmer#1433, #1446).
|
|
5
|
+
*
|
|
6
|
+
* The runner is the one component that supplies it, because the runner is
|
|
7
|
+
* the one that knows where the consumer runs:
|
|
8
|
+
*
|
|
9
|
+
* - A remote HTTP server receives the key only through a templated header,
|
|
10
|
+
* so it needs the server's PUBLIC address: STIGMER_MCP_PUBLIC_ENDPOINT,
|
|
11
|
+
* which the server hands every sandbox it provisions from its own
|
|
12
|
+
* configuration (stigmer/stigmer#1447), or an operator names to a
|
|
13
|
+
* runner it starts by hand.
|
|
14
|
+
* - A stdio server is a child of this runner (only a local runner spawns
|
|
15
|
+
* one; mcp-transport-guard.ts), so the address the runner itself dials,
|
|
16
|
+
* STIGMER_BACKEND_ENDPOINT, is right for it by construction.
|
|
17
|
+
*
|
|
18
|
+
* No page supplies it: a value written into a run's runtime_env sits in the
|
|
19
|
+
* top merge layer, above every value the user saved (stigmer/stigmer#1446).
|
|
20
|
+
*
|
|
21
|
+
* The runner FILLS, it never overrides: a non-empty value already in the
|
|
22
|
+
* environment (one the user saved or typed to aim a server elsewhere)
|
|
23
|
+
* stands. A key nobody can supply stays missing, and a
|
|
24
|
+
* header that templates it fails with the resolver's named
|
|
25
|
+
* PlaceholderResolutionError instead of dialing a guess.
|
|
26
|
+
*
|
|
27
|
+
* Injection is opt-in exactly as for the caller-identity keys
|
|
28
|
+
* (caller-identity.ts): only a server whose spec.env declares the key
|
|
29
|
+
* receives it. Applied before filterEnvToDeclaredKeys at execution
|
|
30
|
+
* (mcp-resolver.ts) and before placeholder resolution at discovery
|
|
31
|
+
* (activities/discover-mcp-server.ts), so both phases share one rule.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
35
|
+
import type { Config } from "../config.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reserved env key: the Stigmer server's gRPC dial target, host:port. The
|
|
39
|
+
* name the mcp-server reads its target from (mcp-server/src/config.ts) and
|
|
40
|
+
* `@stigmer/react` keeps out of its setup prompts
|
|
41
|
+
* (sdk/react/src/environment/systemEnvVars.ts); a cross-package string,
|
|
42
|
+
* pinned on every side.
|
|
43
|
+
*/
|
|
44
|
+
export const SERVER_ADDRESS_ENV_KEY = "STIGMER_SERVER_ADDRESS";
|
|
45
|
+
|
|
46
|
+
/** The two runner-known endpoints an address can be filled from. */
|
|
47
|
+
export type PlatformEndpoints = Pick<Config, "mcpPublicEndpoint" | "stigmerBackendEndpoint">;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The gRPC dial target (host:port) for an endpoint, the shape
|
|
51
|
+
* STIGMER_SERVER_ADDRESS wants.
|
|
52
|
+
*
|
|
53
|
+
* An http(s) URL becomes its host with an explicit port: the URL's own, else
|
|
54
|
+
* 80 for http and 443 for https. The port is never left implicit, because
|
|
55
|
+
* the mcp-server derives TLS from it and assumes :443 with TLS for a
|
|
56
|
+
* non-loopback host that has none (mcp-server/src/config.ts). Any other
|
|
57
|
+
* input (a scheme-less host:port, a bare host) is already a dial target and
|
|
58
|
+
* passes through unchanged.
|
|
59
|
+
*/
|
|
60
|
+
export function grpcTarget(endpoint: string): string {
|
|
61
|
+
let url: URL;
|
|
62
|
+
try {
|
|
63
|
+
url = new URL(endpoint);
|
|
64
|
+
} catch {
|
|
65
|
+
return endpoint;
|
|
66
|
+
}
|
|
67
|
+
// `new URL("host:7234")` parses with `host:` as its scheme, so only a
|
|
68
|
+
// real http(s) URL is taken apart.
|
|
69
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
70
|
+
return endpoint;
|
|
71
|
+
}
|
|
72
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
73
|
+
return `${url.hostname}:${port}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The address the platform supplies for a server of this transport, or
|
|
78
|
+
* `null` when the runner knows none: the operator's public endpoint for any
|
|
79
|
+
* transport, else the runner's own backend endpoint for a stdio child.
|
|
80
|
+
*/
|
|
81
|
+
export function platformServerAddress(
|
|
82
|
+
transport: McpServerTransport | undefined,
|
|
83
|
+
endpoints: PlatformEndpoints,
|
|
84
|
+
): string | null {
|
|
85
|
+
if (endpoints.mcpPublicEndpoint) {
|
|
86
|
+
return grpcTarget(endpoints.mcpPublicEndpoint);
|
|
87
|
+
}
|
|
88
|
+
if (transport === "stdio") {
|
|
89
|
+
return grpcTarget(endpoints.stigmerBackendEndpoint);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return the env map with STIGMER_SERVER_ADDRESS filled for `server` when it
|
|
96
|
+
* declares the key and the map holds no non-empty value for it; otherwise the
|
|
97
|
+
* map itself, unchanged. Never mutates its input.
|
|
98
|
+
*/
|
|
99
|
+
export function fillPlatformServerAddress(
|
|
100
|
+
server: McpServer,
|
|
101
|
+
envVars: Record<string, string>,
|
|
102
|
+
endpoints: PlatformEndpoints,
|
|
103
|
+
): Record<string, string> {
|
|
104
|
+
const declared = server.spec?.env ?? {};
|
|
105
|
+
if (!(SERVER_ADDRESS_ENV_KEY in declared) || envVars[SERVER_ADDRESS_ENV_KEY]) {
|
|
106
|
+
return envVars;
|
|
107
|
+
}
|
|
108
|
+
const value = platformServerAddress(server.spec?.serverType.case, endpoints);
|
|
109
|
+
if (value === null) {
|
|
110
|
+
return envVars;
|
|
111
|
+
}
|
|
112
|
+
const source = endpoints.mcpPublicEndpoint
|
|
113
|
+
? "STIGMER_MCP_PUBLIC_ENDPOINT"
|
|
114
|
+
: "STIGMER_BACKEND_ENDPOINT";
|
|
115
|
+
console.info(
|
|
116
|
+
`MCP server '${server.metadata?.slug ?? ""}': ${SERVER_ADDRESS_ENV_KEY} ` +
|
|
117
|
+
`filled from ${source} (no value in the execution environment)`,
|
|
118
|
+
);
|
|
119
|
+
return { ...envVars, [SERVER_ADDRESS_ENV_KEY]: value };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The transports a McpServer spec declares (its `serverType` oneof). */
|
|
123
|
+
type McpServerTransport = NonNullable<NonNullable<McpServer["spec"]>["serverType"]["case"]>;
|
|
@@ -98,15 +98,3 @@ export function injectSynthesizedAttachment(
|
|
|
98
98
|
attachment,
|
|
99
99
|
];
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* The gRPC dial target (host:port) for a backend endpoint URL — the
|
|
104
|
-
* shape STIGMER_SERVER_ADDRESS wants (the bridge warns on schemes).
|
|
105
|
-
*/
|
|
106
|
-
export function grpcTarget(endpoint: string): string {
|
|
107
|
-
try {
|
|
108
|
-
return new URL(endpoint).host;
|
|
109
|
-
} catch {
|
|
110
|
-
return endpoint;
|
|
111
|
-
}
|
|
112
|
-
}
|