History

more debug logs

Rhys Sullivan committed 8 months ago

Commit e3f60e21 parent ede45ad7 files · +93 −110Browse files

Unified · Split

7 files
  1. apps/web/README.md +1 −7
  2. apps/web/app/(console)/tools/page.tsx +2 −33
  3. apps/web/components/tools/tools-view.tsx +0 −10
  4. apps/web/lib/control-plane/server.ts +52 −25
  5. apps/web/lib/external-origin.ts +35 −0
  6. apps/web/lib/mcp/oauth-request.ts +3 −25
  7. packages/persistence-sql/src/sql-internals.ts +0 −10
apps/web/README.md+1−7
@@ -17,21 +17,15 @@ The API route now runs the control-plane server in-process. · 16 unmodified lines
17 17
18Database selection priority:18Database selection priority:
19 19
20- `CONTROL_PLANE_DATABASE_URL`
21- `DATABASE_URL`20- `DATABASE_URL`
22- `POSTGRES_URL`
23 21
24If none are set, fallback is local PGlite data at:22If none are set, fallback is local PGlite data at:
25 23
26- `CONTROL_PLANE_DATA_DIR`
27- default: `.executor-v2/web-state/control-plane-pgdata`24- default: `.executor-v2/web-state/control-plane-pgdata`
28 25
29If needed, `NEXT_PUBLIC_CONTROL_PLANE_BASE_URL` can override the browser base URL.
30
31MCP install URL generation:26MCP install URL generation:
32 27
33- Derives from existing control-plane/frontend config.28- Derives from existing frontend origin and deployment metadata.
34- Prioritizes server-side `CONTROL_PLANE_UPSTREAM_URL` (or server/base control-plane URL) when available.
35- In local dev with no DB URL, defaults to local PGlite-backed control-plane in this Next.js app.29- In local dev with no DB URL, defaults to local PGlite-backed control-plane in this Next.js app.
36 30
37WorkOS auth setup (optional but recommended):31WorkOS auth setup (optional but recommended):
apps/web/app/(console)/tools/page.tsx+2−33
@@ -1,39 +1,8 @@
1import { ToolsView } from "../../../components/tools/tools-view";1import { ToolsView } from "../../../components/tools/tools-view";
2 2import { configuredExternalOriginFromEnv } from "../../../lib/external-origin";
3const normalizeOriginCandidate = (value: string | undefined): string | null => {
4 const trimmed = value?.trim();
5 if (!trimmed) {
6 return null;
7 }
8
9 const withProtocol =
10 trimmed.startsWith("http://") || trimmed.startsWith("https://")
11 ? trimmed
12 : `https://${trimmed}`;
13
14 try {
15 return new URL(withProtocol).origin;
16 } catch {
17 return null;
18 }
19};
20 3
21const resolveMcpBaseUrl = (): string | null => {4const resolveMcpBaseUrl = (): string | null => {
22 const candidates = [5 return configuredExternalOriginFromEnv();
23 process.env.EXECUTOR_PUBLIC_ORIGIN,
24 process.env.NEXT_PUBLIC_APP_ORIGIN,
25 process.env.VERCEL_PROJECT_PRODUCTION_URL,
26 process.env.VERCEL_URL,
27 ];
28
29 for (const candidate of candidates) {
30 const origin = normalizeOriginCandidate(candidate);
31 if (origin) {
32 return origin;
33 }
34 }
35
36 return null;
37};6};
38 7
39const ToolsPage = () => <ToolsView mcpBaseUrl={resolveMcpBaseUrl()} />;8const ToolsPage = () => <ToolsView mcpBaseUrl={resolveMcpBaseUrl()} />;
apps/web/components/tools/tools-view.tsx+0−10
@@ -65,9 +65,6 @@ type SourceGroup = { · 64 unmodified lines
65 toolCount: number;65 toolCount: number;
66};66};
67 67
68const configuredControlPlaneBaseUrl =
69 process.env.NEXT_PUBLIC_CONTROL_PLANE_BASE_URL?.trim() ?? "";
70
71const isLocalHost = (hostname: string): boolean =>68const isLocalHost = (hostname: string): boolean =>
72 hostname === "localhost"69 hostname === "localhost"
73 || hostname === "127.0.0.1"70 || hostname === "127.0.0.1"
@@ -406,13 +403,6 @@ export function ToolsView(props: { · 332 unmodified lines
406 return configured.toString();403 return configured.toString();
407 }404 }
408 405
409 if (configuredControlPlaneBaseUrl.length > 0 && /^https?:\/\//i.test(configuredControlPlaneBaseUrl)) {
410 const configured = new URL(configuredControlPlaneBaseUrl);
411 configured.pathname = "/v1/mcp";
412 configured.searchParams.set("workspaceId", workspaceId);
413 return configured.toString();
414 }
415
416 if (typeof window === "undefined") {406 if (typeof window === "undefined") {
417 return `http://127.0.0.1:8788/v1/mcp?workspaceId=${encodeURIComponent(workspaceId)}`;407 return `http://127.0.0.1:8788/v1/mcp?workspaceId=${encodeURIComponent(workspaceId)}`;
418 }408 }
apps/web/lib/control-plane/server.ts+52−25
@@ -117,30 +117,39 @@ const toAccountScopedIds = (subject: string) => { · 116 unmodified lines
117};117};
118 118
119const resolveDatabaseUrl = (): string | undefined => {119const resolveDatabaseUrl = (): string | undefined => {
120 const override = trim(process.env.CONTROL_PLANE_DATABASE_URL);120 const value = trim(process.env.DATABASE_URL);
121 if (override) {121 return value ? deriveRuntimeDatabaseUrl(value) : undefined;
122 return override;
123 }
124
125 const candidates = [process.env.DATABASE_URL, process.env.POSTGRES_URL];
126
127 for (const candidate of candidates) {
128 const value = trim(candidate);
129 if (value) {
130 return deriveRuntimeDatabaseUrl(value);
131 }
132 }
133
134 return undefined;
135};122};
136 123
137const resolveControlPlaneDataDir = (): string =>124const resolveControlPlaneDataDir = (): string =>
138 trim(process.env.CONTROL_PLANE_DATA_DIR) ?? defaultControlPlaneDataDir;125 defaultControlPlaneDataDir;
139 126
140const resolveStateRootDir = (): string =>127const resolveStateRootDir = (): string =>
141 trim(process.env.CONTROL_PLANE_STATE_ROOT_DIR) ?? defaultControlPlaneStateRootDir;128 defaultControlPlaneStateRootDir;
129
130const openApiSyncRetryDelayMs = 300;
142 131
143const formatCause = (cause: unknown): string => {132const formatCause = (cause: unknown): string => {
133 if (cause && typeof cause === "object") {
134 const maybeError = cause as {
135 message?: unknown;
136 details?: unknown;
137 };
138
139 const details = typeof maybeError.details === "string" ? maybeError.details.trim() : "";
140 const message = typeof maybeError.message === "string" ? maybeError.message.trim() : "";
141
142 if (details.length > 0) {
143 return message.length > 0 && message !== details
144 ? `${message}: ${details}`
145 : details;
146 }
147
148 if (message.length > 0) {
149 return message;
150 }
151 }
152
144 if (cause instanceof Error) {153 if (cause instanceof Error) {
145 return cause.message;154 return cause.message;
146 }155 }
@@ -252,6 +261,18 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 105 unmodified lines
252 const sourceManager = makeSourceManagerService(toolArtifactStore);261 const sourceManager = makeSourceManagerService(toolArtifactStore);
253 const baseSourcesService = makeControlPlaneSourcesService(sourceCatalog);262 const baseSourcesService = makeControlPlaneSourcesService(sourceCatalog);
254 263
264 const fetchOpenApiSpec = (endpoint: string) =>
265 Effect.tryPromise({
266 try: () => fetchOpenApiDocument(endpoint),
267 catch: (cause) => String(cause),
268 }).pipe(Effect.either);
269
270 const refreshOpenApiTools = (input: {
271 source: Parameters<typeof sourceManager.refreshOpenApiArtifact>[0]["source"];
272 openApiSpec: unknown;
273 }) =>
274 sourceManager.refreshOpenApiArtifact(input).pipe(Effect.either);
275
255 const persistSourceErrorState = (source: Parameters<typeof sourceStore.upsert>[0], message: string) => {276 const persistSourceErrorState = (source: Parameters<typeof sourceStore.upsert>[0], message: string) => {
256 const failedSource = {277 const failedSource = {
257 ...source,278 ...source,
@@ -276,10 +297,11 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 18 unmodified lines
276 return source;297 return source;
277 }298 }
278 299
279 const openApiSpecResult = yield* Effect.tryPromise({300 let openApiSpecResult = yield* fetchOpenApiSpec(source.endpoint);
280 try: () => fetchOpenApiDocument(source.endpoint),301 if (openApiSpecResult._tag === "Left") {
281 catch: (cause) => String(cause),302 yield* Effect.sleep(openApiSyncRetryDelayMs);
282 }).pipe(Effect.either);303 openApiSpecResult = yield* fetchOpenApiSpec(source.endpoint);
304 }
283 305
284 if (openApiSpecResult._tag === "Left") {306 if (openApiSpecResult._tag === "Left") {
285 const details = formatCause(openApiSpecResult.left);307 const details = formatCause(openApiSpecResult.left);
@@ -294,12 +316,17 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 8 unmodified lines
294 return yield* persistSourceErrorState(source, message);316 return yield* persistSourceErrorState(source, message);
295 }317 }
296 318
297 const refreshedResult = yield* sourceManager319 let refreshedResult = yield* refreshOpenApiTools({
298 .refreshOpenApiArtifact({320 source,
321 openApiSpec: openApiSpecResult.right,
322 });
323 if (refreshedResult._tag === "Left") {
324 yield* Effect.sleep(openApiSyncRetryDelayMs);
325 refreshedResult = yield* refreshOpenApiTools({
299 source,326 source,
300 openApiSpec: openApiSpecResult.right,327 openApiSpec: openApiSpecResult.right,
301 })328 });
302 .pipe(Effect.either);329 }
303 330
304 if (refreshedResult._tag === "Left") {331 if (refreshedResult._tag === "Left") {
305 const details = formatCause(refreshedResult.left);332 const details = formatCause(refreshedResult.left);
apps/web/lib/external-origin.ts+35−0
@@ -0,0 +1,35 @@
1const normalizeOriginCandidate = (value: string | undefined): string | null => {
2 const trimmed = value?.trim();
3 if (!trimmed) {
4 return null;
5 }
6
7 const withProtocol =
8 trimmed.startsWith("http://") || trimmed.startsWith("https://")
9 ? trimmed
10 : `https://${trimmed}`;
11
12 try {
13 return new URL(withProtocol).origin;
14 } catch {
15 return null;
16 }
17};
18
19export const configuredExternalOriginFromEnv = (): string | null => {
20 const candidates = [
21 process.env.EXECUTOR_PUBLIC_ORIGIN,
22 process.env.NEXT_PUBLIC_APP_ORIGIN,
23 process.env.VERCEL_PROJECT_PRODUCTION_URL,
24 process.env.VERCEL_URL,
25 ];
26
27 for (const candidate of candidates) {
28 const origin = normalizeOriginCandidate(candidate);
29 if (origin) {
30 return origin;
31 }
32 }
33
34 return null;
35};
apps/web/lib/mcp/oauth-request.ts+3−25
@@ -1,3 +1,5 @@
1import { configuredExternalOriginFromEnv } from "../external-origin";
2
1const firstHeaderValue = (value: string | null): string => {3const firstHeaderValue = (value: string | null): string => {
2 if (!value) {4 if (!value) {
3 return "";5 return "";
@@ -6,32 +8,8 @@ const firstHeaderValue = (value: string | null): string => { · 2 unmodified lines
6 return value.split(",")[0]?.trim() ?? "";8 return value.split(",")[0]?.trim() ?? "";
7};9};
8 10
9const configuredExternalOrigin = (): string | null => {
10 const configured =
11 process.env.EXECUTOR_PUBLIC_ORIGIN
12 ?? process.env.NEXT_PUBLIC_APP_ORIGIN
13 ?? process.env.VERCEL_PROJECT_PRODUCTION_URL
14 ?? process.env.VERCEL_URL
15 ?? "";
16
17 if (!configured.trim()) {
18 return null;
19 }
20
21 try {
22 const normalized =
23 configured.startsWith("http://") || configured.startsWith("https://")
24 ? configured
25 : `https://${configured}`;
26
27 return new URL(normalized).origin;
28 } catch {
29 return null;
30 }
31};
32
33export const getExternalOrigin = (request: Request): string => {11export const getExternalOrigin = (request: Request): string => {
34 const configured = configuredExternalOrigin();12 const configured = configuredExternalOriginFromEnv();
35 if (configured) {13 if (configured) {
36 return configured;14 return configured;
37 }15 }
packages/persistence-sql/src/sql-internals.ts+0−10
@@ -80,16 +80,6 @@ const trim = (value: string | undefined): string | undefined => { · 79 unmodified lines
80};80};
81 81
82const resolvePostgresMaxConnections = (): number => {82const resolvePostgresMaxConnections = (): number => {
83 const configured = process.env.CONTROL_PLANE_POSTGRES_MAX_CONNECTIONS
84 ?? process.env.POSTGRES_MAX_CONNECTIONS;
85
86 if (configured) {
87 const parsed = Number.parseInt(configured, 10);
88 if (Number.isFinite(parsed) && parsed > 0) {
89 return parsed;
90 }
91 }
92
93 return process.env.NODE_ENV === "production" ? 1 : 10;83 return process.env.NODE_ENV === "production" ? 1 : 10;
94};84};
95 85