Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions apps/cli/src/daemon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@ describe("canAutoStartLocalDaemonForHost", () => {
});

describe("isExecutorServerReachable", () => {
it.effect("checks the v1.5 API surface instead of the removed scope endpoint", () =>
it.effect("probes the unauthenticated /api/health endpoint without forwarding a credential", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.tryPromise(
() =>
new Promise<{ server: Server; port: number }>((resolve, reject) => {
const server = createServer((request, response) => {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
if (url.pathname === "/api/integrations") {
response.writeHead(200, { "content-type": "application/json" });
response.end("[]");
// The probe must NOT send Authorization, and must hit /api/health.
if (url.pathname === "/api/health" && !request.headers.authorization) {
response.writeHead(200, { "content-type": "text/plain" });
response.end("ok");
return;
}
response.writeHead(404);
Expand All @@ -53,12 +54,6 @@ describe("isExecutorServerReachable", () => {
),
);

const legacyScopeStatus = yield* Effect.tryPromise(() =>
fetch(`http://127.0.0.1:${server.port}/api/scope`),
).pipe(Effect.map((response) => response.status));

expect(legacyScopeStatus).toBe(404);

const reachable = yield* isExecutorServerReachable({
baseUrl: `http://127.0.0.1:${server.port}`,
});
Expand Down
10 changes: 4 additions & 6 deletions apps/cli/src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export interface DaemonSpawnSpec {

export interface ExecutorServerReachabilityInput {
readonly baseUrl: string;
readonly authorization?: string;
}

type ProbeServer = ReturnType<typeof createServer> & {
Expand Down Expand Up @@ -69,11 +68,10 @@ export const isExecutorServerReachable = (
input: ExecutorServerReachabilityInput,
): Effect.Effect<boolean> =>
Effect.tryPromise(async () => {
const url = new URL("/api/integrations", input.baseUrl);
const response = await fetch(url, {
...(input.authorization ? { headers: { authorization: input.authorization } } : {}),
signal: AbortSignal.timeout(2000),
});
// The unauthenticated liveness probe — never forwards a credential, so a
// misconfigured base URL can't leak the bearer token to a third-party host.
const url = new URL("/api/health", input.baseUrl);
const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
await response.body?.cancel();
return response.ok;
}).pipe(Effect.catchCause(() => Effect.succeed(false)));
Expand Down
13 changes: 9 additions & 4 deletions apps/cli/src/local-server-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,15 @@ export const writeLocalServerManifest = (
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
yield* fs.makeDirectory(serverControlDir(path), { recursive: true });
yield* fs.writeFileString(
localServerManifestPath(path),
serializeExecutorLocalServerManifest(manifest),
);
const manifestPath = localServerManifestPath(path);
// The manifest embeds the bearer token; create it owner-only so there's no
// window where it exists world-readable (mode applies only on create). The
// chmod after covers overwriting a pre-existing world-readable file, where
// the create mode is ignored.
yield* fs.writeFileString(manifestPath, serializeExecutorLocalServerManifest(manifest), {
mode: 0o600,
});
yield* fs.chmod(manifestPath, 0o600).pipe(Effect.ignore);
});

export const removeLocalServerManifestIfOwnedBy = (input: {
Expand Down
Loading
Loading