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
14 changes: 12 additions & 2 deletions apps/hub/src/auth-email.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createTransport } from "nodemailer";
import { getLogger } from "@intx/log";
import {
verificationEmail,
invitationEmail,
Expand All @@ -7,6 +8,9 @@ import {
} from "./config/auth-email.ts";
import type { ProductConfig } from "./config/index.ts";
import type { Environment } from "./environment.ts";
import { errorCode, requestErrorMetadata } from "./request-error.ts";

const log = getLogger(["corbits-builder", "auth-email"]);

export function createAuthEmail(
environment: Environment,
Expand Down Expand Up @@ -39,8 +43,14 @@ export function createAuthEmail(
) => {
try {
await transport.sendMail({ to, ...message });
} catch {
// Transport errors can contain mailbox addresses, URL credentials or tokens.
} catch (error) {
// Transport errors can contain mailbox addresses, URL credentials or tokens, so only their
// code is kept.
log.warn("Sending an email failed: {failure}", {
failure,
...errorCode(error),
...requestErrorMetadata(error),
});
throw new Error(failure);
}
};
Expand Down
8 changes: 6 additions & 2 deletions apps/hub/src/auth/coordinator-credential-refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { MiddlewareHandler } from "hono";
import type { TenantEnv } from "@intx/hub-api";
import { getLogger } from "@intx/log";
import { projectCoordinator } from "../db/schema.ts";
import { errorCode, requestErrorMetadata } from "../request-error.ts";

const log = getLogger(["corbits-builder", "coordinator-credentials"]);

Expand Down Expand Up @@ -58,9 +59,12 @@ export function createCoordinatorCredentialRefresh(options: {
),
),
);
} catch {
} catch (error) {
// The credential is already stored; the sidecar picks it up when it reconnects.
log.warn("Project workflow credential refresh unavailable");
log.warn("Project workflow credential refresh unavailable", {
...errorCode(error),
...requestErrorMetadata(error),
});
}
};
}
16 changes: 12 additions & 4 deletions apps/hub/src/builds/app-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery";
import { hexDecode } from "@intx/types/hex";
import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities";
import { INPUTS_DIRECTORY } from "../config/inputs.ts";
import { requestErrorMetadata } from "../request-error.ts";
import { errorCode, requestErrorMetadata } from "../request-error.ts";
import {
excludedFile as excludedSnapshotFile,
linkOrGone,
Expand Down Expand Up @@ -572,9 +572,17 @@ export function createAppRepository(options: {

async function push(signal: AbortSignal) {
const local = await git.resolveRef({ fs, gitdir, ref: MAIN });
const pushed = await git
.resolveRef({ fs, gitdir, ref: PUSHED })
.catch(() => undefined);
let pushed: string | undefined;
try {
pushed = await git.resolveRef({ fs, gitdir, ref: PUSHED });
} catch (error) {
// Pushing again is harmless and rewrites the ref; a broken repository fails the push itself.
if (!(error instanceof Errors.NotFoundError))
log.warn("Reading the last pushed version failed", {
...errorCode(error),
...requestErrorMetadata(error),
});
}
if (local === pushed) return;
const { url, secret } = await pushAccess(signal);
try {
Expand Down
37 changes: 26 additions & 11 deletions apps/hub/src/builds/posix-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { type } from "arktype";
import { getLogger } from "@intx/log";
import { hasCode } from "@intx/types/has-code";
import type { ToolCall, ToolResult } from "@intx/types/runtime";
import { requestErrorMetadata } from "../request-error.ts";
import { errorCode, requestErrorMetadata } from "../request-error.ts";
import type { buildPackageLayout } from "./package-layout.ts";
import { BROWSER_ARGS } from "./browser.ts";
import { CODER_REGISTRY_HOSTS } from "../config/packages.ts";
Expand Down Expand Up @@ -993,6 +993,30 @@ export async function createPosixBuildExecutor(
browserStarting ??= trackBrowser();
return browserStarting;
};
const reloadPreview = async () => {
const page = devApp?.page;
if (page?.exitCode !== null) return;
try {
page.stdin.write("reload\n");
await page.stdin.flush();
} catch (error) {
log.warn("Reloading the preview after a write failed", {
...errorCode(error),
...requestErrorMetadata(error),
});
}
};
const runAfter = async (
previous: Promise<unknown>,
call: ToolCall,
signal: AbortSignal,
) => {
await previous;
const result = await execute(call, signal);
if (call.name === "write_file" || call.name === "edit_file")
void reloadPreview();
return result;
};
return {
policy,
version: SANDBOX_RUNTIME_VERSION,
Expand All @@ -1004,17 +1028,8 @@ export async function createPosixBuildExecutor(
throw new BuildExecutionError("host_gate_invocation_refused");
if (trustedGate && call.id !== trustedGate.nonce)
throw new BuildExecutionError("concurrent_build_tool_refused");
const turn = queue.then(() => execute(call, signal));
const turn = runAfter(queue, call, signal);
queue = turn.catch(() => undefined);
if (call.name === "write_file" || call.name === "edit_file")
void turn
.then(() => {
const page = devApp?.page;
if (page?.exitCode !== null) return;
page.stdin.write("reload\n");
return page.stdin.flush();
})
.catch(() => undefined);
return turn;
},
/**
Expand Down
21 changes: 16 additions & 5 deletions apps/hub/src/builds/sidecar-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import {
type AppRepository,
} from "./app-repository.ts";
import { getLogger } from "@intx/log";
import { requestErrorMetadata } from "../request-error.ts";
import { hasCode } from "@intx/types/has-code";
import { errorCode, requestErrorMetadata } from "../request-error.ts";
import {
PACKAGE_INSTALL_TOOL,
WorkspacePath,
Expand Down Expand Up @@ -428,10 +429,20 @@ export function createSidecarCodingTools(policy: {
typeof path === "string" &&
typeof content === "string"
) {
const existing = await readFile(
resolve(workspace, path),
"utf8",
).catch(() => undefined);
let existing: string | undefined;
try {
existing = await readFile(resolve(workspace, path), "utf8");
} catch (error) {
// The write still runs and reports its own failure, which the model can act on.
if (
!hasCode(error) ||
!["ENOENT", "EISDIR", "ENOTDIR"].includes(error.code)
)
log.warn("Reading the file a write replaces failed", {
...errorCode(error),
...requestErrorMetadata(error),
});
}
if (existing === content)
return {
callId: call.id,
Expand Down
32 changes: 32 additions & 0 deletions apps/hub/src/migrate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect, test } from "bun:test";
import { fileURLToPath } from "node:url";

test("a failed migration prints its error code and never the database URL", async () => {
const child = Bun.spawn(
[
process.execPath,
"--conditions=intx-src",
fileURLToPath(new URL("./migrate.ts", import.meta.url)),
],
{
// Nothing listens on port 1, so the connection is refused.
env: {
PATH: process.env["PATH"],
DATABASE_URL: "postgres://builder:db-password@127.0.0.1:1/builder",
},
stdout: "pipe",
stderr: "pipe",
},
);
const [stdout, stderr, exitCode] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited,
]);
expect(exitCode).toBe(1);
expect(JSON.parse(stderr)).toEqual({
migrationsApplied: false,
errorCode: "ECONNREFUSED",
});
expect(stdout + stderr).not.toContain("db-password");
});
8 changes: 6 additions & 2 deletions apps/hub/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type Environment,
} from "./environment.ts";
import { migrateProductDatabase } from "./db/migrate.ts";
import { errorCode } from "./request-error.ts";

const migrationsFolder = fileURLToPath(
new URL("../migrations/", import.meta.resolve("@intx/db")),
Expand Down Expand Up @@ -39,8 +40,11 @@ if (import.meta.main) {
try {
await migrateDatabase();
console.log(JSON.stringify({ migrationsApplied: true }));
} catch {
console.error(JSON.stringify({ migrationsApplied: false }));
} catch (error) {
// The message can carry the database URL; the code, such as a SQLSTATE, cannot.
console.error(
JSON.stringify({ migrationsApplied: false, ...errorCode(error) }),
);
process.exitCode = 1;
}
}
15 changes: 10 additions & 5 deletions apps/hub/src/provisioning/workflow-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
defaultDockerLimits,
} from "./docker.ts";
import { positiveInteger } from "../environment.ts";
import { errorCode, requestErrorMetadata } from "../request-error.ts";
import { localSidecarDataRoot } from "../sidecar/data-root.ts";
import { createHostCapacity } from "../sidecar/host-capacity.ts";
import {
Expand Down Expand Up @@ -300,12 +301,16 @@ export async function createWorkflowServices(options: {
);
return task;
};
const report = (run: () => Promise<unknown>) =>
track(run)
.then(() => undefined)
.catch(() => {
log.error("Native workflow connection reconciliation failed");
const report = async (run: () => Promise<unknown>) => {
try {
await track(run);
} catch (error) {
log.error("Native workflow connection reconciliation failed", {
...errorCode(error),
...requestErrorMetadata(error),
});
}
};
const unsubscribers = [
router.events.on("sidecar.disconnect", ({ allocated }) =>
report(async () => {
Expand Down
15 changes: 12 additions & 3 deletions apps/hub/src/request-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ const repositoryRoot = fileURLToPath(new URL("../../../", import.meta.url));

// A thrown error's code, such as ENOENT, EAUTH, a Postgres SQLSTATE or isomorphic-git's
// NotFoundError, names what failed without the message's paths; anything else it carries is left out.
export const errorCode = (error: unknown) =>
hasCode(error) && /^[A-Za-z0-9][A-Za-z0-9_]{0,63}$/.test(error.code)
? { errorCode: error.code }
export const errorCode = (error: unknown) => {
const code = codeOf(error);
return code !== undefined && /^[A-Za-z0-9][A-Za-z0-9_]{0,63}$/.test(code)
? { errorCode: code }
: {};
};

// A wrapper such as Drizzle's query error carries no code of its own, only the driver's as its cause.
function codeOf(error: unknown) {
if (hasCode(error)) return error.code;
Comment thread
TheGreatAxios marked this conversation as resolved.
Resolved
if (error instanceof Error && hasCode(error.cause)) return error.cause.code;
Comment thread
TheGreatAxios marked this conversation as resolved.
Resolved
return undefined;
}

// Never emit error-supplied filenames, messages, causes, or custom names.
export function requestErrorMetadata(error: unknown) {
Expand Down