pi-roundtable 0.25.0 → 0.26.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/docs/plugins.md +129 -6
  3. package/package.json +1 -1
  4. package/src/cli/checks/agent-tools.ts +23 -0
  5. package/src/cli/doctor.ts +6 -0
  6. package/src/cli/principal.ts +9 -2
  7. package/src/core/agents/agent-prompt.ts +6 -1
  8. package/src/core/agents/agent-team.ts +2 -0
  9. package/src/core/agents/team-editing.ts +4 -0
  10. package/src/core/agents/team-options.ts +3 -1
  11. package/src/core/agents/team-selection.ts +6 -2
  12. package/src/core/builtin/agent-server.ts +1 -0
  13. package/src/core/builtin/channel-context.ts +21 -0
  14. package/src/core/builtin/discord.ts +4 -1
  15. package/src/core/config/config-schema.ts +22 -6
  16. package/src/core/config/config.ts +30 -2
  17. package/src/core/config/conversations.ts +21 -0
  18. package/src/core/config/profiles.ts +53 -0
  19. package/src/core/contract/channel-context.ts +106 -0
  20. package/src/core/contract/surface.ts +16 -0
  21. package/src/core/conversations/conversation-registry.ts +6 -0
  22. package/src/core/conversations/conversation-retention.ts +107 -0
  23. package/src/core/conversations/conversation-store.ts +6 -0
  24. package/src/core/conversations/conversations-plugin.ts +19 -3
  25. package/src/core/define-roundtable.ts +9 -2
  26. package/src/core/discord/channel-context.ts +47 -78
  27. package/src/core/discord/discord-surface.ts +16 -0
  28. package/src/core/events.ts +1 -0
  29. package/src/core/host-registration.ts +4 -0
  30. package/src/core/host.ts +3 -1
  31. package/src/core/identity/actor-facts.ts +7 -0
  32. package/src/core/identity/identity-plugin.ts +27 -5
  33. package/src/core/identity/identity-port.ts +62 -0
  34. package/src/core/identity/identity-schema.ts +2 -0
  35. package/src/core/identity/identity-service.ts +45 -52
  36. package/src/core/identity/identity-view.ts +1 -0
  37. package/src/core/identity/person-profiles.ts +275 -0
  38. package/src/core/identity/principal-tables.ts +10 -0
  39. package/src/core/identity/profile-choices.ts +45 -0
  40. package/src/core/identity/profile-memory.ts +65 -0
  41. package/src/core/identity/profile-picture.ts +117 -0
  42. package/src/core/identity/profiles-view.ts +20 -0
  43. package/src/core/identity/profiles.ts +82 -0
  44. package/src/core/modules/host-shell/shell-policy.ts +18 -3
  45. package/src/core/plugin.ts +40 -0
  46. package/src/core/registry/contributions.ts +8 -1
  47. package/src/core/runtime/chat-only.ts +32 -0
  48. package/src/core/runtime/conversation-sessions.ts +2 -1
  49. package/src/core/runtime/runtime-plugin.ts +1 -0
  50. package/src/core/runtime/runtime-types.ts +3 -1
  51. package/src/core/runtime/session-factory.ts +19 -30
  52. package/src/core/runtime/session-helpers.ts +27 -0
  53. package/src/core/services.ts +9 -0
  54. package/src/index.ts +18 -0
  55. package/src/kit/index.ts +1 -0
  56. package/src/kit/oidc.ts +16 -0
  57. package/src/testing.ts +1 -0
@@ -107,10 +107,30 @@ export interface EventHandlers {
107
107
  turnProgress?(event: TurnProgressEvent): Promise<void> | void;
108
108
  /** The team changed: an agent or group was created, edited, arranged, archived, or started over. */
109
109
  changed?(): Promise<void> | void;
110
+ /**
111
+ * Something a person did through a plugin's surface, reported by any plugin through
112
+ * `context.events.action`: for example a support inbox's "assign", "reply" or "close". It names
113
+ * the kind, who did it and whether it worked, never the content.
114
+ */
115
+ action?(event: ActionEvent): Promise<void> | void;
110
116
  /** The shutdown drain ended, before any service stops; `left` is the work it gave up on. */
111
117
  shutdown?(left: readonly string[]): Promise<void> | void;
112
118
  }
113
119
 
