pi-session-mcp 0.6.3 → 0.7.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/.pi-session-mcp-cli/config.js +11 -1
- package/.pi-session-mcp-cli/doctor-cli.js +5 -1
- package/.pi-session-mcp-cli/doctor.js +22 -1
- package/.pi-session-mcp-cli/execution-profile.js +1 -0
- package/.pi-session-mcp-cli/extension-spec.js +21 -0
- package/.pi-session-mcp-cli/pi-extensions.js +67 -0
- package/CHANGELOG.md +54 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +2 -2
- package/SECURITY.md +1 -1
- package/dist/capabilities.d.ts +1 -0
- package/dist/capabilities.js +2 -1
- package/dist/client-adapters/mcp-verifier.js +5 -2
- package/dist/config.d.ts +1 -0
- package/dist/config.js +11 -1
- package/dist/doctor-cli.js +5 -1
- package/dist/doctor.d.ts +4 -1
- package/dist/doctor.js +22 -1
- package/dist/execution-profile.d.ts +3 -0
- package/dist/execution-profile.js +1 -0
- package/dist/extension-spec.d.ts +15 -0
- package/dist/extension-spec.js +21 -0
- package/dist/main.js +6 -2
- package/dist/pi-adapter.d.ts +1 -1
- package/dist/pi-adapter.js +4 -0
- package/dist/pi-extensions.d.ts +17 -0
- package/dist/pi-extensions.js +67 -0
- package/dist/sdk-pi-adapter.d.ts +24 -6
- package/dist/sdk-pi-adapter.js +253 -86
- package/dist/server.js +1 -0
- package/dist/session-tools.d.ts +9 -0
- package/dist/session-tools.js +25 -0
- package/dist/setup-command-guardian.js +29 -73
- package/dist/setup-main.js +2 -1
- package/dist/setup-ownership.js +18 -28
- package/dist/setup-platform.d.ts +57 -0
- package/dist/setup-platform.js +195 -0
- package/dist/setup-process.d.ts +6 -3
- package/dist/setup-process.js +85 -93
- package/dist/setup.js +2 -1
- package/dist/stdout-guard-install.d.ts +5 -0
- package/dist/stdout-guard-install.js +6 -0
- package/dist/stdout-guard.d.ts +20 -0
- package/dist/stdout-guard.js +72 -0
- package/docs/acceptance.md +245 -0
- package/docs/adr/0005-macos-setup-platform.md +84 -0
- package/docs/adr/0006-pi-extension-allowlist.md +58 -0
- package/docs/architecture.md +1 -1
- package/docs/client-setup-design.md +21 -9
- package/docs/client-setup.md +56 -10
- package/docs/doctor-design.md +9 -2
- package/docs/installation.md +129 -4
- package/docs/macos-setup-design.md +386 -0
- package/docs/pi-extensions-design.md +421 -0
- package/docs/releasing.md +1 -1
- package/docs/superpowers/plans/2026-10-10-macos-setup.md +1725 -0
- package/docs/superpowers/plans/2026-10-11-pi-extensions.md +2450 -0
- package/docs/threat-model.md +54 -18
- package/docs/tool-contracts.md +8 -3
- package/package.json +1 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# Pi extensions design
|
|
2
|
+
|
|
3
|
+
Status: approved by the owner on 2026-10-11 for implementation. Tracking issue:
|
|
4
|
+
[#34](https://github.com/plagor44-ux/pi-session-mcp/issues/34).
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
An operator can allowlist Pi extensions per execution profile. A profile that
|
|
9
|
+
names an extension gets an embedded Pi session in which that extension is
|
|
10
|
+
loaded with its full capabilities: providers, tools, hooks and system-prompt
|
|
11
|
+
changes. The first target is `pi-cursor-sdk`, which registers the `cursor`
|
|
12
|
+
provider; other provider extensions follow the same path.
|
|
13
|
+
|
|
14
|
+
Success means:
|
|
15
|
+
|
|
16
|
+
- a `cursor` profile works end to end through `pi_session_start`,
|
|
17
|
+
`pi_session_prompt`, `pi_turn_get` and `pi_session_close`, including two
|
|
18
|
+
parallel `cursor` sessions;
|
|
19
|
+
- Doctor reports a missing or mismatched extension offline, without running
|
|
20
|
+
extension code;
|
|
21
|
+
- profiles without `extensions` keep their session-creation path; their only
|
|
22
|
+
change is that prompts pass `expandPromptTemplates: false`, which has no
|
|
23
|
+
effect without templates, skills or extensions;
|
|
24
|
+
- stdout carries only MCP frames even when an extension writes to it;
|
|
25
|
+
- automated tests need no credentials and no network.
|
|
26
|
+
|
|
27
|
+
## Non-goals
|
|
28
|
+
|
|
29
|
+
- Extension sources other than npm packages installed by Pi (`git:`, local
|
|
30
|
+
paths, URLs).
|
|
31
|
+
- Installing or updating extensions. Pi owns that (`pi update --extensions`).
|
|
32
|
+
- Version ranges or "latest". A version is pinned exactly.
|
|
33
|
+
- Pi's built-in extensions (`mcp`, `codemode`, `tool-search`, `llama.cpp`).
|
|
34
|
+
pi-session-mcp keeps its own `mcpServers` bridge.
|
|
35
|
+
- Extension CLI flags (for example `--cursor-runtime`) and extension UI. Flags
|
|
36
|
+
keep their defaults; UI calls go to the SDK's no-op UI.
|
|
37
|
+
- Extension slash commands triggered by client prompt text.
|
|
38
|
+
- Process isolation of extension code. Extensions run in the server process.
|
|
39
|
+
|
|
40
|
+
## Owner decisions
|
|
41
|
+
|
|
42
|
+
| Question | Decision |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| Capability scope | Full: providers, tools, hooks, system-prompt changes. |
|
|
45
|
+
| Naming and pinning | `npm:<name>@<exact version>`, resolved from Pi's user install; any other installed version fails closed. |
|
|
46
|
+
| Granularity | Per execution profile. |
|
|
47
|
+
| `read-only` profiles | Extensions allowed; the model's tools stay read/grep/find/ls. |
|
|
48
|
+
| Integration approach | Pi SDK session lifecycle API (`createAgentSessionServices`, `createAgentSessionFromServices`, `AgentSessionRuntime`). |
|
|
49
|
+
|
|
50
|
+
## Current behavior and constraints
|
|
51
|
+
|
|
52
|
+
`createPhase0ResourceLoader` (`src/sdk-pi-adapter.ts`) builds a
|
|
53
|
+
`DefaultResourceLoader` with `noExtensions: true`, no skills, templates or
|
|
54
|
+
themes, and a disabled system prompt. `SdkPiSessionAdapter.create` resolves
|
|
55
|
+
provider, model, authentication and availability against one `ModelRuntime`
|
|
56
|
+
shared by all sessions, and only then builds the resource loader and calls
|
|
57
|
+
`createAgentSession`.
|
|
58
|
+
|
|
59
|
+
Facts about Pi SDK 1.1.0 that shape this design, verified against the
|
|
60
|
+
installed package:
|
|
61
|
+
|
|
62
|
+
1. An extension's `registerProvider` calls are queued at load time and applied
|
|
63
|
+
to the `ModelRuntime` when the session's extension runner binds its core,
|
|
64
|
+
which happens inside session construction. A provider check that runs before
|
|
65
|
+
the session exists does not see an extension provider.
|
|
66
|
+
`createAgentSessionServices` applies the queued registrations to the
|
|
67
|
+
services' runtime, so the provider is visible before the session exists.
|
|
68
|
+
2. Provider registrations are global to a `ModelRuntime` instance; the last
|
|
69
|
+
registration for an id wins. `pi-cursor-sdk` fails sibling sessions that
|
|
70
|
+
share a runtime with `CURSOR_PROVIDER_OWNERSHIP_ERROR`. An extension session
|
|
71
|
+
therefore needs its own runtime.
|
|
72
|
+
3. `createAgentSession` does not emit `session_start`; only
|
|
73
|
+
`session.bindExtensions()` does. `pi-cursor-sdk` refuses to stream until it
|
|
74
|
+
has seen `session_start`.
|
|
75
|
+
4. `AgentSession.dispose()` does not emit `session_shutdown`;
|
|
76
|
+
`AgentSessionRuntime.dispose()` does. `pi-cursor-sdk` registers cleanup on
|
|
77
|
+
`session_shutdown`.
|
|
78
|
+
5. `session.prompt(text)` dispatches extension slash commands unless it is
|
|
79
|
+
called with `expandPromptTemplates: false`.
|
|
80
|
+
6. With an explicit `tools` list, every tool, extension tools included, is
|
|
81
|
+
filtered by the session's allowlist.
|
|
82
|
+
7. `noExtensions: true` suppresses settings and auto-discovered extensions but
|
|
83
|
+
still loads `additionalExtensionPaths`.
|
|
84
|
+
8. `DefaultPackageManager.resolve()` runs `npm install` for missing or
|
|
85
|
+
mismatched npm packages, and `getInstalledPath(source, "user")` falls back
|
|
86
|
+
to a synchronous `npm root -g` on a miss and accepts legacy global
|
|
87
|
+
installs; neither is side-effect free.
|
|
88
|
+
9. `pi-cursor-sdk` and `@cursor/sdk` call `console.log`. In a stdio MCP server
|
|
89
|
+
such output corrupts the protocol stream.
|
|
90
|
+
|
|
91
|
+
The relevant SDK functions are root exports of
|
|
92
|
+
`@earendil-works/pi-coding-agent`: `createAgentSessionServices`,
|
|
93
|
+
`createAgentSessionFromServices`, `createAgentSessionRuntime`,
|
|
94
|
+
`AgentSessionRuntime`, `getAgentDir` and
|
|
95
|
+
`ModelRuntime`. No internal module is imported.
|
|
96
|
+
|
|
97
|
+
## Design
|
|
98
|
+
|
|
99
|
+
### Configuration
|
|
100
|
+
|
|
101
|
+
Every execution profile accepts an optional `extensions` array:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
"code-cursor": {
|
|
105
|
+
"default": false,
|
|
106
|
+
"permissionProfile": "coding",
|
|
107
|
+
"provider": "cursor",
|
|
108
|
+
"model": "<cursor-model-id>",
|
|
109
|
+
"thinkingLevel": "high",
|
|
110
|
+
"extensions": ["npm:pi-cursor-sdk@0.5.3"]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Rules, enforced by `configSchema`:
|
|
115
|
+
|
|
116
|
+
- Each entry matches `npm:<name>@<version>`. `<name>` is an npm package name,
|
|
117
|
+
optionally scoped (`@scope/name`), lowercase. `<version>` is an exact semver
|
|
118
|
+
version with optional prerelease and build metadata. Ranges, tags, `git:`,
|
|
119
|
+
paths and URLs are rejected.
|
|
120
|
+
- At most 8 entries per profile. A package name appears at most once per
|
|
121
|
+
profile.
|
|
122
|
+
- The field is allowed on `read-only` and `coding` profiles, including the
|
|
123
|
+
default profile.
|
|
124
|
+
- An invalid entry fails the whole configuration with
|
|
125
|
+
`invalid_execution_profile_config`, like every other schema violation.
|
|
126
|
+
|
|
127
|
+
The parsed profile keeps `extensions` as a frozen array of
|
|
128
|
+
`{ name, version }` pairs plus the original spec string. The configuration
|
|
129
|
+
fingerprint already covers the field because it hashes the file bytes.
|
|
130
|
+
|
|
131
|
+
### Extension resolution
|
|
132
|
+
|
|
133
|
+
A new module `src/pi-extensions.ts` owns resolution. It is used by the adapter
|
|
134
|
+
and by Doctor.
|
|
135
|
+
|
|
136
|
+
`resolveExtension(spec, dependencies)`:
|
|
137
|
+
|
|
138
|
+
1. Computes Pi's user install location `<agent dir>/npm/node_modules/<name>`,
|
|
139
|
+
with the agent directory from `getAgentDir()`. It never calls the SDK
|
|
140
|
+
package manager, never spawns npm and never installs. A package installed
|
|
141
|
+
anywhere else counts as not installed.
|
|
142
|
+
2. No path: `extension_not_installed`.
|
|
143
|
+
3. Reads `<dir>/package.json`. Unreadable or invalid JSON:
|
|
144
|
+
`extension_load_failed`. A `version` that differs from the pinned version:
|
|
145
|
+
`extension_version_mismatch`.
|
|
146
|
+
4. Reads `pi.extensions` from the manifest. It must be a non-empty array of
|
|
147
|
+
relative paths. Every entry must resolve, after symlink resolution, to a
|
|
148
|
+
file inside the real package directory. Otherwise: `extension_load_failed`.
|
|
149
|
+
`pi.extensions` entries must be plain relative file paths; globs and
|
|
150
|
+
overrides, which Pi accepts, fail with `extension_load_failed`.
|
|
151
|
+
5. Returns the real package directory. The SDK receives that directory, not
|
|
152
|
+
the `npm:` source, so it cannot start a temporary install.
|
|
153
|
+
|
|
154
|
+
Dependencies (install-location lookup, file reads, real-path resolution, stat) are
|
|
155
|
+
injectable for tests.
|
|
156
|
+
|
|
157
|
+
### Session creation
|
|
158
|
+
|
|
159
|
+
Profiles without `extensions` keep today's path unchanged: shared runtime,
|
|
160
|
+
`createPhase0ResourceLoader`, `createAgentSession`.
|
|
161
|
+
|
|
162
|
+
For a profile with `extensions`, `SdkPiSessionAdapter.create` runs:
|
|
163
|
+
|
|
164
|
+
1. **Resolve.** Resolve every extension as above.
|
|
165
|
+
2. **Runtime.** Create a fresh `ModelRuntime` for this session through the
|
|
166
|
+
existing `modelRuntimeFactory`. It reads Pi's normal authentication store.
|
|
167
|
+
3. **Services.** Call `createAgentSessionServices` with that runtime, the
|
|
168
|
+
workspace as `cwd` and `agentDir`, an in-memory untrusted settings manager,
|
|
169
|
+
and resource-loader options identical to `createPhase0ResourceLoader` plus
|
|
170
|
+
`additionalExtensionPaths` set to the resolved package directories. Any
|
|
171
|
+
services diagnostic of type `error`, or an exception, is
|
|
172
|
+
`extension_load_failed`.
|
|
173
|
+
4. **Checks.** Run the existing provider, model, `checkAuth`, `getAuth` and
|
|
174
|
+
`getAvailable` checks against the services' runtime, with the existing
|
|
175
|
+
error codes and deadlines.
|
|
176
|
+
5. **External MCP tools.** Connect configured `mcpServers` as today.
|
|
177
|
+
6. **Session.** Call `createAgentSessionFromServices` with the model, thinking
|
|
178
|
+
level, an in-memory session manager, the tool list (see Tools) and the MCP
|
|
179
|
+
custom tools. The session is wrapped so that it can be disposed through
|
|
180
|
+
`AgentSessionRuntime` (see Lifecycle).
|
|
181
|
+
7. **Bind.** Call `session.bindExtensions({ mode: "print" })` without a UI
|
|
182
|
+
context, so the SDK's no-op UI applies and `session_start` is emitted. A
|
|
183
|
+
failure is `extension_load_failed`.
|
|
184
|
+
8. **Postchecks.** Model, thinking level and active tools. For extension
|
|
185
|
+
sessions no active tool may be outside the composed allowlist, but fewer
|
|
186
|
+
active tools are allowed: an extension may deactivate tools, as
|
|
187
|
+
`pi-cursor-sdk` does with its own tool in `session_start`. Sessions without
|
|
188
|
+
extensions keep the exact-equality check as today.
|
|
189
|
+
|
|
190
|
+
Step 3, step 6 and step 7 are each bounded by the existing session-creation
|
|
191
|
+
deadline (`SESSION_CREATION_TIMEOUT_MS`). A failure after step 6 disposes the
|
|
192
|
+
session through the lifecycle path, which emits `session_shutdown`. A session
|
|
193
|
+
that resolves after its deadline is disposed the same way, never admitted.
|
|
194
|
+
Extension code loaded in step 3 cannot be unloaded; a failure between step 3
|
|
195
|
+
and step 6 leaves no session to dispose.
|
|
196
|
+
|
|
197
|
+
### Tools, hooks and prompts
|
|
198
|
+
|
|
199
|
+
- **`read-only`.** The tool list is `read`, `grep`, `find`, `ls`, exactly as
|
|
200
|
+
today. Extension tools are registered but inactive. An extension that calls
|
|
201
|
+
`setActiveTools` cannot activate a tool outside the list; a contract test
|
|
202
|
+
proves this against the real SDK. If the SDK does not enforce it, the
|
|
203
|
+
adapter adds its own check before the implementation is accepted.
|
|
204
|
+
- **`coding`.** The tool list is the built-in coding tools, the granted MCP
|
|
205
|
+
tools, and every tool registered by the profile's extensions. The adapter
|
|
206
|
+
reads the registered tool names from the services' resource loader after
|
|
207
|
+
step 3.
|
|
208
|
+
- **Name conflicts, both profiles.** An extension tool whose name equals a
|
|
209
|
+
built-in tool (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`), a
|
|
210
|
+
granted MCP tool or a tool of another extension fails with
|
|
211
|
+
`extension_tool_conflict`. This also stops an extension from replacing a
|
|
212
|
+
built-in tool inside a `read-only` profile.
|
|
213
|
+
A name must also match `[A-Za-z0-9_.:][A-Za-z0-9_.:-]*`: the SDK reads `*`
|
|
214
|
+
in an allowlist as a glob and a leading `+` or `-` as a modifier, so any
|
|
215
|
+
other name fails the same way, in both profiles.
|
|
216
|
+
The check runs after load and again after binding, because a `session_start`
|
|
217
|
+
handler can register a tool during `bindExtensions`. If the registered tool
|
|
218
|
+
set then differs from the one the session was created with, the start fails
|
|
219
|
+
with `extension_tool_conflict`. Tools an extension registers later, during a
|
|
220
|
+
turn, are not re-checked; allowlisted extensions are trusted code.
|
|
221
|
+
- **Hook errors while binding.** A hook error reported while binding fails the
|
|
222
|
+
start with `extension_load_failed`. Failure cleanup disposes the session
|
|
223
|
+
runtime, bounded by the creation deadline, concurrently with closing the
|
|
224
|
+
granted MCP connections.
|
|
225
|
+
- **MCP servers registered by extensions.** An extension that registers an MCP
|
|
226
|
+
server while loading or starting fails the start with
|
|
227
|
+
`extension_load_failed`, because the built-in MCP extension is not loaded.
|
|
228
|
+
- **Hooks.** All extension hooks run in both permission profiles, including
|
|
229
|
+
`before_agent_start` system-prompt changes, `tool_call`, `tool_result`,
|
|
230
|
+
`before_provider_request`, `before_provider_headers` and `context`.
|
|
231
|
+
- **Prompts.** Every `session.prompt()` call, for every profile, passes
|
|
232
|
+
`expandPromptTemplates: false`. Client prompt text never dispatches an
|
|
233
|
+
extension command, a skill command or a prompt template.
|
|
234
|
+
- **UI and flags.** No UI context is bound; extension UI calls return the
|
|
235
|
+
SDK's no-op results. `extensionFlagValues` is empty, so extension flags keep
|
|
236
|
+
their defaults.
|
|
237
|
+
|
|
238
|
+
### Lifecycle and close
|
|
239
|
+
|
|
240
|
+
The handle of an extension session owns the `AgentSessionRuntime` that wraps
|
|
241
|
+
its services and session. `close`, the close cleanup after an abort deadline,
|
|
242
|
+
registry shutdown and late-start disposal call `AgentSessionRuntime.dispose()`,
|
|
243
|
+
which emits `session_shutdown` and disposes the session. Existing deadlines,
|
|
244
|
+
idempotent close and tombstones do not change. MCP connections close
|
|
245
|
+
concurrently with the runtime, as they close concurrently with the session
|
|
246
|
+
today; the handle's disposal completes when both have settled.
|
|
247
|
+
|
|
248
|
+
How the session is wrapped in an `AgentSessionRuntime` uses only the exported
|
|
249
|
+
`createAgentSessionRuntime` and `AgentSessionRuntime` API. The exact wiring is
|
|
250
|
+
fixed in the implementation plan and proven by a contract test that observes
|
|
251
|
+
`session_shutdown`.
|
|
252
|
+
|
|
253
|
+
### stdout guard
|
|
254
|
+
|
|
255
|
+
A new module `src/stdout-guard.ts` runs first in `src/main.ts`, before any
|
|
256
|
+
other import with side effects:
|
|
257
|
+
|
|
258
|
+
- It captures the original `process.stdout.write`.
|
|
259
|
+
- It builds a write-through `Proxy` over the real stdout: `write` and `end`
|
|
260
|
+
resolve to the original bound methods and every other function is bound to
|
|
261
|
+
the real stream, so queued frames reach stdout synchronously. `main` passes
|
|
262
|
+
`new StdioServerTransport(process.stdin, thatProxy)` to `serveStdio`
|
|
263
|
+
through its `transport` option.
|
|
264
|
+
- It also patches `process.stdout.end` for foreign callers: a foreign `end`
|
|
265
|
+
neither leaks content nor closes the real stdout.
|
|
266
|
+
- It replaces `process.stdout.write` and `console.log`, `console.info` and
|
|
267
|
+
`console.debug`. Their output is dropped. The guard writes at most one
|
|
268
|
+
structured stderr line per second, carrying the bytes and the number of
|
|
269
|
+
writes suppressed since its previous line:
|
|
270
|
+
`{"level":"warning","event":"foreign_stdout_suppressed","bytes":<n>,"writes":<m>}`.
|
|
271
|
+
Pending counts are flushed on shutdown. The content is never written
|
|
272
|
+
anywhere, because it may contain prompts or secrets.
|
|
273
|
+
|
|
274
|
+
The guard is active for every server start, not only with extensions. A child
|
|
275
|
+
process started with inherited stdio writes to file descriptor 1 directly and
|
|
276
|
+
cannot be intercepted in Node.js; this is a documented residual risk.
|
|
277
|
+
|
|
278
|
+
### Capabilities projection
|
|
279
|
+
|
|
280
|
+
`pi_capabilities_get` adds `extensions` to every execution profile: an array of
|
|
281
|
+
the configured spec strings, empty for profiles without extensions. The field
|
|
282
|
+
is path-free and secret-free. `capabilitiesSchema` and
|
|
283
|
+
`docs/tool-contracts.md` change accordingly.
|
|
284
|
+
|
|
285
|
+
### Doctor
|
|
286
|
+
|
|
287
|
+
Doctor gains two checks after the workspace checks, one per profile and
|
|
288
|
+
extension, with the subject `<profile>:<package name>`:
|
|
289
|
+
|
|
290
|
+
- `extension_missing` (error) when the resolver finds no installed package;
|
|
291
|
+
- `extension_version_mismatch` (error) when the installed manifest version
|
|
292
|
+
differs, or the manifest is unreadable.
|
|
293
|
+
|
|
294
|
+
Doctor reads manifests only; it never imports extension code. It stays
|
|
295
|
+
offline. `setup --verify` includes Doctor and therefore these checks.
|
|
296
|
+
|
|
297
|
+
### Diagnostics
|
|
298
|
+
|
|
299
|
+
The adapter's diagnostic sink gains the stages `extension_resolve`,
|
|
300
|
+
`extension_load` and `extension_bind`. A diagnostic carries the profile alias
|
|
301
|
+
and the error code; a resolution failure also carries the failing extension as
|
|
302
|
+
`<name>@<version>`. Disposal stays best effort and reports nothing, as today. It never carries a path, an extension error message, or prompt content.
|
|
303
|
+
|
|
304
|
+
## Failure semantics
|
|
305
|
+
|
|
306
|
+
New `PiSessionCreationErrorCode` values, all fail closed before a session is
|
|
307
|
+
admitted:
|
|
308
|
+
|
|
309
|
+
| Code | Message | When |
|
|
310
|
+
| --- | --- | --- |
|
|
311
|
+
| `extension_not_installed` | Configured Pi extension is not installed | Pi has no user install of the package. |
|
|
312
|
+
| `extension_version_mismatch` | Configured Pi extension version is not installed | The installed version differs from the pinned version. |
|
|
313
|
+
| `extension_load_failed` | Configured Pi extension could not be loaded | Invalid manifest, entry outside the package, load or bind error, services diagnostic of type `error`. |
|
|
314
|
+
| `extension_tool_conflict` | Configured Pi extension tools conflict | An extension tool name collides with a built-in, MCP or other extension tool. |
|
|
315
|
+
|
|
316
|
+
Existing codes keep their meaning. An extension provider that is unknown after
|
|
317
|
+
loading is `unknown_provider`.
|
|
318
|
+
|
|
319
|
+
Known limit: `pi-cursor-sdk` returns a placeholder key when no Cursor key is
|
|
320
|
+
stored, so `checkAuth` and `getAuth` succeed and the first prompt fails with
|
|
321
|
+
`turn_failed`. Detecting the placeholder would depend on extension internals;
|
|
322
|
+
the limit is documented instead.
|
|
323
|
+
|
|
324
|
+
## Trust boundary and limits
|
|
325
|
+
|
|
326
|
+
- An allowlisted extension is code the operator trusts. It runs in the server
|
|
327
|
+
process with the user's full rights and may start processes, use the
|
|
328
|
+
network, and read and write files. `pi-cursor-sdk` writes below
|
|
329
|
+
`~/.pi/agent`, `~/.cursor/projects/` and `<workspace>/.pi/`.
|
|
330
|
+
- Pinning the version prevents silent updates. It does not protect against a
|
|
331
|
+
modified installed package directory, which is user-writable code like any
|
|
332
|
+
other.
|
|
333
|
+
- `read-only` limits the model's tool set, not the extension's code.
|
|
334
|
+
- pi-session-mcp keeps its own state in memory only. Extensions may persist
|
|
335
|
+
their own state outside its control. AGENTS.md and `docs/architecture.md`
|
|
336
|
+
state this next to the product boundary.
|
|
337
|
+
- The MCP client remains one trusted local peer. It selects profile aliases
|
|
338
|
+
only and cannot name an extension.
|
|
339
|
+
|
|
340
|
+
## Testing
|
|
341
|
+
|
|
342
|
+
Automated tests use no credentials and no network.
|
|
343
|
+
|
|
344
|
+
- **Config** (`test/config.test.ts`): valid scoped and unscoped specs;
|
|
345
|
+
rejection of ranges, tags, `git:`, paths, duplicates and more than 8
|
|
346
|
+
entries; the field on read-only and coding profiles.
|
|
347
|
+
- **Resolution** (`test/pi-extensions.test.ts`), with an injected install-location
|
|
348
|
+
lookup and file system: not installed, version mismatch, invalid manifest,
|
|
349
|
+
missing `pi.extensions`, an entry or symlink escaping the package directory,
|
|
350
|
+
and the guarantee that `resolve()` and install methods are never called.
|
|
351
|
+
- **Doctor** (`test/doctor.test.ts`): both new checks, check order, and that
|
|
352
|
+
no extension module is imported.
|
|
353
|
+
- **Adapter** (`test/sdk-pi-adapter.test.ts`), with injected services and
|
|
354
|
+
session factories:
|
|
355
|
+
- step order, and the provider being visible only after services;
|
|
356
|
+
- two sessions getting two runtimes;
|
|
357
|
+
- `bindExtensions` being called;
|
|
358
|
+
- `session_shutdown` on close, on a failure after services, and on a late
|
|
359
|
+
start after the deadline;
|
|
360
|
+
- tool lists for read-only and coding, and `extension_tool_conflict`;
|
|
361
|
+
- `expandPromptTemplates: false` on every prompt;
|
|
362
|
+
- profiles without extensions taking the unchanged path.
|
|
363
|
+
- **stdout guard** (`test/stdout-guard.test.ts`): foreign `write` and
|
|
364
|
+
`console.*` output does not reach stdout, transport writes do, and the
|
|
365
|
+
stderr event carries only a byte count. Regressions cover synchronous
|
|
366
|
+
delivery of every queued transport frame and a foreign `end` that must not
|
|
367
|
+
close stdout.
|
|
368
|
+
- **SDK contract with the real SDK** (`test/sdk-extension-contract.test.ts`):
|
|
369
|
+
a fixture extension in `test/fixtures/pi-extension-fixture`, installed into a
|
|
370
|
+
temporary Pi agent directory, registers:
|
|
371
|
+
- a provider with a canned, network-free stream;
|
|
372
|
+
- a tool and a slash command;
|
|
373
|
+
- a `setActiveTools` attempt;
|
|
374
|
+
- `console.log` and `process.stdout.write` calls;
|
|
375
|
+
- `session_start` and `session_shutdown` hooks;
|
|
376
|
+
- a `before_agent_start` hook that repeats the widening attempt (adding
|
|
377
|
+
`bash`, `write`, `edit` and the fixture tool) on every prompt.
|
|
378
|
+
|
|
379
|
+
The test proves that:
|
|
380
|
+
- the provider resolves and a prompt completes;
|
|
381
|
+
- read-only keeps the fixture tool and the built-in write tools inactive,
|
|
382
|
+
including against the mid-turn `before_agent_start` widening attempt;
|
|
383
|
+
- the slash command is not dispatched;
|
|
384
|
+
- both lifecycle events are observed.
|
|
385
|
+
- **stdio end to end** (`test/process-extension.test.ts`): the built server runs
|
|
386
|
+
with a configuration that uses the fixture extension. Every stdout frame
|
|
387
|
+
parses, and stderr contains `foreign_stdout_suppressed`.
|
|
388
|
+
|
|
389
|
+
## Documentation and governance
|
|
390
|
+
|
|
391
|
+
- ADR 0006 records the decision and its trust boundary.
|
|
392
|
+
- `docs/installation.md`: the `extensions` rule in the configuration section.
|
|
393
|
+
- `docs/tool-contracts.md`: the capabilities field and the new error codes.
|
|
394
|
+
- `docs/threat-model.md` and `docs/architecture.md`: the trust boundary, the
|
|
395
|
+
stdout guard and its residual risk, and extension-owned persistence.
|
|
396
|
+
- `docs/doctor-design.md`: the two new checks.
|
|
397
|
+
- A new section "Pi extensions" in `docs/installation.md` with the
|
|
398
|
+
`pi-cursor-sdk` example and the placeholder-key limit.
|
|
399
|
+
- AGENTS.md: the product-boundary sentence names extension-owned persistence
|
|
400
|
+
and the allowlist rule.
|
|
401
|
+
- `docs/acceptance.md`: the manual live check below.
|
|
402
|
+
- CHANGELOG.md: an entry under Unreleased.
|
|
403
|
+
|
|
404
|
+
## Acceptance on real hardware
|
|
405
|
+
|
|
406
|
+
Manual, not in CI, recorded in `docs/acceptance.md`:
|
|
407
|
+
|
|
408
|
+
1. `pi-cursor-sdk@0.5.3` installed by Pi with a stored Cursor key.
|
|
409
|
+
2. A `code-cursor` profile: `pi_session_start`, a "PONG" prompt, `pi_turn_get`
|
|
410
|
+
completed, `pi_session_close`.
|
|
411
|
+
3. Two parallel `cursor` sessions, each completing a prompt.
|
|
412
|
+
4. After close, no Cursor child process remains, and stderr shows no
|
|
413
|
+
`foreign_stdout_suppressed` storm.
|
|
414
|
+
5. Record how the Cursor local runtime starts its child processes (inherited or
|
|
415
|
+
piped stdio).
|
|
416
|
+
|
|
417
|
+
## Delivery
|
|
418
|
+
|
|
419
|
+
One branch (`feat/pi-extensions`) and one pull request for #34, with commits
|
|
420
|
+
grouped by concern. The owner's live configuration gets a `cursor` profile only
|
|
421
|
+
after the acceptance above.
|
package/docs/releasing.md
CHANGED
|
@@ -12,7 +12,7 @@ does not authorize any of them.
|
|
|
12
12
|
[ADR 0003](adr/0003-post-mvp-maintenance.md) sets the rule:
|
|
13
13
|
|
|
14
14
|
- A change that keeps the public MCP contract ships as a patch release
|
|
15
|
-
(`0.
|
|
15
|
+
(for example `0.7.x`). This includes Pi SDK updates.
|
|
16
16
|
- A change to the public contract needs a minor release and its own decision.
|
|
17
17
|
|
|
18
18
|
The public contract is the set of eight tools with their fields, states and
|