@ikenga/contract 0.5.0 → 0.6.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 (48) hide show
  1. package/dist/browser.d.ts +624 -0
  2. package/dist/browser.d.ts.map +1 -0
  3. package/dist/browser.js +264 -0
  4. package/dist/browser.js.map +1 -0
  5. package/dist/engine/acp.d.ts +271 -0
  6. package/dist/engine/acp.d.ts.map +1 -0
  7. package/dist/engine/acp.js +13 -0
  8. package/dist/engine/acp.js.map +1 -0
  9. package/dist/{engine.d.ts → engine/adapter.d.ts} +60 -243
  10. package/dist/engine/adapter.d.ts.map +1 -0
  11. package/dist/{engine.js → engine/adapter.js} +14 -6
  12. package/dist/engine/adapter.js.map +1 -0
  13. package/dist/engine/errors.d.ts +17 -0
  14. package/dist/engine/errors.d.ts.map +1 -0
  15. package/dist/engine/errors.js +19 -0
  16. package/dist/engine/errors.js.map +1 -0
  17. package/dist/engine/index.d.ts +12 -0
  18. package/dist/engine/index.d.ts.map +1 -0
  19. package/dist/engine/index.js +12 -0
  20. package/dist/engine/index.js.map +1 -0
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/manifest.d.ts +147 -0
  26. package/dist/manifest.d.ts.map +1 -1
  27. package/dist/manifest.js +32 -1
  28. package/dist/manifest.js.map +1 -1
  29. package/dist/registry.d.ts +216 -0
  30. package/dist/registry.d.ts.map +1 -1
  31. package/dist/registry.js +23 -0
  32. package/dist/registry.js.map +1 -1
  33. package/dist/rpc.d.ts +1 -1
  34. package/dist/rpc.d.ts.map +1 -1
  35. package/package.json +7 -3
  36. package/src/browser.test.ts +350 -0
  37. package/src/browser.ts +364 -0
  38. package/src/{engine.ts → engine/acp.ts} +49 -198
  39. package/src/engine/adapter.ts +243 -0
  40. package/src/{engine.test.ts → engine/engine.test.ts} +33 -2
  41. package/src/engine/errors.ts +20 -0
  42. package/src/engine/index.ts +12 -0
  43. package/src/index.ts +2 -1
  44. package/src/manifest.ts +35 -1
  45. package/src/registry.ts +25 -0
  46. package/src/rpc.ts +1 -1
  47. package/dist/engine.d.ts.map +0 -1
  48. package/dist/engine.js.map +0 -1