120
+ /** One thing a person did through a plugin's surface, as a handler hears of it. */
121
+ export interface ActionEvent {
122
+ /** What was done, in the reporting plugin's words, such as "assign" or "close". */
123
+ kind: string;
124
+ /** The `Speaker.id` of who did it. */
125
+ actor: string;
126
+ principalId: string;
127
+ /** The conversation or page it concerns, when there is one. */
128
+ channel?: ChannelKey | `page:${string}`;
129
+ /** Whether it worked. */
130
+ ok: boolean;
131
+ at: Date;
132
+ }
133
+
114
134
  /** What the core reports to the plugins' handlers. */
115
135
  export interface EventSink {
116
136
  turnStarted(turn: TurnEvent): void;
@@ -118,6 +138,8 @@ export interface EventSink {
118
138
  /** Optional, so a sink of your own written before it still fits. */
119
139
  turnProgress?(event: TurnProgressEvent): void;
120
140
  changed(): void;
141
+ /** Optional, as `turnProgress` is: a sink of your own written before it still fits. */
142
+ action?(event: ActionEvent): void;
121
143
  }
122
144
 
123
145
  /** What a prompt section is built for: one agent's turn. */
@@ -265,6 +287,12 @@ export interface LinkedSessions {
265
287
  agentSelection(): ToolSelection;
266
288
  }
267
289
 
290
+ /**
291
+ * What the agents' turns may use of the machine: "full" gives them the shell and file tools, and
292
+ * "chat-only" none of them, as `agentServer: { tools: "chat-only" }` configures.
293
+ */
294
+ export type AgentTools = "full" | "chat-only";
295
+
268
296
  /** The host's own locale and time zone, as its plugins read them. */
269
297
  export interface HostEnv {
270
298
  readonly locale: Locale;
@@ -284,6 +312,12 @@ export interface HostEnv {
284
312
  * agent wears the same ones.
285
313
  */
286
314
  readonly characterSkins: readonly Hex[];
315
+ /**
316
+ * Whether the agents' turns carry the shell and file tools. A plugin whose tools reach the host
317
+ * checks it before it offers them to agents; under "chat-only" the host refuses to start with a
318
+ * plugin that declares `hostAccess`.
319
+ */
320
+ readonly agentTools: AgentTools;
287
321
  }
288
322
 
289
323
  /** What the host gives every plugin. */
@@ -391,6 +425,12 @@ export interface RoundtablePlugin {
391
425
  * dropped plugin provides a service this one does not replace.
392
426
  */
393
427
  replaces?: readonly ServiceKey<unknown>[];
428
+ /**
429
+ * Marks a plugin whose tools reach the host's machine, such as one that runs shells or edits
430
+ * its files for the agents. A host that configures `agentServer: { tools: "chat-only" }`
431
+ * refuses to start with one registered, with a ConfigError that names the plugin.
432
+ */
433
+ hostAccess?: true;
394
434
  setup(context: PluginContext): Promise<Contribution> | Contribution;
395
435
  /**
396
436
  * Runs once every plugin is set up and linked, before any service starts; a failure stops the
@@ -25,6 +25,7 @@ import {
25
25
  type DirectChannelProvider,
26
26
  directChannelsPort,
27
27
  } from "../presence/direct-channels.ts";
28
+ import { refuseShellTools } from "../runtime/chat-only.ts";
28
29
  import {
29
30
  compileSessionPlan,
30
31
  type SessionTool,
@@ -451,8 +452,14 @@ export async function collectContributions(
451
452
  registry.holdRules.push(tool.hold);
452
453
  }
453
454
  }
454
- if (contribution.agentSelection)
455
+ if (contribution.agentSelection) {
456
+ if (context.env.agentTools === "chat-only")
457
+ refuseShellTools(
458
+ contribution.agentSelection().tools,
459
+ `plugin ${plugin.name}: agentSelection`,
460
+ );
455
461
  registry.agentSelections.push(contribution.agentSelection);
462
+ }
456
463
  if (events) registry.handlers.push({ plugin: plugin.name, events });
457
464
  registry.routes.push(...http);
458
465
  registry.holdRules.push(...holdRules);
@@ -0,0 +1,32 @@
1
+ import { ConfigError } from "../domain/errors.ts";
2
+ import { SHELL_TOOLS } from "../modules/host-shell/shell-policy.ts";
3
+ import type { RoundtablePlugin } from "../plugin.ts";
4
+
5
+ const CHAT_ONLY = 'agentServer: { tools: "chat-only" }';
6
+
7
+ /** The shell and file tools among these names. */
8
+ export function shellToolsIn(names: Iterable<string>): string[] {
9
+ const shell: readonly string[] = SHELL_TOOLS;
10
+ return [...new Set(names)].filter((name) => shell.includes(name));
11
+ }
12
+
13
+ /**
14
+ * Refuses names that carry the machine's shell and file tools: the one place a chat-only host
15
+ * checks, where a selection turns into active tools. `where` says whose selection it is.
16
+ */
17
+ export function refuseShellTools(names: Iterable<string>, where: string): void {
18
+ const found = shellToolsIn(names);
19
+ if (found.length > 0)
20
+ throw new ConfigError(
21
+ `${where} names ${found.join(", ")}, which ${CHAT_ONLY} withholds from every turn. Take the tool out of the selection, or remove tools: "chat-only" from agentServer to give the agents the shell.`,
22
+ );
23
+ }
24
+
25
+ /** Refuses, at start, a plugin whose tools reach the host: an agent of a chat-only host must not. */
26
+ export function refuseHostAccess(plugins: readonly RoundtablePlugin[]): void {
27
+ const reaching = plugins.find((plugin) => plugin.hostAccess === true);
28
+ if (reaching)
29
+ throw new ConfigError(
30
+ `plugin ${reaching.name} declares hostAccess: its tools reach the host's machine, and the configuration says ${CHAT_ONLY}. Remove the plugin, or remove tools: "chat-only" from agentServer.`,
31
+ );
32
+ }
@@ -102,7 +102,8 @@ export class ConversationSessions {
102
102
  this.#factory.link().holds,
103
103
  this.#options.owner,
104
104
  await this.#options.confirmations.load(key),
105
- agents
105
+ // A chat-only host's agents have no shell, so their gate judges no workspace.
106
+ agents && this.#options.agentTools !== "chat-only"
106
107
  ? {
107
108
  workspace: agents.workDir,
108
109
  ...(agents.scratchDir ? { scratchDir: agents.scratchDir } : {}),
@@ -199,6 +199,7 @@ export function runtimePlugin(
199
199
  },
200
200
  ...turnPolicy(options),
201
201
  sessions: context.sessions,
202
+ agentTools: context.env.agentTools,
202
203
  agentDir: options.agentDir,
203
204
  modelRuntime: options.modelRuntime,
204
205
  dataDir: options.dataDir,
@@ -16,7 +16,7 @@ import type { Logger } from "../log.ts";
16
16
  import type { EffortPolicy, ThinkingLevel, ThinkingPicker } from "../models.ts";
17
17
  import type { Autonomy } from "../modes/autonomy.ts";
18
18
  import type { ConversationModes } from "../modes/conversation-modes.ts";
19
- import type { LinkedSessions } from "../plugin.ts";
19
+ import type { AgentTools, LinkedSessions } from "../plugin.ts";
20
20
  import {
21
21
  planOrder,
22
22
  type SessionContext,
@@ -75,6 +75,8 @@ export interface PiAgentRuntimeOptions {
75
75
  sessions: () => LinkedSessions;
76
76
  /** The agent server's sessions: their shared workspace, model, and skills. */
77
77
  agents?: AgentSessions;
78
+ /** "chat-only": no turn carries the shell and file tools, and agent sessions have no workspace; default "full". */
79
+ agentTools?: AgentTools;
78
80
  /**
79
81
  * The prompts of a channel for an interactive turn's scope, made from its speaker and its
80
82
  * conversation's visibility; undefined for a channel that cannot show them. Without it every
@@ -3,14 +3,11 @@ import { join } from "node:path";
3
3
  import {
4
4
  type AgentSession,
5
5
  createAgentSession,
6
- createBashToolDefinition,
7
6
  DefaultResourceLoader,
8
7
  type ModelRuntime,
9
8
  type SessionManager,
10
- type ToolDefinition,
11
9
  } from "@earendil-works/pi-coding-agent";
12
10
  import { channelSegment } from "../attachments/attachment-dir.ts";
13
- import type { ImageDrawer } from "../contract/providers.ts";
14
11
  import type { ClientPort } from "../domain/client.ts";
15
12
  import type { ChannelKey } from "../domain/conversation.ts";
16
13
  import { ConfigError } from "../domain/errors.ts";
@@ -29,16 +26,13 @@ import {
29
26
  ATTACH_FILE_TOOL,
30
27
  attachFileExtension,
31
28
  } from "../shared/attach-file-tool.ts";
32
- import {
33
- GENERATE_IMAGE_TOOL,
34
- generateImageExtension,
35
- } from "../shared/generate-image-tool.ts";
29
+ import { GENERATE_IMAGE_TOOL } from "../shared/generate-image-tool.ts";
36
30
  import { packageDir } from "../shared/package-dir.ts";
37
31
  import { readAttachmentExtension } from "../shared/read-attachment-tool.ts";
38
- import type { WorkspaceRoots } from "../shared/workspace-files.ts";
39
32
  import { type Speaker, THE_SPEAKER } from "../speakers.ts";
40
33
  import { currentToolNames } from "../tool-tiers.ts";
41
34
  import { toolTurn } from "../tool-turn.ts";
35
+ import { refuseShellTools } from "./chat-only.ts";
42
36
  import {
43
37
  CompactionTiers,
44
38
  compactionEngine,
@@ -64,7 +58,6 @@ import type { PromptSlot } from "./prompt-slot.ts";
64
58
  import type {
65
59
  ChannelSession,
66
60
  CompactionEnd,
67
- CoreExtensions,
68
61
  PiAgentRuntimeOptions,
69
62
  } from "./runtime-types.ts";
70
63
  import {
@@ -78,6 +71,9 @@ import {
78
71
  sessionAddressee,
79
72
  turnAddressee,
80
73
  } from "./session-conversation.ts";
74
+ import { imageTools, scratchBash } from "./session-helpers.ts";
75
+
76
+ export { imageTools, scratchBash };
81
77
 
82
78
  /** What a session factory asks of the runtime that owns the turns. */
83
79
  export interface SessionFactoryDeps {
@@ -139,11 +135,22 @@ export class SessionFactory {
139
135
  );
140
136
  }
141
137
 
138
+ /** Whether the host's agents carry no shell or file tools. */
139
+ get chatOnly(): boolean {
140
+ return this.#options.agentTools === "chat-only";
141
+ }
142
+
142
143
  get plan(): SessionPlan {
143
144
  return this.link().plan;
144
145
  }
145
146
 
147
+ /**
148
+ * The tools a selection turns into. A chat-only host refuses a selection that names a shell or
149
+ * file tool, whatever built it (the agent team, `context.turns`, a plugin), so the turn fails
150
+ * closed before any session runs it.
151
+ */
146
152
  toolsFor(selection: ToolSelection): string[] {
153
+ if (this.chatOnly) refuseShellTools(selection.tools, "a tool selection");
147
154
  const groups = planOrder(this.plan).flatMap(
148
155
  (tool) => tool.snapshot().groups ?? [],
149
156
  );
@@ -360,7 +367,8 @@ export class SessionFactory {
360
367
  },
361
368
  conversation,
362
369
  addressee,
363
- ...(agents
370
+ // A chat-only host's agents work in no workspace.
371
+ ...(agents && !this.chatOnly
364
372
  ? {
365
373
  workspace: {
366
374
  workspace: cwd,
@@ -450,7 +458,7 @@ export class SessionFactory {
450
458
  cwd,
451
459
  agentDir,
452
460
  // Pi's bash under its own name, run with TMPDIR at the agents' scratch dir.
453
- ...(agents?.scratchDir
461
+ ...(agents?.scratchDir && !this.chatOnly
454
462
  ? { customTools: [scratchBash(cwd, agents.scratchDir)] }
455
463
  : {}),
456
464
  thinkingLevel: thinking,
@@ -479,22 +487,3 @@ export class SessionFactory {
479
487
  return Object.assign(state, { session });
480
488
  }
481
489
  }
482
-
483
- /** `generate_image` for a session of a host with an image provider; nothing without one. */
484
- export function imageTools(
485
- images: ImageDrawer | undefined,
486
- roots: WorkspaceRoots,
487
- ): Pick<CoreExtensions, "generateImage"> {
488
- return images ? { generateImage: generateImageExtension(images, roots) } : {};
489
- }
490
-
491
- /** Pi's bash tool with TMPDIR set to `scratchDir`, so tools that read TMPDIR and GNU `mktemp` write there (BSD `mktemp` needs `-p "$TMPDIR"`). */
492
- export function scratchBash(cwd: string, scratchDir: string): ToolDefinition {
493
- // SAFETY: Pi's bash definition is a ToolDefinition typed by its own parameter schema, which the session takes as any custom tool.
494
- return createBashToolDefinition(cwd, {
495
- spawnHook: (context) => ({
496
- ...context,
497
- env: { ...context.env, TMPDIR: scratchDir },
498
- }),
499
- }) as unknown as ToolDefinition;
500
- }
@@ -0,0 +1,27 @@
1
+ import {
2
+ createBashToolDefinition,
3
+ type ToolDefinition,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import type { ImageDrawer } from "../contract/providers.ts";
6
+ import { generateImageExtension } from "../shared/generate-image-tool.ts";
7
+ import type { WorkspaceRoots } from "../shared/workspace-files.ts";
8
+ import type { CoreExtensions } from "./runtime-types.ts";
9
+
10
+ /** `generate_image` for a session of a host with an image provider; nothing without one. */
11
+ export function imageTools(
12
+ images: ImageDrawer | undefined,
13
+ roots: WorkspaceRoots,
14
+ ): Pick<CoreExtensions, "generateImage"> {
15
+ return images ? { generateImage: generateImageExtension(images, roots) } : {};
16
+ }
17
+
18
+ /** Pi's bash tool with TMPDIR set to `scratchDir`, so tools that read TMPDIR and GNU `mktemp` write there (BSD `mktemp` needs `-p "$TMPDIR"`). */
19
+ export function scratchBash(cwd: string, scratchDir: string): ToolDefinition {
20
+ // SAFETY: Pi's bash definition is a ToolDefinition typed by its own parameter schema, which the session takes as any custom tool.
21
+ return createBashToolDefinition(cwd, {
22
+ spawnHook: (context) => ({
23
+ ...context,
24
+ env: { ...context.env, TMPDIR: scratchDir },
25
+ }),
26
+ }) as unknown as ToolDefinition;
27
+ }
@@ -3,6 +3,7 @@ import type { CharacterInput, CharacterSpec } from "../characters/index.ts";
3
3
  import type { Agent, AgentGroup } from "./agents/agent-rules.ts";
4
4
  import type { AvatarMode } from "./agents/agent-tools.ts";
5
5
  import type { TeamStatus } from "./agents/team-status.ts";
6
+ import type { ChannelContexts } from "./contract/channel-context.ts";
6
7
  import type {
7
8
  BackgroundRunsAs,
8
9
  ScheduledOutcome,
@@ -14,6 +15,7 @@ import type { PendingConfirmation } from "./domain/conversation.ts";
14
15
  import type { HttpRoute } from "./http/listeners.ts";
15
16
  import type { IdentityLinks } from "./identity/identity-links.ts";
16
17
  import type { IdentityService } from "./identity/identity-service.ts";
18
+ import type { PersonProfiles } from "./identity/profiles.ts";
17
19
  import type { ThinkingLevel, ThinkingSetting } from "./models.ts";
18
20
  import type { ConversationModes } from "./modes/conversation-modes.ts";
19
21
  import type {
@@ -53,6 +55,10 @@ import type { DispatchThreads } from "./threads/dispatch-threads.ts";
53
55
  */
54
56
  export const IDENTITY: ServiceKey<IdentityService> =
55
57
  serviceKey<IdentityService>("roundtable.identity");
58
+ /** How the core names a person (linked accounts' names and pictures, the chosen one, plugin sources); from the `identity` plugin. */
59
+ export const PROFILES: ServiceKey<PersonProfiles> = serviceKey<PersonProfiles>(
60
+ "roundtable.profiles",
61
+ );
56
62
  /**
57
63
  * Links a person's own account to their principal, which `IDENTITY` cannot. Provided by the
58
64
  * `identity` plugin only when the configuration lists providers in `access.selfLink`; read it with
@@ -92,6 +98,9 @@ export const MODES: ServiceKey<ConversationModes> =
92
98
  */
93
99
  export const THREADS: ServiceKey<DispatchThreads> =
94
100
  serviceKey<DispatchThreads>("roundtable.threads");
101
+ /** The channel context of any surface, chosen by the message's channel key as `THREADS` is; provided by the `channel-context` plugin, which every host registers. */
102
+ export const CHANNEL_CONTEXT: ServiceKey<ChannelContexts> =
103
+ serviceKey<ChannelContexts>("roundtable.channel-context");
95
104
  /** The agent server: its team, its directory, the runtime that runs every agent turn. Provided by the agent-server plugin. */
96
105
  export const AGENTS: ServiceKey<AgentServer> = serviceKey<AgentServer>(
97
106
  "roundtable.agents",
package/src/index.ts CHANGED
@@ -47,7 +47,9 @@ export type {
47
47
  RoundtableConfig,
48
48
  TierConfig,
49
49
  } from "./core/config/config.ts";
50
+ export type { ConversationsConfig } from "./core/config/conversations.ts";
50
51
  export type { LimitsConfig } from "./core/config/limits.ts";
52
+ export type { ProfilesConfig } from "./core/config/profiles.ts";
51
53
  export type { SkillsConfig } from "./core/config/skills-config.ts";
52
54
  export type {
53
55
  AttachmentPort,
@@ -58,6 +60,12 @@ export type {
58
60
  TurnAttachmentOptions,
59
61
  } from "./core/contract/attachments.ts";
60
62
  export { AttachmentRefusal } from "./core/contract/attachments.ts";
63
+ export type {
64
+ ChannelContextSource,
65
+ ChannelContextSourceAttachment,
66
+ ChannelContexts,
67
+ ChannelContextVerdict,
68
+ } from "./core/contract/channel-context.ts";
61
69
  export type {
62
70
  Admission,
63
71
  AttachmentContent,
@@ -213,6 +221,12 @@ export type {
213
221
  RoleSource,
214
222
  } from "./core/identity/principal-store.ts";
215
223
  export { SYSTEM_PRINCIPAL } from "./core/identity/principal-store.ts";
224
+ export type {
225
+ PersonDisplay,
226
+ PersonProfiles,
227
+ ProfileRules,
228
+ ProfileSource,
229
+ } from "./core/identity/profiles.ts";
216
230
  export type {
217
231
  Approval,
218
232
  ApprovalDetails,
@@ -306,6 +320,8 @@ export type {
306
320
  } from "./core/modules/skills/skill-rules.ts";
307
321
  export { SkillConflictError } from "./core/modules/skills/skill-rules.ts";
308
322
  export type {
323
+ ActionEvent,
324
+ AgentTools,
309
325
  Contribution,
310
326
  EventHandlers,
311
327
  EventSink,
@@ -360,6 +376,7 @@ export type {
360
376
  export {
361
377
  AGENTS,
362
378
  BACKGROUND_TURNS,
379
+ CHANNEL_CONTEXT,
363
380
  CONVERSATIONS,
364
381
  DELEGATION,
365
382
  IDENTITY,
@@ -367,6 +384,7 @@ export {
367
384
  MEMORY,
368
385
  MODES,
369
386
  PRECHECKS,
387
+ PROFILES,
370
388
  PROMPTS,
371
389
  RUNTIME,
372
390
  SCHEDULES,
package/src/kit/index.ts CHANGED
@@ -116,6 +116,7 @@ export {
116
116
  SCHEDULE_TOOLS,
117
117
  scheduleToolSpecs,
118
118
  } from "./mirror.ts";
119
+ export { oidcIdentity, oidcProvider } from "./oidc.ts";
119
120
  export type {
120
121
  OpsAlertKind,
121
122
  OpsAlertsOptions,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The identity provider of an OpenID issuer, as `access.owners[].identities`, `selfLink.providers`
3
+ * and `ActorFacts.provider` write it: `oidc:<base64url(issuer)>`. The issuer is encoded so its own
4
+ * colons stay apart from the subject's.
5
+ */
6
+ export function oidcProvider(issuer: string): string {
7
+ return `oidc:${Buffer.from(issuer).toString("base64url")}`;
8
+ }
9
+
10
+ /**
11
+ * The identity of one OpenID subject, `oidcProvider(issuer)` and the subject: the form
12
+ * `roundtable principal link` and `access.owners[].identities` take, and the 0.8 web speaker id.
13
+ */
14
+ export function oidcIdentity(issuer: string, subject: string): string {
15
+ return `${oidcProvider(issuer)}:${subject}`;
16
+ }
package/src/testing.ts CHANGED
@@ -455,6 +455,7 @@ export async function testPlugin(
455
455
  timeZone: "UTC",
456
456
  now: () => new Date(),
457
457
  characterSkins: CATALOG.skins,
458
+ agentTools: "full",
458
459
  ...options.env,
459
460
  };
460
461
  // What the agent server does with the slot: the provider's runtime serves `context.turns`.