Summary
Four related gaps across packages/engine/src/api/server.ts. Bundled because they're all about the API's defenses against malformed input and inconsistent error responses; one PR can address them.
A. Non-finite limit guards
Some endpoints already use the safe Number.isFinite(rawLimit) ? rawLimit : default pattern (server.ts:329, 331, 550, 552), but two don't:
server.ts:203-216 — /api/files: limit = Math.min(parseInt(...), 1000) with no Number.isFinite guard. ?limit=abc produces NaN, binds NaN into LIMIT ?, which sqlite coerces to 0 (silently empty) or throws.
server.ts:735 — /api/batches: same shape.
B. Unhandled JSON parse failures
Several handlers do await c.req.json() with no .catch. Malformed JSON throws past the route, bypasses the per-route error shape, and falls into Hono's default error path with a generic 500. Inconsistent with server.ts:449, 677, 716 which already use .catch(() => ({})). Sites missing the catch: server.ts:336-352, 488, 522, 544, 563, 594, 629, 699-712.
C. PUT /api/settings / POST /api/roles / POST /api/rules accept unvalidated bodies
server.ts:425-430 — PUT settings writes body.settings verbatim into the catalog with no schema validation; a malformed payload corrupts throttleProfiles and propagates through onSettingsChanged to the scheduler.
server.ts:489, 503 — Role create/update bodies cast as CreateRoleInput/UpdateRoleInput with no runtime validation. A malformed body like drivePriority: "not-an-array" reaches SQL.
server.ts:522-526 — POST /api/rules has no try/catch; a rules.create() validation throw becomes a 500 instead of a 400 with the schema reason.
D. Internal-error leak + inconsistent error shape
server.ts:687-689, 497, 511 — (err as Error).message returned to the client for any thrown error. Leaks internal stack-bearing detail and conflates "user error" with "server error."
server.ts:587-591, 622-626, 667-672 — Non-DriveError handlers throw err, which surfaces as a default Hono 500 with no JSON body. Different shape for "operation failed" depending on the route.
Background
From the 2026-05-17 multi-agent full-repo review.
Acceptance criteria
A. Limit guards
B. JSON parse
C. Settings/roles/rules validation
D. Error shape consistency
Files affected (likely)
packages/engine/src/api/server.ts
packages/engine/src/api/server.test.ts
- Possibly
packages/shared/src/types.ts — validators for Settings, CreateRoleInput, UpdateRoleInput
packages/engine/package.json — if zod is chosen, add as dep (already a candidate in standards/dependency-discipline.md)
Suggested approach
Order: D (the central handler) → B (helper) → C (validators) → A (one-line fixes). Doing D first means C and B can be smaller.
Out of scope
- AbortSignal/timeout on requests (the UI's
api/client.ts lack of timeout is a separate, UI-scoped issue — excluded from this issue per instruction).
- Auth (loopback-only; not in scope per spec §11).
- Rate limiting (loopback-only).
References
- Code:
packages/engine/src/api/server.ts
- Spec §11 — API design
Summary
Four related gaps across
packages/engine/src/api/server.ts. Bundled because they're all about the API's defenses against malformed input and inconsistent error responses; one PR can address them.A. Non-finite limit guards
Some endpoints already use the safe
Number.isFinite(rawLimit) ? rawLimit : defaultpattern (server.ts:329, 331, 550, 552), but two don't:server.ts:203-216—/api/files:limit = Math.min(parseInt(...), 1000)with noNumber.isFiniteguard.?limit=abcproducesNaN, binds NaN intoLIMIT ?, which sqlite coerces to 0 (silently empty) or throws.server.ts:735—/api/batches: same shape.B. Unhandled JSON parse failures
Several handlers do
await c.req.json()with no.catch. Malformed JSON throws past the route, bypasses the per-route error shape, and falls into Hono's default error path with a generic 500. Inconsistent withserver.ts:449, 677, 716which already use.catch(() => ({})). Sites missing the catch:server.ts:336-352, 488, 522, 544, 563, 594, 629, 699-712.C. PUT /api/settings / POST /api/roles / POST /api/rules accept unvalidated bodies
server.ts:425-430— PUT settings writesbody.settingsverbatim into the catalog with no schema validation; a malformed payload corruptsthrottleProfilesand propagates throughonSettingsChangedto the scheduler.server.ts:489, 503— Role create/update bodies cast asCreateRoleInput/UpdateRoleInputwith no runtime validation. A malformed body likedrivePriority: "not-an-array"reaches SQL.server.ts:522-526— POST/api/ruleshas no try/catch; arules.create()validation throw becomes a 500 instead of a 400 with the schema reason.D. Internal-error leak + inconsistent error shape
server.ts:687-689, 497, 511—(err as Error).messagereturned to the client for any thrown error. Leaks internal stack-bearing detail and conflates "user error" with "server error."server.ts:587-591, 622-626, 667-672— Non-DriveError handlersthrow err, which surfaces as a default Hono 500 with no JSON body. Different shape for "operation failed" depending on the route.Background
From the 2026-05-17 multi-agent full-repo review.
Acceptance criteria
A. Limit guards
/api/filesand/api/batchesuse the sameNumber.isFinite(rawLimit) ? rawLimit : defaultpattern as the other endpoints./api/files?limit=abcreturns either a documented default or 400 — never NaN-bound SQL.B. JSON parse
parseJsonBody<T>(c): Promise<T | { error: 'invalid-json' }>helper that doesawait c.req.json().catch(() => null).nullparse result returns 400{ error: 'invalid-json' }.Content-Type: application/jsonbody that isn't valid JSON to one representative endpoint — assert 400.C. Settings/roles/rules validation
Settingsshape (zod or hand-written guard) before save. On failure: 400 with the path that failed./api/ruleswrapsrules.create()in try/catch and translatesRuleError/ValidationErrorto 400; falls through to generic 500 only for unknown errors.D. Error shape consistency
app.onError(...)at the framework level: known typed errors (Rule/Drive/Quarantine/Integrity/Catalog/Scan) → mapped statuses with a structured body{ code, error }; unknown errors → 500 with{ error: 'internal' }and the original logged at error level.(err as Error).message-to-client pattern at:497, :511, :687-689is replaced by the centralized handler.Error('internal secret detail')from one of the routes — assert response body does NOT contain the message verbatim.RuleError('INVALID_GLOB', ...)— assert response is 400 withcode: 'INVALID_GLOB'.Files affected (likely)
packages/engine/src/api/server.tspackages/engine/src/api/server.test.tspackages/shared/src/types.ts— validators forSettings,CreateRoleInput,UpdateRoleInputpackages/engine/package.json— if zod is chosen, add as dep (already a candidate in standards/dependency-discipline.md)Suggested approach
Order: D (the central handler) → B (helper) → C (validators) → A (one-line fixes). Doing D first means C and B can be smaller.
Out of scope
api/client.tslack of timeout is a separate, UI-scoped issue — excluded from this issue per instruction).References
packages/engine/src/api/server.ts