Skip to content

Commit 04dbdd6

Browse files
nrcrewsclaude
andcommitted
fix: admin show, network status, and content validation
Surfaced by a manual run of the CLI test plan against the latest release. - `admin agent show` now always prints an allowlist row — count + entries when populated, "(empty)" otherwise — instead of dropping the line for empty allowlists. - `network status` when not running uses the same key-value format as the running case (`running: false`, `network: …`) instead of a free-text "is not running" message. - POST /sessions/:id/messages and parseInitialMessage now reject empty string, empty array, and empty TextPart with 400 INVALID_CONTENT, matching the tightened ASP Content schema (asp@HEAD). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4461dfa commit 04dbdd6

4 files changed

Lines changed: 85 additions & 16 deletions

File tree

‎src/commands/admin.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,9 +362,13 @@ function renderLocalAgent(agent: AgentWire | AgentWithTokenWire): void {
362362
if ("token" in agent && typeof agent.token === "string") {
363363
out(` ${"token".padEnd(pad)} ${agent.token}`);
364364
}
365-
if (agent.allowlist.length > 0) {
366-
out(` ${"allowlist".padEnd(pad)} ${[...agent.allowlist].join(", ")}`);
367-
}
365+
out(
366+
` ${"allowlist".padEnd(pad)} ${
367+
agent.allowlist.length === 0
368+
? "(empty)"
369+
: `${agent.allowlist.length} — ${[...agent.allowlist].join(", ")}`
370+
}`,
371+
);
368372
if (agent.card_body !== null && agent.card_body.length > 0) {
369373
out(" card");
370374
for (const line of agent.card_body.split("\n")) {

‎src/commands/network.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ export function registerNetworkCommand(program: Command): void {
9696
if (opts.json) {
9797
console.log(renderJson(payload));
9898
} else {
99-
console.log(`Network "${config.network.name}" is not running.`);
99+
console.log(
100+
renderKeyValues(
101+
profileTitle("Local operator status", config),
102+
payload,
103+
),
104+
);
100105
}
101106
return;
102107
}

‎src/operator/routes/sessions.ts‎

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,7 @@ export function registerSessionRoutes(router: Router, ctx: SessionRoutesContext)
7878
router.add("POST", "/sessions/:id/messages", async (rc) => {
7979
const agent = requireAgent(rc.req, ctx.repo.agents);
8080
const body = await parseJsonBody(rc.req);
81-
const content = body.content;
82-
if (content === undefined || content === null) {
83-
throw new BadRequestError("content is required", "INVALID_CONTENT");
84-
}
81+
const content = requireNonEmptyContent(body.content, "content");
8582
const idempotencyKey = parseOptionalString(body.idempotency_key, "idempotency_key");
8683
const metadata = parseOptionalObject(body.metadata, "metadata");
8784
const result = ctx.service.sendMessage({
@@ -182,16 +179,53 @@ function parseInitialMessage(raw: unknown): { readonly content: unknown; readonl
182179
);
183180
}
184181
const o = raw as Record<string, unknown>;
185-
if (o.content === undefined) {
186-
throw new BadRequestError(
187-
"initial_message.content is required",
188-
"INVALID_REQUEST",
189-
);
190-
}
182+
const content = requireNonEmptyContent(o.content, "initial_message.content");
191183
const metadata = parseOptionalObject(o.metadata, "initial_message.metadata");
192184
return metadata === undefined
193-
? { content: o.content }
194-
: { content: o.content, metadata };
185+
? { content }
186+
: { content, metadata };
187+
}
188+
189+
function requireNonEmptyContent(raw: unknown, field: string): unknown {
190+
// Mirrors the ASP schema (common.json#/$defs/Content): empty payloads
191+
// are rejected. String form `minLength: 1`, array form `minItems: 1`,
192+
// and `TextPart.text` is itself `minLength: 1`.
193+
if (raw === undefined || raw === null) {
194+
throw new BadRequestError(`${field} is required`, "INVALID_CONTENT");
195+
}
196+
if (typeof raw === "string") {
197+
if (raw.length === 0) {
198+
throw new BadRequestError(`${field} must not be empty`, "INVALID_CONTENT");
199+
}
200+
return raw;
201+
}
202+
if (Array.isArray(raw)) {
203+
if (raw.length === 0) {
204+
throw new BadRequestError(
205+
`${field} must contain at least one part`,
206+
"INVALID_CONTENT",
207+
);
208+
}
209+
for (const part of raw) {
210+
if (
211+
typeof part === "object" &&
212+
part !== null &&
213+
(part as { type?: unknown }).type === "text" &&
214+
typeof (part as { text?: unknown }).text === "string" &&
215+
((part as { text: string }).text.length === 0)
216+
) {
217+
throw new BadRequestError(
218+
`${field} text part must not be empty`,
219+
"INVALID_CONTENT",
220+
);
221+
}
222+
}
223+
return raw;
224+
}
225+
throw new BadRequestError(
226+
`${field} must be a string or an array of parts`,
227+
"INVALID_CONTENT",
228+
);
195229
}
196230

197231
function parseAfterSequence(url: URL): number {

‎tests/operator-sessions.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,32 @@ describe("operator sessions — join + send_message", () => {
447447
assert.equal(r2.message_id, r1.message_id);
448448
assert.equal(r2.sequence, r1.sequence);
449449
});
450+
451+
it("rejects empty content with 400 INVALID_CONTENT", async () => {
452+
await adminRegister(h, "@alice.bot");
453+
454+
const create = await fetch(`${h.baseUrl}/sessions`, {
455+
method: "POST",
456+
headers: agentHeaders(h, "@alice.bot"),
457+
body: JSON.stringify({ invite: [] }),
458+
});
459+
const { session_id } = (await create.json()) as { session_id: string };
460+
461+
const cases: { name: string; body: unknown }[] = [
462+
{ name: "empty string", body: { content: "" } },
463+
{ name: "empty array", body: { content: [] } },
464+
];
465+
for (const { name, body } of cases) {
466+
const res = await fetch(`${h.baseUrl}/sessions/${session_id}/messages`, {
467+
method: "POST",
468+
headers: agentHeaders(h, "@alice.bot"),
469+
body: JSON.stringify(body),
470+
});
471+
assert.equal(res.status, 400, `${name}: expected 400`);
472+
const err = (await res.json()) as { error?: { code?: string } };
473+
assert.equal(err.error?.code, "INVALID_CONTENT", `${name}: error code`);
474+
}
475+
});
450476
});
451477

452478
describe("operator sessions — leave + end + history", () => {

0 commit comments

Comments
 (0)