@stigmer/runner 3.1.5 → 3.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/activities/discover-mcp-server.js +23 -0
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +39 -1
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-provision.d.ts +23 -4
- package/dist/activities/execute-cursor/workspace-provision.js +17 -6
- package/dist/activities/execute-cursor/workspace-provision.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.js +8 -1
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +1 -1
- package/dist/activities/execute-deep-agent/post-stream.d.ts +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +1 -1
- package/dist/activities/execute-deep-agent/streaming-terminal.d.ts +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +1 -1
- package/dist/shared/execution-status-writer.d.ts +31 -0
- package/dist/shared/execution-status-writer.js +43 -0
- package/dist/shared/execution-status-writer.js.map +1 -0
- package/dist/shared/mcp-oauth-detect.d.ts +53 -0
- package/dist/shared/mcp-oauth-detect.js +99 -0
- package/dist/shared/mcp-oauth-detect.js.map +1 -0
- package/dist/shared/workspace/writeback-coordinator.d.ts +120 -0
- package/dist/shared/workspace/writeback-coordinator.js +393 -0
- package/dist/shared/workspace/writeback-coordinator.js.map +1 -0
- package/package.json +2 -2
- package/src/activities/discover-mcp-server.ts +25 -0
- package/src/activities/execute-cursor/__tests__/workspace-provision.test.ts +23 -15
- package/src/activities/execute-cursor/index.ts +41 -1
- package/src/activities/execute-cursor/workspace-provision.ts +39 -7
- package/src/activities/execute-deep-agent/__tests__/post-stream.test.ts +1 -1
- package/src/activities/execute-deep-agent/index.ts +8 -1
- package/src/activities/execute-deep-agent/inline-publisher.ts +1 -1
- package/src/activities/execute-deep-agent/post-stream.ts +1 -1
- package/src/activities/execute-deep-agent/status-builder.ts +1 -1
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +1 -1
- package/src/activities/execute-deep-agent/streaming-terminal.ts +1 -1
- package/src/activities/execute-deep-agent/streaming.ts +1 -1
- package/src/activities/execute-deep-agent/v3-status-builder.ts +1 -1
- package/src/shared/__tests__/mcp-oauth-detect.test.ts +147 -0
- package/src/shared/execution-status-writer.ts +56 -0
- package/src/shared/mcp-oauth-detect.ts +112 -0
- package/src/shared/workspace/__tests__/writeback-coordinator.integration.test.ts +230 -0
- package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +477 -0
- package/src/{activities/execute-deep-agent → shared/workspace}/writeback-coordinator.ts +206 -94
- package/dist/activities/execute-deep-agent/execution-status-writer.d.ts +0 -17
- package/dist/activities/execute-deep-agent/execution-status-writer.js +0 -9
- package/dist/activities/execute-deep-agent/execution-status-writer.js.map +0 -1
- package/dist/activities/execute-deep-agent/writeback-coordinator.d.ts +0 -71
- package/dist/activities/execute-deep-agent/writeback-coordinator.js +0 -299
- package/dist/activities/execute-deep-agent/writeback-coordinator.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/writeback-coordinator.test.ts +0 -426
- package/src/activities/execute-deep-agent/execution-status-writer.ts +0 -19
|
@@ -10,18 +10,38 @@
|
|
|
10
10
|
import type { Config } from "../../config.js";
|
|
11
11
|
import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
12
12
|
import { WorkspaceProvisioner } from "../../shared/workspace/provisioner.js";
|
|
13
|
+
import type { ProvisionResult, WorkspaceBackend } from "../../shared/workspace/types.js";
|
|
13
14
|
import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
|
|
14
15
|
import { ensurePlatformDir } from "../../shared/workspace/platform-dir.js";
|
|
15
16
|
import { resolveSessionWorkspaceRoot } from "../../shared/workspace/session-root.js";
|
|
16
17
|
|
|
18
|
+
/** What {@link provisionCursorWorkspace} hands back to the harness. */
|
|
19
|
+
export interface CursorWorkspaceProvision {
|
|
20
|
+
/** The directories the agent should operate in (never empty). */
|
|
21
|
+
readonly workspaceDirs: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Per-entry provision outcomes — carries the git metadata (repo URL, base
|
|
24
|
+
* branch, credential state) the write-back coordinator needs. Empty for a
|
|
25
|
+
* session with no workspace entries.
|
|
26
|
+
*/
|
|
27
|
+
readonly provisionResults: ProvisionResult[];
|
|
28
|
+
/**
|
|
29
|
+
* The backend the entries were provisioned through — the write-back
|
|
30
|
+
* coordinator executes its git commands through it.
|
|
31
|
+
*/
|
|
32
|
+
readonly workspaceBackend: WorkspaceBackend;
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
36
|
* Provision the session's workspace entries for a LOCAL Cursor agent.
|
|
19
37
|
*
|
|
20
38
|
* Clones git-repo entries (using the user's GITHUB_TOKEN from the resolved
|
|
21
39
|
* execution environment) and mounts local-path entries, then returns the
|
|
22
|
-
* directories the agent should operate in
|
|
23
|
-
*
|
|
24
|
-
*
|
|
40
|
+
* directories the agent should operate in — along with the provision results
|
|
41
|
+
* and backend the git write-back coordinator needs. A session with no
|
|
42
|
+
* workspace entries gets its own empty per-session directory (see
|
|
43
|
+
* session-root.ts) — never the shared root, which would leak other sessions'
|
|
44
|
+
* files into it.
|
|
25
45
|
*
|
|
26
46
|
* provisionGit is idempotent (it reuses an existing clone), so this is safe
|
|
27
47
|
* to call on every execution, including multi-turn and HITL reinvocations.
|
|
@@ -31,13 +51,21 @@ export async function provisionCursorWorkspace(
|
|
|
31
51
|
session: Session,
|
|
32
52
|
envVars: Record<string, string>,
|
|
33
53
|
sessionId: string,
|
|
34
|
-
): Promise<
|
|
54
|
+
): Promise<CursorWorkspaceProvision> {
|
|
35
55
|
const entries = session.spec?.workspaceEntries ?? [];
|
|
56
|
+
const platformDir = await ensurePlatformDir(sessionId);
|
|
57
|
+
|
|
36
58
|
if (entries.length === 0) {
|
|
37
|
-
|
|
59
|
+
const sessionRoot = await resolveSessionWorkspaceRoot(
|
|
60
|
+
config.workspaceRootDir, entries, sessionId,
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
workspaceDirs: [sessionRoot],
|
|
64
|
+
provisionResults: [],
|
|
65
|
+
workspaceBackend: new LocalWorkspaceBackend(sessionRoot, platformDir),
|
|
66
|
+
};
|
|
38
67
|
}
|
|
39
68
|
|
|
40
|
-
const platformDir = await ensurePlatformDir(sessionId);
|
|
41
69
|
const backend = new LocalWorkspaceBackend(config.workspaceRootDir, platformDir);
|
|
42
70
|
|
|
43
71
|
const provisioner = new WorkspaceProvisioner();
|
|
@@ -53,5 +81,9 @@ export async function provisionCursorWorkspace(
|
|
|
53
81
|
.map((result) => result.rootDir)
|
|
54
82
|
.filter((dir): dir is string => Boolean(dir));
|
|
55
83
|
|
|
56
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
workspaceDirs: dirs.length > 0 ? dirs : [config.workspaceRootDir],
|
|
86
|
+
provisionResults: results,
|
|
87
|
+
workspaceBackend: backend,
|
|
88
|
+
};
|
|
57
89
|
}
|
|
@@ -3,7 +3,7 @@ import { create } from "@bufbuild/protobuf";
|
|
|
3
3
|
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
4
4
|
import { processPostStream } from "../post-stream.js";
|
|
5
5
|
import type { InlinePublisher } from "../inline-publisher.js";
|
|
6
|
-
import type { WriteBackCoordinator } from "
|
|
6
|
+
import type { WriteBackCoordinator } from "../../../shared/workspace/writeback-coordinator.js";
|
|
7
7
|
|
|
8
8
|
function mockInlinePublisher(): InlinePublisher {
|
|
9
9
|
return {
|
|
@@ -45,7 +45,7 @@ import { streamExecution, type StreamResult } from "./streaming.js";
|
|
|
45
45
|
import { loadStreamingConfig } from "../../shared/streaming-scheduler.js";
|
|
46
46
|
import { StatusBuilder } from "./status-builder.js";
|
|
47
47
|
import { InlinePublisher } from "./inline-publisher.js";
|
|
48
|
-
import { WriteBackCoordinator } from "
|
|
48
|
+
import { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
|
|
49
49
|
import { processPostStream } from "./post-stream.js";
|
|
50
50
|
import { resolveResumeInput, type GraphStateSnapshot } from "./hitl.js";
|
|
51
51
|
import { captureApprovalArtifacts } from "./approval-file-change.js";
|
|
@@ -184,6 +184,13 @@ export function createDeepAgentActivities(config: Config) {
|
|
|
184
184
|
? new WriteBackCoordinator({
|
|
185
185
|
statusWriter: statusBuilder,
|
|
186
186
|
executionId,
|
|
187
|
+
// Branch/PR are session-scoped: every turn of this session
|
|
188
|
+
// appends commits to the same stigmer/<session-id> branch.
|
|
189
|
+
sessionId: setup.session.metadata?.id ?? "",
|
|
190
|
+
// The PR API token, plumbed from the resolved execution env —
|
|
191
|
+
// gitMetadata.repoUrl is token-stripped by construction, so the
|
|
192
|
+
// coordinator can never recover it from provisioning state.
|
|
193
|
+
githubToken: setup.mergedEnvVars.GITHUB_TOKEN ?? "",
|
|
187
194
|
provisionResults: setup.provisionResults,
|
|
188
195
|
workspaceEntries: workspaceEntries as any,
|
|
189
196
|
workspaceBackend: setup.workspaceBackend,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
21
21
|
import type { ArtifactStorage } from "../../shared/artifact-storage.js";
|
|
22
22
|
import type { WorkspaceBackend } from "../../shared/workspace/types.js";
|
|
23
|
-
import type { ExecutionStatusWriter } from "
|
|
23
|
+
import type { ExecutionStatusWriter } from "../../shared/execution-status-writer.js";
|
|
24
24
|
import { utcTimestamp } from "../../shared/status.js";
|
|
25
25
|
import { isSecretLikePath } from "../../shared/filereview/secret-paths.js";
|
|
26
26
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
14
14
|
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
15
15
|
import type { InlinePublisher } from "./inline-publisher.js";
|
|
16
|
-
import type { WriteBackCoordinator } from "
|
|
16
|
+
import type { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
|
|
17
17
|
import { autoPublishWrittenFiles } from "./auto-publish.js";
|
|
18
18
|
|
|
19
19
|
export interface PostStreamOptions {
|
|
@@ -31,7 +31,7 @@ import { classifyTool } from "../../shared/tool-kind.js";
|
|
|
31
31
|
import { applyTodoUpdate } from "../../shared/todos.js";
|
|
32
32
|
import { ExecutionState } from "./execution-state.js";
|
|
33
33
|
import { utcTimestamp } from "../../shared/status.js";
|
|
34
|
-
import type { ExecutionStatusWriter } from "
|
|
34
|
+
import type { ExecutionStatusWriter } from "../../shared/execution-status-writer.js";
|
|
35
35
|
import {
|
|
36
36
|
UsageAccumulator,
|
|
37
37
|
extractToolResult,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { V3ProtocolEvent } from "./v3-event-recorder.js";
|
|
10
10
|
import type { InlinePublisher } from "./inline-publisher.js";
|
|
11
|
-
import type { WriteBackCoordinator } from "
|
|
11
|
+
import type { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
|
|
12
12
|
import { extractFilePath, isFileModifyingTool } from "../../shared/file-tools.js";
|
|
13
13
|
|
|
14
14
|
interface CachedToolInput {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { create } from "@bufbuild/protobuf";
|
|
9
9
|
import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
10
10
|
import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
11
|
-
import type { ExecutionStatusWriter } from "
|
|
11
|
+
import type { ExecutionStatusWriter } from "../../shared/execution-status-writer.js";
|
|
12
12
|
import type { StreamResult } from "./streaming.js";
|
|
13
13
|
import { slimStatus, utcTimestamp } from "../../shared/status.js";
|
|
14
14
|
|
|
@@ -39,7 +39,7 @@ import type { ToolOutputOffloadContext } from "../../shared/status-offload.js";
|
|
|
39
39
|
import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
40
40
|
import type { GracefulStopMiddleware } from "../../middleware/index.js";
|
|
41
41
|
import type { InlinePublisher } from "./inline-publisher.js";
|
|
42
|
-
import type { WriteBackCoordinator } from "
|
|
42
|
+
import type { WriteBackCoordinator } from "../../shared/workspace/writeback-coordinator.js";
|
|
43
43
|
import { createV2EventRecorder } from "./event-recorder.js";
|
|
44
44
|
import { streamExecutionV3 } from "./streaming-v3.js";
|
|
45
45
|
import { extractFilePath, isFileModifyingTool } from "../../shared/file-tools.js";
|
|
@@ -30,7 +30,7 @@ import { classifyTool } from "../../shared/tool-kind.js";
|
|
|
30
30
|
import { applyTodoUpdate } from "../../shared/todos.js";
|
|
31
31
|
import { ExecutionState } from "./execution-state.js";
|
|
32
32
|
import { utcTimestamp } from "../../shared/status.js";
|
|
33
|
-
import type { ExecutionStatusWriter } from "
|
|
33
|
+
import type { ExecutionStatusWriter } from "../../shared/execution-status-writer.js";
|
|
34
34
|
import type { ApprovalPolicyProvider } from "./status-builder.js";
|
|
35
35
|
import type { StigmerRunEvent, V3UsagePayload } from "./v3-events.js";
|
|
36
36
|
import { namespaceDepth } from "./v3-events.js";
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the HTTP MCP OAuth-challenge classifier.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that `detectOAuthChallenge` recognizes a real MCP OAuth challenge
|
|
5
|
+
* (401 + `WWW-Authenticate: Bearer ...` per RFC 9728), and does NOT misfire on
|
|
6
|
+
* a plain invalid-token 401, a success, a non-OAuth error, or a network fault —
|
|
7
|
+
* so it can only ever turn an opaque failure into a clearer one, never mask a
|
|
8
|
+
* different cause.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
OAuthRequiredError,
|
|
14
|
+
detectOAuthChallenge,
|
|
15
|
+
isOAuthChallenge,
|
|
16
|
+
parseResourceMetadataUrl,
|
|
17
|
+
} from "../mcp-oauth-detect.js";
|
|
18
|
+
|
|
19
|
+
function mockFetchOnce(response: {
|
|
20
|
+
status: number;
|
|
21
|
+
wwwAuthenticate?: string;
|
|
22
|
+
}): void {
|
|
23
|
+
const headers = new Headers();
|
|
24
|
+
if (response.wwwAuthenticate !== undefined) {
|
|
25
|
+
headers.set("www-authenticate", response.wwwAuthenticate);
|
|
26
|
+
}
|
|
27
|
+
vi.stubGlobal(
|
|
28
|
+
"fetch",
|
|
29
|
+
vi.fn(async () =>
|
|
30
|
+
new Response(null, { status: response.status, headers }),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.unstubAllGlobals();
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("isOAuthChallenge", () => {
|
|
41
|
+
it("matches the MCP auth spec Bearer + OAuth realm challenge", () => {
|
|
42
|
+
expect(
|
|
43
|
+
isOAuthChallenge('Bearer realm="OAuth", resource_metadata="https://x/.well-known/oauth-protected-resource"'),
|
|
44
|
+
).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("matches a Bearer challenge that only advertises resource_metadata", () => {
|
|
48
|
+
expect(
|
|
49
|
+
isOAuthChallenge('Bearer resource_metadata="https://x/.well-known/oauth-protected-resource"'),
|
|
50
|
+
).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not match a plain Bearer 401 (invalid API key, not an OAuth requirement)", () => {
|
|
54
|
+
expect(isOAuthChallenge('Bearer error="invalid_token"')).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("does not match a non-Bearer scheme", () => {
|
|
58
|
+
expect(isOAuthChallenge('Basic realm="oauth"')).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("is empty-safe", () => {
|
|
62
|
+
expect(isOAuthChallenge("")).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("parseResourceMetadataUrl", () => {
|
|
67
|
+
it("extracts the resource_metadata URL", () => {
|
|
68
|
+
expect(
|
|
69
|
+
parseResourceMetadataUrl(
|
|
70
|
+
'Bearer realm="OAuth", resource_metadata="https://mcp.notion.com/.well-known/oauth-protected-resource/mcp"',
|
|
71
|
+
),
|
|
72
|
+
).toBe("https://mcp.notion.com/.well-known/oauth-protected-resource/mcp");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns undefined when absent", () => {
|
|
76
|
+
expect(parseResourceMetadataUrl('Bearer realm="OAuth"')).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("detectOAuthChallenge", () => {
|
|
81
|
+
it("returns an OAuthRequiredError for a 401 OAuth challenge", async () => {
|
|
82
|
+
mockFetchOnce({
|
|
83
|
+
status: 401,
|
|
84
|
+
wwwAuthenticate:
|
|
85
|
+
'Bearer realm="OAuth", resource_metadata="https://mcp.notion.com/.well-known/oauth-protected-resource/mcp"',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const result = await detectOAuthChallenge("https://mcp.notion.com/mcp", {}, "notion");
|
|
89
|
+
|
|
90
|
+
expect(result).toBeInstanceOf(OAuthRequiredError);
|
|
91
|
+
expect(result?.serverSlug).toBe("notion");
|
|
92
|
+
expect(result?.resourceMetadataUrl).toBe(
|
|
93
|
+
"https://mcp.notion.com/.well-known/oauth-protected-resource/mcp",
|
|
94
|
+
);
|
|
95
|
+
// The message is self-contained and carries the "requires OAuth" marker the
|
|
96
|
+
// connect wrappers key on.
|
|
97
|
+
expect(result?.message).toContain("requires OAuth");
|
|
98
|
+
expect(result?.message).toContain("notion");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns null for a 401 that is NOT an OAuth challenge (invalid static token)", async () => {
|
|
102
|
+
mockFetchOnce({ status: 401, wwwAuthenticate: 'Bearer error="invalid_token"' });
|
|
103
|
+
expect(
|
|
104
|
+
await detectOAuthChallenge("https://api.example.com/mcp", {}, "example"),
|
|
105
|
+
).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns null on a non-401 response", async () => {
|
|
109
|
+
mockFetchOnce({ status: 200 });
|
|
110
|
+
expect(
|
|
111
|
+
await detectOAuthChallenge("https://api.example.com/mcp", {}, "example"),
|
|
112
|
+
).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("returns null (never throws) when the probe itself fails", async () => {
|
|
116
|
+
vi.stubGlobal(
|
|
117
|
+
"fetch",
|
|
118
|
+
vi.fn(async () => {
|
|
119
|
+
throw new Error("network down");
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
expect(
|
|
123
|
+
await detectOAuthChallenge("https://api.example.com/mcp", {}, "example"),
|
|
124
|
+
).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("forwards the resolved headers to the probe request", async () => {
|
|
128
|
+
const fetchMock = vi.fn(
|
|
129
|
+
async (_url: string | URL, _init?: RequestInit) =>
|
|
130
|
+
new Response(null, { status: 200 }),
|
|
131
|
+
);
|
|
132
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
133
|
+
|
|
134
|
+
await detectOAuthChallenge(
|
|
135
|
+
"https://api.example.com/mcp",
|
|
136
|
+
{ Authorization: "Bearer stale-token" },
|
|
137
|
+
"example",
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
141
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
142
|
+
expect((init?.headers as Record<string, string>).Authorization).toBe(
|
|
143
|
+
"Bearer stale-token",
|
|
144
|
+
);
|
|
145
|
+
expect(init?.method).toBe("POST");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared interface for proto mutation consumed by side-effect classes
|
|
3
|
+
* (InlinePublisher, WriteBackCoordinator) and streaming loops.
|
|
4
|
+
*
|
|
5
|
+
* The deep-agent harness implements it on its v2 StatusBuilder and
|
|
6
|
+
* V3StatusBuilder; the Cursor harness — which mutates a bare status proto
|
|
7
|
+
* with no builder — uses {@link statusProtoWriter}. Side-effect classes
|
|
8
|
+
* depend only on this interface, never on a specific implementation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
12
|
+
import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
13
|
+
import type { WorkspaceWriteBack } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
|
|
14
|
+
|
|
15
|
+
export interface ExecutionStatusWriter {
|
|
16
|
+
readonly currentStatus: AgentExecutionStatus;
|
|
17
|
+
readonly forceNextUpdate: boolean;
|
|
18
|
+
clearForceFlag(): void;
|
|
19
|
+
addArtifact(artifact: ExecutionArtifact): void;
|
|
20
|
+
addWriteBack(wb: WorkspaceWriteBack): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An {@link ExecutionStatusWriter} over a bare status proto, for harnesses
|
|
25
|
+
* that mutate the status directly instead of through a builder (the Cursor
|
|
26
|
+
* harness). Mutations apply synchronously to the given proto; the caller's
|
|
27
|
+
* own persist points pick them up — the force flag is a streaming-loop
|
|
28
|
+
* concern the direct-mutation style has no use for, so it stays `false`.
|
|
29
|
+
*
|
|
30
|
+
* Write-backs upsert by `workspaceEntryName` — the same contract as the
|
|
31
|
+
* deep-agent `StatusBuilder.addWriteBack`: each git-backed workspace entry
|
|
32
|
+
* carries at most one record, progressing through phases.
|
|
33
|
+
*/
|
|
34
|
+
export function statusProtoWriter(status: AgentExecutionStatus): ExecutionStatusWriter {
|
|
35
|
+
return {
|
|
36
|
+
get currentStatus() {
|
|
37
|
+
return status;
|
|
38
|
+
},
|
|
39
|
+
forceNextUpdate: false,
|
|
40
|
+
clearForceFlag() {},
|
|
41
|
+
addArtifact(artifact: ExecutionArtifact) {
|
|
42
|
+
status.artifacts.push(artifact);
|
|
43
|
+
},
|
|
44
|
+
addWriteBack(wb: WorkspaceWriteBack) {
|
|
45
|
+
const backs = status.workspaceWriteBacks;
|
|
46
|
+
const idx = backs.findIndex(
|
|
47
|
+
(b) => b.workspaceEntryName === wb.workspaceEntryName,
|
|
48
|
+
);
|
|
49
|
+
if (idx >= 0) {
|
|
50
|
+
backs[idx] = wb;
|
|
51
|
+
} else {
|
|
52
|
+
backs.push(wb);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth-challenge classification for HTTP MCP endpoints.
|
|
3
|
+
*
|
|
4
|
+
* When an HTTP MCP endpoint requires OAuth (per the MCP Authorization spec /
|
|
5
|
+
* RFC 9728), an unauthenticated or statically-tokened request is answered with
|
|
6
|
+
* `401` and a `WWW-Authenticate: Bearer ...` header pointing at OAuth protected
|
|
7
|
+
* resource metadata. The MCP client SDK surfaces this only as an opaque
|
|
8
|
+
* aggregate error ("unhandled errors in a TaskGroup"), which tells the user
|
|
9
|
+
* nothing.
|
|
10
|
+
*
|
|
11
|
+
* This module turns that opaque failure into a precise, actionable signal: it
|
|
12
|
+
* re-probes the endpoint once (only on the failure path, so the happy path pays
|
|
13
|
+
* nothing) and, if it sees an OAuth challenge, returns an {@link
|
|
14
|
+
* OAuthRequiredError} whose message tells the user to connect via OAuth instead
|
|
15
|
+
* of a manual token.
|
|
16
|
+
*
|
|
17
|
+
* Kept transport-library-agnostic (uses global `fetch`) so it is reusable by
|
|
18
|
+
* connect-time discovery, execution-time error enrichment, and any other caller
|
|
19
|
+
* that holds an MCP endpoint URL + headers.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const OAUTH_PROBE_TIMEOUT_MS = 10_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Raised when an HTTP MCP endpoint answers with an OAuth authentication
|
|
26
|
+
* challenge. The message is self-contained and user-facing: it survives the
|
|
27
|
+
* Temporal boundary and is shown by the connect error wrappers verbatim.
|
|
28
|
+
*
|
|
29
|
+
* The literal phrase "requires OAuth" is a stable marker the Go/Java connect
|
|
30
|
+
* wrappers match to avoid appending a generic "check your credentials" suffix.
|
|
31
|
+
*/
|
|
32
|
+
export class OAuthRequiredError extends Error {
|
|
33
|
+
constructor(
|
|
34
|
+
public readonly serverSlug: string,
|
|
35
|
+
public readonly resourceMetadataUrl?: string,
|
|
36
|
+
) {
|
|
37
|
+
super(
|
|
38
|
+
`MCP server '${serverSlug}' requires OAuth: its endpoint returned an ` +
|
|
39
|
+
`authentication challenge (HTTP 401). A manually-entered API token will ` +
|
|
40
|
+
`not work here — connect it with the OAuth "Sign in" flow instead.`,
|
|
41
|
+
);
|
|
42
|
+
this.name = "OAuthRequiredError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether a `WWW-Authenticate` header value is an OAuth challenge.
|
|
48
|
+
*
|
|
49
|
+
* The MCP auth spec emits `Bearer realm="OAuth", resource_metadata="..."`. We
|
|
50
|
+
* require the `Bearer` scheme plus either an OAuth realm or a resource-metadata
|
|
51
|
+
* pointer, so a plain `Bearer` 401 from a static-token API (an invalid key, not
|
|
52
|
+
* an OAuth requirement) is not misclassified.
|
|
53
|
+
*/
|
|
54
|
+
export function isOAuthChallenge(wwwAuthenticate: string): boolean {
|
|
55
|
+
const value = wwwAuthenticate.toLowerCase();
|
|
56
|
+
if (!value.includes("bearer")) return false;
|
|
57
|
+
return value.includes("oauth") || value.includes("resource_metadata");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Extract the `resource_metadata` URL from a `WWW-Authenticate` value, if present. */
|
|
61
|
+
export function parseResourceMetadataUrl(
|
|
62
|
+
wwwAuthenticate: string,
|
|
63
|
+
): string | undefined {
|
|
64
|
+
const match = /resource_metadata="([^"]+)"/i.exec(wwwAuthenticate);
|
|
65
|
+
return match?.[1];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Probe an HTTP MCP endpoint once to decide whether its failure is an OAuth
|
|
70
|
+
* challenge. Returns an {@link OAuthRequiredError} to throw, or `null` when the
|
|
71
|
+
* endpoint is not asking for OAuth (so the caller rethrows the original error).
|
|
72
|
+
*
|
|
73
|
+
* Never throws: any probe/network failure returns `null` so this classification
|
|
74
|
+
* step can never mask or replace the original discovery error.
|
|
75
|
+
*/
|
|
76
|
+
export async function detectOAuthChallenge(
|
|
77
|
+
url: string,
|
|
78
|
+
headers: Record<string, string> | undefined,
|
|
79
|
+
slug: string,
|
|
80
|
+
): Promise<OAuthRequiredError | null> {
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(url, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
Accept: "application/json, text/event-stream",
|
|
87
|
+
...(headers ?? {}),
|
|
88
|
+
},
|
|
89
|
+
// A minimal MCP initialize request — enough to trigger the endpoint's
|
|
90
|
+
// auth check without establishing a session.
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
jsonrpc: "2.0",
|
|
93
|
+
id: 1,
|
|
94
|
+
method: "initialize",
|
|
95
|
+
params: {},
|
|
96
|
+
}),
|
|
97
|
+
signal: AbortSignal.timeout(OAUTH_PROBE_TIMEOUT_MS),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (response.status !== 401) return null;
|
|
101
|
+
|
|
102
|
+
const wwwAuthenticate = response.headers.get("www-authenticate") ?? "";
|
|
103
|
+
if (!isOAuthChallenge(wwwAuthenticate)) return null;
|
|
104
|
+
|
|
105
|
+
return new OAuthRequiredError(
|
|
106
|
+
slug,
|
|
107
|
+
parseResourceMetadataUrl(wwwAuthenticate),
|
|
108
|
+
);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|