@stigmer/runner 3.40.0 → 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/prompt-builder.d.ts +70 -35
- package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
- package/dist/activities/execute-deep-agent/prompt-builder.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.d.ts +18 -13
- package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
- 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/prompt-sections.d.ts +6 -28
- package/dist/shared/prompt-sections.js +6 -35
- package/dist/shared/prompt-sections.js.map +1 -1
- package/dist/shared/skill-resolver.d.ts +2 -3
- package/dist/shared/skill-resolver.js +2 -3
- package/dist/shared/skill-resolver.js.map +1 -1
- 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/sources/git.js +13 -6
- package/dist/shared/workspace/sources/git.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__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
- package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -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__/prompt-builder.test.ts +127 -199
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
- 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/prompt-builder.ts +104 -92
- 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 +43 -38
- 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__/prompt-sections.test.ts +2 -54
- 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/prompt-sections.ts +6 -55
- package/src/shared/skill-resolver.ts +2 -3
- package/src/shared/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- package/src/shared/workspace/sources/git.ts +14 -8
- 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
- package/dist/shared/skill-relevance.d.ts +0 -65
- package/dist/shared/skill-relevance.js +0 -175
- package/dist/shared/skill-relevance.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
- package/src/shared/__tests__/skill-relevance.test.ts +0 -292
- package/src/shared/skill-relevance.ts +0 -222
package/src/runner.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { join } from "node:path";
|
|
|
18
18
|
import { homedir, tmpdir } from "node:os";
|
|
19
19
|
import type { Config, TokenRef } from "./config.js";
|
|
20
20
|
import { DEFAULT_CURSOR_AGENT_RESOLVE_TIMEOUT_MS, DEFAULT_CURSOR_STREAM_STALL_TIMEOUT_MS, DEFAULT_WORKSPACE_LOCK_TIMEOUT_MS } from "./config.js";
|
|
21
|
+
import type { NativeConnection } from "@temporalio/worker";
|
|
21
22
|
import type { WorkerActivities } from "./worker.js";
|
|
22
23
|
import { resolveRunnerBootstrap, refreshRunnerAccessToken } from "./bootstrap.js";
|
|
23
24
|
import { createRunnerTokenCoordinator } from "./runner-token-coordinator.js";
|
|
@@ -348,7 +349,7 @@ export async function createStigmerRunner(
|
|
|
348
349
|
const shutdownController = registerWorkerShutdownSignal(config.taskQueue);
|
|
349
350
|
|
|
350
351
|
const { startWorker } = await import("./worker.js");
|
|
351
|
-
const worker = await startWorker({ config, activities, payloadCodecs });
|
|
352
|
+
const { worker, connection } = await startWorker({ config, activities, payloadCodecs });
|
|
352
353
|
markBoot("worker_created");
|
|
353
354
|
|
|
354
355
|
return {
|
|
@@ -363,18 +364,10 @@ export async function createStigmerRunner(
|
|
|
363
364
|
emitRunnerBootTiming({ task_queue: config.taskQueue, mode: config.mode });
|
|
364
365
|
await worker.run();
|
|
365
366
|
console.log("Worker stopped");
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// must not mask a clean stop.
|
|
371
|
-
try {
|
|
372
|
-
await shutdownHarnesses(adaptersOf(HARNESS_ADAPTERS));
|
|
373
|
-
} catch (err) {
|
|
374
|
-
console.error(
|
|
375
|
-
`[runner] Harness shutdown failed after the worker drained (continuing): ${err instanceof Error ? err.message : String(err)}`,
|
|
376
|
-
);
|
|
377
|
-
}
|
|
367
|
+
await releaseAfterDrain({
|
|
368
|
+
shutdownHarnesses: () => shutdownHarnesses(adaptersOf(HARNESS_ADAPTERS)),
|
|
369
|
+
connection,
|
|
370
|
+
});
|
|
378
371
|
},
|
|
379
372
|
shutdown() {
|
|
380
373
|
tokenRenewal?.stop();
|
|
@@ -404,6 +397,54 @@ function validateOptions(options: StigmerRunnerOptions): void {
|
|
|
404
397
|
// localhost when no token is present.
|
|
405
398
|
}
|
|
406
399
|
|
|
400
|
+
/** What a drained static worker still holds, released by {@link releaseAfterDrain}. */
|
|
401
|
+
export interface DrainedWorkerResources {
|
|
402
|
+
/** Releases what every harness still holds (the registry's `shutdownHarnesses` over the table). */
|
|
403
|
+
readonly shutdownHarnesses: () => Promise<void>;
|
|
404
|
+
/** The Temporal connection the drained worker polled on; this runner opened it, so it closes it. */
|
|
405
|
+
readonly connection: Pick<NativeConnection, "close">;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The static runner's teardown once `worker.run()` has resolved, in the
|
|
410
|
+
* manager's order (runner-manager.ts `shutdown`): harnesses, then the
|
|
411
|
+
* Temporal connection.
|
|
412
|
+
*
|
|
413
|
+
* - Harnesses first: they release what they still hold (the Cursor harness:
|
|
414
|
+
* its parked session agents, whose executor leases dispose with the
|
|
415
|
+
* process, #215) over their own channels, never the Temporal one.
|
|
416
|
+
* - The connection last, and only after the drain: the SDK refuses to close
|
|
417
|
+
* a connection a worker still holds, and `run()` releases it on its way
|
|
418
|
+
* out.
|
|
419
|
+
*
|
|
420
|
+
* Each step is logged, never thrown: the drain has already happened and the
|
|
421
|
+
* process is exiting, so a teardown failure must not mask a clean stop, and
|
|
422
|
+
* one step failing does not skip the next.
|
|
423
|
+
*/
|
|
424
|
+
export async function releaseAfterDrain(
|
|
425
|
+
resources: DrainedWorkerResources,
|
|
426
|
+
logError: (line: string) => void = (line) => console.error(line),
|
|
427
|
+
): Promise<void> {
|
|
428
|
+
try {
|
|
429
|
+
await resources.shutdownHarnesses();
|
|
430
|
+
} catch (err) {
|
|
431
|
+
logError(
|
|
432
|
+
`[runner] Harness shutdown failed after the worker drained (continuing): ${errorText(err)}`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
await resources.connection.close();
|
|
437
|
+
} catch (err) {
|
|
438
|
+
logError(
|
|
439
|
+
`[runner] Temporal connection close failed after the worker drained (continuing): ${errorText(err)}`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function errorText(err: unknown): string {
|
|
445
|
+
return err instanceof Error ? err.message : String(err);
|
|
446
|
+
}
|
|
447
|
+
|
|
407
448
|
/**
|
|
408
449
|
* The runtime `Config` for the static root. The refs are the root's: it
|
|
409
450
|
* creates them before mapping and writes them on every rotation (the
|
|
@@ -437,9 +478,10 @@ export function mapOptionsToConfig(
|
|
|
437
478
|
stigmerTokenRef: tokenRef,
|
|
438
479
|
stigmerRunnerTokenRef: runnerTokenRef,
|
|
439
480
|
proxyTokenRef: tokenRef,
|
|
440
|
-
// Env-only like the env-loaded path (loadConfig): the bridge
|
|
441
|
-
//
|
|
481
|
+
// Env-only like the env-loaded path (loadConfig): the bridge and public
|
|
482
|
+
// endpoints are deployment topology, not per-session state.
|
|
442
483
|
mcpBridgeEndpoint: process.env.STIGMER_MCP_BRIDGE_ENDPOINT ?? null,
|
|
484
|
+
mcpPublicEndpoint: process.env.STIGMER_MCP_PUBLIC_ENDPOINT || null,
|
|
443
485
|
cursorApiKey: proxyActive
|
|
444
486
|
? (options.cursorApiKey ?? "proxy-managed")
|
|
445
487
|
: (options.cursorApiKey ?? ""),
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
mergeApprovalPolicies,
|
|
12
12
|
lookupMcpToolPolicy,
|
|
13
13
|
resolveApprovalMessage,
|
|
14
|
+
resolveBuiltInApprovalMessage,
|
|
14
15
|
deriveActiveLeases,
|
|
15
16
|
resolveApprovalProvenance,
|
|
16
17
|
resolveToolApproval,
|
|
@@ -551,6 +552,14 @@ describe("resolveToolApproval — THE gate decision, shared by the gate and the
|
|
|
551
552
|
});
|
|
552
553
|
});
|
|
553
554
|
|
|
555
|
+
it("a gated native file write waits with the file named on its card (#1112)", () => {
|
|
556
|
+
expect(resolveToolApproval("write_file", "", { file_path: "/workspace/src/app.ts", content: "x" }, new Map(), NO_CATEGORIES)).toEqual({
|
|
557
|
+
requiresApproval: true,
|
|
558
|
+
message: "Write file: /workspace/src/app.ts",
|
|
559
|
+
source: "builtin_category",
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
554
563
|
it("a mutating built-in whose category is leased runs, source approval_lease", () => {
|
|
555
564
|
expect(resolveToolApproval("execute", "", { command: "ls" }, new Map(), new Set<ToolApprovalCategory>(["shell"]))).toEqual({
|
|
556
565
|
requiresApproval: false,
|
|
@@ -565,6 +574,38 @@ describe("resolveToolApproval — THE gate decision, shared by the gate and the
|
|
|
565
574
|
});
|
|
566
575
|
});
|
|
567
576
|
|
|
577
|
+
describe("resolveBuiltInApprovalMessage — the one card wording for a gated built-in, either harness", () => {
|
|
578
|
+
it("names the file a native write or edit touches: deepagents' file tools send file_path (#1112)", () => {
|
|
579
|
+
expect(resolveBuiltInApprovalMessage("write_file", { file_path: "/workspace/src/app.ts", content: "x" })).toBe("Write file: /workspace/src/app.ts");
|
|
580
|
+
expect(resolveBuiltInApprovalMessage("edit_file", { file_path: "/workspace/README.md", old_string: "a", new_string: "b" })).toBe("Write file: /workspace/README.md");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("names the file from path in the Cursor stream's taxonomy, and from file_path in its hook's", () => {
|
|
584
|
+
expect(resolveBuiltInApprovalMessage("edit", { path: "src/app.ts" })).toBe("Write file: src/app.ts");
|
|
585
|
+
expect(resolveBuiltInApprovalMessage("Write", { file_path: "src/app.ts" })).toBe("Write file: src/app.ts");
|
|
586
|
+
expect(resolveBuiltInApprovalMessage("Delete", { file_path: "old.txt" })).toBe("Delete: old.txt");
|
|
587
|
+
expect(resolveBuiltInApprovalMessage("delete", { path: "old.txt" })).toBe("Delete: old.txt");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("prefers path when a call carries both names", () => {
|
|
591
|
+
expect(resolveBuiltInApprovalMessage("write_file", { path: "a.txt", file_path: "b.txt" })).toBe("Write file: a.txt");
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("still reads <unknown> when a file call names no file at all", () => {
|
|
595
|
+
expect(resolveBuiltInApprovalMessage("write_file", { content: "x" })).toBe("Write file: <unknown>");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("words a shell card from the command, in either taxonomy", () => {
|
|
599
|
+
expect(resolveBuiltInApprovalMessage("execute", { command: "rm -rf build" })).toBe("Run command: rm -rf build");
|
|
600
|
+
expect(resolveBuiltInApprovalMessage("Shell", { command: "ls" })).toBe("Run command: ls");
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("answers undefined for a built-in that is not gated", () => {
|
|
604
|
+
expect(resolveBuiltInApprovalMessage("read_file", { file_path: "/x" })).toBeUndefined();
|
|
605
|
+
expect(resolveBuiltInApprovalMessage("Read", { file_path: "/x" })).toBeUndefined();
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
568
609
|
describe("toProtoPolicySource", () => {
|
|
569
610
|
it("maps undefined (no governing layer) to UNSPECIFIED", () => {
|
|
570
611
|
expect(toProtoPolicySource(undefined)).toBe(ApprovalPolicySource.UNSPECIFIED);
|
|
@@ -647,6 +647,7 @@ describe("loadArtifactStorageConfig", () => {
|
|
|
647
647
|
temporalConnection: {},
|
|
648
648
|
stigmerBackendEndpoint: "http://localhost:7234",
|
|
649
649
|
mcpBridgeEndpoint: null,
|
|
650
|
+
mcpPublicEndpoint: null,
|
|
650
651
|
cursorApiKey: "",
|
|
651
652
|
workspaceRootDir: "/tmp",
|
|
652
653
|
maxConcurrentActivities: 5,
|
|
@@ -5,6 +5,10 @@ import {
|
|
|
5
5
|
extractRuntimeEnvForServer,
|
|
6
6
|
} from "../connect-backfill.js";
|
|
7
7
|
import type { ResolvedMcpServer } from "../mcp-resolver.js";
|
|
8
|
+
import { testConfig } from "../../__test-utils__/config-fixture.js";
|
|
9
|
+
|
|
10
|
+
/** The runner endpoints resolution fills STIGMER_SERVER_ADDRESS from. */
|
|
11
|
+
const PLATFORM_ENDPOINTS = testConfig();
|
|
8
12
|
|
|
9
13
|
function makeServer(overrides: Partial<ResolvedMcpServer> = {}): ResolvedMcpServer {
|
|
10
14
|
return {
|
|
@@ -125,7 +129,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
125
129
|
const client = makeMockClient();
|
|
126
130
|
|
|
127
131
|
const result = await backfillMcpServersIfNeeded(
|
|
128
|
-
client, servers, [], {}, "org", "stdio-allowed",
|
|
132
|
+
client, servers, [], {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
129
133
|
);
|
|
130
134
|
|
|
131
135
|
expect(result).toBe(servers);
|
|
@@ -138,7 +142,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
138
142
|
const client = makeMockClient();
|
|
139
143
|
|
|
140
144
|
const result = await backfillMcpServersIfNeeded(
|
|
141
|
-
client, servers, [], {}, "org", "stdio-allowed",
|
|
145
|
+
client, servers, [], {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
142
146
|
);
|
|
143
147
|
|
|
144
148
|
expect(result).toBe(servers);
|
|
@@ -156,7 +160,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
156
160
|
});
|
|
157
161
|
|
|
158
162
|
const result = await backfillMcpServersIfNeeded(
|
|
159
|
-
client, servers, usages, {}, "org", "stdio-allowed",
|
|
163
|
+
client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
160
164
|
);
|
|
161
165
|
|
|
162
166
|
expect(client.getMcpServerByReference).toHaveBeenCalledOnce();
|
|
@@ -182,7 +186,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
182
186
|
});
|
|
183
187
|
|
|
184
188
|
const result = await backfillMcpServersIfNeeded(
|
|
185
|
-
client, servers, usages, {}, "org", "stdio-allowed",
|
|
189
|
+
client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
186
190
|
);
|
|
187
191
|
|
|
188
192
|
expect(client.connectMcpServer).toHaveBeenCalledOnce();
|
|
@@ -207,7 +211,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
207
211
|
});
|
|
208
212
|
|
|
209
213
|
await backfillMcpServersIfNeeded(
|
|
210
|
-
client, servers, usages, mergedEnv, "org", "stdio-allowed",
|
|
214
|
+
client, servers, usages, mergedEnv, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
211
215
|
);
|
|
212
216
|
|
|
213
217
|
expect(client.connectMcpServer).toHaveBeenCalledWith(
|
|
@@ -225,7 +229,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
225
229
|
});
|
|
226
230
|
|
|
227
231
|
const result = await backfillMcpServersIfNeeded(
|
|
228
|
-
client, servers, usages, {}, "org", "stdio-allowed",
|
|
232
|
+
client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
229
233
|
);
|
|
230
234
|
|
|
231
235
|
expect(result).toBe(servers);
|
|
@@ -243,7 +247,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
243
247
|
});
|
|
244
248
|
|
|
245
249
|
const result = await backfillMcpServersIfNeeded(
|
|
246
|
-
client, servers, usages, {}, "org", "stdio-allowed",
|
|
250
|
+
client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
247
251
|
);
|
|
248
252
|
|
|
249
253
|
expect(result).toBe(servers);
|
|
@@ -262,7 +266,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
262
266
|
});
|
|
263
267
|
|
|
264
268
|
await backfillMcpServersIfNeeded(
|
|
265
|
-
client, servers, usages, {}, "org", "stdio-allowed", onHeartbeat,
|
|
269
|
+
client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS, onHeartbeat,
|
|
266
270
|
);
|
|
267
271
|
|
|
268
272
|
expect(onHeartbeat).toHaveBeenCalledTimes(2);
|
|
@@ -275,7 +279,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
275
279
|
const client = makeMockClient();
|
|
276
280
|
|
|
277
281
|
const result = await backfillMcpServersIfNeeded(
|
|
278
|
-
client, servers, [], {}, "org", "stdio-allowed",
|
|
282
|
+
client, servers, [], {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
279
283
|
);
|
|
280
284
|
|
|
281
285
|
expect(client.getMcpServerByReference).not.toHaveBeenCalled();
|
|
@@ -295,7 +299,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
295
299
|
});
|
|
296
300
|
|
|
297
301
|
const result = await backfillMcpServersIfNeeded(
|
|
298
|
-
client, servers, usages, {}, "org", "stdio-allowed",
|
|
302
|
+
client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
299
303
|
);
|
|
300
304
|
|
|
301
305
|
expect(client.connectMcpServer).not.toHaveBeenCalled();
|
|
@@ -338,7 +342,7 @@ describe("backfillMcpServersIfNeeded", () => {
|
|
|
338
342
|
});
|
|
339
343
|
|
|
340
344
|
const result = await backfillMcpServersIfNeeded(
|
|
341
|
-
client, servers, usages, {}, "org", "stdio-allowed",
|
|
345
|
+
client, servers, usages, {}, "org", "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
342
346
|
);
|
|
343
347
|
|
|
344
348
|
expect(callCount).toBe(2);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { mcpServerToResolved, mergeMcpServerUsages, resolveMcpServers } from "../mcp-resolver.js";
|
|
3
3
|
import { McpTransportError } from "../mcp-transport-guard.js";
|
|
4
|
+
import { testConfig } from "../../__test-utils__/config-fixture.js";
|
|
5
|
+
|
|
6
|
+
/** The runner endpoints resolution fills STIGMER_SERVER_ADDRESS from. */
|
|
7
|
+
const PLATFORM_ENDPOINTS = testConfig();
|
|
4
8
|
|
|
5
9
|
function makeUsage(
|
|
6
10
|
slug: string,
|
|
@@ -60,7 +64,7 @@ describe("resolveMcpServers — transport guard integration", () => {
|
|
|
60
64
|
const client = clientReturning({ filesystem: stdioMcpServer("filesystem") });
|
|
61
65
|
|
|
62
66
|
await expect(
|
|
63
|
-
resolveMcpServers(client, [makeUsage("filesystem")], {}, "stdio-forbidden"),
|
|
67
|
+
resolveMcpServers(client, [makeUsage("filesystem")], {}, "stdio-forbidden", PLATFORM_ENDPOINTS),
|
|
64
68
|
).rejects.toThrow(McpTransportError);
|
|
65
69
|
});
|
|
66
70
|
|
|
@@ -75,7 +79,7 @@ describe("resolveMcpServers — transport guard integration", () => {
|
|
|
75
79
|
client,
|
|
76
80
|
[makeUsage("github"), makeUsage("filesystem")],
|
|
77
81
|
{},
|
|
78
|
-
"stdio-forbidden",
|
|
82
|
+
"stdio-forbidden", PLATFORM_ENDPOINTS,
|
|
79
83
|
),
|
|
80
84
|
).rejects.toThrow(McpTransportError);
|
|
81
85
|
});
|
|
@@ -84,7 +88,7 @@ describe("resolveMcpServers — transport guard integration", () => {
|
|
|
84
88
|
const client = clientReturning({ github: httpMcpServer("github") });
|
|
85
89
|
|
|
86
90
|
const result = await resolveMcpServers(
|
|
87
|
-
client, [makeUsage("github")], {}, "stdio-forbidden",
|
|
91
|
+
client, [makeUsage("github")], {}, "stdio-forbidden", PLATFORM_ENDPOINTS,
|
|
88
92
|
);
|
|
89
93
|
|
|
90
94
|
expect(result.resolvedServers).toHaveLength(1);
|
|
@@ -95,7 +99,7 @@ describe("resolveMcpServers — transport guard integration", () => {
|
|
|
95
99
|
const client = clientReturning({ filesystem: stdioMcpServer("filesystem") });
|
|
96
100
|
|
|
97
101
|
const result = await resolveMcpServers(
|
|
98
|
-
client, [makeUsage("filesystem")], {}, "stdio-allowed",
|
|
102
|
+
client, [makeUsage("filesystem")], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
99
103
|
);
|
|
100
104
|
|
|
101
105
|
expect(result.resolvedServers).toHaveLength(1);
|
|
@@ -106,7 +110,7 @@ describe("resolveMcpServers — transport guard integration", () => {
|
|
|
106
110
|
const client = clientReturning({});
|
|
107
111
|
|
|
108
112
|
const result = await resolveMcpServers(
|
|
109
|
-
client, [makeUsage("ghost")], {}, "stdio-forbidden",
|
|
113
|
+
client, [makeUsage("ghost")], {}, "stdio-forbidden", PLATFORM_ENDPOINTS,
|
|
110
114
|
);
|
|
111
115
|
|
|
112
116
|
expect(result.resolvedServers).toHaveLength(0);
|
|
@@ -123,7 +127,7 @@ describe("resolveMcpServers — enabled_tools threading (issue #350)", () => {
|
|
|
123
127
|
const client = clientReturning({ github: httpMcpServer("github") });
|
|
124
128
|
|
|
125
129
|
const result = await resolveMcpServers(
|
|
126
|
-
client, [makeUsage("github", "test-org", ["create_pr"])], {}, "stdio-allowed",
|
|
130
|
+
client, [makeUsage("github", "test-org", ["create_pr"])], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
127
131
|
);
|
|
128
132
|
|
|
129
133
|
expect(result.resolvedServers[0].enabledTools).toEqual(["create_pr"]);
|
|
@@ -135,7 +139,7 @@ describe("resolveMcpServers — enabled_tools threading (issue #350)", () => {
|
|
|
135
139
|
});
|
|
136
140
|
|
|
137
141
|
const result = await resolveMcpServers(
|
|
138
|
-
client, [makeUsage("github")], {}, "stdio-allowed",
|
|
142
|
+
client, [makeUsage("github")], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
139
143
|
);
|
|
140
144
|
|
|
141
145
|
expect(result.resolvedServers[0].enabledTools).toEqual(["search_code"]);
|
|
@@ -145,7 +149,7 @@ describe("resolveMcpServers — enabled_tools threading (issue #350)", () => {
|
|
|
145
149
|
const client = clientReturning({ github: httpMcpServer("github") });
|
|
146
150
|
|
|
147
151
|
const result = await resolveMcpServers(
|
|
148
|
-
client, [makeUsage("github")], {}, "stdio-allowed",
|
|
152
|
+
client, [makeUsage("github")], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
149
153
|
);
|
|
150
154
|
|
|
151
155
|
expect(result.resolvedServers[0].enabledTools).toBeUndefined();
|
|
@@ -179,7 +183,7 @@ describe("resolveMcpServers — tool_approval_overrides threading (issue #349)",
|
|
|
179
183
|
makeUsage("slack"),
|
|
180
184
|
],
|
|
181
185
|
{},
|
|
182
|
-
"stdio-allowed",
|
|
186
|
+
"stdio-allowed", PLATFORM_ENDPOINTS,
|
|
183
187
|
);
|
|
184
188
|
|
|
185
189
|
const bySlug = new Map(result.resolvedServers.map((s) => [s.slug, s]));
|
|
@@ -208,7 +212,7 @@ describe("resolveMcpServers — tool_approval_overrides threading (issue #349)",
|
|
|
208
212
|
[makeUsage("github", "test-org", [], [{ toolName: "push", requiresApproval: false }])],
|
|
209
213
|
);
|
|
210
214
|
|
|
211
|
-
const result = await resolveMcpServers(client, merged, {}, "stdio-allowed");
|
|
215
|
+
const result = await resolveMcpServers(client, merged, {}, "stdio-allowed", PLATFORM_ENDPOINTS);
|
|
212
216
|
|
|
213
217
|
expect(result.resolvedServers[0].toolApprovalOverrides).toEqual([
|
|
214
218
|
{ toolName: "push", requiresApproval: false },
|
|
@@ -216,6 +220,64 @@ describe("resolveMcpServers — tool_approval_overrides threading (issue #349)",
|
|
|
216
220
|
});
|
|
217
221
|
});
|
|
218
222
|
|
|
223
|
+
describe("resolveMcpServers — the platform STIGMER_SERVER_ADDRESS (stigmer/stigmer#1433)", () => {
|
|
224
|
+
function declaringAddress(server: any, headers: Record<string, string> = {}) {
|
|
225
|
+
server.spec.env = { STIGMER_SERVER_ADDRESS: {} };
|
|
226
|
+
if (server.spec.serverType.case === "http") server.spec.serverType.value.headers = headers;
|
|
227
|
+
return server;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
it("fills a stdio server's missing address from the runner's backend endpoint", async () => {
|
|
231
|
+
const client = clientReturning({ stigmer: declaringAddress(stdioMcpServer("stigmer")) });
|
|
232
|
+
|
|
233
|
+
const result = await resolveMcpServers(
|
|
234
|
+
client, [makeUsage("stigmer")], {}, "stdio-allowed", PLATFORM_ENDPOINTS,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// testConfig's backend endpoint is http://127.0.0.1:1.
|
|
238
|
+
expect(result.resolvedServers[0].env).toEqual({ STIGMER_SERVER_ADDRESS: "127.0.0.1:1" });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("keeps the address the execution environment carries", async () => {
|
|
242
|
+
const client = clientReturning({ stigmer: declaringAddress(stdioMcpServer("stigmer")) });
|
|
243
|
+
|
|
244
|
+
const result = await resolveMcpServers(
|
|
245
|
+
client, [makeUsage("stigmer")], { STIGMER_SERVER_ADDRESS: "api.example.com:443" },
|
|
246
|
+
"stdio-allowed", PLATFORM_ENDPOINTS,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
expect(result.resolvedServers[0].env).toEqual({ STIGMER_SERVER_ADDRESS: "api.example.com:443" });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("templates an http header from the operator's public endpoint", async () => {
|
|
253
|
+
const client = clientReturning({
|
|
254
|
+
remote: declaringAddress(httpMcpServer("remote"), { "X-Stigmer-Server": "${STIGMER_SERVER_ADDRESS}" }),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const result = await resolveMcpServers(
|
|
258
|
+
client, [makeUsage("remote")], {}, "stdio-forbidden",
|
|
259
|
+
testConfig({ mcpPublicEndpoint: "https://api.example.com" }),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(result.resolvedServers[0].headers).toEqual({ "X-Stigmer-Server": "api.example.com:443" });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("never hands an http server the backend endpoint: an unknown address drops it with the named error", async () => {
|
|
266
|
+
const client = clientReturning({
|
|
267
|
+
remote: declaringAddress(httpMcpServer("remote"), { "X-Stigmer-Server": "${STIGMER_SERVER_ADDRESS}" }),
|
|
268
|
+
});
|
|
269
|
+
const errorLog = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
270
|
+
|
|
271
|
+
const result = await resolveMcpServers(
|
|
272
|
+
client, [makeUsage("remote")], {}, "stdio-forbidden", PLATFORM_ENDPOINTS,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
expect(result.resolvedServers).toEqual([]);
|
|
276
|
+
expect(errorLog).toHaveBeenCalledWith(expect.stringContaining("STIGMER_SERVER_ADDRESS"));
|
|
277
|
+
errorLog.mockRestore();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
219
281
|
describe("mergeMcpServerUsages — session-wins-per-slug (shared by both harnesses)", () => {
|
|
220
282
|
it("session usage replaces the agent's WHOLE usage for the same slug, including enabled_tools", () => {
|
|
221
283
|
const merged = mergeMcpServerUsages(
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
15
15
|
import {
|
|
16
|
+
ORGANIZATION_PROVIDER_KEY_SENTINEL,
|
|
16
17
|
PLATFORM_CAPACITY_SENTINEL,
|
|
17
18
|
classifyModelCallError,
|
|
18
19
|
describeExecutionError,
|
|
@@ -29,6 +30,20 @@ function middlewareWrap(inner: Error): Error {
|
|
|
29
30
|
return wrapped;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Wrap as a native turn does: LangChain adds one MiddlewareError per
|
|
35
|
+
* middleware implementing `wrapModelCall`, and the production stack has
|
|
36
|
+
* more than ten of them (stigmer/stigmer#1450).
|
|
37
|
+
*/
|
|
38
|
+
function wrapLikeMiddlewareStack(inner: Error, layers: number): Error {
|
|
39
|
+
let wrapped = inner;
|
|
40
|
+
for (let layer = 0; layer < layers; layer++) wrapped = middlewareWrap(wrapped);
|
|
41
|
+
return wrapped;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A middleware stack deeper than any native turn's today. */
|
|
45
|
+
const DEEP_STACK = 30;
|
|
46
|
+
|
|
32
47
|
/** Mimic a provider SDK APIError: message + numeric `.status`. */
|
|
33
48
|
function sdkError(status: number, message: string): Error {
|
|
34
49
|
return Object.assign(new Error(message), { status });
|
|
@@ -62,13 +77,17 @@ describe("unwrapModelError", () => {
|
|
|
62
77
|
expect(unwrapModelError(wrapped)).toBe(root);
|
|
63
78
|
});
|
|
64
79
|
|
|
65
|
-
it("
|
|
80
|
+
it("walks to the root however deep the middleware stack wraps it", () => {
|
|
81
|
+
const root = sdkError(401, "401 authentication_error");
|
|
82
|
+
expect(unwrapModelError(wrapLikeMiddlewareStack(root, DEEP_STACK))).toBe(root);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("ends a cause cycle at the first error seen twice", () => {
|
|
66
86
|
const a = new Error("a");
|
|
67
87
|
const b = new Error("b");
|
|
68
88
|
a.cause = b;
|
|
69
89
|
b.cause = a;
|
|
70
|
-
|
|
71
|
-
expect(unwrapModelError(a)).toBeInstanceOf(Error);
|
|
90
|
+
expect(unwrapModelError(a)).toBe(b);
|
|
72
91
|
});
|
|
73
92
|
|
|
74
93
|
it("passes non-Error values through", () => {
|
|
@@ -538,3 +557,52 @@ describe("describeExecutionError", () => {
|
|
|
538
557
|
expect(errorMessage).toContain("platform-side issue");
|
|
539
558
|
});
|
|
540
559
|
});
|
|
560
|
+
|
|
561
|
+
describe("classifyModelCallError on the organization's own provider key", () => {
|
|
562
|
+
const proxied = { proxyMode: true, provider: "anthropic" as const, modelId: "claude-sonnet-4-6" };
|
|
563
|
+
const marked = (message: string) => `${message} [code: ${ORGANIZATION_PROVIDER_KEY_SENTINEL}]`;
|
|
564
|
+
|
|
565
|
+
it("words the key's billing exhaustion as the organization's account, never platform capacity", () => {
|
|
566
|
+
const err = middlewareWrap(sdkError(400, marked(ANTHROPIC_BILLING_MESSAGE)));
|
|
567
|
+
const classified = classifyModelCallError(err, proxied);
|
|
568
|
+
expect(classified?.code).toBe("LLM_PROVIDER_BILLING");
|
|
569
|
+
expect(classified?.retryable).toBe(false);
|
|
570
|
+
expect(classified?.message).toContain("organization's own Anthropic key");
|
|
571
|
+
expect(classified?.message).not.toContain(PLATFORM_CAPACITY_SENTINEL);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("words a rejected key as the organization's key, not an expired Stigmer session", () => {
|
|
575
|
+
const err = sdkError(401, marked('401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}'));
|
|
576
|
+
const classified = classifyModelCallError(err, proxied);
|
|
577
|
+
expect(classified?.code).toBe("LLM_AUTHENTICATION_ERROR");
|
|
578
|
+
expect(classified?.message).toContain("rejected your organization's own key");
|
|
579
|
+
expect(classified?.message).not.toContain("Stigmer platform");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it("words a rejected key as the organization's through a native turn's middleware stack", () => {
|
|
583
|
+
const rejected = sdkError(401, marked('401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}'));
|
|
584
|
+
const { errorType, errorMessage } = describeExecutionError(wrapLikeMiddlewareStack(rejected, DEEP_STACK), proxied);
|
|
585
|
+
expect(errorType).toBe("LLM_AUTHENTICATION_ERROR");
|
|
586
|
+
expect(errorMessage).toContain("rejected your organization's own key");
|
|
587
|
+
expect(errorMessage).not.toContain("MiddlewareError");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("words a forbidden model as the key's account permission", () => {
|
|
591
|
+
const classified = classifyModelCallError(sdkError(403, marked("403 permission_error")), proxied);
|
|
592
|
+
expect(classified?.code).toBe("LLM_PERMISSION_DENIED");
|
|
593
|
+
expect(classified?.message).toContain("denied your organization's own key");
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("classifies the key's other failures as direct mode would", () => {
|
|
597
|
+
const rateLimited = classifyModelCallError(sdkError(429, marked("429 rate_limit_error")), proxied);
|
|
598
|
+
expect(rateLimited?.code).toBe("LLM_RATE_LIMIT");
|
|
599
|
+
const serverError = classifyModelCallError(sdkError(529, marked("529 overloaded_error")), proxied);
|
|
600
|
+
expect(serverError?.code).toBe("LLM_PROVIDER_ERROR");
|
|
601
|
+
expect(serverError?.retryable).toBe(true);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("leaves an unmarked proxied 401 on the platform wording", () => {
|
|
605
|
+
const classified = classifyModelCallError(sdkError(401, "401 unauthorized"), proxied);
|
|
606
|
+
expect(classified?.message).toContain("Stigmer platform rejected");
|
|
607
|
+
});
|
|
608
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the platform STIGMER_SERVER_ADDRESS: the one dial-target
|
|
3
|
+
* rule, which endpoint answers for which transport, the fill that never
|
|
4
|
+
* overrides, and that the address is the only key the platform fills: a
|
|
5
|
+
* declared STIGMER_API_KEY is the user's to save (stigmer/stigmer#1446).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from "vitest";
|
|
9
|
+
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
10
|
+
import {
|
|
11
|
+
SERVER_ADDRESS_ENV_KEY,
|
|
12
|
+
fillPlatformServerAddress,
|
|
13
|
+
grpcTarget,
|
|
14
|
+
platformServerAddress,
|
|
15
|
+
type PlatformEndpoints,
|
|
16
|
+
} from "../platform-server-address.js";
|
|
17
|
+
|
|
18
|
+
const LOCAL: PlatformEndpoints = {
|
|
19
|
+
stigmerBackendEndpoint: "http://localhost:7234",
|
|
20
|
+
mcpPublicEndpoint: null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const WITH_PUBLIC: PlatformEndpoints = {
|
|
24
|
+
stigmerBackendEndpoint: "http://stigmer-server.internal:80",
|
|
25
|
+
mcpPublicEndpoint: "https://api.example.com",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function server(
|
|
29
|
+
transport: "stdio" | "http",
|
|
30
|
+
declared: readonly string[] = [SERVER_ADDRESS_ENV_KEY],
|
|
31
|
+
): McpServer {
|
|
32
|
+
const env = Object.fromEntries(declared.map((key) => [key, {}]));
|
|
33
|
+
const serverType = transport === "stdio"
|
|
34
|
+
? { case: "stdio", value: { command: "stigmer", args: ["mcp-server"] } }
|
|
35
|
+
: { case: "http", value: { url: "https://mcp.example.com/mcp", headers: {} } };
|
|
36
|
+
return { metadata: { slug: "stigmer" }, spec: { serverType, env } } as unknown as McpServer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("grpcTarget", () => {
|
|
40
|
+
it.each([
|
|
41
|
+
["http://localhost:7234", "localhost:7234"],
|
|
42
|
+
["https://api.stigmer.ai", "api.stigmer.ai:443"],
|
|
43
|
+
["https://api.stigmer.ai:8443", "api.stigmer.ai:8443"],
|
|
44
|
+
["http://api.local", "api.local:80"],
|
|
45
|
+
["http://stigmer.lan:80", "stigmer.lan:80"],
|
|
46
|
+
["http://[::1]:7234", "[::1]:7234"],
|
|
47
|
+
["http://localhost:7234/", "localhost:7234"],
|
|
48
|
+
["https://api.stigmer.ai/v1/rpc", "api.stigmer.ai:443"],
|
|
49
|
+
])("takes an http(s) URL apart with an explicit port: %s -> %s", (input, expected) => {
|
|
50
|
+
expect(grpcTarget(input)).toBe(expected);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each([
|
|
54
|
+
["localhost:7234"],
|
|
55
|
+
["stigmer-server.stigmer-prod.svc.cluster.local:80"],
|
|
56
|
+
["api.stigmer.ai"],
|
|
57
|
+
["not a url"],
|
|
58
|
+
])("passes a value that is not an http(s) URL through: %s", (input) => {
|
|
59
|
+
expect(grpcTarget(input)).toBe(input);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("platformServerAddress", () => {
|
|
64
|
+
it("answers the operator's public endpoint for every transport", () => {
|
|
65
|
+
expect(platformServerAddress("http", WITH_PUBLIC)).toBe("api.example.com:443");
|
|
66
|
+
expect(platformServerAddress("stdio", WITH_PUBLIC)).toBe("api.example.com:443");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("answers the runner's own backend endpoint for a stdio child", () => {
|
|
70
|
+
expect(platformServerAddress("stdio", LOCAL)).toBe("localhost:7234");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("knows no address for a remote server without a public endpoint", () => {
|
|
74
|
+
expect(platformServerAddress("http", LOCAL)).toBeNull();
|
|
75
|
+
expect(platformServerAddress(undefined, LOCAL)).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("fillPlatformServerAddress", () => {
|
|
80
|
+
it("fills a declared, missing address for a stdio server", () => {
|
|
81
|
+
const env = { OTHER: "x" };
|
|
82
|
+
const result = fillPlatformServerAddress(server("stdio"), env, LOCAL);
|
|
83
|
+
expect(result).toEqual({ OTHER: "x", [SERVER_ADDRESS_ENV_KEY]: "localhost:7234" });
|
|
84
|
+
expect(env).toEqual({ OTHER: "x" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("fills a declared, empty address", () => {
|
|
88
|
+
const result = fillPlatformServerAddress(
|
|
89
|
+
server("stdio"),
|
|
90
|
+
{ [SERVER_ADDRESS_ENV_KEY]: "" },
|
|
91
|
+
LOCAL,
|
|
92
|
+
);
|
|
93
|
+
expect(result[SERVER_ADDRESS_ENV_KEY]).toBe("localhost:7234");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("never overrides a value already present", () => {
|
|
97
|
+
const env = { [SERVER_ADDRESS_ENV_KEY]: "other.example:7234" };
|
|
98
|
+
expect(fillPlatformServerAddress(server("stdio"), env, WITH_PUBLIC)).toBe(env);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("gives nothing to a server that does not declare the key", () => {
|
|
102
|
+
const env = {};
|
|
103
|
+
expect(fillPlatformServerAddress(server("stdio", ["OTHER"]), env, LOCAL)).toBe(env);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("fills a remote server only from the public endpoint", () => {
|
|
107
|
+
expect(
|
|
108
|
+
fillPlatformServerAddress(server("http"), {}, WITH_PUBLIC)[SERVER_ADDRESS_ENV_KEY],
|
|
109
|
+
).toBe("api.example.com:443");
|
|
110
|
+
const env = {};
|
|
111
|
+
expect(fillPlatformServerAddress(server("http"), env, LOCAL)).toBe(env);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("fills the address alone, never a declared STIGMER_API_KEY", () => {
|
|
115
|
+
const result = fillPlatformServerAddress(
|
|
116
|
+
server("http", [SERVER_ADDRESS_ENV_KEY, "STIGMER_API_KEY"]),
|
|
117
|
+
{},
|
|
118
|
+
WITH_PUBLIC,
|
|
119
|
+
);
|
|
120
|
+
expect(result).toEqual({ [SERVER_ADDRESS_ENV_KEY]: "api.example.com:443" });
|
|
121
|
+
});
|
|
122
|
+
});
|