package/src/browser.ts ADDED
@@ -0,0 +1,364 @@
1
+ // pkg-browser MCP I/O types.
2
+ //
3
+ // Shared by the `@ikenga/mcp-browser` MCP server (which exposes these as
4
+ // tool inputs/outputs to Claude), the shell's `/iyke/browser/*` HTTP
5
+ // handlers (which validate them on the wire), and any other client that
6
+ // drives a child-webview pane.
7
+ //
8
+ // Pane addressing: the MCP server allocates `b1, b2, …` ids on `open()`
9
+ // and maps them internally to the kernel's `(pkg_id, pane_id)` pair. The
10
+ // kernel identifiers stay private; agents only see `bN`.
11
+ //
12
+ // Element refs: a snapshot returns opaque `e0, e1, …` refs that map to
13
+ // live DOM nodes inside the child webview. Refs invalidate on the next
14
+ // snapshot or page navigation — the same convention used by Iyke's
15
+ // `iyke_dom` so a model that knows one drives the other.
16
+ //
17
+ // This module is intentionally Zod-only (no separate JSON Schema export).
18
+ // Consumers that need JSON Schema can call `zodToJsonSchema()` themselves;
19
+ // the schema generator under `scripts/generate-schemas.mjs` does not need
20
+ // to know about browser tools.
21
+
22
+ import { z } from 'zod';
23
+
24
+ // ---------- Brands ----------
25
+
26
+ /** Pattern for pane ids allocated by the MCP server. */
27
+ export const BROWSER_PANE_ID_PATTERN = /^b[1-9]\d*$/;
28
+
29
+ /** Pattern for element refs returned by a snapshot. */
30
+ export const BROWSER_REF_PATTERN = /^e\d+$/;
31
+
32
+ export const BrowserPaneIdSchema = z
33
+ .string()
34
+ .regex(BROWSER_PANE_ID_PATTERN, 'must match /^b[1-9]\\d*$/ (e.g. "b1", "b42")')
35
+ .brand<'BrowserPaneId'>();
36
+
37
+ export type BrowserPaneId = z.infer<typeof BrowserPaneIdSchema>;
38
+
39
+ export const BrowserRefSchema = z
40
+ .string()
41
+ .regex(BROWSER_REF_PATTERN, 'must match /^e\\d+$/ (e.g. "e0", "e12")')
42
+ .brand<'BrowserRef'>();
43
+
44
+ export type BrowserRef = z.infer<typeof BrowserRefSchema>;
45
+
46
+ // ---------- Snapshot ----------
47
+ //
48
+ // Mirrors the shape of Iyke's `DomResult` so models can move between the
49
+ // two MCPs without re-learning the format. `text` is the Playwright-style
50
+ // plaintext tree; `nodes` is the structured form keyed by ref.
51
+
52
+ export const BrowserSnapshotNodeSchema = z.object({
53
+ ref: BrowserRefSchema,
54
+ role: z.string(),
55
+ /** Accessible name (label, alt text, innerText, etc.). */
56
+ name: z.string().optional(),
57
+ /** Current value for inputs / selects / textareas. */
58
+ value: z.string().optional(),
59
+ /** Best-effort tag name for non-ARIA debugging. */
60
+ tag: z.string().optional(),
61
+ /** Truthy iff `checked`/`aria-checked` is set on a checkbox/radio. */
62
+ checked: z.boolean().optional(),
63
+ /** Truthy iff the element is `disabled`/`aria-disabled`. */
64
+ disabled: z.boolean().optional(),
65
+ /** Truthy iff `aria-expanded === "true"`. */
66
+ expanded: z.boolean().optional(),
67
+ /** Truthy iff `aria-selected === "true"`. */
68
+ selected: z.boolean().optional(),
69
+ /** Hidden / aria-hidden / display:none. Always set when `all=true`. */
70
+ hidden: z.boolean().optional(),
71
+ /** Refs of direct children, in document order. Omitted on leaves. */
72
+ children: z.array(BrowserRefSchema).optional(),
73
+ });
74
+
75
+ export type BrowserSnapshotNode = z.infer<typeof BrowserSnapshotNodeSchema>;
76
+
77
+ export const BrowserSnapshotSchema = z.object({
78
+ id: BrowserPaneIdSchema,
79
+ url: z.string(),
80
+ title: z.string(),
81
+ /** Playwright-style accessibility tree, ref-annotated. */
82
+ text: z.string(),
83
+ /** Flat list keyed by ref. The first entry is the document root. */
84
+ nodes: z.array(BrowserSnapshotNodeSchema),
85
+ /** Monotonic counter for this pane; used to detect stale refs. */
86
+ snapshotId: z.number().int().nonnegative(),
87
+ });
88
+
89
+ export type BrowserSnapshot = z.infer<typeof BrowserSnapshotSchema>;
90
+
91
+ // ---------- Pane lifecycle ----------
92
+
93
+ const RectSchema = z.object({
94
+ x: z.number().int(),
95
+ y: z.number().int(),
96
+ w: z.number().int().positive(),
97
+ h: z.number().int().positive(),
98
+ });
99
+
100
+ export type BrowserRect = z.infer<typeof RectSchema>;
101
+
102
+ export const BrowserOpenInputSchema = z
103
+ .object({
104
+ url: z.string().url(),
105
+ /**
106
+ * Named cookie/storage jar slug. Must be declared in the pkg manifest's
107
+ * `capabilities.webview.partitions` unless it's `"default"`. Mutually
108
+ * exclusive with `session`.
109
+ */
110
+ partition: z.string().optional(),
111
+ /**
112
+ * Named-session handle (see {@link BrowserSessionSchema}). The MCP
113
+ * server resolves this to a partition before calling the shell, so
114
+ * multiple sessions can share an underlying partition if the caller
115
+ * declared it that way at create time.
116
+ */
117
+ session: z.string().optional(),
118
+ /**
119
+ * Optional initial rect in main-window-client coordinates. Omitted when
120
+ * the FE owns layout (the kernel will refuse without a rect; the MCP
121
+ * layer is expected to supply a sane default if the caller didn't).
122
+ */
123
+ rect: RectSchema.optional(),
124
+ })
125
+ .refine((v) => !(v.partition && v.session), {
126
+ message: 'pass at most one of `partition` / `session`',
127
+ });
128
+
129
+ export type BrowserOpenInput = z.infer<typeof BrowserOpenInputSchema>;
130
+
131
+ export const BrowserOpenResultSchema = z.object({
132
+ id: BrowserPaneIdSchema,
133
+ url: z.string(),
134
+ partition: z.string(),
135
+ });
136
+
137
+ export type BrowserOpenResult = z.infer<typeof BrowserOpenResultSchema>;
138
+
139
+ export const BrowserPaneIdInputSchema = z.object({ id: BrowserPaneIdSchema });
140
+ export type BrowserPaneIdInput = z.infer<typeof BrowserPaneIdInputSchema>;
141
+
142
+ export const BrowserOkResultSchema = z.object({ ok: z.literal(true) });
143
+ export type BrowserOkResult = z.infer<typeof BrowserOkResultSchema>;
144
+
145
+ export const BrowserListEntrySchema = z.object({
146
+ id: BrowserPaneIdSchema,
147
+ url: z.string(),
148
+ partition: z.string(),
149
+ focused: z.boolean(),
150
+ });
151
+
152
+ export type BrowserListEntry = z.infer<typeof BrowserListEntrySchema>;
153
+
154
+ export const BrowserListResultSchema = z.object({
155
+ panes: z.array(BrowserListEntrySchema),
156
+ });
157
+
158
+ export type BrowserListResult = z.infer<typeof BrowserListResultSchema>;
159
+
160
+ // ---------- Navigation ----------
161
+
162
+ export const BrowserGotoInputSchema = z.object({
163
+ id: BrowserPaneIdSchema,
164
+ url: z.string().url(),
165
+ });
166
+
167
+ export type BrowserGotoInput = z.infer<typeof BrowserGotoInputSchema>;
168
+
169
+ export const BrowserNavResultSchema = z.object({
170
+ id: BrowserPaneIdSchema,
171
+ url: z.string(),
172
+ });
173
+
174
+ export type BrowserNavResult = z.infer<typeof BrowserNavResultSchema>;
175
+
176
+ // ---------- Inspection ----------
177
+
178
+ export const BrowserSnapshotInputSchema = z.object({
179
+ id: BrowserPaneIdSchema,
180
+ /** Substring filter against role/name/value. */
181
+ query: z.string().optional(),
182
+ /** Include hidden + aria-hidden elements. */
183
+ all: z.boolean().optional(),
184
+ });
185
+
186
+ export type BrowserSnapshotInput = z.infer<typeof BrowserSnapshotInputSchema>;
187
+
188
+ export const BrowserReadTextInputSchema = z.object({
189
+ id: BrowserPaneIdSchema,
190
+ ref: BrowserRefSchema,
191
+ });
192
+
193
+ export type BrowserReadTextInput = z.infer<typeof BrowserReadTextInputSchema>;
194
+
195
+ export const BrowserReadTextResultSchema = z.object({
196
+ text: z.string(),
197
+ });
198
+
199
+ export type BrowserReadTextResult = z.infer<typeof BrowserReadTextResultSchema>;
200
+
201
+ export const BrowserScreenshotInputSchema = z.object({
202
+ id: BrowserPaneIdSchema,
203
+ /** Override default output path (`~/.local/share/ikenga/screenshots/…`). */
204
+ out_path: z.string().optional(),
205
+ });
206
+
207
+ export type BrowserScreenshotInput = z.infer<typeof BrowserScreenshotInputSchema>;
208
+
209
+ export const BrowserScreenshotResultSchema = z.object({
210
+ path: z.string(),
211
+ width: z.number().int().positive(),
212
+ height: z.number().int().positive(),
213
+ bytes: z.number().int().nonnegative(),
214
+ });
215
+
216
+ export type BrowserScreenshotResult = z.infer<typeof BrowserScreenshotResultSchema>;
217
+
218
+ // ---------- Interaction ----------
219
+ //
220
+ // `click` / `fill` / `select` / `press_key` all accept either a `ref` (from
221
+ // a prior snapshot, most reliable), a CSS `selector`, or — for `click` —
222
+ // an innerText `text` match. The MCP server enforces exactly-one; the
223
+ // schema accepts any combination so the validator can return a clearer
224
+ // error message than Zod's `refine` would.
225
+
226
+ const InteractionTargetFields = {
227
+ ref: BrowserRefSchema.optional(),
228
+ selector: z.string().optional(),
229
+ } as const;
230
+
231
+ export const BrowserClickInputSchema = z.object({
232
+ id: BrowserPaneIdSchema,
233
+ ...InteractionTargetFields,
234
+ /** innerText substring match. Lower-priority than ref/selector. */
235
+ text: z.string().optional(),
236
+ });
237
+
238
+ export type BrowserClickInput = z.infer<typeof BrowserClickInputSchema>;
239
+
240
+ export const BrowserFillInputSchema = z.object({
241
+ id: BrowserPaneIdSchema,
242
+ ...InteractionTargetFields,
243
+ text: z.string(),
244
+ /** Default false (append). True overwrites the existing value. */
245
+ replace: z.boolean().optional(),
246
+ });
247
+
248
+ export type BrowserFillInput = z.infer<typeof BrowserFillInputSchema>;
249
+
250
+ export const BrowserSelectInputSchema = z.object({
251
+ id: BrowserPaneIdSchema,
252
+ ...InteractionTargetFields,
253
+ /** Option `value` attribute (preferred) or visible label. */
254
+ value: z.string(),
255
+ });
256
+
257
+ export type BrowserSelectInput = z.infer<typeof BrowserSelectInputSchema>;
258
+
259
+ export const BrowserPressKeyInputSchema = z.object({
260
+ id: BrowserPaneIdSchema,
261
+ ...InteractionTargetFields,
262
+ /** Key combo, e.g. "Enter", "Escape", "Ctrl+S", "Meta+K", "Shift+Tab". */
263
+ combo: z.string(),
264
+ });
265
+
266
+ export type BrowserPressKeyInput = z.infer<typeof BrowserPressKeyInputSchema>;
267
+
268
+ // ---------- wait_for ----------
269
+
270
+ export const BROWSER_WAIT_FOR_KINDS = [
271
+ 'url',
272
+ 'ref',
273
+ 'text',
274
+ 'gone-text',
275
+ 'selector',
276
+ 'gone-selector',
277
+ 'idle',
278
+ ] as const;
279
+
280
+ export type BrowserWaitForKind = (typeof BROWSER_WAIT_FOR_KINDS)[number];
281
+
282
+ export const BrowserWaitForInputSchema = z.object({
283
+ id: BrowserPaneIdSchema,
284
+ kind: z.enum(BROWSER_WAIT_FOR_KINDS),
285
+ /**
286
+ * Predicate value. Required for `url`/`ref`/`text`; ignored for `idle`.
287
+ * - `url`: substring match against `location.href`.
288
+ * - `ref`: wait for an element with this ref to appear in the next
289
+ * snapshot (i.e. snapshot is re-taken until the ref exists).
290
+ * - `text`: substring match against the snapshot's rendered text.
291
+ */
292
+ value: z.string().optional(),
293
+ /** Default 10_000ms; clamped to [100, 60_000] by the server. */
294
+ timeout_ms: z.number().int().min(100).max(60_000).optional(),
295
+ });
296
+
297
+ export type BrowserWaitForInput = z.infer<typeof BrowserWaitForInputSchema>;
298
+
299
+ export const BrowserWaitForResultSchema = z.object({
300
+ satisfied: z.boolean(),
301
+ elapsed_ms: z.number().int().nonnegative(),
302
+ message: z.string().optional(),
303
+ });
304
+
305
+ export type BrowserWaitForResult = z.infer<typeof BrowserWaitForResultSchema>;
306
+
307
+ // ---------- eval (escape hatch) ----------
308
+ //
309
+ // Not surfaced through the MCP tool list by default; exposed only for the
310
+ // pkg-browser MCP's own internal scripting (a11y snapshot injection,
311
+ // console hooks). FE / Tauri-IPC callers never see this.
312
+
313
+ export const BrowserEvalInputSchema = z.object({
314
+ id: BrowserPaneIdSchema,
315
+ /**
316
+ * JavaScript source to evaluate in the child webview's page context.
317
+ * Fire-and-forget at the kernel layer; the caller is expected to ferry
318
+ * results back via a known callback shape if needed.
319
+ */
320
+ script: z.string(),
321
+ });
322
+
323
+ export type BrowserEvalInput = z.infer<typeof BrowserEvalInputSchema>;
324
+
325
+ // ---------- Named sessions (Phase 4) ----------
326
+ //
327
+ // A session is a human-friendly handle for a cookie/storage partition. The
328
+ // kernel's on-disk partition data is the source of truth; this table just
329
+ // maps `(pkg_id, name) → partition`. Deleting a session preserves the
330
+ // partition data — a future create with the same partition slug picks
331
+ // the cookies back up.
332
+ //
333
+ // Timestamps are unix milliseconds (integers), matching the shell's
334
+ // SQLite-backed storage. JSON callers should treat them as opaque ints.
335
+
336
+ export const BrowserSessionSchema = z.object({
337
+ pkg_id: z.string(),
338
+ name: z.string().min(1).max(64),
339
+ partition: z.string(),
340
+ created_at: z.number().int().nonnegative(),
341
+ last_used_at: z.number().int().nonnegative().nullable().optional(),
342
+ });
343
+
344
+ export type BrowserSession = z.infer<typeof BrowserSessionSchema>;
345
+
346
+ export const BrowserSessionCreateInputSchema = z.object({
347
+ name: z.string().min(1).max(64),
348
+ /** Defaults to a sanitized form of `name` if omitted. */
349
+ partition: z.string().optional(),
350
+ });
351
+
352
+ export type BrowserSessionCreateInput = z.infer<typeof BrowserSessionCreateInputSchema>;
353
+
354
+ export const BrowserSessionDeleteInputSchema = z.object({
355
+ name: z.string().min(1).max(64),
356
+ });
357
+
358
+ export type BrowserSessionDeleteInput = z.infer<typeof BrowserSessionDeleteInputSchema>;
359
+
360
+ export const BrowserSessionListResultSchema = z.object({
361
+ sessions: z.array(BrowserSessionSchema),
362
+ });
363
+
364
+ export type BrowserSessionListResult = z.infer<typeof BrowserSessionListResultSchema>;
@@ -1,205 +1,16 @@
1
- // Engine adapter contract. Implementations:
2
- // - engine-claude-code (default, ships preinstalled)
3
- // - engine-codex (future)
4
- // - engine-aider (future)
5
- // - engine-noop (testing / shell-without-AI mode)
6
-
7
- import { z } from 'zod';
8
-
9
- export interface SessionOpts {
10
- cwd?: string;
11
- systemPrompt?: string;
12
- toolAllowList?: string[];
13
- /** Caller pkg id, used by the engine for per-pkg billing/audit. */
14
- callerPkg?: string;
15
- }
16
-
17
- export interface Session {
18
- readonly id: string;
19
- /** Best-effort cancellation. Resolves once the engine has stopped streaming. */
20
- cancel(): Promise<void>;
21
- }
22
-
23
- export type EngineEvent =
24
- | { type: 'message_delta'; text: string }
25
- | { type: 'tool_use'; tool: string; input: unknown; toolUseId: string }
26
- | { type: 'tool_result'; toolUseId: string; output: unknown; isError?: boolean }
27
- | { type: 'thinking_delta'; text: string }
28
- | { type: 'usage'; inputTokens: number; outputTokens: number; cacheCreationTokens?: number; cacheReadTokens?: number }
29
- | { type: 'done'; reason: 'stop' | 'cancel' | 'error'; error?: string };
30
-
31
- export interface McpServerSpec {
32
- id: string;
33
- command: string;
34
- args?: string[];
35
- env?: Record<string, string>;
36
- }
37
-
38
- // ---------- Capabilities (single source of truth) ----------
39
-
40
- /**
41
- * Capability flags every engine adapter advertises. This is the canonical
42
- * shape consumed by both the shell-side ChatAdapter layer and any pkg that
43
- * needs to reason about adapter features (wizard, settings UI, telemetry).
44
- *
45
- * Fields are intentionally a *superset* of what any single adapter
46
- * supports — an adapter sets the ones it implements to `true` and the
47
- * rest to `false`. Adding a new flag is a non-breaking schema change so
48
- * long as adapters default it to `false` until they implement it.
49
- */
50
- export const AgentCapabilitiesSchema = z.object({
51
- /** Streams partial responses as they arrive (vs. all-at-once). */
52
- streaming: z.boolean(),
53
- /** Invokes external tools / function calls during a turn. */
54
- toolUse: z.boolean(),
55
- /** Emits an extended-thinking / reasoning channel separate from output. */
56
- thinking: z.boolean(),
57
- /** Produces structured artifacts (code blocks, files, images) the host renders. */
58
- artifacts: z.boolean(),
59
- /** Accepts file attachments as part of an input. */
60
- fileAttachments: z.boolean(),
61
- /** Accepts image input (vision). */
62
- imageInput: z.boolean(),
63
- /** Recognises a `/slash` command vocabulary. */
64
- slashCommands: z.boolean(),
65
- /** Lets the user switch models mid-thread. */
66
- modelSwitching: z.boolean(),
67
- /** Uses prompt-caching for repeated context (Anthropic-specific today). */
68
- promptCaching: z.boolean(),
69
- /** Runs agentic tools (recursive sub-agents, long-running loops). */
70
- agenticTools: z.boolean(),
71
- /** Speaks MCP — can register and route through MCP servers. */
72
- mcp: z.boolean(),
73
- /** Can resume a prior session by id. */
74
- sessionResume: z.boolean(),
75
- });
76
- export type AgentCapabilities = z.infer<typeof AgentCapabilitiesSchema>;
77
-
78
- // ---------- Engine pkg manifest "engine" block ----------
79
-
80
- /**
81
- * Per-adapter onboarding requirements surfaced in the first-run wizard.
82
- * The wizard composes these instead of hardcoding per-agent forms —
83
- * every engine pkg owns its own setup story.
84
- */
85
- export const EngineOnboardingSchema = z.object({
86
- /** Stronghold-vault keys this adapter needs at runtime (e.g. `ANTHROPIC_API_KEY`). */
87
- requiredVaultKeys: z.array(z.string()).default([]),
88
- /** Plain env-vars the adapter expects on the host shell. */
89
- requiredEnvVars: z.array(z.string()).default([]),
90
- /**
91
- * CLI command the user can run to authenticate. The wizard surfaces this
92
- * as a copy-to-clipboard hint — it never shells out on the user's behalf.
93
- */
94
- authCommand: z.string().optional(),
95
- /** Docs URL for setting up this adapter. */
96
- docsUrl: z.string().url().optional(),
97
- });
98
- export type EngineOnboarding = z.infer<typeof EngineOnboardingSchema>;
99
-
100
- /**
101
- * Manifest block declared by engine-* pkgs. Read by the shell's
102
- * `AdapterLoader` at pkg-discovery time; consumed by the wizard to compose
103
- * adapter-specific onboarding hints.
104
- */
105
- export const EngineProvidesSchema = z.object({
106
- /**
107
- * Stable id — matches the detection-side agent id (e.g. `claude-code`,
108
- * `codex`, `aider`, `noop`). The wizard's `selected_agent_id` is matched
109
- * against this field at adapter-resolve time.
110
- */
111
- agentId: z.string().min(1),
112
- /** Display name; overrides any detection-side display if both present. */
113
- display: z.string().optional(),
114
- /** Snapshot of what this adapter implements. */
115
- capabilities: AgentCapabilitiesSchema,
116
- /** Onboarding requirements composed by the wizard. */
117
- onboarding: EngineOnboardingSchema.default({
118
- requiredVaultKeys: [],
119
- requiredEnvVars: [],
120
- }),
121
- });
122
- export type EngineProvides = z.infer<typeof EngineProvidesSchema>;
123
-
124
- // ---------- Engine adapter runtime contract ----------
125
-
126
- export interface Engine {
127
- /** Stable identifier — matches the pkg id of the engine adapter. */
128
- readonly id: string;
129
-
130
- /** Human-readable adapter version. */
131
- readonly version: string;
132
-
133
- /**
134
- * Static metadata copied from the loading manifest's `engine` block.
135
- * Required: every adapter must surface its agentId / display / capabilities
136
- * / onboarding so the shell and wizard can introspect without re-parsing
137
- * the manifest.
138
- */
139
- readonly metadata: {
140
- agentId: string;
141
- display: string;
142
- capabilities: AgentCapabilities;
143
- onboarding: EngineOnboarding;
144
- };
145
-
146
- /** Open a new session. Sessions are cheap; create one per pkg invocation. */
147
- startSession(opts: SessionOpts): Promise<Session>;
148
-
149
- /** Send input and stream events. The iterable completes on `done`. */
150
- stream(session: Session, input: string): AsyncIterable<EngineEvent>;
151
-
152
- /** Register an MCP server with the engine. Idempotent on `id`. */
153
- registerMcpServer(spec: McpServerSpec): Promise<void>;
154
-
155
- /** Unregister an MCP server. */
156
- unregisterMcpServer(id: string): Promise<void>;
157
-
158
- /** Health check — used by the kernel before routing pkg requests. */
159
- healthCheck(): Promise<{ ok: boolean; reason?: string }>;
160
- }
161
-
162
- // ---------- Adapter loader contract ----------
163
-
164
1
  /**
165
- * Loader the shell uses to bring engine-* pkgs online at boot, tear them
166
- * down on removal, and gracefully fall back when the user's
167
- * `selected_agent_id` has no installed adapter.
2
+ * ACP (Agent Client Protocol) shapes. Phase 10: a second, ACP-shaped
3
+ * contract sits alongside the legacy `Engine` interface in `./adapter.ts`.
4
+ * The two coexist while Phase 11 retires the legacy adapter. New engines
5
+ * (in-process Rust ACP, Node ACP sidecars, etc.) target `AcpEngine`;
6
+ * existing consumers keep the `Engine` shape until they migrate.
168
7
  *
169
- * Implementation lives shell-side (post-Phase-7); this interface lets
170
- * the contract pin the shape so engine pkgs and the shell agree on the
171
- * load lifecycle.
8
+ * Method names mirror ACP's verbatim so the wire layer and the adapter
9
+ * layer share vocabulary `newSession`, `prompt`, `cancel`, `setMode`,
10
+ * `loadSession`, `forkSession`, `requestPermission`.
172
11
  */
