History

elicit credentials

Rhys Sullivan committed 8 months ago

Commit 4b6e5d71 parent 53347ce15 files · +1930 −204Browse files

Unified · Split

15 files
  1. apps/executor/src/cli/main.ts +67 −13
  2. packages/control-plane/src/api/local/api.ts +0 −8
  3. packages/control-plane/src/api/local/http.ts +4 −18
  4. packages/control-plane/src/api/sources/api.ts +46 −0
  5. packages/control-plane/src/api/sources/http.ts +580 −2
  6. packages/control-plane/src/persistence/repos/execution-interactions-repo.ts +14 −0
  7. packages/control-plane/src/persistence/repos/tool-artifacts-repo.ts +5 −3
  8. packages/control-plane/src/runtime/control-plane-runtime.test.ts +283 −2
  9. packages/control-plane/src/runtime/executor-tools.ts +152 −112
  10. packages/control-plane/src/runtime/index.ts +13 −4
  11. packages/control-plane/src/runtime/local-operations.ts +293 −5
  12. packages/control-plane/src/runtime/source-auth-service.ts +134 −15
  13. packages/control-plane/src/runtime/workspace-execution-environment.test.ts +190 −11
  14. packages/control-plane/src/runtime/workspace-execution-environment.ts +148 −9
  15. packages/runtime-local-inproc/src/index.ts +1 −2
