pi-roundtable-sandbox 0.7.19 → 0.8.0

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.8.0
6
+
7
+ - `apiKey` and `oauthToken` receive `{ channel, speaker }` (`SandboxCredentialScope`, a new export) before every model call, so a host can pick a key or a subscription per channel or speaker, such as `apiKey: ({ channel }) => keys.get(channel)`. A zero-argument function keeps working. A turn keeps the scope it started with while its requests are in flight, and a getter that returns `undefined` or an empty string fails the call: no other credential is used. The worker still sees only the placeholder key.
8
+
3
9
  ## 0.7.18
4
10
 
5
11
  - Preserve a reset requested during broker startup or an active turn for the following turn; a successful turn consumes only the reset generation it started with.
package/README.md CHANGED
@@ -10,7 +10,7 @@ Channel memory and recent conversation text persist in the channel's dedicated w
10
10
 
11
11
  ## Requirements
12
12
 
13
- - Bun 1.4.2 or later and pi-roundtable `>=0.7.0 <0.8.0`.
13
+ - Bun 1.4.2 or later and pi-roundtable `>=0.8.0 <0.9.0`.
14
14
  - A native Linux host with a local Docker daemon and a non-root service account permitted to run Docker.
15
15
  - The worker UID/GID match that account; root workers are refused.
16
16
  - A trusted OpenAI-compatible, non-streaming Chat Completions endpoint supporting function tools and `max_tokens`.