173
- export interface AdapterLoader {
174
- /**
175
- * Load + register the engine for a given pkg manifest. The manifest must
176
- * carry an `engine` block (see `EngineProvidesSchema`); the loader
177
- * resolves the adapter implementation and threads the manifest's
178
- * metadata into the returned `Engine.metadata`.
179
- */
180
- load(manifest: { id: string; engine: EngineProvides }): Promise<Engine>;
181
-
182
- /** Unload — used on pkg removal. After this resolves the agentId is unregistered. */
183
- unload(agentId: string): Promise<void>;
184
-
185
- /**
186
- * Fallback returned when the requested agentId has no registered loader.
187
- * Implementations MUST return a working `engine-noop` (or equivalent
188
- * inert) adapter so the chat surface never dead-ends.
189
- */
190
- fallback(): Engine;
191
- }
192
12
 
193
- // ─── ACP (Agent Client Protocol) shapes ───────────────────────────────────────
194
- //
195
- // Phase 10: a second, ACP-shaped contract sits alongside the legacy `Engine`
196
- // interface above. The two coexist while Phase 11 retires the legacy
197
- // adapter. New engines (in-process Rust ACP, Node ACP sidecars, etc.) target
198
- // `AcpEngine`; existing consumers keep the `Engine` shape until they migrate.
199
- //
200
- // The names mirror ACP's method names verbatim so the wire layer and the
201
- // adapter layer share vocabulary — `newSession`, `prompt`, `cancel`,
202
- // `setMode`, `loadSession`, `forkSession`, `requestPermission`.
13
+ import type { McpServerSpec } from './adapter.js';
203
14
 
