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
7 changes: 3 additions & 4 deletions src/handlers/config/config.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import { createRootHandler } from "../index";
import { createSilentLogger, TestCoreClient, testIO } from "../../testing";
import { DefaultGlobalConfigAccessor } from "../../globalConfig";
import { InputValidationError } from "../../errors";
import { FsReadWriteJson } from "../../io";

describe("config", () => {
Expand Down Expand Up @@ -94,13 +95,11 @@ describe("config", () => {
});

test("throws on invalid key", async () => {
// TODO: swap to validation error.
await expect(run(["nonexistent.key"])).rejects.toThrow(TypeError);
await expect(run(["nonexistent.key"])).rejects.toThrow(InputValidationError);
});

test("throws on invalid value for key", async () => {
// TODO: swap to validation error
await expect(run(["telemetry.enabled", "banana"])).rejects.toThrow(TypeError);
await expect(run(["telemetry.enabled", "banana"])).rejects.toThrow(InputValidationError);
});

test("coerces values based on schema", async () => {
Expand Down
16 changes: 7 additions & 9 deletions src/handlers/config/handler.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import z from "zod";
import { createHandler, argument, GlobalConfigAccessorKey } from "../../router";
import { JsonRendererKey } from "../../tui";
import { InputValidationError } from "../../errors";
import { DEFAULT_GLOBAL_CONFIG, type GlobalConfig } from "../../globalConfig";

/*
Expand Down Expand Up @@ -63,16 +64,14 @@ function coerceValue(current: unknown, raw: string, path: string): unknown {
const normalized = raw.trim().toLowerCase();
if (normalized === "true") return true;
if (normalized === "false") return false;
// TODO: mark as validation error.
throw new TypeError(`Cannot coerce "${raw}" to boolean at "${path}"`);
throw new InputValidationError(`Cannot coerce "${raw}" to boolean at "${path}"`);
}

case "number": {
const trimmed = raw.trim();
const n = Number(trimmed);
if (trimmed === "" || Number.isNaN(n)) {
// TODO: mark as validation error.
throw new TypeError(`Cannot coerce "${raw}" to number at "${path}"`);
throw new InputValidationError(`Cannot coerce "${raw}" to number at "${path}"`);
}
return n;
}
Expand All @@ -81,15 +80,14 @@ function coerceValue(current: unknown, raw: string, path: string): unknown {
try {
return JSON.parse(raw);
} catch (e) {
// TODO: mark as validation error.

throw new TypeError(`Cannot coerce "${raw}" to object at "${path}"`, { cause: e });
throw new InputValidationError(`Cannot coerce "${raw}" to object at "${path}"`, {
cause: e,
});
}
}

default:
// TODO: mark as validation error.
throw new TypeError(`Unsupported target type "${typeof current}" at "${path}"`);
throw new InputValidationError(`Unsupported target type "${typeof current}" at "${path}"`);
}
}
/** Type guard that narrows `value` to a plain object record. */
Expand Down
3 changes: 2 additions & 1 deletion src/handlers/harness/create/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { createHandler, flag } from "../../../router";
import type { Core } from "../../types.tsx";
import { coreOptsFromCtx, parseJsonFlag } from "../../utils.tsx";
import { JsonRendererKey } from "../../../tui";
import { InputValidationError } from "../../../errors";
import { parameterHelp } from "../parameterHelp.tsx";

export const createCreateHarnessHandler = (core: Core) =>
Expand Down Expand Up @@ -93,7 +94,7 @@ export const createCreateHarnessHandler = (core: Core) =>
// Required at runtime but declared optional so that a bare
// `harness create` falls through to the TUI middleware instead.
if (!flags["name"]) {
throw new TypeError("required option '--name <name>' not specified");
throw new InputValidationError("required option '--name <name>' not specified");
}

const response = await core.harness.createHarness(
Expand Down
3 changes: 2 additions & 1 deletion src/handlers/harness/delete/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../router";
import type { Core } from "../../types.tsx";
import { coreOptsFromCtx } from "../../utils.tsx";
import { JsonRendererKey } from "../../../tui";
import { InputValidationError } from "../../../errors";

export const createDeleteHarnessHandler = (core: Core) =>
createHandler({
Expand All @@ -21,7 +22,7 @@ export const createDeleteHarnessHandler = (core: Core) =>
// Required at runtime but declared optional so that a bare
// `harness delete` falls through to the TUI middleware instead.
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}

const response = await core.harness.deleteHarness(
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/harness/endpoint/create/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
import type { Core } from "../../../types.tsx";
import { coreOptsFromCtx, parseJsonFlag } from "../../../utils.tsx";
import { JsonRendererKey } from "../../../../tui";
import { InputValidationError } from "../../../../errors";

export const createCreateEndpointHandler = (core: Core) =>
createHandler({
Expand All @@ -23,10 +24,10 @@ export const createCreateEndpointHandler = (core: Core) =>
// Required at runtime but declared optional so that a bare
// `harness endpoint create` falls through to the TUI middleware instead.
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}
if (!flags["name"]) {
throw new TypeError("required option '--name <name>' not specified");
throw new InputValidationError("required option '--name <name>' not specified");
}

const response = await core.harness.createHarnessEndpoint(
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/harness/endpoint/delete/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
import type { Core } from "../../../types.tsx";
import { coreOptsFromCtx } from "../../../utils.tsx";
import { JsonRendererKey } from "../../../../tui";
import { InputValidationError } from "../../../../errors";

export const createDeleteEndpointHandler = (core: Core) =>
createHandler({
Expand All @@ -17,10 +18,10 @@ export const createDeleteEndpointHandler = (core: Core) =>
// Required at runtime but declared optional so that a bare
// `harness endpoint delete` falls through to the TUI middleware instead.
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}
if (!flags["qualifier"]) {
throw new TypeError("required option '--qualifier <qualifier>' not specified");
throw new InputValidationError("required option '--qualifier <qualifier>' not specified");
}

const response = await core.harness.deleteHarnessEndpoint(
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/harness/endpoint/get/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
import type { Core } from "../../../types.tsx";
import { coreOptsFromCtx } from "../../../utils.tsx";
import { JsonRendererKey } from "../../../../tui";
import { InputValidationError } from "../../../../errors";

export const createGetEndpointHandler = (core: Core) =>
createHandler({
Expand All @@ -14,10 +15,10 @@ export const createGetEndpointHandler = (core: Core) =>
],
handle: async (ctx, flags) => {
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}
if (!flags["qualifier"]) {
throw new TypeError("required option '--qualifier <qualifier>' not specified");
throw new InputValidationError("required option '--qualifier <qualifier>' not specified");
}

const endpoint = await core.harness.getHarnessEndpoint(
Expand Down
3 changes: 2 additions & 1 deletion src/handlers/harness/endpoint/list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
import type { Core } from "../../../types.tsx";
import { coreOptsFromCtx } from "../../../utils.tsx";
import { JsonRendererKey } from "../../../../tui";
import { InputValidationError } from "../../../../errors";

export const createListEndpointsHandler = (core: Core) =>
createHandler({
Expand All @@ -15,7 +16,7 @@ export const createListEndpointsHandler = (core: Core) =>
],
handle: async (ctx, flags) => {
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}

const endpoints = await core.harness.listHarnessEndpoints(
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/harness/endpoint/update/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
import type { Core } from "../../../types.tsx";
import { coreOptsFromCtx } from "../../../utils.tsx";
import { JsonRendererKey } from "../../../../tui";
import { InputValidationError } from "../../../../errors";

export const createUpdateEndpointHandler = (core: Core) =>
createHandler({
Expand All @@ -18,10 +19,10 @@ export const createUpdateEndpointHandler = (core: Core) =>
// Required at runtime but declared optional so that a bare
// `harness endpoint update` falls through to the TUI middleware instead.
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}
if (!flags["qualifier"]) {
throw new TypeError("required option '--qualifier <qualifier>' not specified");
throw new InputValidationError("required option '--qualifier <qualifier>' not specified");
}

const response = await core.harness.updateHarnessEndpoint(
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/harness/exec/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Core } from "../../types.tsx";
import { coreOptsFromCtx } from "../../utils.tsx";
import { JsonKey } from "../../keys.tsx";
import { JsonRendererKey, renderTuiAt } from "../../../tui";
import { InputValidationError } from "../../../errors";
import { applyExecEvent, finishExec, newExecItem } from "../invoke/transcript.tsx";

export const createExecHarnessHandler = (core: Core, io: AppIO) =>
Expand Down Expand Up @@ -34,15 +35,15 @@ export const createExecHarnessHandler = (core: Core, io: AppIO) =>
// Required at runtime but declared optional so that a bare `harness exec`
// falls through to the TUI middleware instead.
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}
// Without a command, open the interactive exec screen at this harness —
// resuming the given session and targeting the given qualifier when
// passed. The one-shot CLI run below needs --command (and is the only
// shape JSON mode supports).
if (!flags["command"]) {
if (ctx.require(JsonKey)) {
throw new TypeError("required option '--command <command>' not specified");
throw new InputValidationError("required option '--command <command>' not specified");
}
let path = `${ctx.require(PathKey)}/${flags["id"]}`;
if (flags["session-id"]) path += `/${flags["session-id"]}`;
Expand Down
3 changes: 2 additions & 1 deletion src/handlers/harness/get/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../router";
import type { Core } from "../../types.tsx";
import { coreOptsFromCtx } from "../../utils.tsx";
import { JsonRendererKey } from "../../../tui";
import { InputValidationError } from "../../../errors";

export const createGetHarnessHandler = (core: Core) =>
createHandler({
Expand All @@ -11,7 +12,7 @@ export const createGetHarnessHandler = (core: Core) =>
flags: [flag("id", "the ID of the harness", z.string().max(48).optional())],
handle: async (ctx, flags) => {
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}

const harness = await core.harness.getHarness(flags["id"], coreOptsFromCtx(ctx));
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/harness/invoke/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Core } from "../../types.tsx";
import { coreOptsFromCtx } from "../../utils.tsx";
import { JsonKey } from "../../keys.tsx";
import { JsonRendererKey, renderTuiAt } from "../../../tui";
import { InputValidationError } from "../../../errors";
import {
applyEvent,
finishTurn,
Expand Down Expand Up @@ -35,15 +36,15 @@ export const createInvokeHarnessHandler = (core: Core, io: AppIO) =>
// These are required at runtime but declared optional so that a bare
// `harness invoke` falls through to the TUI middleware instead.
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}
// Without a prompt, open the interactive chat at this harness — resuming
// the given session and targeting the given qualifier when passed. The
// one-shot CLI transcript below needs --prompt (and is the only shape
// JSON mode supports).
if (!flags["prompt"]) {
if (ctx.require(JsonKey)) {
throw new TypeError("required option '--prompt <text>' not specified");
throw new InputValidationError("required option '--prompt <text>' not specified");
}
let path = `${ctx.require(PathKey)}/${flags["id"]}`;
if (flags["session-id"]) path += `/${flags["session-id"]}`;
Expand Down
3 changes: 2 additions & 1 deletion src/handlers/harness/update/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { createHandler, flag } from "../../../router";
import type { Core } from "../../types.tsx";
import { coreOptsFromCtx, parseJsonFlag } from "../../utils.tsx";
import { JsonRendererKey } from "../../../tui";
import { InputValidationError } from "../../../errors";
import { parameterHelp } from "../parameterHelp.tsx";

// updated wraps a PATCH-semantics field: `--<flag> <json>` replaces the value,
Expand Down Expand Up @@ -110,7 +111,7 @@ export const createUpdateHarnessHandler = (core: Core) =>
// Required at runtime but declared optional so that a bare
// `harness update` falls through to the TUI middleware instead.
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}

const response = await core.harness.updateHarness(
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/harness/version/get/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
import type { Core } from "../../../types.tsx";
import { coreOptsFromCtx } from "../../../utils.tsx";
import { JsonRendererKey } from "../../../../tui";
import { InputValidationError } from "../../../../errors";

export const createGetVersionHandler = (core: Core) =>
createHandler({
Expand All @@ -14,10 +15,10 @@ export const createGetVersionHandler = (core: Core) =>
],
handle: async (ctx, flags) => {
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}
if (!flags["version"]) {
throw new TypeError("required option '--version <version>' not specified");
throw new InputValidationError("required option '--version <version>' not specified");
}

const harness = await core.harness.getHarnessVersion(
Expand Down
3 changes: 2 additions & 1 deletion src/handlers/harness/version/list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHandler, flag } from "../../../../router";
import type { Core } from "../../../types.tsx";
import { coreOptsFromCtx } from "../../../utils.tsx";
import { JsonRendererKey } from "../../../../tui";
import { InputValidationError } from "../../../../errors";

export const createListVersionsHandler = (core: Core) =>
createHandler({
Expand All @@ -15,7 +16,7 @@ export const createListVersionsHandler = (core: Core) =>
],
handle: async (ctx, flags) => {
if (!flags["id"]) {
throw new TypeError("required option '--id <id>' not specified");
throw new InputValidationError("required option '--id <id>' not specified");
}

const versions = await core.harness.listHarnessVersions(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import z from "zod";
import { InputValidationError } from "../../../../errors";
import { createHandler, flag } from "../../../../router";
import { JsonRendererKey } from "../../../../tui";
import type { Core } from "../../../types";
Expand All @@ -25,17 +26,21 @@ export const createCreateApiKeyCredentialProviderHandler = (core: Core, io: AppI
],
handle: async (ctx, flags) => {
if (!flags.name) {
throw new TypeError("required option '--name <name>' not specified");
throw new InputValidationError("required option '--name <name>' not specified");
}

const hasApiKey = flags["api-key"] !== undefined;
const hasSecretRef = flags["api-key-secret-reference"] !== undefined;

if (hasApiKey && hasSecretRef) {
throw new TypeError("--api-key and --api-key-secret-reference are mutually exclusive");
throw new InputValidationError(
"--api-key and --api-key-secret-reference are mutually exclusive",
);
}

if (!hasApiKey && !hasSecretRef) {
throw new TypeError("either --api-key or --api-key-secret-reference is required");
throw new InputValidationError(
"either --api-key or --api-key-secret-reference is required",
);
}

const resolver = new SourceResolver({ stdin: io.stdin });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One input-validation path appears to be missing from this migration the SourceResolutionError in src/io/source.ts still extends TypeError. Missing file:// sources and invalid UTF-8 therefore continue to be classified as internal. Could it extend InputValidationError, with a regression test asserting source: "user"? I also get it if you think this is better as a separate PR. I think this may have been because I was working on the SourceResolver at the same time as you were creating the AgentCore Error.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import z from "zod";
import { InputValidationError } from "../../../../errors";
import { createHandler, flag } from "../../../../router";
import { JsonRendererKey } from "../../../../tui";
import type { Core } from "../../../types";
Expand All @@ -11,7 +12,7 @@ export const createDeleteApiKeyCredentialProviderHandler = (core: Core) =>
flags: [flag("name", "the name of the API key credential provider", z.string().optional())],
handle: async (ctx, flags) => {
if (!flags.name) {
throw new TypeError("required option '--name <name>' not specified");
throw new InputValidationError("required option '--name <name>' not specified");
}

ctx
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import z from "zod";
import { InputValidationError } from "../../../../errors";
import { createHandler, flag } from "../../../../router";
import { JsonRendererKey } from "../../../../tui";
import type { Core } from "../../../types";
Expand All @@ -11,7 +12,7 @@ export const createGetApiKeyCredentialProviderHandler = (core: Core) =>
flags: [flag("name", "the name of the API key credential provider", z.string().optional())],
handle: async (ctx, flags) => {
if (!flags.name) {
throw new TypeError("required option '--name <name>' not specified");
throw new InputValidationError("required option '--name <name>' not specified");
}

ctx
Expand Down
Loading
Loading