pi-roundtable-sandbox 0.7.13 → 0.7.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.14
4
+
5
+ - A model request the broker refuses (an unknown role, a native tool, remote media, a remote schema) is answered with a 400 in the Anthropic API's error shape (`invalid_request_error`) instead of a 502, and logged as `sandbox upstream call failed`. Claude Code 2.1.284 (agent SDK 0.3.284) sends a mid-conversation `role: "system"` message when MCP tools arrive late; the broker refused it with a 502, which Claude Code retried about ten times over several minutes before the turn failed. The refusal of a role now reads `messages.N: Unexpected role "system": the input message role must be "user" or "assistant"`, which Claude Code recognizes: it resends without system messages for the rest of that conversation. The broker still never forwards a system message.
6
+
3
7
  ## 0.7.13
4
8
 
5
9
  - `precheckScriptRunner` forwards only the tools approved with the script (pi-roundtable 0.7.13's `PrecheckScriptContext.tools`) among those `grant` allows, and takes `toolName(server, tool)`, the name the host's hold rules know a tool by (default the tool's own name).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable-sandbox",
3
- "version": "0.7.13",
3
+ "version": "0.7.14",
4
4
  "description": "Sealed guest channels and an allow-listed credential broker for pi-roundtable",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@
52
52
  "@biomejs/biome": "2.5.15",
53
53
  "@types/bun": "1.4.2",
54
54
  "discord.js": "14.27.0",
55
- "pi-roundtable": "0.7.13",
55
+ "pi-roundtable": "0.7.14",
56
56
  "typebox": "1.3.34",
57
57
  "typescript": "7.0.2"
58
58
  }
package/src/pi-broker.ts CHANGED
@@ -616,13 +616,29 @@ export class PiSandboxBroker {
616
616
  if (value && value.length <= 256) headers.set(name, value);
617
617
  }
618
618
  } else {
619
- body = piModelInput(
620
- input,
621
- this.#options.model,
622
- url.pathname.endsWith("/count_tokens"),
623
- this.#options.maxOutputTokens ?? 128_000,
624
- turn.context.thinking,
625
- );
619
+ try {
620
+ body = piModelInput(
621
+ input,
622
+ this.#options.model,
623
+ url.pathname.endsWith("/count_tokens"),
624
+ this.#options.maxOutputTokens ?? 128_000,
625
+ turn.context.thinking,
626
+ );
627
+ } catch (error) {
628
+ // The request itself is refused: a 400 the client does not retry unchanged, in the
629
+ // API's error shape so it can resend without what was refused. A 502 would be retried.
630
+ failed(secret, { status: 400, error });
631
+ return Response.json(
632
+ {
633
+ type: "error",
634
+ error: {
635
+ type: "invalid_request_error",
636
+ message: error instanceof Error ? error.message : String(error),
637
+ },
638
+ },
639
+ { status: 400 },
640
+ );
641
+ }
626
642
  target = `${this.#options.upstream ?? "https://api.anthropic.com"}${url.pathname.slice("/anthropic".length)}${url.search}`;
627
643
  secret = await this.#options.oauthToken();
628
644
  for (const name of HEADERS) {
@@ -106,12 +106,14 @@ export function piModelInput(
106
106
  throw new Error("Invalid messages");
107
107
  const body: Record<string, unknown> = {
108
108
  model,
109
- messages: input.messages.map((message: unknown) => {
110
- if (
111
- !isRecord(message) ||
112
- !["user", "assistant"].includes(String(message.role))
113
- )
114
- throw new Error("Invalid role");
109
+ messages: input.messages.map((message: unknown, index) => {
110
+ if (!isRecord(message)) throw new Error("Invalid message");
111
+ // Worded as the Anthropic API words it, so a client that sends a mid-conversation
112
+ // `role: "system"` message (Claude Code does) recognizes the refusal and resends without it.
113
+ if (!["user", "assistant"].includes(String(message.role)))
114
+ throw new Error(
115
+ `messages.${index}: Unexpected role ${JSON.stringify(String(message.role))}: the input message role must be "user" or "assistant"`,
116
+ );
115
117
  return { role: message.role, content: blocks(message.content) };
116
118
  }),
117
119
  };