204
15
  /** ACP `ProtocolVersion`. Numeric. V1 = 1. */
205
16
  export type AcpProtocolVersion = number;
@@ -483,3 +294,43 @@ export interface AcpEngine {
483
294
  /** Subscribe to OS-attention notifications. Returns a sync unsubscribe. */
484
295
  onNotify(callback: (payload: AcpNotifyPayload) => void): () => void;
485
296
  }
297
+
298
+ // ── Host bridge (ACP-shaped) ──────────────────────────────────────────────────
299
+
300
+ /** Synchronous unsubscribe handle. */
301
+ export type AcpUnlisten = () => void;
302
+
303
+ /**
304
+ * Tauri-side surface the host shell exposes for the ACP engine. The shell
305
+ * binds these to its `acp_*` Tauri commands and `acp://*` event listeners.
306
+ *
307
+ * Each `on*` returns a Promise of an unsubscribe fn — the engine wraps that
308
+ * so callers get a sync unsubscribe.
309
+ */
310
+ export interface AcpHost {
311
+ initialize(req: AcpInitializeRequest): Promise<AcpInitializeResponse>;
312
+ newSession(req: AcpNewSessionRequest): Promise<AcpNewSessionResponse>;
313
+ prompt(req: AcpPromptRequest): Promise<AcpPromptResponse>;
314
+ cancel(sessionId: string): Promise<void>;
315
+ setMode(sessionId: string, modeId: AcpSessionModeId): Promise<void>;
316
+ loadSession(sessionId: string): Promise<AcpLoadSessionResponse>;
317
+ forkSession(
318
+ sourceSessionId: string,
319
+ opts?: AcpForkOpts,
320
+ ): Promise<AcpForkResult>;
321
+ listenSession(
322
+ sessionId: string,
323
+ onUpdate: (notification: AcpSessionNotification) => void,
324
+ ): Promise<AcpUnlisten>;
325
+ listenPermissionRequests(
326
+ sessionId: string,
327
+ onRequest: (envelope: AcpPermissionRequestEnvelope) => void,
328
+ ): Promise<AcpUnlisten>;
329
+ respondPermission(
330
+ requestId: string,
331
+ response: AcpRequestPermissionResponse,
332
+ ): Promise<void>;
333
+ listenNotify(
334
+ callback: (payload: AcpNotifyPayload) => void,
335
+ ): Promise<AcpUnlisten>;
336
+ }