@av-pi-studio/server 0.0.92 → 0.0.94

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 (30) hide show
  1. package/dist/agent/agent-manager.d.ts +8 -0
  2. package/dist/agent/agent-manager.js +11 -2
  3. package/dist/agent/agent-ui/agent-ui-rpc.d.ts +20 -0
  4. package/dist/agent/agent-ui/agent-ui-rpc.js +24 -0
  5. package/dist/agent/agent-ui/agent-ui-service.d.ts +55 -0
  6. package/dist/agent/agent-ui/agent-ui-service.js +237 -0
  7. package/dist/agent/mcp-server.d.ts +37 -6
  8. package/dist/agent/mcp-server.js +43 -6
  9. package/dist/agent/pi-home.d.ts +16 -0
  10. package/dist/agent/pi-home.js +17 -0
  11. package/dist/agent/provider-auth/pi-auth-runtime.d.ts +123 -0
  12. package/dist/agent/provider-auth/pi-auth-runtime.js +103 -0
  13. package/dist/agent/provider-auth/provider-auth-rpc.d.ts +29 -0
  14. package/dist/agent/provider-auth/provider-auth-rpc.js +43 -0
  15. package/dist/agent/provider-auth/provider-auth-service.d.ts +80 -0
  16. package/dist/agent/provider-auth/provider-auth-service.js +259 -0
  17. package/dist/agent/provider-contract.d.ts +29 -0
  18. package/dist/agent/providers/mock/mock-provider.d.ts +121 -2
  19. package/dist/agent/providers/mock/mock-provider.js +130 -1
  20. package/dist/agent/providers/mock/ui-script.d.ts +52 -0
  21. package/dist/agent/providers/mock/ui-script.js +224 -0
  22. package/dist/agent/providers/pi/agent.d.ts +1 -0
  23. package/dist/agent/providers/pi/agent.js +57 -9
  24. package/dist/daemon/bootstrap.js +30 -7
  25. package/dist/daemon/dev-bootstrap.d.ts +6 -0
  26. package/dist/daemon/dev-bootstrap.js +28 -14
  27. package/dist/daemon/index.d.ts +1 -0
  28. package/dist/daemon/index.js +4 -0
  29. package/dist/extensions/curated-packs.js +3 -0
  30. package/package.json +4 -4
@@ -1,5 +1,6 @@
1
1
  import type { AgentStatus } from "@av-pi-studio/protocol";
2
2
  import type { AgentRecord } from "../persistence/entity-schemas.js";
3
+ import type { Logger } from "../logging/logger.js";
3
4
  import type { AgentSession } from "./provider-contract.js";
4
5
  /**
5
6
  * AgentManager — the single source of truth for agent lifecycle state
@@ -48,6 +49,13 @@ export interface AgentManagerDeps {
48
49
  deleteAgent?: (cwd: string, id: string) => Promise<boolean>;
49
50
  /** Loop service hook: recover `running` loops as `stopped` with an interruption log entry. */
50
51
  onRecoverLoops?: () => Promise<void> | void;
52
+ /** Session-attach hook (features/extension-ui-rpc.md § New/changed files) — invoked at the end of
53
+ * `attachSession()`, the single choke point every spawn/resume/import path already funnels
54
+ * through (`agent-service.ts:104`, `:228`). A throwing hook is logged and never prevents the
55
+ * session from being attached. Optional: existing constructions that pass none behave exactly
56
+ * as before. */
57
+ onSessionAttached?: (agentId: string, session: AgentSession) => void;
58
+ logger?: Logger;
51
59
  now?: () => string;
52
60
  }