apps/executor/src/cli/main.ts+67−13
@@ -45,6 +45,27 @@ const toError = (cause: unknown): Error => · 44 unmodified lines
4545 const sleep = (ms: number) =>
4646 Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, ms)));
4747
48+const openUrlInBrowser = (url: string): Effect.Effect<void, never, never> =>
49+ Effect.sync(() => {
50+ const cmd =
51+ process.platform === "darwin"
52+ ? ["open", url]
53+ : process.platform === "win32"
54+ ? ["cmd", "/c", "start", "", url]
55+ : ["xdg-open", url];
56+
57+ try {
58+ const child = spawn(cmd[0]!, cmd.slice(1), {
59+ detached: true,
60+ stdio: "ignore",
61+ });
62+ child.on("error", () => undefined);
63+ child.unref();
64+ } catch {
65+ // Best-effort browser launch only; always leave the URL in stdout.
66+ }
67+ }).pipe(Effect.catchAll(() => Effect.void));
68+
4869 const promptLine = (prompt: string): Effect.Effect<string, Error, never> =>
4970 Effect.tryPromise({
5071 try: async () => {
@@ -254,8 +275,8 @@ const printRootHelp = (workflow: string) => · 203 unmodified lines
254275 "",
255276 "USAGE",
256277 "",
257− " executor call [code] [--file text] [--stdin] [--base-url text]",
258− " executor resume --execution-id text [--base-url text]",
278+ " executor call [code] [--file text] [--stdin] [--base-url text] [--no-open]",
279+ " executor resume --execution-id text [--base-url text] [--no-open]",
259280 "",
260281 "CALL WORKFLOW",
261282 "",
@@ -281,7 +302,7 @@ const printCallHelp = (workflow: string) => · 19 unmodified lines
281302 "",
282303 "USAGE",
283304 "",
284− " executor call [code] [--file text] [--stdin] [--base-url text]",
305+ " executor call [code] [--file text] [--stdin] [--base-url text] [--no-open]",
285306 "",
286307 "DESCRIPTION",
287308 "",
@@ -301,15 +322,18 @@ const printCallHelp = (workflow: string) => · 13 unmodified lines
301322 " Read code from stdin.",
302323 " --base-url text",
303324 " Override the executor server base URL.",
325+ " --no-open",
326+ " Print interaction URLs without opening a browser.",
304327 "",
305328 "EXAMPLES",
306329 "",
307330 ' executor call \'const matches = await tools.discover({ query: "github issues", limit: 5 }); return matches;\'',
308331 ' executor call \'const matches = await tools.discover({ query: "repo details", limit: 1 }); const path = matches.bestPath; return await tools.describe.tool({ path, includeSchemas: true });\'',
309332 ' executor call \'return await tools.executor.sources.add({ endpoint: "https://example.com/mcp", name: "Example", namespace: "example" });\'',
310− ' executor call \'return await tools.executor.sources.add({ kind: "openapi", endpoint: "https://api.github.com", specUrl: "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json", name: "GitHub", namespace: "github", auth: { kind: "bearer" } });\'',
333+ ' executor call \'return await tools.executor.sources.add({ kind: "openapi", endpoint: "https://api.github.com", specUrl: "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json", name: "GitHub", namespace: "github" });\'',
311334 " cat script.ts | executor call --stdin",
312335 " executor call --file script.ts",
336+ " executor call --no-open --file script.ts",
313337 " executor resume --execution-id exec_123",
314338 ].join("\n"));
315339 });
@@ -564,17 +588,37 @@ const promptStructuredInteraction = (parsed: { · 248 unmodified lines
564588 return content;
565589 });
566590
567−const promptInteraction = (interaction: ExecutionInteraction) =>
591+const printUrlInteraction = (input: {
592+ message: string;
593+ url: string | null;
594+ shouldOpen: boolean;
595+}) =>
596+ Effect.gen(function* () {
597+ yield* Effect.sync(() => {
598+ process.stdout.write(`${input.message}\n${input.url ?? ""}\n`);
599+ });
600+
601+ if (input.shouldOpen && input.url) {
602+ yield* openUrlInBrowser(input.url);
603+ }
604+ });
605+
606+const promptInteraction = (input: {
607+ interaction: ExecutionInteraction;
608+ shouldOpenUrls: boolean;
609+}) =>
568610 Effect.gen(function* () {
569− const parsed = parseInteractionPayload(interaction);
611+ const parsed = parseInteractionPayload(input.interaction);
570612
571613 if (!process.stdin.isTTY || !process.stdout.isTTY || parsed === null) {
572614 return null;
573615 }
574616
575617 if (parsed.mode === "url") {
576− yield* Effect.sync(() => {
577− process.stdout.write(`${parsed.message}\n${parsed.url ?? ""}\n`);
618+ yield* printUrlInteraction({
619+ message: parsed.message,
620+ url: parsed.url ?? null,
621+ shouldOpen: input.shouldOpenUrls,
578622 });
579623 return null;
580624 }
@@ -708,6 +752,7 @@ const driveExecution = (input: { · 127 unmodified lines
708752 client: ControlPlaneClient;
709753 workspaceId: ExecutionEnvelope["execution"]["workspaceId"];
710754 envelope: ExecutionEnvelope;
755+ shouldOpenUrls: boolean;
711756 }) =>
712757 Effect.gen(function* () {
713758 let current = input.envelope;
@@ -721,8 +766,10 @@ const driveExecution = (input: { · 7 unmodified lines
721766
722767 const parsed = parseInteractionPayload(pending);
723768 if (parsed?.mode === "url") {
724− yield* Effect.sync(() => {
725− process.stdout.write(`${parsed.message}\n${parsed.url ?? ""}\n`);
769+ yield* printUrlInteraction({
770+ message: parsed.message,
771+ url: parsed.url ?? null,
772+ shouldOpen: input.shouldOpenUrls,
726773 });
727774
728775 if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -738,7 +785,10 @@ const driveExecution = (input: { · 9 unmodified lines
738785 continue;
739786 }
740787
741− const responseJson = yield* promptInteraction(pending);
788+ const responseJson = yield* promptInteraction({
789+ interaction: pending,
790+ shouldOpenUrls: input.shouldOpenUrls,
791+ });
742792 if (responseJson === null) {
743793 yield* Effect.sync(() => {
744794 console.log(JSON.stringify({
@@ -790,8 +840,9 @@ const callCommand = Command.make( · 45 unmodified lines
790840 file: Options.text("file").pipe(Options.optional),
791841 stdin: Options.boolean("stdin").pipe(Options.withDefault(false)),
792842 baseUrl: Options.text("base-url").pipe(Options.withDefault(DEFAULT_SERVER_BASE_URL)),
843+ noOpen: Options.boolean("no-open").pipe(Options.withDefault(false)),
793844 },
794− ({ code, file, stdin, baseUrl }) =>
845+ ({ code, file, stdin, baseUrl, noOpen }) =>
795846 Effect.gen(function* () {
796847 const resolvedCode = yield* readCode({
797848 code: Option.getOrUndefined(code),
@@ -814,6 +865,7 @@ const callCommand = Command.make( · 16 unmodified lines
814865 client,
815866 workspaceId: installation.workspaceId,
816867 envelope: created,
868+ shouldOpenUrls: !noOpen,
817869 });
818870
819871 yield* printExecution(settled);
@@ -825,8 +877,9 @@ const resumeCommand = Command.make( · 5 unmodified lines
825877 {
826878 executionId: Options.text("execution-id"),
827879 baseUrl: Options.text("base-url").pipe(Options.withDefault(DEFAULT_SERVER_BASE_URL)),
880+ noOpen: Options.boolean("no-open").pipe(Options.withDefault(false)),
828881 },
829− ({ executionId, baseUrl }) =>
882+ ({ executionId, baseUrl, noOpen }) =>
830883 Effect.gen(function* () {
831884 yield* ensureServer(baseUrl);
832885 const { installation, client } = yield* getLocalAuthedClient(baseUrl);
@@ -844,6 +897,7 @@ const resumeCommand = Command.make( · 11 unmodified lines
844897 client,
845898 workspaceId: installation.workspaceId,
846899 envelope: execution,
900+ shouldOpenUrls: !noOpen,
847901 });
848902
849903 yield* printExecution(settled);
packages/control-plane/src/api/local/api.ts+0−8
@@ -1,5 +1,4 @@
11 import { HttpApiEndpoint, HttpApiGroup } from "@effect/platform";
2−import * as Schema from "effect/Schema";
32 import { LocalInstallationSchema } from "#schema";
43
54 import {
@@ -16,11 +15,4 @@ export class LocalApi extends HttpApiGroup.make("local") · 10 unmodified lines
1615 .addError(ControlPlaneNotFoundError)
1716 .addError(ControlPlaneStorageError),
1817 )
19− .add(
20− HttpApiEndpoint.get("oauthCallback")`/local/oauth/callback`
21− .addSuccess(Schema.String)
22− .addError(ControlPlaneBadRequestError)
23− .addError(ControlPlaneNotFoundError)
24− .addError(ControlPlaneStorageError),
25− )
2618 .prefix("/v1") {}
packages/control-plane/src/api/local/http.ts+4−18
@@ -1,7 +1,8 @@
1−import { HttpApiBuilder, HttpServerRequest } from "@effect/platform";
2−import * as Effect from "effect/Effect";
31 import {
4− completeSourceAuthCallback,
2+ HttpApiBuilder,
3+} from "@effect/platform";
4+
5+import {
56 getLocalInstallation,
67 } from "../../runtime/local-operations";
78
@@ -14,20 +15,5 @@ export const ControlPlaneLocalLive = HttpApiBuilder.group( · 6 unmodified lines
1415 handlers
1516 .handle("installation", () =>
1617 getLocalInstallation(),
17− )
18− .handle("oauthCallback", () =>
19− Effect.gen(function* () {
20− const request = yield* HttpServerRequest.HttpServerRequest;
21− const requestUrl = new URL(request.url, "http://127.0.0.1");
22−
23− const source = yield* completeSourceAuthCallback({
24− state: requestUrl.searchParams.get("state") ?? "",
25− code: requestUrl.searchParams.get("code"),
26− error: requestUrl.searchParams.get("error"),
27− errorDescription: requestUrl.searchParams.get("error_description"),
28− });
29−
30− return `Source connected: ${source.id}. You can close this window.`;
31− }),
3218 ),
3319 );
packages/control-plane/src/api/sources/api.ts+46−0
@@ -1,5 +1,6 @@
11 import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform";
22 import {
3+ ExecutionInteractionIdSchema,
34 SourceAuthSchema,
45 SourceIdSchema,
56 SourceKindSchema,
@@ -72,6 +73,26 @@ export type UpdateSourcePayload = typeof UpdateSourcePayloadSchema.Type; · 66 unmodified lines
7273 const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema);
7374 const sourceIdParam = HttpApiSchema.param("sourceId", SourceIdSchema);
7475
76+const CredentialPageUrlParamsSchema = Schema.Struct({
77+ interactionId: ExecutionInteractionIdSchema,
78+});
79+
80+const CredentialSubmitPayloadSchema = Schema.Struct({
81+ action: Schema.optional(Schema.Literal("submit", "continue", "cancel")),
82+ token: Schema.optional(Schema.String),
83+});
84+
85+const CredentialOauthCompleteUrlParamsSchema = Schema.Struct({
86+ state: Schema.String,
87+ code: Schema.optional(Schema.String),
88+ error: Schema.optional(Schema.String),
89+ error_description: Schema.optional(Schema.String),
90+});
91+
92+const HtmlSchema = HttpApiSchema.Text({
93+ contentType: "text/html",
94+});
95+
7596 export class SourcesApi extends HttpApiGroup.make("sources")
7697 .add(
7798 HttpApiEndpoint.get("list")`/workspaces/${workspaceIdParam}/sources`
@@ -117,4 +138,29 @@ export class SourcesApi extends HttpApiGroup.make("sources") · 39 unmodified lines
117138 .addError(ControlPlaneForbiddenError)
118139 .addError(ControlPlaneStorageError),
119140 )
141+ .add(
142+ HttpApiEndpoint.get("credentialPage")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/credentials`
143+ .setUrlParams(CredentialPageUrlParamsSchema)
144+ .addSuccess(HtmlSchema)
145+ .addError(ControlPlaneBadRequestError)
146+ .addError(ControlPlaneNotFoundError)
147+ .addError(ControlPlaneStorageError),
148+ )
149+ .add(
150+ HttpApiEndpoint.post("credentialSubmit")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/credentials`
151+ .setUrlParams(CredentialPageUrlParamsSchema)
152+ .setPayload(CredentialSubmitPayloadSchema)
153+ .addSuccess(HtmlSchema)
154+ .addError(ControlPlaneBadRequestError)
155+ .addError(ControlPlaneNotFoundError)
156+ .addError(ControlPlaneStorageError),
157+ )
158+ .add(
159+ HttpApiEndpoint.get("credentialComplete")`/workspaces/${workspaceIdParam}/sources/${sourceIdParam}/credentials/oauth/complete`
160+ .setUrlParams(CredentialOauthCompleteUrlParamsSchema)
161+ .addSuccess(Schema.String)
162+ .addError(ControlPlaneBadRequestError)
163+ .addError(ControlPlaneNotFoundError)
164+ .addError(ControlPlaneStorageError),
165+ )
120166 .prefix("/v1") {}
packages/control-plane/src/api/sources/http.ts+580−2
@@ -1,5 +1,13 @@
1−import { HttpApiBuilder } from "@effect/platform";
2−import type { WorkspaceId } from "#schema";
1+import {
2+ HttpApiBuilder,
3+ HttpServerResponse,
4+} from "@effect/platform";
5+import type {
6+ ExecutionInteraction,
7+ Source,
8+ WorkspaceId,
9+} from "#schema";
10+import * as Effect from "effect/Effect";
311
412 import { requirePermission, withPolicy } from "#domain";
513 import {
@@ -9,7 +17,17 @@ import { · 3 unmodified lines
917 removeSource,
1018 updateSource,
1119 } from "../../runtime/sources-operations";
20+import {
21+ completeSourceCredentialSetup,
22+ getSourceCredentialInteraction,
23+ submitSourceCredentialInteraction,
24+} from "../../runtime/local-operations";
1225
26+import {
27+ ControlPlaneBadRequestError,
28+ ControlPlaneNotFoundError,
29+ ControlPlaneStorageError,
30+} from "../errors";
1331 import { ControlPlaneApi } from "../api";
1432 import { withWorkspaceRequestActor } from "../http-auth";
1533
@@ -25,6 +43,446 @@ const requireWriteSources = (workspaceId: WorkspaceId) => · 9 unmodified lines
2543 workspaceId,
2644 });
2745
46+const escapeHtml = (value: string): string =>
47+ value
48+ .replaceAll("&", "&amp;")
49+ .replaceAll("<", "&lt;")
50+ .replaceAll(">", "&gt;")
51+ .replaceAll('"', "&quot;")
52+ .replaceAll("'", "&#39;");
53+
54+const credentialPageDocument = (input: {
55+ title: string;
56+ eyebrow: string;
57+ message: string;
58+ sourceLabel: string;
59+ endpoint: string;
60+ interactionId: string;
61+ error?: string;
62+ state?: "pending" | "stored" | "continued" | "cancelled" | "inactive";
63+}) => `<!doctype html>
64+<html lang="en">
65+ <head>
66+ <meta charset="utf-8" />
67+ <meta name="viewport" content="width=device-width, initial-scale=1" />
68+ <title>${escapeHtml(input.title)}</title>
69+ <link rel="preconnect" href="https://fonts.googleapis.com" />
70+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
71+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet" />
72+ <style>
73+ :root {
74+ --bg: #fafaf9;
75+ --surface: #ffffff;
76+ --text: #1c1917;
77+ --text-secondary: #78716c;
78+ --text-tertiary: #a8a29e;
79+ --border: #e7e5e4;
80+ --accent: #1d4ed8;
81+ --accent-light: #eff6ff;
82+ --green: #16a34a;
83+ --green-light: #f0fdf4;
84+ --green-border: #bbf7d0;
85+ --red: #dc2626;
86+ --red-light: #fef2f2;
87+ --red-border: #fecaca;
88+ --amber: #d97706;
89+ --amber-light: #fffbeb;
90+ --amber-border: #fde68a;
91+ }
92+
93+ * { box-sizing: border-box; margin: 0; padding: 0; }
94+
95+ body {
96+ min-height: 100vh;
97+ background: var(--bg);
98+ color: var(--text);
99+ font-family: 'DM Sans', system-ui, -apple-system, sans-serif;
100+ display: flex;
101+ justify-content: center;
102+ padding: 80px 24px 48px;
103+ -webkit-font-smoothing: antialiased;
104+ }
105+
106+ .page {
107+ width: 100%;
108+ max-width: 540px;
109+ }
110+
111+ /* Status pill at the very top */
112+ .status-pill {
113+ display: inline-flex;
114+ align-items: center;
115+ gap: 7px;
116+ padding: 5px 14px 5px 10px;
117+ border-radius: 999px;
118+ font-family: 'DM Mono', monospace;
119+ font-size: 12px;
120+ font-weight: 500;
121+ letter-spacing: 0.01em;
122+ margin-bottom: 28px;
123+ }
124+
125+ .status-pill::before {
126+ content: '';
127+ width: 7px;
128+ height: 7px;
129+ border-radius: 50%;
130+ flex-shrink: 0;
131+ }
132+
133+ .status-pill[data-state='pending'] {
134+ background: var(--amber-light);
135+ color: var(--amber);
136+ border: 1px solid var(--amber-border);
137+ }
138+ .status-pill[data-state='pending']::before { background: var(--amber); }
139+
140+ .status-pill[data-state='stored'] {
141+ background: var(--green-light);
142+ color: var(--green);
143+ border: 1px solid var(--green-border);
144+ }
145+ .status-pill[data-state='stored']::before { background: var(--green); }
146+
147+ .status-pill[data-state='continued'] {
148+ background: var(--green-light);
149+ color: var(--green);
150+ border: 1px solid var(--green-border);
151+ }
152+ .status-pill[data-state='continued']::before { background: var(--green); }
153+
154+ .status-pill[data-state='cancelled'],
155+ .status-pill[data-state='inactive'] {
156+ background: #f5f5f4;
157+ color: var(--text-secondary);
158+ border: 1px solid var(--border);
159+ }
160+ .status-pill[data-state='cancelled']::before,
161+ .status-pill[data-state='inactive']::before { background: var(--text-tertiary); }
162+
163+ /* Title area */
164+ h1 {
165+ font-size: 28px;
166+ font-weight: 600;
167+ letter-spacing: -0.025em;
168+ line-height: 1.2;
169+ margin-bottom: 10px;
170+ }
171+
172+ .subtitle {
173+ font-size: 15px;
174+ line-height: 1.6;
175+ color: var(--text-secondary);
176+ }
177+
178+ /* Divider */
179+ hr {
180+ border: none;
181+ height: 1px;
182+ background: var(--border);
183+ margin: 36px 0;
184+ }
185+
186+ /* Source detail rows */
187+ .detail {
188+ display: flex;
189+ flex-direction: column;
190+ gap: 6px;
191+ margin-bottom: 24px;
192+ }
193+
194+ .detail-label {
195+ font-size: 13px;
196+ font-weight: 500;
197+ color: var(--text-secondary);
198+ }
199+
200+ .detail-value {
201+ font-size: 15px;
202+ line-height: 1.5;
203+ word-break: break-word;
204+ }
205+
206+ .detail-mono {
207+ font-family: 'DM Mono', monospace;
208+ font-size: 13px;
209+ line-height: 1.5;
210+ padding: 10px 14px;
211+ background: #f5f5f4;
212+ border: 1px solid var(--border);
213+ border-radius: 8px;
214+ word-break: break-all;
215+ color: var(--text);
216+ }
217+
218+ .detail-id {
219+ font-family: 'DM Mono', monospace;
220+ font-size: 12px;
221+ color: var(--text-tertiary);
222+ }
223+
224+ /* Result card for stored/cancelled/inactive */
225+ .result-card {
226+ padding: 32px;
227+ border-radius: 12px;
228+ text-align: center;
229+ }
230+
231+ .result-card.success {
232+ background: var(--green-light);
233+ border: 1px solid var(--green-border);
234+ }
235+
236+ .result-card.neutral {
237+ background: #f5f5f4;
238+ border: 1px solid var(--border);
239+ }
240+
241+ .result-card p {
242+ font-size: 15px;
243+ line-height: 1.6;
244+ color: var(--text-secondary);
245+ }
246+
247+ .result-card .result-heading {
248+ font-weight: 600;
249+ font-size: 15px;
250+ color: var(--text);
251+ margin-bottom: 6px;
252+ }
253+
254+ /* Form */
255+ .alert {
256+ padding: 12px 14px;
257+ border-radius: 8px;
258+ background: var(--red-light);
259+ border: 1px solid var(--red-border);
260+ color: var(--red);
261+ font-size: 14px;
262+ line-height: 1.5;
263+ margin-bottom: 20px;
264+ }
265+
266+ .field { margin-bottom: 20px; }
267+
268+ label {
269+ display: block;
270+ font-size: 14px;
271+ font-weight: 500;
272+ margin-bottom: 6px;
273+ }
274+
275+ input[type='password'] {
276+ width: 100%;
277+ padding: 10px 14px;
278+ background: var(--surface);
279+ border: 1px solid var(--border);
280+ border-radius: 8px;
281+ font-family: 'DM Mono', monospace;
282+ font-size: 14px;
283+ color: var(--text);
284+ transition: border-color 0.15s, box-shadow 0.15s;
285+ }
286+
287+ input[type='password']::placeholder { color: var(--text-tertiary); }
288+
289+ input[type='password']:focus {
290+ outline: none;
291+ border-color: var(--accent);
292+ box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.1);
293+ }
294+
295+ .hint {
296+ margin-top: 8px;
297+ font-size: 13px;
298+ line-height: 1.5;
299+ color: var(--text-tertiary);
300+ }
301+
302+ .actions {
303+ display: flex;
304+ gap: 10px;
305+ margin-top: 24px;
306+ }
307+
308+ button {
309+ appearance: none;
310+ border: none;
311+ border-radius: 8px;
312+ padding: 10px 20px;
313+ font-family: inherit;
314+ font-size: 14px;
315+ font-weight: 500;
316+ cursor: pointer;
317+ transition: background 0.15s;
318+ }
319+
320+ .primary {
321+ background: var(--text);
322+ color: var(--surface);
323+ }
324+ .primary:hover { background: #292524; }
325+
326+ .secondary {
327+ background: transparent;
328+ color: var(--text-secondary);
329+ border: 1px solid var(--border);
330+ }
331+ .secondary:hover { background: #f5f5f4; }
332+
333+ @media (max-width: 540px) {
334+ body { padding: 48px 20px 32px; }
335+ h1 { font-size: 24px; }
336+ }
337+ </style>
338+ </head>
339+ <body>
340+ <main class="page">
341+ <div class="status-pill" data-state="${escapeHtml(input.state ?? "pending")}">${escapeHtml(input.state ?? "pending")}</div>
342+
343+ <h1>${escapeHtml(input.title)}</h1>
344+ <p class="subtitle">${escapeHtml(input.message)}</p>
345+
346+ <hr />
347+
348+ <div class="detail">
349+ <span class="detail-label">Source</span>
350+ <span class="detail-value">${escapeHtml(input.sourceLabel)}</span>
351+ </div>
352+
353+ <div class="detail">
354+ <span class="detail-label">API endpoint</span>
355+ <div class="detail-mono">${escapeHtml(input.endpoint)}</div>
356+ </div>
357+
358+ <div class="detail">
359+ <span class="detail-label">Interaction</span>
360+ <span class="detail-id">${escapeHtml(input.interactionId)}</span>
361+ </div>
362+
363+ <hr />
364+
365+ ${
366+ input.error
367+ ? `<p class="alert">${escapeHtml(input.error)}</p>`
368+ : ""
369+ }
370+ ${
371+ input.state === "stored"
372+ ? `<div class="result-card success">
373+ <p class="result-heading">Credential stored</p>
374+ <p>Executor resumed the source connection request. You can close this window.</p>
375+ </div>`
376+ : input.state === "continued"
377+ ? `<div class="result-card success">
378+ <p class="result-heading">Continuing without auth</p>
379+ <p>Executor resumed the source connection without stored credentials. You can close this window.</p>
380+ </div>`
381+ : input.state === "cancelled"
382+ ? `<div class="result-card neutral">
383+ <p class="result-heading">Request cancelled</p>
384+ <p>Executor was notified. You can close this window.</p>
385+ </div>`
386+ : input.state === "inactive"
387+ ? `<div class="result-card neutral">
388+ <p class="result-heading">No longer active</p>
389+ <p>This credential request has expired. Start a new source connection flow if needed.</p>
390+ </div>`
391+ : `<form method="post">
392+ <div class="field">
393+ <label for="token">Personal access token</label>
394+ <input
395+ id="token"
396+ name="token"
397+ type="password"
398+ autocomplete="off"
399+ spellcheck="false"
400+ placeholder="ghp_..."
401+ />
402+ <p class="hint">
403+ Add a token to enable authenticated requests, or continue without auth if this API supports public access.
404+ </p>
405+ </div>
406+ <div class="actions">
407+ <button class="primary" type="submit" name="action" value="submit">Connect token</button>
408+ <button class="secondary" type="submit" name="action" value="continue" formnovalidate>Continue without auth</button>
409+ <button class="secondary" type="submit" name="action" value="cancel" formnovalidate>Cancel</button>
410+ </div>
411+ </form>`
412+ }
413+ </main>
414+ </body>
415+</html>`;
416+
417+const htmlResponse = (html: string, status = 200) =>
418+ HttpServerResponse.html(html).pipe(
419+ HttpServerResponse.setStatus(status),
420+ );
421+
422+const credentialErrorResponse = (input: {
423+ title: string;
424+ message: string;
425+ status: number;
426+}) =>
427+ htmlResponse(
428+ credentialPageDocument({
429+ title: input.title,
430+ eyebrow: "Executor",
431+ message: input.message,
432+ sourceLabel: "Unavailable",
433+ endpoint: "Unavailable",
434+ interactionId: "unavailable",
435+ state: "inactive",
436+ error: input.message,
437+ }),
438+ input.status,
439+ );
440+
441+const credentialErrorStatus = (
442+ error: ControlPlaneBadRequestError | ControlPlaneNotFoundError | ControlPlaneStorageError,
443+) =>
444+ error instanceof ControlPlaneNotFoundError
445+ ? 404
446+ : error instanceof ControlPlaneBadRequestError
447+ ? 400
448+ : 500;
449+
450+const credentialSubmitErrorResponse = (input: {
451+ workspaceId: WorkspaceId;
452+ sourceId: Source["id"];
453+ interactionId: ExecutionInteraction["id"];
454+ message: string;
455+ status: number;
456+}) =>
457+ getSourceCredentialInteraction({
458+ workspaceId: input.workspaceId,
459+ sourceId: input.sourceId,
460+ interactionId: input.interactionId,
461+ }).pipe(
462+ Effect.match({
463+ onFailure: () =>
464+ credentialErrorResponse({
465+ title: "Source Credential Request Failed",
466+ message: input.message,
467+ status: input.status,
468+ }),
469+ onSuccess: (interaction) =>
470+ htmlResponse(
471+ credentialPageDocument({
472+ title: "Configure Source Access",
473+ eyebrow: "Executor",
474+ message: interaction.message,
475+ sourceLabel: interaction.sourceLabel,
476+ endpoint: interaction.endpoint,
477+ interactionId: interaction.interactionId,
478+ error: input.message,
479+ state: "pending",
480+ }),
481+ input.status,
482+ ),
483+ }),
484+ );
485+
28486 export const ControlPlaneSourcesLive = HttpApiBuilder.group(
29487 ControlPlaneApi,
30488 "sources",
@@ -74,5 +532,125 @@ export const ControlPlaneSourcesLive = HttpApiBuilder.group( · 43 unmodified lines
74532 }),
75533 ),
76534 ),
535+ )
536+ .handle("credentialPage", ({ path, urlParams }) =>
537+ getSourceCredentialInteraction({
538+ workspaceId: path.workspaceId,
539+ sourceId: path.sourceId,
540+ interactionId: urlParams.interactionId,
541+ }).pipe(
542+ Effect.map((interaction) =>
543+ htmlResponse(
544+ credentialPageDocument({
545+ title: "Configure Source Access",
546+ eyebrow: "Executor",
547+ message: interaction.message,
548+ sourceLabel: interaction.sourceLabel,
549+ endpoint: interaction.endpoint,
550+ interactionId: interaction.interactionId,
551+ state: interaction.status === "pending" ? "pending" : "inactive",
552+ }),
553+ )
554+ ),
555+ Effect.catchTags({
556+ ControlPlaneNotFoundError: (error) =>
557+ Effect.succeed(
558+ credentialErrorResponse({
559+ title: "Source Credential Request Unavailable",
560+ message: error.message,
561+ status: credentialErrorStatus(error),
562+ }),
563+ ),
564+ ControlPlaneStorageError: (error) =>
565+ Effect.succeed(
566+ credentialErrorResponse({
567+ title: "Source Credential Request Failed",
568+ message: error.message,
569+ status: credentialErrorStatus(error),
570+ }),
571+ ),
572+ }),
573+ ),
574+ )
575+ .handle("credentialSubmit", ({ path, urlParams, payload }) =>
576+ submitSourceCredentialInteraction({
577+ workspaceId: path.workspaceId,
578+ sourceId: path.sourceId,
579+ interactionId: urlParams.interactionId,
580+ action:
581+ payload.action === "cancel"
582+ ? "cancel"
583+ : payload.action === "continue"
584+ ? "continue"
585+ : "submit",
586+ token: payload.token,
587+ }).pipe(
588+ Effect.map((result) =>
589+ htmlResponse(
590+ credentialPageDocument({
591+ title:
592+ result.kind === "stored"
593+ ? "Source Credential Stored"
594+ : result.kind === "continued"
595+ ? "Source Access Updated"
596+ : "Source Credential Request Cancelled",
597+ eyebrow: "Executor",
598+ message:
599+ result.kind === "stored"
600+ ? "Executor stored the credential and resumed the source connection."
601+ : result.kind === "continued"
602+ ? "Executor resumed the source connection without stored credentials."
603+ : "Executor cancelled the source credential request.",
604+ sourceLabel: result.sourceLabel,
605+ endpoint: result.endpoint,
606+ interactionId: urlParams.interactionId,
607+ state:
608+ result.kind === "stored"
609+ ? "stored"
610+ : result.kind === "continued"
611+ ? "continued"
612+ : "cancelled",
613+ }),
614+ )
615+ ),
616+ Effect.catchTags({
617+ ControlPlaneBadRequestError: (error) =>
618+ credentialSubmitErrorResponse({
619+ workspaceId: path.workspaceId,
620+ sourceId: path.sourceId,
621+ interactionId: urlParams.interactionId,
622+ message: error.message,
623+ status: credentialErrorStatus(error),
624+ }),
625+ ControlPlaneNotFoundError: (error) =>
626+ Effect.succeed(
627+ credentialErrorResponse({
628+ title: "Source Credential Request Unavailable",
629+ message: error.message,
630+ status: credentialErrorStatus(error),
631+ }),
632+ ),
633+ ControlPlaneStorageError: (error) =>
634+ Effect.succeed(
635+ credentialErrorResponse({
636+ title: "Source Credential Request Failed",
637+ message: error.message,
638+ status: credentialErrorStatus(error),
639+ }),
640+ ),
641+ }),
642+ ),
643+ )
644+ .handle("credentialComplete", ({ path, urlParams }) =>
645+ completeSourceCredentialSetup({
646+ workspaceId: path.workspaceId,
647+ sourceId: path.sourceId,
648+ state: urlParams.state,
649+ code: urlParams.code,
650+ error: urlParams.error,
651+ errorDescription: urlParams.error_description,
652+ }).pipe(
653+ Effect.map((source) => `Source connected: ${source.id}. You can close this window.`),
654+ ),
77655 ),
78656 );
packages/control-plane/src/persistence/repos/execution-interactions-repo.ts+14−0
@@ -18,6 +18,20 @@ export const createExecutionInteractionsRepo = ( · 17 unmodified lines
1818 client: DrizzleClient,
1919 tables: DrizzleTables,
2020 ) => ({
21+ getById: (interactionId: ExecutionInteraction["id"]) =>
22+ client.use("rows.execution_interactions.get_by_id", async (db) => {
23+ const rows = await db
24+ .select()
25+ .from(tables.executionInteractionsTable)
26+ .where(eq(tables.executionInteractionsTable.id, interactionId))
27+ .limit(1);
28+
29+ const row = firstOption(rows);
30+ return Option.isSome(row)
31+ ? Option.some(decodeExecutionInteraction(row.value))
32+ : Option.none<ExecutionInteraction>();
33+ }),
34+
2135 listByExecutionId: (executionId: ExecutionInteraction["executionId"]) =>
2236 client.use("rows.execution_interactions.list_by_execution_id", async (db) => {
2337 const rows = await db
packages/control-plane/src/persistence/repos/tool-artifacts-repo.ts+5−3
@@ -178,7 +178,7 @@ export const createToolArtifactsRepo = ( · 177 unmodified lines
178178 },
179179 ) =>
180180 client.use("rows.tool_artifacts.search_by_workspace", async (db) => {
181− const rows = await db
181+ const baseQuery = db
182182 .select()
183183 .from(tables.toolArtifactsTable)
184184 .where(
@@ -191,8 +191,10 @@ export const createToolArtifactsRepo = ( · 6 unmodified lines
191191 .orderBy(
192192 asc(tables.toolArtifactsTable.searchNamespace),
193193 asc(tables.toolArtifactsTable.path),
194− )
195− .limit(input.limit ?? 500);
194+ );
195+ const rows = input.limit !== undefined
196+ ? await baseQuery.limit(input.limit)
197+ : await baseQuery;
196198
197199 return rows.map((row) => decodeStoredToolArtifactRecord(row));
198200 }),
packages/control-plane/src/runtime/control-plane-runtime.test.ts+283−2
@@ -1,10 +1,22 @@
11 import { describe, expect, it } from "@effect/vitest";
22 import { assertTrue } from "@effect/vitest/utils";
33 import * as Effect from "effect/Effect";
4+import * as Fiber from "effect/Fiber";
5+import * as Option from "effect/Option";
46
57 import type { AccountId } from "#schema";
6−
7−import { createSqlControlPlaneRuntime } from "./index";
8+import {
9+ ExecutionIdSchema,
10+ ExecutionInteractionIdSchema,
11+ SecretMaterialIdSchema,
12+ SourceIdSchema,
13+} from "#schema";
14+import type { ToolPath } from "@executor-v3/codemode-core";
15+
16+import {
17+ createSqlControlPlaneRuntime,
18+ LiveExecutionManagerService,
19+} from "./index";
820 import { withControlPlaneClient } from "./test-http-client";
921
1022 const makeRuntime = Effect.acquireRelease(
@@ -102,6 +114,275 @@ describe("control-plane-runtime", () => { · 91 unmodified lines
102114 }),
103115 );
104116
117+ it.scoped("captures credential requests through the local HTML flow without persisting raw tokens", () =>
118+ Effect.gen(function* () {
119+ const runtime = yield* makeRuntime;
120+ const installation = runtime.localInstallation;
121+ const executionId = ExecutionIdSchema.make("exec_local_credential");
122+ const sourceId = SourceIdSchema.make("src_local_credential");
123+ const interactionSuffix = "executor.sources.add:test";
124+ const interactionId = ExecutionInteractionIdSchema.make(
125+ `${executionId}:${interactionSuffix}`,
126+ );
127+ const now = Date.now();
128+
129+ yield* runtime.persistence.rows.executions.insert({
130+ id: executionId,
131+ workspaceId: installation.workspaceId,
132+ createdByAccountId: installation.accountId,
133+ status: "running",
134+ code: "return await tools.executor.sources.add(...)",
135+ resultJson: null,
136+ errorText: null,
137+ logsJson: null,
138+ startedAt: now,
139+ completedAt: null,
140+ createdAt: now,
141+ updatedAt: now,
142+ });
143+
144+ yield* runtime.persistence.rows.sources.insert({
145+ id: sourceId,
146+ workspaceId: installation.workspaceId,
147+ name: "GitHub",
148+ kind: "openapi",
149+ endpoint: "https://api.github.com",
150+ status: "auth_required",
151+ enabled: true,
152+ namespace: "github",
153+ transport: null,
154+ queryParamsJson: null,
155+ headersJson: null,
156+ specUrl: "https://example.com/github-openapi.yaml",
157+ defaultHeadersJson: null,
158+ authKind: "none",
159+ authHeaderName: null,
160+ authPrefix: null,
161+ sourceHash: null,
162+ lastError: null,
163+ createdAt: now,
164+ updatedAt: now,
165+ });
166+
167+ const interactionFiber = yield* Effect.gen(function* () {
168+ const liveExecutionManager = yield* LiveExecutionManagerService;
169+ const onElicitation = liveExecutionManager.createOnElicitation({
170+ rows: runtime.persistence.rows,
171+ executionId,
172+ });
173+
174+ return yield* onElicitation({
175+ interactionId: interactionSuffix,
176+ path: "executor.sources.add" as ToolPath,
177+ sourceKey: "executor",
178+ args: {
179+ kind: "openapi",
180+ endpoint: "https://api.github.com",
181+ specUrl: "https://example.com/github-openapi.yaml",
182+ name: "GitHub",
183+ workspaceId: installation.workspaceId,
184+ sourceId,
185+ },
186+ elicitation: {
187+ mode: "url",
188+ message: "Open the secure credential page to connect GitHub",
189+ url: `http://127.0.0.1/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/sources/${encodeURIComponent(sourceId)}/credentials?interactionId=${encodeURIComponent(interactionId)}`,
190+ elicitationId: interactionSuffix,
191+ },
192+ });
193+ }).pipe(
194+ Effect.provide(runtime.runtimeLayer),
195+ Effect.fork,
196+ );
197+
198+ yield* Effect.yieldNow();
199+
200+ const pendingInteraction = yield* runtime.persistence.rows.executionInteractions
201+ .getPendingByExecutionId(executionId);
202+ assertTrue(Option.isSome(pendingInteraction));
203+ expect(pendingInteraction.value.id).toBe(interactionId);
204+
205+ const page = yield* withControlPlaneClient(
206+ { runtime },
207+ (client) =>
208+ client.sources.credentialPage({
209+ path: {
210+ workspaceId: installation.workspaceId,
211+ sourceId,
212+ },
213+ urlParams: {
214+ interactionId: pendingInteraction.value.id,
215+ },
216+ }),
217+ );
218+ expect(page).toContain("Configure Source Access");
219+ expect(page).toContain("GitHub");
220+ expect(page).toContain("Continue without auth");
221+
222+ const submittedPage = yield* withControlPlaneClient(
223+ { runtime },
224+ (client) =>
225+ client.sources.credentialSubmit({
226+ path: {
227+ workspaceId: installation.workspaceId,
228+ sourceId,
229+ },
230+ urlParams: {
231+ interactionId: pendingInteraction.value.id,
232+ },
233+ payload: {
234+ action: "submit",
235+ token: "ghp_local_test_token",
236+ },
237+ }),
238+ );
239+ expect(submittedPage).toContain("Credential Stored");
240+
241+ const response = yield* Fiber.join(interactionFiber);
242+ expect(response.action).toBe("accept");
243+ expect(response.content?.authKind).toBe("bearer");
244+ expect(typeof response.content?.tokenSecretMaterialId).toBe("string");
245+
246+ const tokenSecretMaterialId = SecretMaterialIdSchema.make(
247+ String(response.content?.tokenSecretMaterialId),
248+ );
249+ const storedSecret = yield* runtime.persistence.rows.secretMaterials.getById(
250+ tokenSecretMaterialId,
251+ );
252+ assertTrue(Option.isSome(storedSecret));
253+ expect(storedSecret.value.value).toBe("ghp_local_test_token");
254+
255+ const storedInteraction = yield* runtime.persistence.rows.executionInteractions.getById(
256+ pendingInteraction.value.id,
257+ );
258+ assertTrue(Option.isSome(storedInteraction));
259+ expect(storedInteraction.value.responseJson).toContain("\"authKind\":\"bearer\"");
260+ expect(storedInteraction.value.responseJson).toContain("tokenSecretMaterialId");
261+ expect(storedInteraction.value.responseJson).not.toContain("ghp_local_test_token");
262+ }),
263+ );
264+
265+ it.scoped("allows continuing an OpenAPI source credential request without auth", () =>
266+ Effect.gen(function* () {
267+ const runtime = yield* makeRuntime;
268+ const installation = runtime.localInstallation;
269+ const executionId = ExecutionIdSchema.make("exec_local_credential_continue");
270+ const sourceId = SourceIdSchema.make("src_local_credential_continue");
271+ const interactionSuffix = "executor.sources.add:continue";
272+ const interactionId = ExecutionInteractionIdSchema.make(
273+ `${executionId}:${interactionSuffix}`,
274+ );
275+ const now = Date.now();
276+
277+ yield* runtime.persistence.rows.executions.insert({
278+ id: executionId,
279+ workspaceId: installation.workspaceId,
280+ createdByAccountId: installation.accountId,
281+ status: "running",
282+ code: "return await tools.executor.sources.add(...)",
283+ resultJson: null,
284+ errorText: null,
285+ logsJson: null,
286+ startedAt: now,
287+ completedAt: null,
288+ createdAt: now,
289+ updatedAt: now,
290+ });
291+
292+ yield* runtime.persistence.rows.sources.insert({
293+ id: sourceId,
294+ workspaceId: installation.workspaceId,
295+ name: "GitHub",
296+ kind: "openapi",
297+ endpoint: "https://api.github.com",
298+ status: "auth_required",
299+ enabled: true,
300+ namespace: "github",
301+ transport: null,
302+ queryParamsJson: null,
303+ headersJson: null,
304+ specUrl: "https://example.com/github-openapi.yaml",
305+ defaultHeadersJson: null,
306+ authKind: "none",
307+ authHeaderName: null,
308+ authPrefix: null,
309+ sourceHash: null,
310+ lastError: null,
311+ createdAt: now,
312+ updatedAt: now,
313+ });
314+
315+ const interactionFiber = yield* Effect.gen(function* () {
316+ const liveExecutionManager = yield* LiveExecutionManagerService;
317+ const onElicitation = liveExecutionManager.createOnElicitation({
318+ rows: runtime.persistence.rows,
319+ executionId,
320+ });
321+
322+ return yield* onElicitation({
323+ interactionId: interactionSuffix,
324+ path: "executor.sources.add" as ToolPath,
325+ sourceKey: "executor",
326+ args: {
327+ kind: "openapi",
328+ endpoint: "https://api.github.com",
329+ specUrl: "https://example.com/github-openapi.yaml",
330+ name: "GitHub",
331+ workspaceId: installation.workspaceId,
332+ sourceId,
333+ },
334+ elicitation: {
335+ mode: "url",
336+ message: "Open the secure credential page to connect GitHub",
337+ url: `http://127.0.0.1/v1/workspaces/${encodeURIComponent(installation.workspaceId)}/sources/${encodeURIComponent(sourceId)}/credentials?interactionId=${encodeURIComponent(interactionId)}`,
338+ elicitationId: interactionSuffix,
339+ },
340+ });
341+ }).pipe(
342+ Effect.provide(runtime.runtimeLayer),
343+ Effect.fork,
344+ );
345+
346+ yield* Effect.yieldNow();
347+
348+ const pendingInteraction = yield* runtime.persistence.rows.executionInteractions
349+ .getPendingByExecutionId(executionId);
350+ assertTrue(Option.isSome(pendingInteraction));
351+
352+ const submittedPage = yield* withControlPlaneClient(
353+ { runtime },
354+ (client) =>
355+ client.sources.credentialSubmit({
356+ path: {
357+ workspaceId: installation.workspaceId,
358+ sourceId,
359+ },
360+ urlParams: {
361+ interactionId: pendingInteraction.value.id,
362+ },
363+ payload: {
364+ action: "continue",
365+ },
366+ }),
367+ );
368+
369+ expect(submittedPage).toContain("Continuing without auth");
370+
371+ const response = yield* Fiber.join(interactionFiber);
372+ expect(response.action).toBe("accept");
373+ expect(response.content).toEqual({
374+ authKind: "none",
375+ });
376+
377+ const storedInteraction = yield* runtime.persistence.rows.executionInteractions.getById(
378+ pendingInteraction.value.id,
379+ );
380+ assertTrue(Option.isSome(storedInteraction));
381+ expect(storedInteraction.value.responseJson).toContain("\"authKind\":\"none\"");
382+ expect(storedInteraction.value.responseJson).not.toContain("tokenSecretMaterialId");
383+ }),
384+ );
385+
105386 it.scoped("scopes organization list/get to memberships", () =>
106387 Effect.gen(function* () {
107388 const runtime = yield* makeRuntime;
packages/control-plane/src/runtime/executor-tools.ts+152−112
@@ -19,6 +19,7 @@ import * as Schema from "effect/Schema"; · 18 unmodified lines
1919
2020 import {
2121 type ExecutorAddSourceInput,
22+ type ExecutorOpenApiSourceAuthInput,
2223 type RuntimeSourceAuthService,
2324 } from "./source-auth-service";
2425 import {
@@ -26,19 +27,6 @@ import { · 1 unmodified line
2627 deriveSchemaTypeSignature,
2728 } from "./schema-type-signature";
2829
29−const ExecutorOpenApiSourceAuthInputSchema = Schema.Union(
30− Schema.Struct({
31− kind: Schema.Literal("none"),
32− }),
33− Schema.Struct({
34− kind: Schema.Literal("bearer"),
35− headerName: Schema.optional(Schema.NullOr(Schema.String)),
36− prefix: Schema.optional(Schema.NullOr(Schema.String)),
37− token: Schema.optional(Schema.NullOr(Schema.String)),
38− tokenEnvVar: Schema.optional(Schema.NullOr(Schema.String)),
39− }),
40−);
41−
4230 const ExecutorMcpSourceAddInputSchema = Schema.Struct({
4331 kind: Schema.optional(Schema.Literal("mcp")),
4432 endpoint: Schema.String,
@@ -52,7 +40,6 @@ const ExecutorOpenApiSourceAddInputSchema = Schema.Struct({ · 7 unmodified lines
5240 specUrl: Schema.String,
5341 name: Schema.optional(Schema.NullOr(Schema.String)),
5442 namespace: Schema.optional(Schema.NullOr(Schema.String)),
55− auth: Schema.optional(Schema.NullOr(ExecutorOpenApiSourceAuthInputSchema)),
5643 });
5744
5845 const ExecutorSourcesAddSchema = Schema.Union(
@@ -100,7 +87,7 @@ export const EXECUTOR_SOURCES_ADD_HELP_LINES = [ · 41 unmodified lines
10087 ' Omit kind or set kind: "mcp". endpoint is the MCP server URL.',
10188 `- OpenAPI: ${EXECUTOR_SOURCES_ADD_OPENAPI_INPUT_SIGNATURE}`,
10289 " endpoint is the base API URL. specUrl is the OpenAPI document URL.",
103− " If credentials are needed, executor prompts with form or URL interaction.",
90+ " executor handles the credential setup for you.",
10491 ] as const;
10592
10693 export const buildExecutorSourcesAddDescription = (): string =>
@@ -133,11 +120,34 @@ const resolveOpenApiSourceLabel = (input: { · 26 unmodified lines
133120 endpoint: string;
134121 }): string => trimOrNull(input.name) ?? input.endpoint;
135122
136−const promptForBearerToken = (input: {
137− args: Extract<ExecutorAddSourceInput, { kind: "openapi" }>;
123+const resolveLocalCredentialUrl = (input: {
124+ baseUrl: string;
125+ workspaceId: WorkspaceId;
126+ sourceId: Source["id"];
127+ executionId: string;
128+ interactionId: string;
129+}): string =>
130+ new URL(
131+ `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/sources/${encodeURIComponent(input.sourceId)}/credentials?interactionId=${encodeURIComponent(`${input.executionId}:${input.interactionId}`)}`,
132+ input.baseUrl,
133+ ).toString();
134+
135+const promptForSourceCredentialSelection = (input: {
136+ args: {
137+ workspaceId: WorkspaceId;
138+ sourceId: Source["id"];
139+ kind: "openapi";
140+ endpoint: string;
141+ specUrl: string;
142+ name?: string | null;
143+ namespace?: string | null;
144+ };
145+ source: Source;
146+ executionId: string;
138147 interactionId: string;
139148 path: ToolPath;
140149 sourceKey: string;
150+ localServerBaseUrl: string | null;
141151 metadata?: ToolMetadata;
142152 invocation?: ToolInvocationContext;
143153 onElicitation?: OnElicitation;
@@ -149,6 +159,12 @@ const promptForBearerToken = (input: { · 5 unmodified lines
149159 );
150160 }
151161
162+ if (input.localServerBaseUrl === null) {
163+ return yield* Effect.fail(
164+ new Error("executor.sources.add requires a local server base URL for credential capture"),
165+ );
166+ }
167+
152168 const response: ElicitationResponse = yield* input.onElicitation({
153169 interactionId: input.interactionId,
154170 path: input.path,
@@ -157,40 +173,49 @@ const promptForBearerToken = (input: { · 2 unmodified lines
157173 metadata: input.metadata,
158174 context: input.invocation,
159175 elicitation: {
160− mode: "form",
161− message: `Enter the API token to connect ${resolveOpenApiSourceLabel(input.args)}`,
162− requestedSchema: {
163− type: "object",
164− properties: {
165− token: {
166− type: "string",
167− title: "API token",
168− },
169− },
170− required: ["token"],
171− additionalProperties: false,
172− },
176+ mode: "url",
177+ message: `Open the secure credential page to connect ${input.source.name}`,
178+ url: resolveLocalCredentialUrl({
179+ baseUrl: input.localServerBaseUrl,
180+ workspaceId: input.args.workspaceId,
181+ sourceId: input.args.sourceId,
182+ executionId: input.executionId,
183+ interactionId: input.interactionId,
184+ }),
185+ elicitationId: input.interactionId,
173186 },
174187 }).pipe(Effect.mapError((cause) => cause instanceof Error ? cause : new Error(String(cause))));
175188
176189 if (response.action !== "accept") {
177190 return yield* Effect.fail(
178− new Error(`Source add was not completed for ${resolveOpenApiSourceLabel(input.args)}`),
191+ new Error(`Source credential setup was not completed for ${input.source.name}`),
179192 );
180193 }
181194
182− const token =
183− response.content && typeof response.content.token === "string"
184− ? response.content.token.trim()
195+ const authKind =
196+ response.content && typeof response.content.authKind === "string"
197+ ? response.content.authKind.trim()
198+ : "";
199+
200+ if (authKind === "none") {
201+ return { kind: "none" } satisfies ExecutorOpenApiSourceAuthInput;
202+ }
203+
204+ const tokenSecretMaterialId =
205+ response.content && typeof response.content.tokenSecretMaterialId === "string"
206+ ? response.content.tokenSecretMaterialId.trim()
185207 : "";
186208
187− if (token.length === 0) {
209+ if (authKind !== "bearer" || tokenSecretMaterialId.length === 0) {
188210 return yield* Effect.fail(
189− new Error("API token was not provided for executor.sources.add"),
211+ new Error("Credential capture did not return a valid source auth choice for executor.sources.add"),
190212 );
191213 }
192214
193− return token;
215+ return {
216+ kind: "bearer",
217+ tokenSecretMaterialId,
218+ } satisfies ExecutorOpenApiSourceAuthInput;
194219 });
195220
196221 export const createExecutorToolMap = (input: {
@@ -205,30 +230,18 @@ export const createExecutorToolMap = (input: { · 8 unmodified lines
205230 execute: async (
206231 args:
207232 | {
208− kind?: "mcp";
209− endpoint: string;
210− name?: string | null;
211− namespace?: string | null;
212− }
233+ kind?: "mcp";
234+ endpoint: string;
235+ name?: string | null;
236+ namespace?: string | null;
237+ }
213238 | {
214− kind: "openapi";
215− endpoint: string;
216− specUrl: string;
217− name?: string | null;
218− namespace?: string | null;
219− auth?:
220− | {
221− kind: "none";
222− }
223− | {
224− kind: "bearer";
225− headerName?: string | null;
226− prefix?: string | null;
227− token?: string | null;
228− tokenEnvVar?: string | null;
229− }
230− | null;
231− },
239+ kind: "openapi";
240+ endpoint: string;
241+ specUrl: string;
242+ name?: string | null;
243+ namespace?: string | null;
244+ },
232245 context,
233246 ): Promise<Source> => {
234247 const executionId = toExecutionId(context?.invocation?.runId);
@@ -238,50 +251,74 @@ export const createExecutorToolMap = (input: { · 3 unmodified lines
238251 const preparedArgs: ExecutorAddSourceInput =
239252 args.kind === "openapi"
240253 ? {
241− ...args,
242− workspaceId: input.workspaceId,
243− executionId,
244− interactionId,
245− auth:
246− args.auth?.kind === "bearer"
247− && !trimOrNull(args.auth.token)
248− && !trimOrNull(args.auth.tokenEnvVar)
249− ? {
250− ...args.auth,
251− token: await Effect.runPromise(
252− promptForBearerToken({
253− args: {
254− ...args,
255− auth: args.auth,
256− workspaceId: input.workspaceId,
257− executionId,
258− interactionId,
259− },
260− interactionId,
261− path: context.path ?? asToolPath("executor.sources.add"),
262− sourceKey: context.sourceKey,
263− metadata: context.metadata,
264− invocation: context.invocation,
265− onElicitation: context.onElicitation,
266− }),
267− ),
268− }
269− : args.auth ?? null,
270− }
254+ ...args,
255+ workspaceId: input.workspaceId,
256+ executionId,
257+ interactionId,
258+ }
271259 : {
272− kind: args.kind,
273− endpoint: args.endpoint,
274− name: args.name ?? null,
275− namespace: args.namespace ?? null,
276− workspaceId: input.workspaceId,
277− executionId,
278− interactionId,
279− };
260+ kind: args.kind,
261+ endpoint: args.endpoint,
262+ name: args.name ?? null,
263+ namespace: args.namespace ?? null,
264+ workspaceId: input.workspaceId,
265+ executionId,
266+ interactionId,
267+ };
280268 const result = await Effect.runPromise(
281269 input.sourceAuthService.addExecutorSource(
282270 preparedArgs,
283271 context?.onElicitation
284272 ? {
273+ mcpDiscoveryElicitation: {
274+ onElicitation: context.onElicitation,
275+ path: context.path ?? asToolPath("executor.sources.add"),
276+ sourceKey: context.sourceKey,
277+ args,
278+ metadata: context.metadata,
279+ invocation: context.invocation,
280+ },
281+ }
282+ : undefined,
283+ ),
284+ );
285+
286+ if (result.kind === "connected") {
287+ return result.source;
288+ }
289+
290+ if (result.kind === "credential_required") {
291+ const preparedOpenApiArgs = preparedArgs as Extract<
292+ ExecutorAddSourceInput,
293+ { kind: "openapi" }
294+ >;
295+ const selectedAuth = await Effect.runPromise(
296+ promptForSourceCredentialSelection({
297+ args: {
298+ ...preparedOpenApiArgs,
299+ workspaceId: input.workspaceId,
300+ sourceId: result.source.id,
301+ },
302+ source: result.source,
303+ executionId,
304+ interactionId,
305+ path: context?.path ?? asToolPath("executor.sources.add"),
306+ sourceKey: context?.sourceKey ?? "executor",
307+ localServerBaseUrl: input.sourceAuthService.getLocalServerBaseUrl(),
308+ metadata: context?.metadata,
309+ invocation: context?.invocation,
310+ onElicitation: context?.onElicitation,
311+ }),
312+ );
313+
314+ const completed = await Effect.runPromise(
315+ input.sourceAuthService.addExecutorSource(
316+ {
317+ ...preparedOpenApiArgs,
318+ auth: selectedAuth,
319+ },
320+ context?.onElicitation
321+ ? {
285322 mcpDiscoveryElicitation: {
286323 onElicitation: context.onElicitation,
287324 path: context.path ?? asToolPath("executor.sources.add"),
@@ -291,12 +328,15 @@ export const createExecutorToolMap = (input: { · 3 unmodified lines
291328 invocation: context.invocation,
292329 },
293330 }
294− : undefined,
295− ),
296− );
331+ : undefined,
332+ ),
333+ );
297334
298− if (result.kind === "connected") {
299− return result.source;
335+ if (completed.kind === "connected") {
336+ return completed.source;
337+ }
338+
339+ throw new Error(`Source add was not completed for ${result.source.id}`);
300340 }
301341
302342 if (!context?.onElicitation) {
@@ -332,13 +372,13 @@ export const createExecutorToolMap = (input: { · 29 unmodified lines
332372 );
333373 },
334374 },
335− metadata: {
336− inputHint: EXECUTOR_SOURCES_ADD_INPUT_HINT,
337− outputHint: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE,
338− inputSchemaJson: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON,
339− outputSchemaJson: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON,
340− sourceKey: "executor",
341− interaction: "auto",
342− },
343− }),
375+ metadata: {
376+ inputHint: EXECUTOR_SOURCES_ADD_INPUT_HINT,
377+ outputHint: EXECUTOR_SOURCES_ADD_OUTPUT_SIGNATURE,
378+ inputSchemaJson: EXECUTOR_SOURCES_ADD_INPUT_SCHEMA_JSON,
379+ outputSchemaJson: EXECUTOR_SOURCES_ADD_OUTPUT_SCHEMA_JSON,
380+ sourceKey: "executor",
381+ interaction: "auto",
382+ },
383+ }),
344384 });
packages/control-plane/src/runtime/index.ts+13−4
@@ -103,10 +103,19 @@ const createRuntimeLayerFromContext = ( · 102 unmodified lines
103103 | RuntimeSourceAuthServiceTag
104104 | RuntimeExecutionResolverService
105105 >,
106−): RuntimeControlPlaneLayer =>
107− Layer.succeedContext(
108− context.pipe(Context.pick(...runtimeContextTags)),
109− );
106+): RuntimeControlPlaneLayer => {
107+ const runtimeContext = context.pipe(
108+ Context.pick(...runtimeContextTags),
109+ ) as Context.Context<
110+ ControlPlaneActorResolver
111+ | ControlPlaneStore
112+ | LiveExecutionManagerService
113+ | RuntimeSourceAuthServiceTag
114+ | RuntimeExecutionResolverService
115+ >;
116+
117+ return Layer.succeedContext(runtimeContext);
118+};
110119
111120 export const createRuntimeControlPlaneLayer = (
112121 options: RuntimeControlPlaneOptions = {},
packages/control-plane/src/runtime/local-operations.ts+293−5
@@ -1,7 +1,19 @@
11 import { ControlPlanePersistenceError } from "#persistence";
2+import type {
3+ Execution,
4+ ExecutionInteraction,
5+ Source,
6+ WorkspaceId,
7+} from "#schema";
8+import {
9+ SourceIdSchema,
10+ WorkspaceIdSchema,
11+} from "#schema";
212 import * as Effect from "effect/Effect";
13+import * as Option from "effect/Option";
314
415 import { loadLocalInstallation } from "./local-installation";
16+import { LiveExecutionManagerService } from "./live-execution";
517 import {
618 operationErrors,
719 } from "./operation-errors";
@@ -10,9 +22,145 @@ import { ControlPlaneStore } from "./store"; · 2 unmodified lines
1022
1123 const localOps = {
1224 installation: operationErrors("local.installation.get"),
13− oauthCallback: operationErrors("local.oauth.callback"),
25+ sourceCredentialComplete: operationErrors("sources.credentials.complete"),
26+ sourceCredentialPage: operationErrors("sources.credentials.page"),
27+ sourceCredentialSubmit: operationErrors("sources.credentials.submit"),
1428 } as const;
1529
30+type SourceCredentialInteraction = {
31+ interactionId: ExecutionInteraction["id"];
32+ executionId: Execution["id"];
33+ status: ExecutionInteraction["status"];
34+ message: string;
35+ workspaceId: WorkspaceId;
36+ sourceId: Source["id"];
37+ sourceLabel: string;
38+ endpoint: string;
39+};
40+
41+const isRecord = (value: unknown): value is Record<string, unknown> =>
42+ typeof value === "object" && value !== null;
43+
44+const asString = (value: unknown): string | null =>
45+ typeof value === "string" ? value : null;
46+
47+const trimOrNull = (value: string | null | undefined): string | null => {
48+ if (value === null || value === undefined) {
49+ return null;
50+ }
51+
52+ const trimmed = value.trim();
53+ return trimmed.length > 0 ? trimmed : null;
54+};
55+
56+const decodeSourceCredentialInteraction = (
57+ interaction: ExecutionInteraction,
58+): Omit<SourceCredentialInteraction, "sourceLabel" | "endpoint"> | null => {
59+ try {
60+ const payload = JSON.parse(interaction.payloadJson) as unknown;
61+ if (!isRecord(payload)) {
62+ return null;
63+ }
64+
65+ const args = payload.args;
66+ const elicitation = payload.elicitation;
67+ if (!isRecord(args) || !isRecord(elicitation)) {
68+ return null;
69+ }
70+
71+ if (
72+ payload.path !== "executor.sources.add"
73+ || args.kind !== "openapi"
74+ || elicitation.mode !== "url"
75+ ) {
76+ return null;
77+ }
78+
79+ const workspaceId = trimOrNull(asString(args.workspaceId));
80+ const sourceId = trimOrNull(asString(args.sourceId));
81+ const message = trimOrNull(asString(elicitation.message));
82+ if (workspaceId === null || sourceId === null || message === null) {
83+ return null;
84+ }
85+
86+ return {
87+ interactionId: interaction.id,
88+ executionId: interaction.executionId,
89+ status: interaction.status,
90+ message,
91+ workspaceId: WorkspaceIdSchema.make(workspaceId),
92+ sourceId: SourceIdSchema.make(sourceId),
93+ };
94+ } catch {
95+ return null;
96+ }
97+};
98+
99+const loadSourceCredentialInteraction = (input: {
100+ workspaceId: WorkspaceId;
101+ sourceId: Source["id"];
102+ interactionId: ExecutionInteraction["id"];
103+ operation:
104+ | typeof localOps.sourceCredentialPage
105+ | typeof localOps.sourceCredentialSubmit;
106+}) =>
107+ Effect.gen(function* () {
108+ const store = yield* ControlPlaneStore;
109+ const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
110+
111+ const stored = yield* store.executionInteractions.getById(input.interactionId).pipe(
112+ Effect.mapError((error) =>
113+ error instanceof ControlPlanePersistenceError
114+ ? input.operation.storage(error)
115+ : input.operation.unknownStorage(
116+ error,
117+ `Failed loading execution interaction ${input.interactionId}`,
118+ ),
119+ ),
120+ );
121+
122+ if (Option.isNone(stored)) {
123+ return yield* Effect.fail(
124+ input.operation.notFound(
125+ "Source credential request not found",
126+ `interactionId=${input.interactionId}`,
127+ ),
128+ );
129+ }
130+
131+ const decoded = decodeSourceCredentialInteraction(stored.value);
132+ if (
133+ decoded === null
134+ || decoded.workspaceId !== input.workspaceId
135+ || decoded.sourceId !== input.sourceId
136+ ) {
137+ return yield* Effect.fail(
138+ input.operation.notFound(
139+ "Source credential request not found",
140+ `workspaceId=${input.workspaceId} sourceId=${input.sourceId} interactionId=${input.interactionId}`,
141+ ),
142+ );
143+ }
144+
145+ const source = yield* sourceAuthService.getSourceById({
146+ workspaceId: input.workspaceId,
147+ sourceId: input.sourceId,
148+ }).pipe(
149+ Effect.mapError((error) =>
150+ input.operation.unknownStorage(
151+ error,
152+ `Failed loading source ${input.sourceId}`,
153+ ),
154+ ),
155+ );
156+
157+ return {
158+ ...decoded,
159+ sourceLabel: source.name,
160+ endpoint: source.endpoint,
161+ } satisfies SourceCredentialInteraction;
162+ });
163+
16164 export const getLocalInstallation = () =>
17165 Effect.gen(function* () {
18166 const store = yield* ControlPlaneStore;
@@ -40,7 +188,147 @@ export const getLocalInstallation = () => · 21 unmodified lines
40188 return installation;
41189 });
42190
43−export const completeSourceAuthCallback = (input: {
191+export const getSourceCredentialInteraction = (input: {
192+ workspaceId: WorkspaceId;
193+ sourceId: Source["id"];
194+ interactionId: ExecutionInteraction["id"];
195+}) =>
196+ loadSourceCredentialInteraction({
197+ ...input,
198+ operation: localOps.sourceCredentialPage,
199+ });
200+
201+export const submitSourceCredentialInteraction = (input: {
202+ workspaceId: WorkspaceId;
203+ sourceId: Source["id"];
204+ interactionId: ExecutionInteraction["id"];
205+ action: "submit" | "continue" | "cancel";
206+ token?: string | null;
207+}) =>
208+ Effect.gen(function* () {
209+ const interaction = yield* loadSourceCredentialInteraction({
210+ workspaceId: input.workspaceId,
211+ sourceId: input.sourceId,
212+ interactionId: input.interactionId,
213+ operation: localOps.sourceCredentialSubmit,
214+ });
215+
216+ if (interaction.status !== "pending") {
217+ return yield* Effect.fail(
218+ localOps.sourceCredentialSubmit.badRequest(
219+ "Source credential request is no longer active",
220+ `interactionId=${interaction.interactionId} status=${interaction.status}`,
221+ ),
222+ );
223+ }
224+
225+ const liveExecutionManager = yield* LiveExecutionManagerService;
226+
227+ if (input.action === "cancel") {
228+ const resumed = yield* liveExecutionManager.resolveInteraction({
229+ executionId: interaction.executionId,
230+ response: {
231+ action: "cancel",
232+ },
233+ });
234+
235+ if (!resumed) {
236+ return yield* Effect.fail(
237+ localOps.sourceCredentialSubmit.badRequest(
238+ "Source credential request is no longer resumable",
239+ `interactionId=${interaction.interactionId}`,
240+ ),
241+ );
242+ }
243+
244+ return {
245+ kind: "cancelled" as const,
246+ sourceLabel: interaction.sourceLabel,
247+ endpoint: interaction.endpoint,
248+ };
249+ }
250+
251+ if (input.action === "continue") {
252+ const resumed = yield* liveExecutionManager.resolveInteraction({
253+ executionId: interaction.executionId,
254+ response: {
255+ action: "accept",
256+ content: {
257+ authKind: "none",
258+ },
259+ },
260+ });
261+
262+ if (!resumed) {
263+ return yield* Effect.fail(
264+ localOps.sourceCredentialSubmit.badRequest(
265+ "Source credential request is no longer resumable",
266+ `interactionId=${interaction.interactionId}`,
267+ ),
268+ );
269+ }
270+
271+ return {
272+ kind: "continued" as const,
273+ sourceLabel: interaction.sourceLabel,
274+ endpoint: interaction.endpoint,
275+ };
276+ }
277+
278+ const token = trimOrNull(input.token);
279+ if (token === null) {
280+ return yield* Effect.fail(
281+ localOps.sourceCredentialSubmit.badRequest(
282+ "Missing token",
283+ `interactionId=${interaction.interactionId}`,
284+ ),
285+ );
286+ }
287+
288+ const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
289+ const tokenSecretMaterialId = yield* sourceAuthService.storeSecretMaterial({
290+ purpose: "auth_material",
291+ value: token,
292+ }).pipe(
293+ Effect.mapError((error) =>
294+ error instanceof ControlPlanePersistenceError
295+ ? localOps.sourceCredentialSubmit.storage(error)
296+ : localOps.sourceCredentialSubmit.unknownStorage(
297+ error,
298+ `Failed storing credential material for interaction ${interaction.interactionId}`,
299+ ),
300+ ),
301+ );
302+ const resumed = yield* liveExecutionManager.resolveInteraction({
303+ executionId: interaction.executionId,
304+ response: {
305+ action: "accept",
306+ content: {
307+ authKind: "bearer",
308+ tokenSecretMaterialId,
309+ },
310+ },
311+ });
312+
313+ if (!resumed) {
314+ return yield* Effect.fail(
315+ localOps.sourceCredentialSubmit.badRequest(
316+ "Source credential request is no longer resumable",
317+ `interactionId=${interaction.interactionId}`,
318+ ),
319+ );
320+ }
321+
322+ return {
323+ kind: "stored" as const,
324+ sourceLabel: interaction.sourceLabel,
325+ endpoint: interaction.endpoint,
326+ };
327+ });
328+
329+export const completeSourceCredentialSetup = (input: {
330+ workspaceId: WorkspaceId;
331+ sourceId: Source["id"];
44332 state: string;
45333 code?: string | null;
46334 error?: string | null;
@@ -49,11 +337,11 @@ export const completeSourceAuthCallback = (input: { · 2 unmodified lines
49337 Effect.gen(function* () {
50338 const sourceAuthService = yield* RuntimeSourceAuthServiceTag;
51339
52− return yield* sourceAuthService.completeSourceAuthCallback(input).pipe(
340+ return yield* sourceAuthService.completeSourceCredentialSetup(input).pipe(
53341 Effect.mapError((error) =>
54− localOps.oauthCallback.unknownStorage(
342+ localOps.sourceCredentialComplete.unknownStorage(
55343 error,
56− "Failed completing source auth callback",
344+ "Failed completing source credential setup",
57345 ),
58346 ),
59347 );
packages/control-plane/src/runtime/source-auth-service.ts+134−15
@@ -18,6 +18,7 @@ import { · 17 unmodified lines
1818 } from "#persistence";
1919 import {
2020 ExecutionIdSchema,
21+ type SecretMaterialId,
2122 SecretMaterialIdSchema,
2223 Source,
2324 SourceAuthSession,
@@ -52,7 +53,6 @@ import { · 28 unmodified lines
5253 } from "./tool-artifacts";
5354
5455 export const CONTROL_PLANE_SECRET_PROVIDER_ID = "control-plane";
55−const OAUTH_CALLBACK_PATH = "/v1/local/oauth/callback";
5656
5757 const trimOrNull = (value: string | null | undefined): string | null => {
5858 if (value === null || value === undefined) {
@@ -101,8 +101,15 @@ const decodeJson = <A>(input: { · 42 unmodified lines
101101 }
102102 };
103103
104−const resolveRedirectUrl = (baseUrl: string): string =>
105− new URL(OAUTH_CALLBACK_PATH, baseUrl).toString();
104+const resolveSourceCredentialOauthCompleteUrl = (input: {
105+ baseUrl: string;
106+ workspaceId: WorkspaceId;
107+ sourceId: Source["id"];
108+}): string =>
109+ new URL(
110+ `/v1/workspaces/${encodeURIComponent(input.workspaceId)}/sources/${encodeURIComponent(input.sourceId)}/credentials/oauth/complete`,
111+ input.baseUrl,
112+ ).toString();
106113
107114 const normalizeEndpoint = (endpoint: string): string => {
108115 const url = new URL(endpoint.trim());
@@ -347,7 +354,7 @@ const exchangeOAuthAuthorizationCode = (input: { · 238 unmodified lines
347354 captured.tokens = tokens;
348355 },
349356 redirectToAuthorization: () => {
350− throw new Error("Unexpected redirect during OAuth callback completion");
357+ throw new Error("Unexpected redirect while completing source credential setup");
351358 },
352359 saveCodeVerifier: () => undefined,
353360 codeVerifier: () => {
@@ -373,7 +380,7 @@ const exchangeOAuthAuthorizationCode = (input: { · 19 unmodified lines
373380 });
374381
375382 if (result !== "AUTHORIZED" || !captured.tokens) {
376− return yield* Effect.fail(new Error("OAuth callback did not complete authorization"));
383+ return yield* Effect.fail(new Error("OAuth redirect did not complete source credential setup"));
377384 }
378385
379386 return {
@@ -455,6 +462,10 @@ export type ExecutorSourceAddResult = · 75 unmodified lines
455462 kind: "connected";
456463 source: Source;
457464 }
465+ | {
466+ kind: "credential_required";
467+ source: Source;
468+ }
458469 | {
459470 kind: "oauth_required";
460471 source: Source;
@@ -472,6 +483,7 @@ export type ExecutorOpenApiSourceAuthInput = · 11 unmodified lines
472483 prefix?: string | null;
473484 token?: string | null;
474485 tokenEnvVar?: string | null;
486+ tokenSecretMaterialId?: string | null;
475487 };
476488
477489 export type ExecutorAddSourceInput =
@@ -496,6 +508,17 @@ export type ExecutorAddSourceInput = · 18 unmodified lines
496508 auth?: ExecutorOpenApiSourceAuthInput | null;
497509 };
498510
511+const shouldPromptForOpenApiCredentialSetup = (input: {
512+ existing?: Source;
513+ auth?: ExecutorOpenApiSourceAuthInput | null;
514+}): boolean => {
515+ if (input.auth !== undefined) {
516+ return false;
517+ }
518+
519+ return !(input.existing?.kind === "openapi" && input.existing.auth.kind === "bearer");
520+};
521+
499522 const materializeExecutorOpenApiAuth = (input: {
500523 rows: SqlControlPlaneRows;
501524 existing?: Source;
@@ -515,10 +538,21 @@ const materializeExecutorOpenApiAuth = (input: { · 13 unmodified lines
515538 const prefix = auth.prefix ?? "Bearer ";
516539 const token = trimOrNull(auth.token);
517540 const tokenEnvVar = trimOrNull(auth.tokenEnvVar);
541+ const tokenSecretMaterialId = trimOrNull(auth.tokenSecretMaterialId);
542+
543+ if (
544+ token === null
545+ && tokenEnvVar === null
546+ && tokenSecretMaterialId === null
547+ && input.existing?.kind === "openapi"
548+ && input.existing.auth.kind === "bearer"
549+ ) {
550+ return input.existing.auth;
551+ }
518552
519− if (token === null && tokenEnvVar === null) {
553+ if (token === null && tokenEnvVar === null && tokenSecretMaterialId === null) {
520554 return yield* Effect.fail(
521− new Error("Bearer auth requires either token or tokenEnvVar"),
555+ new Error("Bearer auth requires token, tokenEnvVar, or tokenSecretMaterialId"),
522556 );
523557 }
524558
@@ -530,6 +564,11 @@ const materializeExecutorOpenApiAuth = (input: { · 5 unmodified lines
530564 value: token,
531565 }),
532566 }
567+ : tokenSecretMaterialId !== null
568+ ? {
569+ providerId: CONTROL_PLANE_SECRET_PROVIDER_ID,
570+ handle: tokenSecretMaterialId,
571+ }
533572 : {
534573 providerId: "env",
535574 handle: tokenEnvVar!,
@@ -548,13 +587,20 @@ type RuntimeSourceAuthServiceShape = { · 12 unmodified lines
548587 workspaceId: WorkspaceId;
549588 sourceId: Source["id"];
550589 }) => Effect.Effect<Source, Error, never>;
590+ getLocalServerBaseUrl: () => string | null;
591+ storeSecretMaterial: (input: {
592+ purpose: "auth_material" | "oauth_access_token" | "oauth_refresh_token";
593+ value: string;
594+ }) => Effect.Effect<SecretMaterialId, Error, never>;
551595 addExecutorSource: (
552596 input: ExecutorAddSourceInput,
553597 options?: {
554598 mcpDiscoveryElicitation?: McpDiscoveryElicitationContext;
555599 },
556600 ) => Effect.Effect<ExecutorSourceAddResult, Error, never>;
557− completeSourceAuthCallback: (input: {
601+ completeSourceCredentialSetup: (input: {
602+ workspaceId: WorkspaceId;
603+ sourceId: Source["id"];
558604 state: string;
559605 code?: string | null;
560606 error?: string | null;
@@ -602,6 +648,14 @@ export const createRuntimeSourceAuthService = (input: { · 41 unmodified lines
602648 });
603649
604650 return {
651+ getLocalServerBaseUrl: () => input.getLocalServerBaseUrl?.() ?? null,
652+
653+ storeSecretMaterial: ({ purpose, value }) =>
654+ upsertSecretMaterial(input.rows, {
655+ purpose,
656+ value,
657+ }),
658+
605659 getSourceById: ({ workspaceId, sourceId }) =>
606660 loadSourceById(input.rows, {
607661 workspaceId,
@@ -632,12 +686,56 @@ export const createRuntimeSourceAuthService = (input: { · 24 unmodified lines
632686 trimOrNull(sourceInput.namespace)
633687 ?? existing?.namespace
634688 ?? defaultNamespaceFromName(chosenName);
689+ const now = Date.now();
690+
691+ if (shouldPromptForOpenApiCredentialSetup({
692+ existing,
693+ auth: sourceInput.auth,
694+ })) {
695+ const draftSource = existing
696+ ? yield* updateSourceFromPayload({
697+ source: existing,
698+ payload: {
699+ name: chosenName,
700+ endpoint: normalizedEndpoint,
701+ namespace: chosenNamespace,
702+ kind: "openapi",
703+ status: "auth_required",
704+ enabled: true,
705+ specUrl: normalizedSpecUrl,
706+ auth: { kind: "none" },
707+ lastError: null,
708+ },
709+ now,
710+ })
711+ : yield* createSourceFromPayload({
712+ workspaceId: sourceInput.workspaceId,
713+ sourceId: SourceIdSchema.make(`src_${crypto.randomUUID()}`),
714+ payload: {
715+ name: chosenName,
716+ kind: "openapi",
717+ endpoint: normalizedEndpoint,
718+ namespace: chosenNamespace,
719+ status: "auth_required",
720+ enabled: true,
721+ specUrl: normalizedSpecUrl,
722+ auth: { kind: "none" },
723+ },
724+ now,
725+ });
726+
727+ const persistedDraft = yield* persistSource(input.rows, draftSource);
728+ return {
729+ kind: "credential_required",
730+ source: persistedDraft,
731+ } satisfies ExecutorSourceAddResult;
732+ }
733+
635734 const auth = yield* materializeExecutorOpenApiAuth({
636735 rows: input.rows,
637736 existing,
638737 auth: sourceInput.auth,
639738 });
640− const now = Date.now();
641739
642740 const draftSource = existing
643741 ? yield* updateSourceFromPayload({
@@ -812,13 +910,17 @@ export const createRuntimeSourceAuthService = (input: { · 168 unmodified lines
812910 const localServerBaseUrl = input.getLocalServerBaseUrl?.();
813911 if (!localServerBaseUrl) {
814912 return yield* Effect.fail(
815− new Error("Local executor server base URL is unavailable for OAuth callback handling"),
913+ new Error("Local executor server base URL is unavailable for source credential setup"),
816914 );
817915 }
818916
819917 const sessionId = SourceAuthSessionIdSchema.make(`src_auth_${crypto.randomUUID()}`);
820918 const state = crypto.randomUUID();
821− const redirectUrl = resolveRedirectUrl(localServerBaseUrl);
919+ const redirectUrl = resolveSourceCredentialOauthCompleteUrl({
920+ baseUrl: localServerBaseUrl,
921+ workspaceId: sourceInput.workspaceId,
922+ sourceId: persistedDraft.id,
923+ });
822924 const oauthStart = yield* startOAuthAuthorization({
823925 endpoint: normalizedEndpoint,
824926 redirectUrl,
@@ -864,7 +966,14 @@ export const createRuntimeSourceAuthService = (input: { · 39 unmodified lines
864966 } satisfies ExecutorSourceAddResult;
865967 }),
866968
867− completeSourceAuthCallback: ({ state, code, error, errorDescription }) =>
969+ completeSourceCredentialSetup: ({
970+ workspaceId,
971+ sourceId,
972+ state,
973+ code,
974+ error,
975+ errorDescription,
976+ }) =>
868977 Effect.gen(function* () {
869978 const sessionOption = yield* input.rows.sourceAuthSessions.getByState(state);
870979 if (Option.isNone(sessionOption)) {
@@ -872,6 +981,14 @@ export const createRuntimeSourceAuthService = (input: { · 1 unmodified line
872981 }
873982
874983 const session = sessionOption.value;
984+ if (session.workspaceId !== workspaceId || session.sourceId !== sourceId) {
985+ return yield* Effect.fail(
986+ new Error(
987+ `Source auth session ${session.id} does not match workspaceId=${workspaceId} sourceId=${sourceId}`,
988+ ),
989+ );
990+ }
991+
875992 const source = yield* loadSourceById(input.rows, {
876993 workspaceId: session.workspaceId,
877994 sourceId: session.sourceId,
@@ -1015,9 +1132,7 @@ export const createRuntimeSourceAuthService = (input: { · 137 unmodified lines
10151132 } satisfies RuntimeSourceAuthServiceShape;
10161133 };
10171134
1018−export type RuntimeSourceAuthService = ReturnType<
1019− typeof createRuntimeSourceAuthService
1020−>;
1135+export type RuntimeSourceAuthService = RuntimeSourceAuthServiceShape;
10211136
10221137 export class RuntimeSourceAuthServiceTag extends Context.Tag(
10231138 "#runtime/RuntimeSourceAuthServiceTag",
@@ -1045,6 +1160,10 @@ export const ExecutorAddSourceResultSchema = Schema.Union( · 21 unmodified lines
10451160 kind: Schema.Literal("connected"),
10461161 source: SourceSchema,
10471162 }),
1163+ Schema.Struct({
1164+ kind: Schema.Literal("credential_required"),
1165+ source: SourceSchema,
1166+ }),
10481167 Schema.Struct({
10491168 kind: Schema.Literal("oauth_required"),
10501169 source: SourceSchema,
packages/control-plane/src/runtime/workspace-execution-environment.test.ts+190−11
@@ -16,6 +16,7 @@ import { describe, expect, it } from "@effect/vitest"; · 15 unmodified lines
1616 import {
1717 AccountIdSchema,
1818 ExecutionIdSchema,
19+ SecretMaterialIdSchema,
1920 SourceIdSchema,
2021 WorkspaceIdSchema,
2122 } from "#schema";
@@ -413,13 +414,126 @@ const persistConnectedEchoTool = (input: { · 391 unmodified lines
413414 });
414415 });
415416
417+const persistConnectedGithubSearchArtifacts = (input: {
418+ persistence: SqlControlPlanePersistence;
419+ workspaceId: WorkspaceId;
420+ sourceId: SourceId;
421+}) =>
422+ Effect.gen(function* () {
423+ const now = Date.now();
424+
425+ yield* input.persistence.rows.sources.insert({
426+ id: input.sourceId,
427+ workspaceId: input.workspaceId,
428+ name: "GitHub",
429+ kind: "openapi",
430+ endpoint: "https://api.github.com",
431+ status: "connected",
432+ enabled: true,
433+ namespace: "github",
434+ transport: null,
435+ queryParamsJson: null,
436+ headersJson: null,
437+ specUrl: "https://example.com/github-openapi.json",
438+ defaultHeadersJson: null,
439+ authKind: "none",
440+ authHeaderName: null,
441+ authPrefix: null,
442+ sourceHash: null,
443+ lastError: null,
444+ createdAt: now,
445+ updatedAt: now,
446+ });
447+
448+ yield* input.persistence.rows.toolArtifacts.replaceForSource({
449+ workspaceId: input.workspaceId,
450+ sourceId: input.sourceId,
451+ artifacts: [
452+ {
453+ artifact: {
454+ workspaceId: input.workspaceId,
455+ path: "github.actions/get-environment-secret",
456+ toolId: "actions/get-environment-secret",
457+ sourceId: input.sourceId,
458+ title: "Get an environment secret",
459+ description:
460+ "Gets a single environment secret without revealing its encrypted value. Authenticated users must have collaborator access to a repository to create, update, or read secrets.",
461+ searchNamespace: "github.actions",
462+ searchText:
463+ "github.actions/get-environment-secret github.actions get an environment secret gets a single environment secret without revealing its encrypted value authenticated users must have collaborator access to a repository to create update or read secrets GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}",
464+ inputSchemaJson: null,
465+ outputSchemaJson: null,
466+ providerKind: "openapi",
467+ mcpToolName: null,
468+ openApiMethod: "get",
469+ openApiPathTemplate: "/repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}",
470+ openApiOperationHash: "op_actions_get_environment_secret",
471+ openApiRequestBodyRequired: null,
472+ createdAt: now,
473+ updatedAt: now,
474+ },
475+ },
476+ {
477+ artifact: {
478+ workspaceId: input.workspaceId,
479+ path: "github.actions/get-self-hosted-runner-for-org",
480+ toolId: "actions/get-self-hosted-runner-for-org",
481+ sourceId: input.sourceId,
482+ title: "Get a self-hosted runner for an organization",
483+ description:
484+ "Gets a specific self-hosted runner configured in an organization. Authenticated users must have admin access to the organization to use this endpoint.",
485+ searchNamespace: "github.actions",
486+ searchText:
487+ "github.actions/get-self-hosted-runner-for-org github.actions get a self hosted runner for an organization gets a specific self hosted runner configured in an organization authenticated users must have admin access to the organization to use this endpoint GET /orgs/{org}/actions/runners/{runner_id}",
488+ inputSchemaJson: null,
489+ outputSchemaJson: null,
490+ providerKind: "openapi",
491+ mcpToolName: null,
492+ openApiMethod: "get",
493+ openApiPathTemplate: "/orgs/{org}/actions/runners/{runner_id}",
494+ openApiOperationHash: "op_actions_get_self_hosted_runner_for_org",
495+ openApiRequestBodyRequired: null,
496+ createdAt: now,
497+ updatedAt: now,
498+ },
499+ },
500+ {
501+ artifact: {
502+ workspaceId: input.workspaceId,
503+ path: "github.users/get-authenticated",
504+ toolId: "users/get-authenticated",
505+ sourceId: input.sourceId,
506+ title: "Get the authenticated user",
507+ description:
508+ "Gets the authenticated user. OAuth app tokens and personal access tokens need the user scope to include private profile information.",
509+ searchNamespace: "github.users",
510+ searchText:
511+ "github.users/get-authenticated github.users get the authenticated user gets the authenticated user oauth app tokens and personal access tokens need the user scope to include private profile information GET /user",
512+ inputSchemaJson: null,
513+ outputSchemaJson: null,
514+ providerKind: "openapi",
515+ mcpToolName: null,
516+ openApiMethod: "get",
517+ openApiPathTemplate: "/user",
518+ openApiOperationHash: "op_users_get_authenticated",
519+ openApiRequestBodyRequired: null,
520+ createdAt: now,
521+ updatedAt: now,
522+ },
523+ },
524+ ],
525+ });
526+ });
527+
416528 const makeResolver = (persistence: SqlControlPlanePersistence) =>
417529 createWorkspaceExecutionEnvironmentResolver({
418530 rows: persistence.rows,
419531 sourceAuthService: {
532+ getLocalServerBaseUrl: () => null,
533+ storeSecretMaterial: () => Effect.fail(new Error("not implemented in test")),
420534 getSourceById: () => Effect.fail(new Error("not implemented in test")),
421535 addExecutorSource: () => Effect.fail(new Error("not implemented in test")),
422− completeSourceAuthCallback: () => Effect.fail(new Error("not implemented in test")),
536+ completeSourceCredentialSetup: () => Effect.fail(new Error("not implemented in test")),
423537 } as RuntimeSourceAuthService,
424538 });
425539
@@ -429,6 +543,7 @@ const makeResolverWithLiveSourceAuth = (persistence: SqlControlPlanePersistence) · 3 unmodified lines
429543 sourceAuthService: createRuntimeSourceAuthService({
430544 rows: persistence.rows,
431545 liveExecutionManager: createLiveExecutionManager(),
546+ getLocalServerBaseUrl: () => "http://127.0.0.1:8788",
432547 }),
433548 });
434549
@@ -477,6 +592,54 @@ describe("workspace-execution-environment", () => { · 42 unmodified lines
477592 }),
478593 );
479594
595+ it.scoped("prefers namespace and path matches over noisy GitHub boilerplate", () =>
596+ Effect.gen(function* () {
597+ const persistence = yield* makePersistence;
598+ const workspaceId = WorkspaceIdSchema.make("ws_github_discovery");
599+ const sourceId = SourceIdSchema.make("src_github_discovery");
600+ yield* persistConnectedGithubSearchArtifacts({
601+ persistence,
602+ workspaceId,
603+ sourceId,
604+ });
605+
606+ const resolveEnvironment = makeResolver(persistence);
607+ const environment = yield* resolveEnvironment({
608+ workspaceId,
609+ accountId: AccountIdSchema.make("acc_github_discovery"),
610+ executionId: ExecutionIdSchema.make("exec_github_discovery"),
611+ });
612+
613+ const firstQuery = (yield* environment.toolInvoker.invoke({
614+ path: "discover",
615+ args: {
616+ query: "get authenticated user",
617+ limit: 5,
618+ },
619+ })) as {
620+ bestPath: string | null;
621+ results: Array<{ path: string }>;
622+ };
623+
624+ expect(firstQuery.bestPath).toBe("github.users/get-authenticated");
625+ expect(firstQuery.results[0]?.path).toBe("github.users/get-authenticated");
626+
627+ const secondQuery = (yield* environment.toolInvoker.invoke({
628+ path: "discover",
629+ args: {
630+ query: "users get myself current user profile",
631+ limit: 5,
632+ },
633+ })) as {
634+ bestPath: string | null;
635+ results: Array<{ path: string }>;
636+ };
637+
638+ expect(secondQuery.bestPath).toBe("github.users/get-authenticated");
639+ expect(secondQuery.results[0]?.path).toBe("github.users/get-authenticated");
640+ }),
641+ );
642+
480643 it.scoped("invokes persisted MCP tools on demand without re-listing", () =>
481644 Effect.gen(function* () {
482645 const server = yield* makeCountedMcpServer;
@@ -518,10 +681,22 @@ describe("workspace-execution-environment", () => { · 35 unmodified lines
518681 const persistence = yield* makePersistence;
519682 const workspaceId = WorkspaceIdSchema.make("ws_add_openapi");
520683 const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence);
684+ const now = Date.now();
685+ const tokenSecretMaterialId = SecretMaterialIdSchema.make(
686+ "sec_test_openapi_bearer",
687+ );
688+ yield* persistence.rows.secretMaterials.upsert({
689+ id: tokenSecretMaterialId,
690+ purpose: "auth_material",
691+ value: "ghp_test_token",
692+ createdAt: now,
693+ updatedAt: now,
694+ });
521695 let capturedElicitation:
522696 | {
523697 mode?: string;
524− requestedSchema?: Record<string, unknown>;
698+ url?: string;
699+ elicitationId?: string;
525700 }
526701 | null = null;
527702
@@ -533,13 +708,15 @@ describe("workspace-execution-environment", () => { · 5 unmodified lines
533708 Effect.sync(() => {
534709 capturedElicitation = request.elicitation as {
535710 mode?: string;
536− requestedSchema?: Record<string, unknown>;
711+ url?: string;
712+ elicitationId?: string;
537713 };
538714
539715 return {
540716 action: "accept" as const,
541717 content: {
542− token: "ghp_test_token",
718+ authKind: "bearer",
719+ tokenSecretMaterialId,
543720 },
544721 };
545722 }),
@@ -553,9 +730,6 @@ describe("workspace-execution-environment", () => { · 7 unmodified lines
553730 specUrl: specServer.specUrl,
554731 name: "GitHub",
555732 namespace: "github",
556− auth: {
557− kind: "bearer",
558− },
559733 },
560734 context: {
561735 runId: "exec_add_openapi",
@@ -576,11 +750,13 @@ describe("workspace-execution-environment", () => { · 14 unmodified lines
576750 expect(added.auth.kind).toBe("bearer");
577751 expect(added.auth.token?.providerId).toBe("control-plane");
578752 expect(capturedElicitation).toMatchObject({
579− mode: "form",
580− requestedSchema: {
581− type: "object",
582− },
753+ mode: "url",
754+ url: expect.stringContaining("/v1/workspaces/"),
755+ elicitationId: expect.stringContaining("executor.sources.add:"),
583756 });
757+ expect((capturedElicitation as { url?: string } | null)?.url).toContain(
758+ encodeURIComponent("exec_add_openapi:executor.sources.add:"),
759+ );
584760
585761 const storedArtifacts = yield* persistence.rows.toolArtifacts.listByWorkspaceId(
586762 workspaceId,
@@ -656,7 +832,9 @@ describe("workspace-execution-environment", () => { · 69 unmodified lines
656832 expect(described?.path).toBe("executor.sources.add");
657833 expect(described?.description).toContain("Source add input shapes:");
658834 expect(described?.description).toContain("specUrl");
835+ expect(described?.description).toContain("credential setup");
659836 expect(described?.inputHint).toContain("endpoint");
837+ expect(described?.inputHint).not.toContain("auth");
660838
661839 const inputSchema = described?.inputSchemaJson
662840 ? JSON.parse(described.inputSchemaJson) as Record<string, unknown>
@@ -664,6 +842,7 @@ describe("workspace-execution-environment", () => { · 1 unmodified line
664842 expect(inputSchema).not.toBeNull();
665843 expect(JSON.stringify(inputSchema)).toContain("specUrl");
666844 expect(JSON.stringify(inputSchema)).toContain("endpoint");
845+ expect(JSON.stringify(inputSchema)).not.toContain("\"auth\"");
667846 }),
668847 );
669848 });
packages/control-plane/src/runtime/workspace-execution-environment.ts+148−9
@@ -55,17 +55,157 @@ const tokenize = (value: string): string[] => · 54 unmodified lines
5555 value
5656 .trim()
5757 .toLowerCase()
58− .split(/\s+/)
58+ .split(/[^a-z0-9]+/)
5959 .filter(Boolean);
6060
61+const LOW_SIGNAL_QUERY_TOKENS = new Set([
62+ "a",
63+ "an",
64+ "the",
65+ "am",
66+ "as",
67+ "for",
68+ "from",
69+ "get",
70+ "i",
71+ "in",
72+ "is",
73+ "list",
74+ "me",
75+ "my",
76+ "of",
77+ "on",
78+ "or",
79+ "signed",
80+ "to",
81+ "who",
82+]);
83+
84+const singularizeToken = (value: string): string =>
85+ value.length > 3 && value.endsWith("s")
86+ ? value.slice(0, -1)
87+ : value;
88+
89+const tokenEquals = (left: string, right: string): boolean =>
90+ left === right || singularizeToken(left) === singularizeToken(right);
91+
92+const hasTokenMatch = (tokens: readonly string[], queryToken: string): boolean =>
93+ tokens.some((token) => tokenEquals(token, queryToken));
94+
95+const hasSubstringMatch = (value: string, queryToken: string): boolean => {
96+ if (value.includes(queryToken)) {
97+ return true;
98+ }
99+
100+ const singular = singularizeToken(queryToken);
101+ return singular !== queryToken && value.includes(singular);
102+};
103+
104+const queryTokenWeight = (token: string): number =>
105+ LOW_SIGNAL_QUERY_TOKENS.has(token) ? 0.25 : 1;
106+
61107 const scoreArtifact = (
62108 queryTokens: readonly string[],
63− searchText: string,
64−): number =>
65− queryTokens.reduce(
66− (total, token) => total + (searchText.includes(token) ? 1 : 0),
67− 0,
68− );
109+ artifact: StoredToolArtifactRecord,
110+): number => {
111+ const pathText = artifact.path.toLowerCase();
112+ const namespaceText = artifact.searchNamespace.toLowerCase();
113+ const toolIdText = artifact.toolId.toLowerCase();
114+ const titleText = artifact.title?.toLowerCase() ?? "";
115+ const descriptionText = artifact.description?.toLowerCase() ?? "";
116+ const templateText = artifact.openApiPathTemplate?.toLowerCase() ?? "";
117+
118+ const pathTokens = tokenize(`${artifact.path} ${artifact.toolId}`);
119+ const namespaceTokens = tokenize(artifact.searchNamespace);
120+ const titleTokens = tokenize(artifact.title ?? "");
121+ const templateTokens = tokenize(artifact.openApiPathTemplate ?? "");
122+
123+ let score = 0;
124+ let structuralHits = 0;
125+ let namespaceHits = 0;
126+ let pathHits = 0;
127+
128+ for (const token of queryTokens) {
129+ const weight = queryTokenWeight(token);
130+
131+ if (hasTokenMatch(pathTokens, token)) {
132+ score += 12 * weight;
133+ structuralHits += 1;
134+ pathHits += 1;
135+ continue;
136+ }
137+
138+ if (hasTokenMatch(namespaceTokens, token)) {
139+ score += 11 * weight;
140+ structuralHits += 1;
141+ namespaceHits += 1;
142+ continue;
143+ }
144+
145+ if (hasTokenMatch(titleTokens, token)) {
146+ score += 9 * weight;
147+ structuralHits += 1;
148+ continue;
149+ }
150+
151+ if (hasTokenMatch(templateTokens, token)) {
152+ score += 8 * weight;
153+ structuralHits += 1;
154+ continue;
155+ }
156+
157+ if (hasSubstringMatch(pathText, token) || hasSubstringMatch(toolIdText, token)) {
158+ score += 6 * weight;
159+ structuralHits += 1;
160+ pathHits += 1;
161+ continue;
162+ }
163+
164+ if (hasSubstringMatch(namespaceText, token)) {
165+ score += 5 * weight;
166+ structuralHits += 1;
167+ namespaceHits += 1;
168+ continue;
169+ }
170+
171+ if (hasSubstringMatch(titleText, token) || hasSubstringMatch(templateText, token)) {
172+ score += 4 * weight;
173+ structuralHits += 1;
174+ continue;
175+ }
176+
177+ if (hasSubstringMatch(descriptionText, token)) {
178+ score += 0.5 * weight;
179+ }
180+ }
181+
182+ const strongTokens = queryTokens.filter((token) => queryTokenWeight(token) >= 1);
183+ if (strongTokens.length >= 2) {
184+ for (let index = 0; index < strongTokens.length - 1; index += 1) {
185+ const current = strongTokens[index]!;
186+ const next = strongTokens[index + 1]!;
187+ const phrases = [
188+ `${current}-${next}`,
189+ `${current}.${next}`,
190+ `${current}/${next}`,
191+ ];
192+
193+ if (phrases.some((phrase) => pathText.includes(phrase) || templateText.includes(phrase))) {
194+ score += 10;
195+ }
196+ }
197+ }
198+
199+ if (namespaceHits > 0 && pathHits > 0) {
200+ score += 8;
201+ }
202+
203+ if (structuralHits === 0 && score > 0) {
204+ score *= 0.25;
205+ }
206+
207+ return score;
208+};
69209
70210 const toDescriptor = (input: {
71211 artifact: StoredToolArtifactRecord;
@@ -263,7 +403,6 @@ const createWorkspaceToolCatalog = (input: { · 191 unmodified lines
263403 : input.rows.toolArtifacts.searchByWorkspaceId(input.workspaceId, {
264404 namespace,
265405 query,
266− limit: Math.max(limit * 8, 50),
267406 }).pipe(
268407 Effect.mapError((cause) =>
269408 cause instanceof Error ? cause : new Error(String(cause)),
@@ -279,7 +418,7 @@ const createWorkspaceToolCatalog = (input: { · 9 unmodified lines
279418 const persistedHits: SearchHit[] = persisted
280419 .map((artifact) => ({
281420 path: asToolPath(artifact.path),
282− score: scoreArtifact(queryTokens, artifact.searchText),
421+ score: scoreArtifact(queryTokens, artifact),
283422 }))
284423 .filter((hit) => hit.score > 0);
285424
packages/runtime-local-inproc/src/index.ts+1−2
@@ -10,7 +10,7 @@ export type InProcessExecutorOptions = { · 9 unmodified lines
1010 allowFetch?: boolean;
1111 };
1212
13−const DEFAULT_TIMEOUT_MS = 30_000;
13+const DEFAULT_TIMEOUT_MS = 5 * 60_000;
1414
1515 const formatLogArg = (value: unknown): string => {
1616 if (typeof value === "string") {
@@ -179,4 +179,3 @@ export const makeInProcessExecutor = ( · 162 unmodified lines
179179 execute: (code: string, toolInvoker: ToolInvoker) =>
180180 runInProcess(options, code, toolInvoker),
181181 });
182−