@stigmer/server 3.38.0 → 3.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/boot/compose.js +1 -1
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +18 -7
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js +1 -0
- package/dist/boot/config.js.map +1 -1
- package/dist/domain/mcpserver/complete-oauth-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/complete-oauth-connect.js +10 -4
- package/dist/domain/mcpserver/complete-oauth-connect.js.map +1 -1
- package/dist/domain/mcpserver/connect.d.ts +2 -1
- package/dist/domain/mcpserver/connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/connect.js +5 -2
- package/dist/domain/mcpserver/connect.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +7 -3
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/mcpserver/get-oauth-grant-status.js +1 -1
- package/dist/domain/mcpserver/initiate-oauth-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/initiate-oauth-connect.js +8 -4
- package/dist/domain/mcpserver/initiate-oauth-connect.js.map +1 -1
- package/dist/domain/mcpserver/start-connect.d.ts +2 -2
- package/dist/domain/mcpserver/start-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/start-connect.js +10 -4
- package/dist/domain/mcpserver/start-connect.js.map +1 -1
- package/dist/domain/platform/controller.d.ts +7 -5
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js +4 -2
- package/dist/domain/platform/controller.js.map +1 -1
- package/package.json +6 -6
- package/src/boot/__tests__/config.test.ts +31 -0
- package/src/boot/compose.ts +1 -1
- package/src/boot/config.ts +23 -7
- package/src/domain/agentexecution/__tests__/store-faults.test.ts +6 -26
- package/src/domain/mcpserver/__tests__/store-faults.test.ts +333 -0
- package/src/domain/mcpserver/complete-oauth-connect.ts +13 -4
- package/src/domain/mcpserver/connect.ts +7 -3
- package/src/domain/mcpserver/controller.ts +7 -3
- package/src/domain/mcpserver/get-oauth-grant-status.ts +1 -1
- package/src/domain/mcpserver/initiate-oauth-connect.ts +8 -4
- package/src/domain/mcpserver/start-connect.ts +12 -6
- package/src/domain/platform/__tests__/platform.test.ts +55 -3
- package/src/domain/platform/controller.ts +11 -7
- package/src/domain/workflowexecution/__tests__/store-faults.test.ts +6 -26
- package/src/pipeline/__tests__/blind-not-found.test.ts +0 -5
- package/src/pipeline/__tests__/support.ts +48 -5
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract every MCP server load shares: a typed
|
|
3
|
+
* ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`mcp_server not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound
|
|
6
|
+
* that tells a client the server does not exist (stigmer/stigmer#1345).
|
|
7
|
+
*
|
|
8
|
+
* The surfaces: connect, startConnect (its first load, and the attach arm's
|
|
9
|
+
* re-read after this lane lost the start race), initiateOAuthConnect,
|
|
10
|
+
* completeOAuthConnect, and updateVisibility's load step, reached through the
|
|
11
|
+
* registered handler on an in-process router. completeOAuthConnect loads
|
|
12
|
+
* after the pending state is consumed and the code exchanged, so its fault
|
|
13
|
+
* copy sends the user back through the connect flow. The composed suites
|
|
14
|
+
* reach only a real store, which cannot fail selectively, so each surface
|
|
15
|
+
* runs here against a store whose read throws. Every other dependency is
|
|
16
|
+
* untouchable: a load that fails must stop the call before any of them.
|
|
17
|
+
*
|
|
18
|
+
* Out of scope: the NotFound copy itself (wire contract, pinned by the
|
|
19
|
+
* mcpserver conformance suites and the domain's other tests) and the OAuth
|
|
20
|
+
* grant-store reads (stigmer/stigmer#1360).
|
|
21
|
+
*/
|
|
22
|
+
import { create } from "@bufbuild/protobuf";
|
|
23
|
+
import type { ConnectError } from "@connectrpc/connect";
|
|
24
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
25
|
+
import { describe, expect, it, vi } from "vitest";
|
|
26
|
+
|
|
27
|
+
import type { OutboundFetch } from "@stigmer/outbound/egress";
|
|
28
|
+
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
29
|
+
import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
|
|
30
|
+
import {
|
|
31
|
+
CompleteOAuthConnectInputSchema,
|
|
32
|
+
ConnectInputSchema,
|
|
33
|
+
InitiateOAuthConnectInputSchema,
|
|
34
|
+
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
|
|
35
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
36
|
+
|
|
37
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
38
|
+
import { SecretService } from "../../../encryption/encryption.js";
|
|
39
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
40
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
41
|
+
import {
|
|
42
|
+
errorOf,
|
|
43
|
+
failingStore,
|
|
44
|
+
testCallerIdentity,
|
|
45
|
+
untouchable,
|
|
46
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
47
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
48
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
49
|
+
import type {
|
|
50
|
+
PendingOAuthState,
|
|
51
|
+
PendingOAuthStateStore,
|
|
52
|
+
Store,
|
|
53
|
+
} from "../../../store/interface.js";
|
|
54
|
+
|
|
55
|
+
import { completeOAuthConnect } from "../complete-oauth-connect.js";
|
|
56
|
+
import type { McpServerConnectDeps } from "../connect.js";
|
|
57
|
+
import { connect } from "../connect.js";
|
|
58
|
+
import { registerMcpServerServices } from "../controller.js";
|
|
59
|
+
import type { ConnectRun, McpServerConnectEngine } from "../engine.js";
|
|
60
|
+
import { initiateOAuthConnect } from "../initiate-oauth-connect.js";
|
|
61
|
+
import { startConnect } from "../start-connect.js";
|
|
62
|
+
|
|
63
|
+
const silentLogger = createLogger({
|
|
64
|
+
level: "error",
|
|
65
|
+
pretty: false,
|
|
66
|
+
write: () => {},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const SERVER_ID = "mcps_storefault";
|
|
70
|
+
const ORG = "acme";
|
|
71
|
+
|
|
72
|
+
const NOT_FOUND_COPY = `mcp_server not found: ${SERVER_ID}`;
|
|
73
|
+
const LOAD_FAULT_COPY = "failed to load mcp server";
|
|
74
|
+
const CALLBACK_FAULT_COPY =
|
|
75
|
+
"failed to load mcp server — please retry the connect flow";
|
|
76
|
+
|
|
77
|
+
const MISSING = (): Error =>
|
|
78
|
+
new ResourceNotFoundError(`mcp_server/${SERVER_ID}`);
|
|
79
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The connect slice's dependencies with every field untouchable except the
|
|
83
|
+
* ones a surface names: a load that fails must stop the call before the
|
|
84
|
+
* engine, the environments, the grant store or the network.
|
|
85
|
+
*/
|
|
86
|
+
function connectDeps(
|
|
87
|
+
overrides: Partial<McpServerConnectDeps>,
|
|
88
|
+
): McpServerConnectDeps {
|
|
89
|
+
return {
|
|
90
|
+
store: untouchable("store"),
|
|
91
|
+
logger: silentLogger,
|
|
92
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
93
|
+
engineState: untouchable("engineState"),
|
|
94
|
+
environmentReader: untouchable("environmentReader"),
|
|
95
|
+
executionContext: untouchable("executionContext"),
|
|
96
|
+
runnerAuth: untouchable("runnerAuth"),
|
|
97
|
+
managedEnv: untouchable("managedEnv"),
|
|
98
|
+
oauthGrants: untouchable("oauthGrants"),
|
|
99
|
+
pendingOAuthStates: untouchable("pendingOAuthStates"),
|
|
100
|
+
secretService: untouchable("secretService"),
|
|
101
|
+
oauthRedirectUri: "http://127.0.0.1:8234/auth/oauth/callback",
|
|
102
|
+
outboundFetch: untouchable("outboundFetch"),
|
|
103
|
+
...overrides,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A plaintext pending state for SERVER_ID; a keyless SecretService unseals it to itself. */
|
|
108
|
+
const PENDING_STATE: PendingOAuthState = {
|
|
109
|
+
state: "state_storefault",
|
|
110
|
+
codeVerifier: "verifier_storefault",
|
|
111
|
+
clientId: "client_storefault",
|
|
112
|
+
clientSecret: "",
|
|
113
|
+
tokenEndpoint: "https://auth.example.test/token",
|
|
114
|
+
mcpServerId: SERVER_ID,
|
|
115
|
+
identityAccountId: "",
|
|
116
|
+
targetEnvVar: "MCP_ACCESS_TOKEN",
|
|
117
|
+
authMethod: "mcp_oauth",
|
|
118
|
+
tokenAuthMethod: "",
|
|
119
|
+
redirectUri: "http://127.0.0.1:8234/auth/oauth/callback",
|
|
120
|
+
org: ORG,
|
|
121
|
+
createdAt: 0,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const pendingStates: PendingOAuthStateStore = {
|
|
125
|
+
save: () => Promise.reject(new Error("pendingOAuthStates.save reached")),
|
|
126
|
+
getAndDelete: () => Promise.resolve(PENDING_STATE),
|
|
127
|
+
cleanupExpired: () =>
|
|
128
|
+
Promise.reject(new Error("pendingOAuthStates.cleanupExpired reached")),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** The provider's token endpoint: the exchange succeeds, so the load is next. */
|
|
132
|
+
const tokenEndpoint: OutboundFetch = () =>
|
|
133
|
+
Promise.resolve(
|
|
134
|
+
new Response(
|
|
135
|
+
JSON.stringify({ access_token: "at_storefault", token_type: "bearer" }),
|
|
136
|
+
{ status: 200 },
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const connectInput = () =>
|
|
141
|
+
create(ConnectInputSchema, { mcpServerId: SERVER_ID, org: ORG });
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The direct handlers' entry loads. Each validates its input first, so the
|
|
145
|
+
* inputs carry every field the handler requires; completeOAuthConnect also
|
|
146
|
+
* consumes the pending state, unseals it and exchanges the code before its
|
|
147
|
+
* load, so its fakes answer those three steps.
|
|
148
|
+
*/
|
|
149
|
+
const HANDLER_LOADS: ReadonlyArray<
|
|
150
|
+
readonly [string, string, (store: Store) => Promise<unknown>]
|
|
151
|
+
> = [
|
|
152
|
+
[
|
|
153
|
+
"connect",
|
|
154
|
+
LOAD_FAULT_COPY,
|
|
155
|
+
(store) =>
|
|
156
|
+
connect(connectDeps({ store }), connectInput(), testCallerIdentity()),
|
|
157
|
+
],
|
|
158
|
+
[
|
|
159
|
+
"startConnect — the first load",
|
|
160
|
+
LOAD_FAULT_COPY,
|
|
161
|
+
(store) =>
|
|
162
|
+
startConnect(
|
|
163
|
+
connectDeps({ store }),
|
|
164
|
+
connectInput(),
|
|
165
|
+
testCallerIdentity(),
|
|
166
|
+
),
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
"initiateOAuthConnect",
|
|
170
|
+
LOAD_FAULT_COPY,
|
|
171
|
+
(store) =>
|
|
172
|
+
initiateOAuthConnect(
|
|
173
|
+
connectDeps({ store }),
|
|
174
|
+
create(InitiateOAuthConnectInputSchema, {
|
|
175
|
+
mcpServerId: SERVER_ID,
|
|
176
|
+
org: ORG,
|
|
177
|
+
}),
|
|
178
|
+
testCallerIdentity(),
|
|
179
|
+
),
|
|
180
|
+
],
|
|
181
|
+
[
|
|
182
|
+
"completeOAuthConnect — after the code exchange",
|
|
183
|
+
CALLBACK_FAULT_COPY,
|
|
184
|
+
(store) =>
|
|
185
|
+
completeOAuthConnect(
|
|
186
|
+
connectDeps({
|
|
187
|
+
store,
|
|
188
|
+
pendingOAuthStates: pendingStates,
|
|
189
|
+
secretService: SecretService.create(undefined),
|
|
190
|
+
outboundFetch: tokenEndpoint,
|
|
191
|
+
}),
|
|
192
|
+
create(CompleteOAuthConnectInputSchema, {
|
|
193
|
+
mcpServerId: SERVER_ID,
|
|
194
|
+
state: PENDING_STATE.state,
|
|
195
|
+
authorizationCode: "code_storefault",
|
|
196
|
+
}),
|
|
197
|
+
testCallerIdentity(),
|
|
198
|
+
),
|
|
199
|
+
],
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
describe.each(HANDLER_LOADS)("%s", (_surface, faultCopy, run) => {
|
|
203
|
+
it("a missing server answers NotFound with the domain's copy", async () => {
|
|
204
|
+
const error = await errorOf(() => run(failingStore(MISSING())));
|
|
205
|
+
|
|
206
|
+
expect(error.code).toBe(Code.NotFound);
|
|
207
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
211
|
+
const error = await errorOf(() => run(failingStore(LOCKED())));
|
|
212
|
+
|
|
213
|
+
expect(error.code).toBe(Code.Internal);
|
|
214
|
+
expect(error.rawMessage).toBe(faultCopy);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("startConnect — the attach arm's re-read after losing the start race", () => {
|
|
219
|
+
/** No env and no auth block: prepareConnect creates no ExecutionContext and reads no grant. */
|
|
220
|
+
const SERVER = create(McpServerSchema, {
|
|
221
|
+
metadata: { id: SERVER_ID, name: "Store Fault", org: ORG },
|
|
222
|
+
spec: {
|
|
223
|
+
serverType: { case: "stdio", value: { command: "npx", args: [] } },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
async function attachError(reReadError: Error): Promise<ConnectError> {
|
|
228
|
+
const attached: ConnectRun = {
|
|
229
|
+
workflowId: `stigmer/mcp-server/connect/${SERVER_ID}`,
|
|
230
|
+
attached: true,
|
|
231
|
+
result: () =>
|
|
232
|
+
Promise.reject(new Error("result reached on the attach arm")),
|
|
233
|
+
};
|
|
234
|
+
const startOrAttachConnect = vi.fn<
|
|
235
|
+
McpServerConnectEngine["startOrAttachConnect"]
|
|
236
|
+
>(() => Promise.resolve(attached));
|
|
237
|
+
const engine: McpServerConnectEngine = {
|
|
238
|
+
startOrAttachConnect,
|
|
239
|
+
isConnectRunRunning: () =>
|
|
240
|
+
Promise.reject(
|
|
241
|
+
new Error("isConnectRunRunning reached without a CONNECTING record"),
|
|
242
|
+
),
|
|
243
|
+
hasRunnerQueuePollers: () => Promise.resolve(true),
|
|
244
|
+
};
|
|
245
|
+
const getResource = vi
|
|
246
|
+
.fn()
|
|
247
|
+
.mockResolvedValueOnce(SERVER)
|
|
248
|
+
.mockRejectedValueOnce(reReadError);
|
|
249
|
+
|
|
250
|
+
const error = await errorOf(() =>
|
|
251
|
+
startConnect(
|
|
252
|
+
connectDeps({
|
|
253
|
+
store: { getResource } as unknown as Store,
|
|
254
|
+
engineState: () => ({ connected: true, engine }),
|
|
255
|
+
}),
|
|
256
|
+
connectInput(),
|
|
257
|
+
testCallerIdentity(),
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// The start ran once and the store was read twice: the failure is the
|
|
262
|
+
// re-read's, not the first load's.
|
|
263
|
+
expect(startOrAttachConnect).toHaveBeenCalledTimes(1);
|
|
264
|
+
expect(getResource).toHaveBeenCalledTimes(2);
|
|
265
|
+
return error;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
it("a server deleted since the first load answers NotFound with the domain's copy", async () => {
|
|
269
|
+
const error = await attachError(MISSING());
|
|
270
|
+
|
|
271
|
+
expect(error.code).toBe(Code.NotFound);
|
|
272
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
276
|
+
const error = await attachError(LOCKED());
|
|
277
|
+
|
|
278
|
+
expect(error.code).toBe(Code.Internal);
|
|
279
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("updateVisibility — LoadMcpServerForVisibilityUpdate", () => {
|
|
284
|
+
/**
|
|
285
|
+
* The registered handler on an in-process router (the artifact domain's
|
|
286
|
+
* harness): the verifier chain stamps the trusted-local caller and the
|
|
287
|
+
* apiresource interceptor the kind, then Authorize and ValidateProto run
|
|
288
|
+
* and the load is the first step that reads the store. The connect slice
|
|
289
|
+
* is never reached.
|
|
290
|
+
*/
|
|
291
|
+
function updateVisibilityError(store: Store): Promise<ConnectError> {
|
|
292
|
+
const transport = createRouterTransport(
|
|
293
|
+
(router) => {
|
|
294
|
+
registerMcpServerServices(router, {
|
|
295
|
+
store,
|
|
296
|
+
logger: silentLogger,
|
|
297
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
298
|
+
authorizationLifecycle: undefined,
|
|
299
|
+
connect: untouchable("connect"),
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
router: {
|
|
304
|
+
interceptors: [
|
|
305
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
306
|
+
createApiResourceInterceptor(),
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
);
|
|
311
|
+
const command = createClient(McpServerCommandController, transport);
|
|
312
|
+
return errorOf(() =>
|
|
313
|
+
command.updateVisibility({
|
|
314
|
+
resourceId: SERVER_ID,
|
|
315
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
it("a missing server answers NotFound with the domain's copy", async () => {
|
|
321
|
+
const error = await updateVisibilityError(failingStore(MISSING()));
|
|
322
|
+
|
|
323
|
+
expect(error.code).toBe(Code.NotFound);
|
|
324
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
328
|
+
const error = await updateVisibilityError(failingStore(LOCKED()));
|
|
329
|
+
|
|
330
|
+
expect(error.code).toBe(Code.Internal);
|
|
331
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* pin). Disclosed divergence; the Go-side issue files at wrap-up.
|
|
15
15
|
*
|
|
16
16
|
* Proven by mcpserver-connect.conformance.test.ts
|
|
17
|
-
* (CONFORMANCE_TARGET=local-execution)
|
|
18
|
-
* __tests__/
|
|
17
|
+
* (CONFORMANCE_TARGET=local-execution), __tests__/oauth-handshake.test.ts
|
|
18
|
+
* and __tests__/store-faults.test.ts.
|
|
19
19
|
*/
|
|
20
20
|
import { create } from "@bufbuild/protobuf";
|
|
21
21
|
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from "../../pipeline/errors.js";
|
|
43
43
|
import { authorizeDirect } from "../../pipeline/steps/authorize.js";
|
|
44
44
|
import type { PendingOAuthState } from "../../store/interface.js";
|
|
45
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
45
46
|
import type { McpServerConnectDeps } from "./connect.js";
|
|
46
47
|
import { exchangeCode } from "./oauth/token.js";
|
|
47
48
|
|
|
@@ -147,8 +148,16 @@ export async function completeOAuthConnect(
|
|
|
147
148
|
mcpServerId,
|
|
148
149
|
McpServerSchema,
|
|
149
150
|
);
|
|
150
|
-
} catch {
|
|
151
|
-
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error instanceof ResourceNotFoundError) {
|
|
153
|
+
throw notFoundError("mcp_server", mcpServerId);
|
|
154
|
+
}
|
|
155
|
+
// The pending state is consumed and the code exchanged by now, so a
|
|
156
|
+
// retry of this call can only fail: the copy names the way back.
|
|
157
|
+
throw internalError(
|
|
158
|
+
error,
|
|
159
|
+
"failed to load mcp server — please retry the connect flow",
|
|
160
|
+
);
|
|
152
161
|
}
|
|
153
162
|
|
|
154
163
|
let org = pendingState.org;
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* (engine.ts) and never registers a worker.
|
|
13
13
|
*
|
|
14
14
|
* Proven by mcpserver-connect.conformance.test.ts
|
|
15
|
-
* (CONFORMANCE_TARGET=local-execution)
|
|
15
|
+
* (CONFORMANCE_TARGET=local-execution), __tests__/connect.test.ts and
|
|
16
|
+
* __tests__/store-faults.test.ts.
|
|
16
17
|
*/
|
|
17
18
|
import type { OutboundFetch } from "@stigmer/outbound/egress";
|
|
18
19
|
import { create } from "@bufbuild/protobuf";
|
|
@@ -269,8 +270,11 @@ export async function connect(
|
|
|
269
270
|
mcpServerId,
|
|
270
271
|
McpServerSchema,
|
|
271
272
|
);
|
|
272
|
-
} catch {
|
|
273
|
-
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error instanceof ResourceNotFoundError) {
|
|
275
|
+
throw notFoundError("mcp_server", mcpServerId);
|
|
276
|
+
}
|
|
277
|
+
throw internalError(error, "failed to load mcp server");
|
|
274
278
|
}
|
|
275
279
|
|
|
276
280
|
// The annotation's can_connect check after the load and BEFORE the engine
|
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
newValidateVisibilityUpdateStep,
|
|
107
107
|
} from "../../pipeline/steps/validate-visibility.js";
|
|
108
108
|
import type { Store } from "../../store/interface.js";
|
|
109
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
109
110
|
import { completeOAuthConnect } from "./complete-oauth-connect.js";
|
|
110
111
|
import { connect, startBestEffortConnect } from "./connect.js";
|
|
111
112
|
import type { McpServerConnectDeps } from "./connect.js";
|
|
@@ -479,7 +480,7 @@ async function updateVisibility(
|
|
|
479
480
|
return reqCtx.get(UPDATE_VISIBILITY_MCP_SERVER_KEY) as McpServer;
|
|
480
481
|
}
|
|
481
482
|
|
|
482
|
-
/** Loads the server by resource_id;
|
|
483
|
+
/** Loads the server by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
483
484
|
function newLoadMcpServerForVisibilityUpdateStep(
|
|
484
485
|
store: Store,
|
|
485
486
|
): PipelineStep<UpdateVisibilityDesc> {
|
|
@@ -494,8 +495,11 @@ function newLoadMcpServerForVisibilityUpdateStep(
|
|
|
494
495
|
input.resourceId,
|
|
495
496
|
McpServerSchema,
|
|
496
497
|
);
|
|
497
|
-
} catch {
|
|
498
|
-
|
|
498
|
+
} catch (error) {
|
|
499
|
+
if (error instanceof ResourceNotFoundError) {
|
|
500
|
+
throw notFoundError("mcp_server", input.resourceId);
|
|
501
|
+
}
|
|
502
|
+
throw internalError(error, "failed to load mcp server");
|
|
499
503
|
}
|
|
500
504
|
ctx.set(UPDATE_VISIBILITY_MCP_SERVER_KEY, mcpServer);
|
|
501
505
|
},
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* credentials and build the auth URL from its endpoints.
|
|
9
9
|
*
|
|
10
10
|
* Proven by mcpserver-oauth.conformance.test.ts
|
|
11
|
-
* (CONFORMANCE_TARGET=local) and
|
|
12
|
-
* __tests__/
|
|
11
|
+
* (CONFORMANCE_TARGET=local), __tests__/oauth-handshake.test.ts and
|
|
12
|
+
* __tests__/store-faults.test.ts.
|
|
13
13
|
*/
|
|
14
14
|
import { create } from "@bufbuild/protobuf";
|
|
15
15
|
import { randomBytes } from "node:crypto";
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
} from "../../pipeline/errors.js";
|
|
41
41
|
import { authorizeDirect } from "../../pipeline/steps/authorize.js";
|
|
42
42
|
import type { PendingOAuthState } from "../../store/interface.js";
|
|
43
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
43
44
|
import { resolveOAuthAppRef } from "../oauthapp/refresolution.js";
|
|
44
45
|
import { tokenAuthMethodFromSpec } from "./connect.js";
|
|
45
46
|
import type { McpServerConnectDeps } from "./connect.js";
|
|
@@ -73,8 +74,11 @@ export async function initiateOAuthConnect(
|
|
|
73
74
|
mcpServerId,
|
|
74
75
|
McpServerSchema,
|
|
75
76
|
);
|
|
76
|
-
} catch {
|
|
77
|
-
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof ResourceNotFoundError) {
|
|
79
|
+
throw notFoundError("mcp_server", mcpServerId);
|
|
80
|
+
}
|
|
81
|
+
throw internalError(error, "failed to load mcp server");
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
// The annotation's can_connect check AFTER the load — the Java
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
* which is both the repair and the caller's intent.
|
|
22
22
|
*
|
|
23
23
|
* Proven by mcpserver-connect.conformance.test.ts
|
|
24
|
-
* (CONFORMANCE_TARGET=local-execution) and
|
|
25
|
-
* __tests__/
|
|
24
|
+
* (CONFORMANCE_TARGET=local-execution), __tests__/connect.test.ts and
|
|
25
|
+
* __tests__/store-faults.test.ts.
|
|
26
26
|
*/
|
|
27
27
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
28
28
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
@@ -84,8 +84,11 @@ export async function startConnect(
|
|
|
84
84
|
mcpServerId,
|
|
85
85
|
McpServerSchema,
|
|
86
86
|
);
|
|
87
|
-
} catch {
|
|
88
|
-
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error instanceof ResourceNotFoundError) {
|
|
89
|
+
throw notFoundError("mcp_server", mcpServerId);
|
|
90
|
+
}
|
|
91
|
+
throw internalError(error, "failed to load mcp server");
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
// The annotation's can_connect check after the load and BEFORE the engine
|
|
@@ -165,8 +168,11 @@ export async function startConnect(
|
|
|
165
168
|
mcpServerId,
|
|
166
169
|
McpServerSchema,
|
|
167
170
|
);
|
|
168
|
-
} catch {
|
|
169
|
-
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof ResourceNotFoundError) {
|
|
173
|
+
throw notFoundError("mcp_server", mcpServerId);
|
|
174
|
+
}
|
|
175
|
+
throw internalError(error, "failed to load mcp server");
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
178
|
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* executioncontext decrypt lane uses, bound to exactly the named
|
|
15
15
|
* execution id;
|
|
16
16
|
* - getRunnerBootstrapConfig echoes the configured Temporal coordinates
|
|
17
|
-
* with the token fields empty (minting a proxy token is cloud-only)
|
|
17
|
+
* with the token fields empty (minting a proxy token is cloud-only),
|
|
18
|
+
* and publishes the runner-bootstrap address rather than the one the
|
|
19
|
+
* server dials when the two differ (stigmer#1357);
|
|
18
20
|
* - getServerInfo reports the build stamp by default and the release a
|
|
19
21
|
* library consumer states through ComposeOptions.version otherwise
|
|
20
22
|
* (stigmer#1168: unbundled, the stamp is always "dev"), and a
|
|
@@ -278,11 +280,61 @@ describe("platform domain (composed with a stated version)", () => {
|
|
|
278
280
|
});
|
|
279
281
|
});
|
|
280
282
|
|
|
283
|
+
/**
|
|
284
|
+
* stigmer#1357 through the real stack: a server that dials Temporal by a
|
|
285
|
+
* name its runners cannot resolve publishes the address they can, from
|
|
286
|
+
* the environment through loadConfig and the composition root to the
|
|
287
|
+
* wire. The composed-server arm above is the default: with no override,
|
|
288
|
+
* the published address is the server's own.
|
|
289
|
+
*/
|
|
290
|
+
describe("platform domain (composed for runners off the server's network)", () => {
|
|
291
|
+
let server: ComposedServer;
|
|
292
|
+
let client: PlatformClient;
|
|
293
|
+
let dir: string;
|
|
294
|
+
|
|
295
|
+
beforeAll(async () => {
|
|
296
|
+
dir = mkdtempSync(
|
|
297
|
+
path.join(tmpdir(), "platform-domain-runner-address-test-"),
|
|
298
|
+
);
|
|
299
|
+
vi.stubEnv("STIGMER_ENCRYPTION_KEY", ENC_KEY.toString("base64"));
|
|
300
|
+
server = await composeServer({
|
|
301
|
+
config: loadConfig({
|
|
302
|
+
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
303
|
+
DB_PATH: path.join(dir, "stigmer.db"),
|
|
304
|
+
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
305
|
+
TEMPORAL_HOST_PORT: "127.0.0.1:7777",
|
|
306
|
+
TEMPORAL_NAMESPACE: "conformance-ns",
|
|
307
|
+
STIGMER_RUNNER_BOOTSTRAP_TEMPORAL_ADDRESS: "temporal.example.test:7233",
|
|
308
|
+
}),
|
|
309
|
+
logger: silentLogger,
|
|
310
|
+
portOverride: 0,
|
|
311
|
+
host: "127.0.0.1",
|
|
312
|
+
});
|
|
313
|
+
const port = await server.start();
|
|
314
|
+
client = createClient(
|
|
315
|
+
PlatformQueryController,
|
|
316
|
+
createGrpcTransport({ baseUrl: `http://127.0.0.1:${port}` }),
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
afterAll(async () => {
|
|
321
|
+
await server.shutdown();
|
|
322
|
+
rmSync(dir, { recursive: true, force: true });
|
|
323
|
+
vi.unstubAllEnvs();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("getRunnerBootstrapConfig publishes the runner-bootstrap address, with the server's namespace", async () => {
|
|
327
|
+
const config = await client.getRunnerBootstrapConfig({});
|
|
328
|
+
expect(config.temporalAddress).toBe("temporal.example.test:7233");
|
|
329
|
+
expect(config.temporalNamespace).toBe("conformance-ns");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
281
333
|
describe("platform domain (keyless runner-token service)", () => {
|
|
282
334
|
it("a keyless service answers not-minted — fail-soft, never an error", async () => {
|
|
283
335
|
const transport = createRouterTransport((router) => {
|
|
284
336
|
registerPlatformServices(router, {
|
|
285
|
-
|
|
337
|
+
runnerBootstrapTemporalAddress: "localhost:7233",
|
|
286
338
|
temporalNamespace: "default",
|
|
287
339
|
runnerAuthService: newExecutionScopedRunnerCredentialProvider(
|
|
288
340
|
RunnerAuthService.create(undefined),
|
|
@@ -318,7 +370,7 @@ describe("platform domain (license status)", () => {
|
|
|
318
370
|
function clientWith(provider: LicenseStatusProvider) {
|
|
319
371
|
const transport = createRouterTransport((router) => {
|
|
320
372
|
registerPlatformServices(router, {
|
|
321
|
-
|
|
373
|
+
runnerBootstrapTemporalAddress: "localhost:7233",
|
|
322
374
|
temporalNamespace: "default",
|
|
323
375
|
runnerAuthService: newExecutionScopedRunnerCredentialProvider(
|
|
324
376
|
RunnerAuthService.create(undefined),
|
|
@@ -78,12 +78,14 @@ import { TOKEN_TYPE_EXECUTION_SCOPED } from "../../runnerauth/runnerauth.js";
|
|
|
78
78
|
|
|
79
79
|
export interface PlatformControllerDeps {
|
|
80
80
|
/**
|
|
81
|
-
* Temporal coordinates
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
81
|
+
* The Temporal coordinates published to embedded runners. The address
|
|
82
|
+
* is the one a runner should dial, which is not always the one this
|
|
83
|
+
* server dials: a clustered server reaches Temporal by an internal name
|
|
84
|
+
* its desktop runners cannot resolve (stigmer#1357). The composition
|
|
85
|
+
* root passes config.runnerBootstrapTemporalAddress, which defaults to
|
|
86
|
+
* the server's own address; the namespace is the server's.
|
|
85
87
|
*/
|
|
86
|
-
readonly
|
|
88
|
+
readonly runnerBootstrapTemporalAddress: string;
|
|
87
89
|
readonly temporalNamespace: string;
|
|
88
90
|
/**
|
|
89
91
|
* Mints the execution-scoped tokens getRunnerScopedToken hands to
|
|
@@ -202,7 +204,9 @@ async function getLicenseStatus(
|
|
|
202
204
|
|
|
203
205
|
/**
|
|
204
206
|
* Go GetRunnerBootstrapConfig: the Temporal coordinates an embedded runner
|
|
205
|
-
* should connect to so it can self-bootstrap from a token alone
|
|
207
|
+
* should connect to so it can self-bootstrap from a token alone: the
|
|
208
|
+
* address deps name for runners, never assumed to be the server's own
|
|
209
|
+
* (the proto's "the one reachable by the caller"). The
|
|
206
210
|
* RunnerAccessToken / TokenType / RunnerAccessTokenExpiresInSeconds fields
|
|
207
211
|
* are intentionally left empty in OSS: minting an iss=stigmer proxy token
|
|
208
212
|
* is a cloud-only capability (OSS has no Cursor BiDi proxy to authenticate
|
|
@@ -221,7 +225,7 @@ async function getRunnerBootstrapConfig(
|
|
|
221
225
|
ctx: HandlerContext,
|
|
222
226
|
): Promise<GetRunnerBootstrapConfigOutput> {
|
|
223
227
|
const output = create(GetRunnerBootstrapConfigOutputSchema, {
|
|
224
|
-
temporalAddress: deps.
|
|
228
|
+
temporalAddress: deps.runnerBootstrapTemporalAddress,
|
|
225
229
|
temporalNamespace: deps.temporalNamespace,
|
|
226
230
|
});
|
|
227
231
|
const bootstrapCredentials =
|