53
61
  export declare class AgentManager {
@@ -82,8 +82,17 @@ export class AgentManager {
82
82
  }
83
83
  attachSession(id, session) {
84
84
  const managed = this.agents.get(id);
85
- if (managed)
86
- managed.session = session;
85
+ if (!managed)
86
+ return;
87
+ managed.session = session;
88
+ if (!this.deps.onSessionAttached)
89
+ return;
90
+ try {
91
+ this.deps.onSessionAttached(id, session);
92
+ }
93
+ catch (err) {
94
+ this.deps.logger?.warn({ agentId: id, err: err instanceof Error ? err.message : String(err) }, "agent-manager: onSessionAttached hook failed");
95
+ }
87
96
  }
88
97
  /**
89
98
  * Snapshot and persist the live session's resume handle (e.g. Pi's on-disk JSONL session file)
@@ -0,0 +1,20 @@
1
+ import type { Logger } from "../../logging/logger.js";
2
+ import type { HandlerRegistry } from "../../ws/router.js";
3
+ import type { AgentUiService } from "./agent-ui-service.js";
4
+ /**
5
+ * Wires the two `agent_ui_*` RPCs (swe/features/extension-ui-rpc.md § Public contract) onto
6
+ * `AgentUiService`. Modelled directly on `registerFileWatchHandlers`/`registerProviderAuthHandlers`:
7
+ * a thin adapter that stamps no policy of its own — ownership, first-wins resolution, and every
8
+ * error code already live in the service (task-003). Never throws for a domain failure
9
+ * (`not_found`/`unsupported` travel in `payload`); `requestId` is stamped by the router, not here.
10
+ *
11
+ * Registered in **both** bootstraps (task-004's deviation from the production-only
12
+ * `provider_auth`/`file_watch` families) — the mock provider is this family's designated producer,
13
+ * so the dev daemon must be able to drive it end to end.
14
+ */
15
+ export interface AgentUiRpcDeps {
16
+ service: AgentUiService;
17
+ logger?: Logger;
18
+ }
19
+ export declare function registerAgentUiHandlers(registry: HandlerRegistry, deps: AgentUiRpcDeps): void;
20
+ //# sourceMappingURL=agent-ui-rpc.d.ts.map
@@ -0,0 +1,24 @@
1
+ export function registerAgentUiHandlers(registry, deps) {
2
+ const { service } = deps;
3
+ registry.register("agent_ui_respond_request", (ctx) => {
4
+ const uiRequestId = String(ctx.message.uiRequestId ?? "");
5
+ const response = (ctx.message.response ?? {});
6
+ const result = service.respond(uiRequestId, response);
7
+ return {
8
+ type: "agent_ui_respond_response",
9
+ payload: result.error ? { ok: result.ok, error: result.error } : { ok: result.ok },
10
+ };
11
+ });
12
+ registry.register("agent_ui_list_request", (ctx) => {
13
+ const agentId = typeof ctx.message.agentId === "string" ? ctx.message.agentId : undefined;
14
+ return {
15
+ type: "agent_ui_list_response",
16
+ payload: {
17
+ ok: true,
18
+ pending: service.listPending(agentId),
19
+ surfaces: service.listSurfaces(agentId),
20
+ },
21
+ };
22
+ });
23
+ }
24
+ //# sourceMappingURL=agent-ui-rpc.js.map
@@ -0,0 +1,55 @@
1
+ import type { AgentUiPendingRequest, AgentUiSurface } from "@av-pi-studio/protocol";
2
+ import type { Logger } from "../../logging/logger.js";
3
+ import type { Session } from "../../ws/session.js";
4
+ import type { AgentSession, ProviderUiResponse } from "../provider-contract.js";
5
+ export interface AgentUiServiceDeps {
6
+ broadcast: (sessions: Iterable<Session>, message: unknown) => void;
7
+ getActiveSessions: () => Iterable<Session>;
8
+ logger?: Logger;
9
+ /** Timer seams so tests use fake timers instead of wall-clock waits. */
10
+ setTimer?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
11
+ clearTimer?: (handle: ReturnType<typeof setTimeout>) => void;
12
+ }
13
+ export declare class AgentUiService {
14
+ private readonly broadcastFn;
15
+ private readonly getActiveSessions;
16
+ private readonly logger;
17
+ private readonly setTimer;
18
+ private readonly clearTimer;
19
+ private readonly pending;
20
+ private readonly surfaces;
21
+ private readonly channels;
22
+ private readonly loggedUnknownMethods;
23
+ constructor(deps: AgentUiServiceDeps);
24
+ /**
25
+ * `AgentManager.onSessionAttached` hook (task-004 wires the call site; nothing calls this yet).
26
+ * Sweeps first — a forced respawn (`spawnOrResumeSession` always spawns fresh) leaves undead
27
+ * dialogs whose provider ids belong to a dead process, so any prior pending/surfaces/channel for
28
+ * this agent are cancelled as `"aborted"` before the new session's channel is ever subscribed.
29
+ */
30
+ attach(agentId: string, session: AgentSession): void;
31
+ /** First-answer-wins. Unknown/already-resolved/fire-and-forget ids all report `not_found` — a
32
+ * fire-and-forget request was never inserted into `pending` in the first place. */
33
+ respond(uiRequestId: string, response: ProviderUiResponse): {
34
+ ok: boolean;
35
+ error?: string;
36
+ };
37
+ listPending(agentId?: string): AgentUiPendingRequest[];
38
+ listSurfaces(agentId?: string): AgentUiSurface[];
39
+ /**
40
+ * Session-terminal only (archive / delete / re-attach) — NEVER called on interrupt. For each
41
+ * pending entry: best-effort cancel toward **the entry's own captured session** (never a freshly
42
+ * attached one — on the attach-path sweep the new session never issued those provider ids),
43
+ * broadcast the resolution, then drop the agent's pending entries, surfaces, and — critically —
44
+ * call the stored channel `Unsubscribe` before dropping it, so a post-sweep emission from a dying
45
+ * session cannot re-create a surface for an agent that no longer has a live channel.
46
+ */
47
+ sweep(agentId: string, reason: string): void;
48
+ private onProviderRequest;
49
+ /** Pi's own timeout elapsed — Pi already auto-resolved the dialog on its side (docs/rpc.md: "the
50
+ * client does not need to track timeouts"). Deliberately does NOT call `respondToUi`: answering
51
+ * again would target an id Pi has already dropped. */
52
+ private expire;
53
+ private broadcastResolved;
54
+ }
55
+ //# sourceMappingURL=agent-ui-service.d.ts.map
@@ -0,0 +1,237 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /**
3
+ * Correlates the provider UI channel into the daemon's wire family
4
+ * (features/extension-ui-rpc.md § Behavior & algorithms — the pseudocode there is normative). The
5
+ * only stateful piece of the bridge, and deliberately **payload-blind**: every Pi-specific decision
6
+ * (which methods block, surface-key namespacing, clear-by-omission, envelope stamping) was already
7
+ * made by the adapter (task-002); this service does no `method` string comparison beyond the
8
+ * unknown-method diagnostic log.
9
+ *
10
+ * Four behaviors here each prevent a concrete defect — see the task's own "Context / why":
11
+ * - **Wire ids are daemon-minted, never the provider's.** `ProviderUiRequest.requestId` is only
12
+ * promised unique per-process; keying a daemon-global map by it would let one agent's dialog
13
+ * shadow another's. `pending` is keyed by a minted UUID; providers never see it.
14
+ * - **Interrupt touches nothing.** Dialogs are not turn-scoped (`pi-background-tasks` raises
15
+ * questions outside any turn) and surfaces are agent-lifetime state. `sweep` runs ONLY on
16
+ * session-terminal events (archive/delete/re-attach) — nothing else cancels.
17
+ * - **Expiry never answers.** Pi auto-resolves its own timed dialogs; the mirrored timer here
18
+ * exists solely so clients dismiss in step. Sending a second response would target a dead id.
19
+ * - **Resolution broadcasts unconditionally.** A `respondToUi` throw (dead stdin after a crash)
20
+ * still broadcasts from `finally` — otherwise every other client keeps a ghost dialog that no
21
+ * longer appears in `agent_ui_list_response`.
22
+ */
23
+ /** Extension UI method vocabulary as documented (rpc.md § Extension UI Protocol), used ONLY to
24
+ * decide whether an incoming method is diagnosable-as-unknown for the info log below — the single
25
+ * sanctioned exception to "no method string comparison" in this service. Duplicated here rather
26
+ * than imported from the Pi adapter: this service must stay provider-agnostic (never import
27
+ * `providers/pi/*` per root AGENTS.md § Key invariants #3), so this list is deliberately a
28
+ * diagnostics-only echo, not a source of behavior. */
29
+ const KNOWN_METHODS = new Set([
30
+ "select",
31
+ "confirm",
32
+ "input",
33
+ "editor",
34
+ "notify",
35
+ "setStatus",
36
+ "setWidget",
37
+ "setTitle",
38
+ "set_editor_text",
39
+ ]);
40
+ export class AgentUiService {
41
+ broadcastFn;
42
+ getActiveSessions;
43
+ logger;
44
+ setTimer;
45
+ clearTimer;
46
+ pending = new Map();
47
+ surfaces = new Map();
48
+ channels = new Map();
49
+ loggedUnknownMethods = new Set();
50
+ constructor(deps) {
51
+ this.broadcastFn = deps.broadcast;
52
+ this.getActiveSessions = deps.getActiveSessions;
53
+ this.logger = deps.logger;
54
+ this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
55
+ this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
56
+ }
57
+ /**
58
+ * `AgentManager.onSessionAttached` hook (task-004 wires the call site; nothing calls this yet).
59
+ * Sweeps first — a forced respawn (`spawnOrResumeSession` always spawns fresh) leaves undead
60
+ * dialogs whose provider ids belong to a dead process, so any prior pending/surfaces/channel for
61
+ * this agent are cancelled as `"aborted"` before the new session's channel is ever subscribed.
62
+ */
63
+ attach(agentId, session) {
64
+ this.sweep(agentId, "aborted");
65
+ if (!session.onUiRequest)
66
+ return; // provider opted out; nothing to do, no error
67
+ const unsubscribe = session.onUiRequest((req) => this.onProviderRequest(agentId, session, req));
68
+ this.channels.set(agentId, unsubscribe);
69
+ }
70
+ /** First-answer-wins. Unknown/already-resolved/fire-and-forget ids all report `not_found` — a
71
+ * fire-and-forget request was never inserted into `pending` in the first place. */
72
+ respond(uiRequestId, response) {
73
+ const entry = this.pending.get(uiRequestId);
74
+ if (!entry)
75
+ return { ok: false, error: "not_found" };
76
+ if (!entry.session.respondToUi)
77
+ return { ok: false, error: "unsupported" };
78
+ this.pending.delete(uiRequestId);
79
+ if (entry.timer)
80
+ this.clearTimer(entry.timer);
81
+ try {
82
+ entry.session.respondToUi(entry.providerRequestId, response);
83
+ }
84
+ catch (err) {
85
+ // The answer was still accepted (first-wins already consumed it) — swallow + log so the
86
+ // resolution broadcast below still fires; otherwise every other client keeps a ghost dialog.
87
+ this.logger?.warn({
88
+ agentId: entry.agentId,
89
+ requestId: uiRequestId,
90
+ method: entry.method,
91
+ err: errorMessage(err),
92
+ }, "agent-ui: respondToUi failed");
93
+ }
94
+ finally {
95
+ this.broadcastResolved(uiRequestId, entry.agentId, "answered");
96
+ }
97
+ return { ok: true };
98
+ }
99
+ listPending(agentId) {
100
+ const result = [];
101
+ for (const [wireId, entry] of this.pending) {
102
+ if (agentId !== undefined && entry.agentId !== agentId)
103
+ continue;
104
+ result.push({
105
+ requestId: wireId,
106
+ agentId: entry.agentId,
107
+ method: entry.method,
108
+ expectsResponse: true,
109
+ payload: entry.payload,
110
+ ...(entry.surfaceKey !== undefined ? { surfaceKey: entry.surfaceKey } : {}),
111
+ ...(entry.timeoutMs !== undefined ? { timeoutMs: entry.timeoutMs } : {}),
112
+ createdAt: entry.createdAt,
113
+ });
114
+ }
115
+ return result;
116
+ }
117
+ listSurfaces(agentId) {
118
+ if (agentId !== undefined)
119
+ return [...(this.surfaces.get(agentId)?.values() ?? [])];
120
+ const result = [];
121
+ for (const agentSurfaces of this.surfaces.values())
122
+ result.push(...agentSurfaces.values());
123
+ return result;
124
+ }
125
+ /**
126
+ * Session-terminal only (archive / delete / re-attach) — NEVER called on interrupt. For each
127
+ * pending entry: best-effort cancel toward **the entry's own captured session** (never a freshly
128
+ * attached one — on the attach-path sweep the new session never issued those provider ids),
129
+ * broadcast the resolution, then drop the agent's pending entries, surfaces, and — critically —
130
+ * call the stored channel `Unsubscribe` before dropping it, so a post-sweep emission from a dying
131
+ * session cannot re-create a surface for an agent that no longer has a live channel.
132
+ */
133
+ sweep(agentId, reason) {
134
+ for (const [wireId, entry] of this.pending) {
135
+ if (entry.agentId !== agentId)
136
+ continue;
137
+ this.pending.delete(wireId);
138
+ if (entry.timer)
139
+ this.clearTimer(entry.timer);
140
+ if (entry.session.respondToUi) {
141
+ try {
142
+ entry.session.respondToUi(entry.providerRequestId, { cancelled: true });
143
+ }
144
+ catch (err) {
145
+ this.logger?.warn({ agentId, requestId: wireId, method: entry.method, err: errorMessage(err) }, "agent-ui: sweep respondToUi failed");
146
+ }
147
+ }
148
+ this.broadcastResolved(wireId, agentId, reason);
149
+ }
150
+ this.surfaces.delete(agentId);
151
+ const unsubscribe = this.channels.get(agentId);
152
+ if (unsubscribe) {
153
+ unsubscribe();
154
+ this.channels.delete(agentId);
155
+ }
156
+ }
157
+ onProviderRequest(agentId, session, req) {
158
+ const now = Date.now();
159
+ if (req.surfaceKey !== undefined) {
160
+ let agentSurfaces = this.surfaces.get(agentId);
161
+ if (!agentSurfaces) {
162
+ agentSurfaces = new Map();
163
+ this.surfaces.set(agentId, agentSurfaces);
164
+ }
165
+ if (req.removed) {
166
+ agentSurfaces.delete(req.surfaceKey);
167
+ }
168
+ else {
169
+ agentSurfaces.set(req.surfaceKey, {
170
+ agentId,
171
+ method: req.method,
172
+ surfaceKey: req.surfaceKey,
173
+ payload: req.payload,
174
+ updatedAt: now,
175
+ });
176
+ }
177
+ }
178
+ if (!KNOWN_METHODS.has(req.method)) {
179
+ const key = `${agentId}:${req.method}`;
180
+ if (!this.loggedUnknownMethods.has(key)) {
181
+ this.loggedUnknownMethods.add(key);
182
+ this.logger?.info({ agentId, method: req.method }, "agent-ui: unknown extension UI method");
183
+ }
184
+ }
185
+ const wireId = randomUUID();
186
+ if (req.expectsResponse) {
187
+ const entry = {
188
+ agentId,
189
+ providerRequestId: req.requestId,
190
+ session,
191
+ method: req.method,
192
+ payload: req.payload,
193
+ createdAt: now,
194
+ ...(req.surfaceKey !== undefined ? { surfaceKey: req.surfaceKey } : {}),
195
+ ...(req.timeoutMs !== undefined ? { timeoutMs: req.timeoutMs } : {}),
196
+ };
197
+ if (req.timeoutMs)
198
+ entry.timer = this.setTimer(() => this.expire(wireId), req.timeoutMs);
199
+ this.pending.set(wireId, entry);
200
+ }
201
+ this.broadcastFn(this.getActiveSessions(), {
202
+ type: "session",
203
+ message: {
204
+ type: "agent_ui_request",
205
+ requestId: wireId,
206
+ agentId,
207
+ method: req.method,
208
+ expectsResponse: req.expectsResponse,
209
+ payload: req.payload,
210
+ ...(req.surfaceKey !== undefined ? { surfaceKey: req.surfaceKey } : {}),
211
+ ...(req.removed ? { removed: true } : {}),
212
+ ...(req.timeoutMs !== undefined ? { timeoutMs: req.timeoutMs } : {}),
213
+ createdAt: now,
214
+ },
215
+ });
216
+ }
217
+ /** Pi's own timeout elapsed — Pi already auto-resolved the dialog on its side (docs/rpc.md: "the
218
+ * client does not need to track timeouts"). Deliberately does NOT call `respondToUi`: answering
219
+ * again would target an id Pi has already dropped. */
220
+ expire(wireId) {
221
+ const entry = this.pending.get(wireId);
222
+ if (!entry)
223
+ return;
224
+ this.pending.delete(wireId);
225
+ this.broadcastResolved(wireId, entry.agentId, "timeout");
226
+ }
227
+ broadcastResolved(uiRequestId, agentId, reason) {
228
+ this.broadcastFn(this.getActiveSessions(), {
229
+ type: "session",
230
+ message: { type: "agent_ui_resolved", requestId: uiRequestId, agentId, reason },
231
+ });
232
+ }
233
+ }
234
+ function errorMessage(err) {
235
+ return err instanceof Error ? err.message : String(err);
236
+ }
237
+ //# sourceMappingURL=agent-ui-service.js.map
@@ -1,12 +1,34 @@
1
1
  import { z } from "zod";
2
+ import type { AgentUiPendingRequest, AgentUiResponse } from "@av-pi-studio/protocol";
2
3
  /**
3
- * Daemon MCP server (features/mcp-server.md). Hosts orchestration tools at `/mcp/agents` so agents
4
- * can control other agents + the daemon. The tool set mirrors the WS/CLI control plane. When
5
- * `daemon.mcp.injectIntoAgents` is enabled, a per-agent `--mcp-config` is generated for Pi (adapter
6
- * OAuth disabled); user/project MCP files are never touched.
4
+ * Daemon MCP tool registry (features/mcp-server.md). Defines the orchestration tools that are
5
+ * *intended* to be hosted at `/mcp/agents` so agents can control other agents + the daemon, and
6
+ * mirrors the WS/CLI control plane. Owns the tool registry, `create_agent` semantics, and
7
+ * injection-config generation, all injectable for tests.
7
8
  *
8
- * The transport (HTTP server) is wired in bootstrap; this module owns the tool registry, the
9
- * `create_agent` semantics, and the injection-config generation, all injectable for tests.
9
+ * NOT REACHABLE AT RUNTIME. Nothing in this repo constructs an `McpServer` outside
10
+ * `mcp-server.test.ts`. Specifically missing:
11
+ * - no HTTP route serving `MCP_ENDPOINT_PATH` in either bootstrap;
12
+ * - no `McpBackend` implementation anywhere in production code;
13
+ * - no `daemon.mcp` config section (`enabled`/`injectIntoAgents` are read from deps only);
14
+ * - `buildPiArgs` accepts `mcpConfigPath`, but all three Pi spawn sites pass only
15
+ * `appendSystemPrompt`, so no agent is ever handed a `--mcp-config`.
16
+ * Sprint-010/task-001 built this registry and deferred the transport as "a bootstrap step"; that
17
+ * step was never taken. Consequence: every tool here is dormant — including
18
+ * `list_pending_permissions`/`respond_to_permission` (sprint-010) and
19
+ * `list_pending_ui_requests`/`respond_to_ui_request` (sprint-066/task-005) — and the
20
+ * agent-to-agent orchestration in `features/subagents.md` has never run.
21
+ *
22
+ * Two things a future wiring task must handle, neither of which is a simple hookup:
23
+ * 1. `callTool()` is a plain dispatcher, NOT an MCP protocol implementation. A real endpoint
24
+ * needs JSON-RPC plus the `initialize`/`tools/list`/`tools/call` handshake over streamable
25
+ * HTTP or SSE. That layer does not exist here.
26
+ * 2. `injectionConfig()` hardcodes `auth: false, oauth: false`, but the daemon's HTTP server is
27
+ * built with `authenticate: (req) => auth.authenticateHttp(req)` (bootstrap.ts) and production
28
+ * binds `0.0.0.0:6767` by default. Serving these tools unauthenticated would expose
29
+ * `create_agent`/`kill_agent`/`send_agent_prompt` to the network. Bind the route to loopback
30
+ * or carry a token in the injected config — the two existing code paths contradict each other
31
+ * today only because neither runs.
10
32
  */
11
33
  export declare const MCP_ENDPOINT_PATH = "/mcp/agents";
12
34
  export declare const MCP_SERVER_KEY = "pi-studio-agents";
@@ -60,6 +82,15 @@ export interface McpBackend {
60
82
  respondToPermission(requestId: string, response: unknown): {
61
83
  resolved: boolean;
62
84
  };
85
+ /** Extension UI (features/extension-ui-rpc.md § MCP mirror) — closes the deadlock where an
86
+ * orchestrating agent can't answer a child's extension questionnaire. Mirrors the permission
87
+ * pair's shape; its own error vocabulary ("unknown_ui_request"/"unsupported") is intentionally
88
+ * distinct — see the tool registration below for why. */
89
+ listPendingUiRequests(agentId?: string): AgentUiPendingRequest[];
90
+ respondToUiRequest(requestId: string, response: AgentUiResponse): {
91
+ resolved: boolean;
92
+ error?: string;
93
+ };
63
94
  listProviders(): unknown;
64
95
  inspectProvider?(providerId: string): unknown;
65
96
  listModels(providerId: string): unknown;
@@ -3,13 +3,34 @@ import { join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { PARENT_AGENT_ID_LABEL } from "./agent-manager.js";
5
5
  /**
6
- * Daemon MCP server (features/mcp-server.md). Hosts orchestration tools at `/mcp/agents` so agents
7
- * can control other agents + the daemon. The tool set mirrors the WS/CLI control plane. When
8
- * `daemon.mcp.injectIntoAgents` is enabled, a per-agent `--mcp-config` is generated for Pi (adapter
9
- * OAuth disabled); user/project MCP files are never touched.
6
+ * Daemon MCP tool registry (features/mcp-server.md). Defines the orchestration tools that are
7
+ * *intended* to be hosted at `/mcp/agents` so agents can control other agents + the daemon, and
8
+ * mirrors the WS/CLI control plane. Owns the tool registry, `create_agent` semantics, and
9
+ * injection-config generation, all injectable for tests.
10
10
  *
11
- * The transport (HTTP server) is wired in bootstrap; this module owns the tool registry, the
12
- * `create_agent` semantics, and the injection-config generation, all injectable for tests.
11
+ * NOT REACHABLE AT RUNTIME. Nothing in this repo constructs an `McpServer` outside
12
+ * `mcp-server.test.ts`. Specifically missing:
13
+ * - no HTTP route serving `MCP_ENDPOINT_PATH` in either bootstrap;
14
+ * - no `McpBackend` implementation anywhere in production code;
15
+ * - no `daemon.mcp` config section (`enabled`/`injectIntoAgents` are read from deps only);
16
+ * - `buildPiArgs` accepts `mcpConfigPath`, but all three Pi spawn sites pass only
17
+ * `appendSystemPrompt`, so no agent is ever handed a `--mcp-config`.
18
+ * Sprint-010/task-001 built this registry and deferred the transport as "a bootstrap step"; that
19
+ * step was never taken. Consequence: every tool here is dormant — including
20
+ * `list_pending_permissions`/`respond_to_permission` (sprint-010) and
21
+ * `list_pending_ui_requests`/`respond_to_ui_request` (sprint-066/task-005) — and the
22
+ * agent-to-agent orchestration in `features/subagents.md` has never run.
23
+ *
24
+ * Two things a future wiring task must handle, neither of which is a simple hookup:
25
+ * 1. `callTool()` is a plain dispatcher, NOT an MCP protocol implementation. A real endpoint
26
+ * needs JSON-RPC plus the `initialize`/`tools/list`/`tools/call` handshake over streamable
27
+ * HTTP or SSE. That layer does not exist here.
28
+ * 2. `injectionConfig()` hardcodes `auth: false, oauth: false`, but the daemon's HTTP server is
29
+ * built with `authenticate: (req) => auth.authenticateHttp(req)` (bootstrap.ts) and production
30
+ * binds `0.0.0.0:6767` by default. Serving these tools unauthenticated would expose
31
+ * `create_agent`/`kill_agent`/`send_agent_prompt` to the network. Bind the route to loopback
32
+ * or carry a token in the injected config — the two existing code paths contradict each other
33
+ * today only because neither runs.
13
34
  */
14
35
  export const MCP_ENDPOINT_PATH = "/mcp/agents";
15
36
  export const MCP_SERVER_KEY = "pi-studio-agents";
@@ -145,6 +166,22 @@ export class McpServer {
145
166
  const result = b.respondToPermission(args.requestId, args.response);
146
167
  return result.resolved ? { ok: true } : { ok: false, error: "unknown_permission" };
147
168
  });
169
+ // Extension UI (features/extension-ui-rpc.md § MCP mirror). Error vocabulary intentionally
170
+ // differs from `respond_to_permission`'s `unknown_permission`/the WS side's `not_found`: a
171
+ // `resolved:false` with no `error` (never happens for this backend) and an `unsupported`
172
+ // result are distinct outcomes a caller must not conflate with "unknown" — collapsing a live
173
+ // dialog on a provider without `respondToUi` into `unknown_ui_request` would report it as
174
+ // gone when it is actually still answerable over WS by a human.
175
+ this.registerTool("list_pending_ui_requests", z.object({ agentId: z.string().optional() }), (args) => ({ ok: true, requests: b.listPendingUiRequests(args.agentId) }));
176
+ this.registerTool("respond_to_ui_request", z.object({ requestId: z.string(), response: z.unknown() }), (args) => {
177
+ const result = b.respondToUiRequest(args.requestId, args.response);
178
+ if (result.resolved)
179
+ return { ok: true };
180
+ return {
181
+ ok: false,
182
+ error: result.error === "unsupported" ? "unsupported" : "unknown_ui_request",
183
+ };
184
+ });
148
185
  // Providers / models.
149
186
  this.registerTool("list_providers", z.object({}).passthrough(), () => ({
150
187
  ok: true,
@@ -30,4 +30,20 @@ export declare function resolvePiAgentDir(config: PersistedConfig): string | und
30
30
  * single Pi-Studio setting redirects the bundled Pi CLI's entire `~/.pi/agent` tree (models.json,
31
31
  * auth.json, settings.json, sessions/, …) to a custom directory. */
32
32
  export declare function piHomeEnv(config: PersistedConfig): Record<string, string>;
33
+ /** Resolved `auth.json`/`models.json` paths; `undefined` fields let Pi's own defaults decide.
34
+ * Mirrors {@link PiAuthPaths} in `packages/cli/src/auth-runtime.ts` — this is the daemon-side
35
+ * sibling, deliberately not a shared import (that module belongs to a different package). */
36
+ export interface PiAuthPaths {
37
+ authPath?: string;
38
+ modelsPath?: string;
39
+ }
40
+ /**
41
+ * Derive `auth.json`/`models.json` from {@link resolvePiAgentDir} — the single intentional
42
+ * coupling point between the provider-auth RPC family and the spawn path
43
+ * (features/provider-auth-rpc.md § New/changed files). A credential written at this path MUST be
44
+ * the one a daemon-spawned `pi --mode rpc` child reads via `piHomeEnv()`'s
45
+ * `PI_CODING_AGENT_DIR`/`PI_CODING_AGENT_SESSION_DIR`, which is why this derives from the same
46
+ * `resolvePiAgentDir` rather than re-deriving the precedence independently.
47
+ */
48
+ export declare function resolvePiAuthPaths(config: PersistedConfig): PiAuthPaths;
33
49
  //# sourceMappingURL=pi-home.d.ts.map
@@ -46,4 +46,21 @@ export function piHomeEnv(config) {
46
46
  PI_CODING_AGENT_SESSION_DIR: join(agentDir, "sessions"),
47
47
  };
48
48
  }
49
+ /**
50
+ * Derive `auth.json`/`models.json` from {@link resolvePiAgentDir} — the single intentional
51
+ * coupling point between the provider-auth RPC family and the spawn path
52
+ * (features/provider-auth-rpc.md § New/changed files). A credential written at this path MUST be
53
+ * the one a daemon-spawned `pi --mode rpc` child reads via `piHomeEnv()`'s
54
+ * `PI_CODING_AGENT_DIR`/`PI_CODING_AGENT_SESSION_DIR`, which is why this derives from the same
55
+ * `resolvePiAgentDir` rather than re-deriving the precedence independently.
56
+ */
57
+ export function resolvePiAuthPaths(config) {
58
+ const agentDir = resolvePiAgentDir(config);
59
+ if (!agentDir)
60
+ return {};
61
+ return {
62
+ authPath: join(agentDir, "auth.json"),
63
+ modelsPath: join(agentDir, "models.json"),
64
+ };
65
+ }
49
66
  //# sourceMappingURL=pi-home.js.map