@@ -59,6 +59,8 @@ export const guestPlugins = [sandbox({
59
59
  Choose a model available to your endpoint and sign the host into its provider.
60
60
  By default, `context.apiKey(provider)` is read on every request, allowing login refresh without copying credentials into the worker.
61
61
  Alternatively supply `apiKey: () => process.env.MODEL_API_KEY` when the endpoint uses a separate key.
62
+ `apiKey` receives `{ channel, speaker }` (`SandboxCredentialScope`) before every model call, so a host can pick a different key per channel or speaker, for example `apiKey: ({ channel }) => keys.get(channel)`; a zero-argument function keeps working.
63
+ When the hook has no key it should return `undefined` (the call fails closed) rather than fall back to the host's own login.
62
64
  Keep secrets in the service's protected environment, not configuration source or the worker image.
63
65
  All directories must be dedicated to this package and owned by the service account.
64
66
  Existing roots and the state directory must have no group/other permissions; insecure permissions are refused at startup.
@@ -216,7 +218,7 @@ Other model transports are not implicitly proxied by this broker.
216
218
  | --- | --- |
217
219
  | `partyDir`, `image` | Dedicated private, host-owned directory and operator-built Pi image. |
218
220
  | `profiles` | Host allow-list mapping profile names to fixed Anthropic `model` and optional MCP server names. |
219
- | `oauthToken()` | Host-only credential getter called for each model request; supports refresh without container credentials. |
221
+ | `oauthToken({ channel, speaker })` | Host-only credential getter called before each model request with the bound turn's channel and speaker, so a host can use a different subscription per channel or speaker; supports refresh without container credentials. A zero-argument getter still works. Returning `undefined` or an empty string fails the call; no other credential is used. |
220
222
  | `memory.promptBlock(channel, id, name)` | Current admitted speaker's context, at most 100,000 characters; database implementations remain host adapters. |
221
223
  | `effort.judge(text, { level })` | Host-selected `low`, `medium`, `high`, or `xhigh`; the previous channel choice is retained for the next judgment. |
222
224
  | `tools.names`, `tools.call` | Explicit host tool allow-list and callback receiving fixed channel/profile/speaker plus cancellation signal. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable-sandbox",
3
- "version": "0.7.19",
3
+ "version": "0.8.0",
4
4
  "description": "Sealed guest channels and an allow-listed credential broker for pi-roundtable",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": ">=1.0.0 <2",
39
- "pi-roundtable": ">=0.7.0 <0.8.0",
39
+ "pi-roundtable": ">=0.8.0 <0.9.0",
40
40
  "typebox": ">=1.3.34 <2"
41
41
  },
42
42
  "peerDependenciesMeta": {
@@ -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.19",
55
+ "pi-roundtable": "0.8.0",
56
56
  "typebox": "1.3.34",
57
57
  "typescript": "7.0.2"
58
58
  }
package/src/broker.ts CHANGED
@@ -21,6 +21,11 @@ export interface HostToolContext {
21
21
  speaker: { id: string; name: string };
22
22
  signal: AbortSignal;
23
23
  }
24
+ /** Who a model call is made for; a credential hook can pick a token per channel or speaker. */
25
+ export interface SandboxCredentialScope {
26
+ channel: ChannelKey;
27
+ speaker: { id: string; name: string };
28
+ }
24
29
  export interface HostTool extends ToolSpec {
25
30
  run(
26
31
  input: Record<string, unknown>,
@@ -39,7 +44,10 @@ export interface BrokerOptions {
39
44
  model: string;
40
45
  /** Fixed complete URL, not a guest-selected base URL. */
41
46
  modelUrl: string;
42
- apiKey: () => Promise<string | undefined> | string | undefined;
47
+ /** Host-only, read before each model call; a zero-argument function still works. */
48
+ apiKey: (
49
+ scope: SandboxCredentialScope,
50
+ ) => Promise<string | undefined> | string | undefined;
43
51
  tools?: readonly HostTool[];
44
52
  mcp?: readonly McpServer[];
45
53
  /** Allow cleartext endpoints only for local testing. */
@@ -219,7 +227,11 @@ export class SandboxBroker {
219
227
  return new Response("text/function model payload required", {
220
228
  status: 400,
221
229
  });
222
- const key = await this.#options.apiKey();
230
+ const { channel, speaker } = this.#options.context;
231
+ const key = await this.#options.apiKey({
232
+ channel,
233
+ speaker: { id: speaker.id, name: speaker.name },
234
+ });
223
235
  if (!key) throw new Error("model credential unavailable");
224
236
  return await this.#forward(
225
237
  this.#options.modelUrl,
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ export {
19
19
  type HostToolContext,
20
20
  type McpServer,
21
21
  SandboxBroker,
22
+ type SandboxCredentialScope,
22
23
  } from "./broker.ts";
23
24
  export { SandboxChannelStore } from "./channel-store.ts";
24
25
  export {
package/src/pi-broker.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { ChannelKey, Logger } from "pi-roundtable";
2
2
  import { HARD_COMPACT_TOKENS, scrubDiagnostic } from "pi-roundtable/kit";
3
- import { type BrokerListener, listenBroker } from "./broker.ts";
3
+ import {
4
+ type BrokerListener,
5
+ listenBroker,
6
+ type SandboxCredentialScope,
7
+ } from "./broker.ts";
4
8
  import { piModelInput } from "./pi-model-input.ts";
5
9
  import {
6
10
  PI_MEDIA_LIMITS,
@@ -62,8 +66,10 @@ export interface PiCompactor {
62
66
  }
63
67
  export interface PiBrokerOptions {
64
68
  model: string;
65
- /** Host-only, read at each call. Never sent to the worker. */
66
- oauthToken: () => string | Promise<string>;
69
+ /** Host-only, read before each model call for its channel and speaker; a zero-argument function still works. Return `undefined` or an empty string when the scope has no token: the call fails rather than using another credential. Never sent to the worker. */
70
+ oauthToken: (
71
+ scope: SandboxCredentialScope,
72
+ ) => string | undefined | Promise<string | undefined>;
67
73
  tools?: {
68
74
  names: readonly string[];
69
75
  call(
@@ -640,7 +646,12 @@ export class PiSandboxBroker {
640
646
  );
641
647
  }
642
648
  target = `${this.#options.upstream ?? "https://api.anthropic.com"}${url.pathname.slice("/anthropic".length)}${url.search}`;
643
- secret = await this.#options.oauthToken();
649
+ const { channel, speaker } = turn.context;
650
+ secret =
651
+ (await this.#options.oauthToken({
652
+ channel,
653
+ speaker: { id: speaker.id, name: speaker.name },
654
+ })) ?? "";
644
655
  for (const name of HEADERS) {
645
656
  const value = request.headers.get(name);
646
657
  if (value && value.length <= 4096) headers.set(name, value);
package/src/plugin.ts CHANGED
@@ -15,6 +15,7 @@ export interface SandboxOptions
15
15
  initialChannels?: readonly ChannelKey[];
16
16
  /** Host login provider used for each model call, unless apiKey is supplied. */
17
17
  provider?: string;
18
+ /** Read before each model call with its channel and speaker; never falls back to the host login. */
18
19
  apiKey?: SandboxRuntimeOptions["apiKey"];
19
20
  }
20
21
  export interface SandboxService {