@kolisachint/hoocode-agent 0.5.21 → 0.5.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-log.d.ts +44 -0
- package/dist/core/agent-log.d.ts.map +1 -0
- package/dist/core/agent-log.js +74 -0
- package/dist/core/agent-log.js.map +1 -0
- package/dist/core/canvas/discovery.d.ts +60 -0
- package/dist/core/canvas/discovery.d.ts.map +1 -0
- package/dist/core/canvas/discovery.js +83 -0
- package/dist/core/canvas/discovery.js.map +1 -0
- package/dist/core/canvas/launch.d.ts +90 -0
- package/dist/core/canvas/launch.d.ts.map +1 -0
- package/dist/core/canvas/launch.js +176 -0
- package/dist/core/canvas/launch.js.map +1 -0
- package/dist/core/canvas/protocol.d.ts +229 -0
- package/dist/core/canvas/protocol.d.ts.map +1 -0
- package/dist/core/canvas/protocol.js +129 -0
- package/dist/core/canvas/protocol.js.map +1 -0
- package/dist/core/canvas/registry.d.ts +139 -0
- package/dist/core/canvas/registry.d.ts.map +1 -0
- package/dist/core/canvas/registry.js +275 -0
- package/dist/core/canvas/registry.js.map +1 -0
- package/dist/core/canvas/resolver.d.ts +26 -0
- package/dist/core/canvas/resolver.d.ts.map +1 -0
- package/dist/core/canvas/resolver.js +57 -0
- package/dist/core/canvas/resolver.js.map +1 -0
- package/dist/core/canvas/runner.d.ts +101 -0
- package/dist/core/canvas/runner.d.ts.map +1 -0
- package/dist/core/canvas/runner.js +196 -0
- package/dist/core/canvas/runner.js.map +1 -0
- package/dist/core/canvas/sdk-shim/dispatch.test-helpers.d.ts +23 -0
- package/dist/core/canvas/sdk-shim/dispatch.test-helpers.d.ts.map +1 -0
- package/dist/core/canvas/sdk-shim/dispatch.test-helpers.js +36 -0
- package/dist/core/canvas/sdk-shim/dispatch.test-helpers.js.map +1 -0
- package/dist/core/canvas/sdk-shim/index.d.ts +113 -0
- package/dist/core/canvas/sdk-shim/index.d.ts.map +1 -0
- package/dist/core/canvas/sdk-shim/index.js +184 -0
- package/dist/core/canvas/sdk-shim/index.js.map +1 -0
- package/dist/core/canvas/session.d.ts +112 -0
- package/dist/core/canvas/session.d.ts.map +1 -0
- package/dist/core/canvas/session.js +197 -0
- package/dist/core/canvas/session.js.map +1 -0
- package/dist/core/canvas/trust.d.ts +71 -0
- package/dist/core/canvas/trust.d.ts.map +1 -0
- package/dist/core/canvas/trust.js +88 -0
- package/dist/core/canvas/trust.js.map +1 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -10
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/plugins/trust.d.ts +21 -0
- package/dist/core/extensions/plugins/trust.d.ts.map +1 -1
- package/dist/core/extensions/plugins/trust.js +26 -0
- package/dist/core/extensions/plugins/trust.js.map +1 -1
- package/dist/core/lifeguard.d.ts.map +1 -1
- package/dist/core/lifeguard.js +2 -1
- package/dist/core/lifeguard.js.map +1 -1
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +6 -4
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/tools/canvas.d.ts +55 -0
- package/dist/core/tools/canvas.d.ts.map +1 -0
- package/dist/core/tools/canvas.js +159 -0
- package/dist/core/tools/canvas.js.map +1 -0
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +3 -2
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/extensions/core/canvas.d.ts +20 -0
- package/dist/extensions/core/canvas.d.ts.map +1 -0
- package/dist/extensions/core/canvas.js +192 -0
- package/dist/extensions/core/canvas.js.map +1 -0
- package/dist/extensions/core/hoo-core.d.ts +1 -0
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +3 -0
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/paths.d.ts +9 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +16 -0
- package/dist/utils/paths.js.map +1 -1
- package/docs/routing.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas extension runner — forks an extension and speaks the provider protocol.
|
|
3
|
+
*
|
|
4
|
+
* Design: `docs/canvas-extensions-design.md` §3.1, §4, §6.1. Two decisions are
|
|
5
|
+
* load-bearing here:
|
|
6
|
+
*
|
|
7
|
+
* 1. **It forks; it does not import.** hoocode's own extensions load in-process
|
|
8
|
+
* (`core/extensions/loader.ts`), which would put a third-party canvas inside
|
|
9
|
+
* the permission gate with full access to the tool registry and provider
|
|
10
|
+
* credentials. Canvas extensions are strangers, so they get a process
|
|
11
|
+
* boundary — which is also what the Copilot CLI does, so a canvas behaves
|
|
12
|
+
* the same in both hosts.
|
|
13
|
+
* 2. **Lifecycle mirrors the documented CLI contract**: forked child, JSON-RPC
|
|
14
|
+
* over stdio, and shutdown by SIGTERM followed by SIGKILL after 5s.
|
|
15
|
+
*
|
|
16
|
+
* stdout is the protocol channel, exactly as in the Copilot CLI, so a stray
|
|
17
|
+
* `console.log` in an extension corrupts it there and here alike. Rather than
|
|
18
|
+
* failing opaquely, unparseable lines are surfaced through `onStray` so a caller
|
|
19
|
+
* can tell the author to use `session.log` instead.
|
|
20
|
+
*/
|
|
21
|
+
import { spawn } from "node:child_process";
|
|
22
|
+
import { CANVAS_ENVELOPE_VERSION, CanvasMessageDecoder, encodeCanvasMessage, isCanvasChildToHostMessage, } from "./protocol.js";
|
|
23
|
+
import { canvasResolverImportArg } from "./resolver.js";
|
|
24
|
+
/** Code on the error a cancelled or timed-out provider call rejects with. */
|
|
25
|
+
export const CANVAS_ERROR_CODE_ABORTED = "aborted";
|
|
26
|
+
/** Grace period between SIGTERM and SIGKILL, matching the documented CLI contract. */
|
|
27
|
+
export const CANVAS_SHUTDOWN_GRACE_MS = 5_000;
|
|
28
|
+
/**
|
|
29
|
+
* Default ceiling per provider method, so a wedged handler cannot hang a session
|
|
30
|
+
* (design doc §11.4).
|
|
31
|
+
*
|
|
32
|
+
* These differ because the calls do. `canvas.open` may legitimately do real work
|
|
33
|
+
* before it can return a URL — `pr-artifact-explorer` starts a server and
|
|
34
|
+
* `inspect_artifact`-shaped canvases may fetch — whereas an action is a request
|
|
35
|
+
* against an already-open instance, and `close` should be near-instant since the
|
|
36
|
+
* SDK contract makes `onClose` fire-and-forget. A single 30s ceiling for all three
|
|
37
|
+
* was a guess, and wrong at both ends.
|
|
38
|
+
*/
|
|
39
|
+
export const CANVAS_REQUEST_TIMEOUT_MS = {
|
|
40
|
+
"canvas.open": 120_000,
|
|
41
|
+
"canvas.action.invoke": 30_000,
|
|
42
|
+
"canvas.close": 5_000,
|
|
43
|
+
};
|
|
44
|
+
/** Error carrying the `CanvasError.code` an extension handler threw. */
|
|
45
|
+
export class CanvasCallError extends Error {
|
|
46
|
+
code;
|
|
47
|
+
constructor(code, message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "CanvasCallError";
|
|
50
|
+
this.code = code;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Fork an extension and return a handle to its provider surface. */
|
|
54
|
+
export function spawnCanvasExtension(options) {
|
|
55
|
+
const timeouts = { ...CANVAS_REQUEST_TIMEOUT_MS, ...options.requestTimeoutMs };
|
|
56
|
+
const child = spawn(options.runtime.execPath, [...options.runtime.execArgv, "--import", canvasResolverImportArg(options.runtime.shimUrl), options.entry], {
|
|
57
|
+
cwd: options.cwd,
|
|
58
|
+
env: {
|
|
59
|
+
...(options.env ?? process.env),
|
|
60
|
+
HOOCODE_CANVAS_EXTENSION_ID: options.extensionId,
|
|
61
|
+
// The real SDK's joinSession throws without SESSION_ID (design doc §11.2).
|
|
62
|
+
// Our shim does not read it, but an extension that uses the SDK directly
|
|
63
|
+
// would, so setting it is free fidelity. COPILOT_HOME is deliberately left
|
|
64
|
+
// alone: a canvas should not be able to tell the hosts apart.
|
|
65
|
+
SESSION_ID: options.sessionId ?? options.extensionId,
|
|
66
|
+
},
|
|
67
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
68
|
+
});
|
|
69
|
+
const pending = new Map();
|
|
70
|
+
const decoder = new CanvasMessageDecoder();
|
|
71
|
+
let nextId = 1;
|
|
72
|
+
let exited = false;
|
|
73
|
+
let readyResolve;
|
|
74
|
+
let readyReject;
|
|
75
|
+
const ready = new Promise((resolve, reject) => {
|
|
76
|
+
readyResolve = resolve;
|
|
77
|
+
readyReject = reject;
|
|
78
|
+
});
|
|
79
|
+
const settleAll = (reason) => {
|
|
80
|
+
for (const call of pending.values()) {
|
|
81
|
+
call.dispose();
|
|
82
|
+
call.reject(reason);
|
|
83
|
+
}
|
|
84
|
+
pending.clear();
|
|
85
|
+
};
|
|
86
|
+
const handle = (message) => {
|
|
87
|
+
switch (message.type) {
|
|
88
|
+
case "ready":
|
|
89
|
+
readyResolve?.(message);
|
|
90
|
+
return;
|
|
91
|
+
case "log":
|
|
92
|
+
options.onLog?.(message.message, message.level, message.ephemeral);
|
|
93
|
+
return;
|
|
94
|
+
default: {
|
|
95
|
+
// A response for an id no longer pending is one whose caller stopped
|
|
96
|
+
// waiting — aborted or timed out. Dropping it is the whole point.
|
|
97
|
+
const call = pending.get(message.id);
|
|
98
|
+
if (!call)
|
|
99
|
+
return;
|
|
100
|
+
pending.delete(message.id);
|
|
101
|
+
call.dispose();
|
|
102
|
+
if (message.type === "response")
|
|
103
|
+
call.resolve(message.result);
|
|
104
|
+
else
|
|
105
|
+
call.reject(new CanvasCallError(message.code, message.message));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
child.stdout.setEncoding("utf8");
|
|
110
|
+
child.stdout.on("data", (chunk) => {
|
|
111
|
+
const { values, strays } = decoder.push(chunk);
|
|
112
|
+
for (const line of strays)
|
|
113
|
+
options.onStray?.(line);
|
|
114
|
+
for (const value of values) {
|
|
115
|
+
if (isCanvasChildToHostMessage(value))
|
|
116
|
+
handle(value);
|
|
117
|
+
else
|
|
118
|
+
options.onStray?.(JSON.stringify(value));
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
child.stderr.setEncoding("utf8");
|
|
122
|
+
child.stderr.on("data", (chunk) => options.onStderr?.(chunk));
|
|
123
|
+
child.on("error", (error) => {
|
|
124
|
+
exited = true;
|
|
125
|
+
readyReject?.(error);
|
|
126
|
+
settleAll(error);
|
|
127
|
+
});
|
|
128
|
+
child.on("exit", (code, signal) => {
|
|
129
|
+
exited = true;
|
|
130
|
+
const reason = new Error(`Canvas extension "${options.extensionId}" exited (code ${String(code)}, signal ${String(signal)}).`);
|
|
131
|
+
readyReject?.(reason);
|
|
132
|
+
settleAll(reason);
|
|
133
|
+
options.onExit?.(code, signal);
|
|
134
|
+
});
|
|
135
|
+
const call = (method, params, callOptions) => {
|
|
136
|
+
if (exited) {
|
|
137
|
+
return Promise.reject(new Error(`Canvas extension "${options.extensionId}" is not running.`));
|
|
138
|
+
}
|
|
139
|
+
const signal = callOptions?.signal;
|
|
140
|
+
if (signal?.aborted) {
|
|
141
|
+
// Nothing is written to the child: a request nobody awaits should not exist.
|
|
142
|
+
return Promise.reject(new CanvasCallError(CANVAS_ERROR_CODE_ABORTED, `Canvas ${method} was cancelled before it started.`));
|
|
143
|
+
}
|
|
144
|
+
const id = nextId;
|
|
145
|
+
nextId += 1;
|
|
146
|
+
const timeoutMs = timeouts[method];
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const abandon = (error) => {
|
|
149
|
+
const entry = pending.get(id);
|
|
150
|
+
if (!entry)
|
|
151
|
+
return;
|
|
152
|
+
pending.delete(id);
|
|
153
|
+
entry.dispose();
|
|
154
|
+
reject(error);
|
|
155
|
+
};
|
|
156
|
+
const timer = timeoutMs > 0
|
|
157
|
+
? setTimeout(() => abandon(new CanvasCallError(CANVAS_ERROR_CODE_ABORTED, `Canvas extension "${options.extensionId}" did not answer ${method} within ${timeoutMs}ms.`)), timeoutMs)
|
|
158
|
+
: undefined;
|
|
159
|
+
const onAbort = () => abandon(new CanvasCallError(CANVAS_ERROR_CODE_ABORTED, `Canvas ${method} was cancelled.`));
|
|
160
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
161
|
+
pending.set(id, {
|
|
162
|
+
resolve,
|
|
163
|
+
reject,
|
|
164
|
+
dispose: () => {
|
|
165
|
+
if (timer)
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
signal?.removeEventListener("abort", onAbort);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
child.stdin.write(encodeCanvasMessage({ envelope: CANVAS_ENVELOPE_VERSION, type: "request", id, method, params }));
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
extensionId: options.extensionId,
|
|
175
|
+
ready,
|
|
176
|
+
get running() {
|
|
177
|
+
return !exited;
|
|
178
|
+
},
|
|
179
|
+
open: (params, callOptions) => call("canvas.open", params, callOptions),
|
|
180
|
+
close: (params, callOptions) => call("canvas.close", params, callOptions),
|
|
181
|
+
invokeAction: (params, callOptions) => call("canvas.action.invoke", params, callOptions),
|
|
182
|
+
terminate: () => new Promise((resolve) => {
|
|
183
|
+
if (exited) {
|
|
184
|
+
resolve();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const kill = setTimeout(() => child.kill("SIGKILL"), CANVAS_SHUTDOWN_GRACE_MS);
|
|
188
|
+
child.once("exit", () => {
|
|
189
|
+
clearTimeout(kill);
|
|
190
|
+
resolve();
|
|
191
|
+
});
|
|
192
|
+
child.kill("SIGTERM");
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../../src/core/canvas/runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Canvas extension runner — forks an extension and speaks the provider protocol.\n *\n * Design: `docs/canvas-extensions-design.md` §3.1, §4, §6.1. Two decisions are\n * load-bearing here:\n *\n * 1. **It forks; it does not import.** hoocode's own extensions load in-process\n * (`core/extensions/loader.ts`), which would put a third-party canvas inside\n * the permission gate with full access to the tool registry and provider\n * credentials. Canvas extensions are strangers, so they get a process\n * boundary — which is also what the Copilot CLI does, so a canvas behaves\n * the same in both hosts.\n * 2. **Lifecycle mirrors the documented CLI contract**: forked child, JSON-RPC\n * over stdio, and shutdown by SIGTERM followed by SIGKILL after 5s.\n *\n * stdout is the protocol channel, exactly as in the Copilot CLI, so a stray\n * `console.log` in an extension corrupts it there and here alike. Rather than\n * failing opaquely, unparseable lines are surfaced through `onStray` so a caller\n * can tell the author to use `session.log` instead.\n */\n\nimport { type ChildProcessWithoutNullStreams, spawn } from \"node:child_process\";\nimport {\n\tCANVAS_ENVELOPE_VERSION,\n\ttype CanvasChildToHostMessage,\n\tCanvasMessageDecoder,\n\ttype CanvasProviderCloseRequest,\n\ttype CanvasProviderInvokeActionRequest,\n\ttype CanvasProviderMethod,\n\ttype CanvasProviderOpenRequest,\n\ttype CanvasReadyMessage,\n\tencodeCanvasMessage,\n\tisCanvasChildToHostMessage,\n\ttype JsonValue,\n} from \"./protocol.js\";\nimport { canvasResolverImportArg } from \"./resolver.js\";\n\n/** Code on the error a cancelled or timed-out provider call rejects with. */\nexport const CANVAS_ERROR_CODE_ABORTED = \"aborted\";\n\n/** Per-call options. A signal cancels the wait; the child is reconciled by the caller. */\nexport interface CanvasCallOptions {\n\t/**\n\t * Stop waiting when this aborts.\n\t *\n\t * The provider protocol has no cancel verb, so aborting only ends *our* wait —\n\t * the child may still be working and may still answer. `registry.ts` is what\n\t * reconciles that, by closing the instance it will never see (see its `abandon`).\n\t */\n\tsignal?: AbortSignal;\n}\n\n/** Grace period between SIGTERM and SIGKILL, matching the documented CLI contract. */\nexport const CANVAS_SHUTDOWN_GRACE_MS = 5_000;\n\n/**\n * Default ceiling per provider method, so a wedged handler cannot hang a session\n * (design doc §11.4).\n *\n * These differ because the calls do. `canvas.open` may legitimately do real work\n * before it can return a URL — `pr-artifact-explorer` starts a server and\n * `inspect_artifact`-shaped canvases may fetch — whereas an action is a request\n * against an already-open instance, and `close` should be near-instant since the\n * SDK contract makes `onClose` fire-and-forget. A single 30s ceiling for all three\n * was a guess, and wrong at both ends.\n */\nexport const CANVAS_REQUEST_TIMEOUT_MS: Readonly<Record<CanvasProviderMethod, number>> = {\n\t\"canvas.open\": 120_000,\n\t\"canvas.action.invoke\": 30_000,\n\t\"canvas.close\": 5_000,\n};\n\n/** How the child is launched. Injectable so tests can run the TypeScript shim under tsx. */\nexport interface CanvasRuntime {\n\t/** Executable to fork. */\n\texecPath: string;\n\t/** Arguments that precede the entry file, excluding the resolver `--import`. */\n\texecArgv: string[];\n\t/** Absolute `file:` URL of the module the SDK specifier resolves to. */\n\tshimUrl: string;\n}\n\n/** Everything needed to fork one extension. */\nexport interface CanvasRunnerOptions {\n\t/** Provider identifier — the extension directory name. */\n\textensionId: string;\n\t/** Absolute path to the extension's `extension.mjs`. */\n\tentry: string;\n\t/** Value for the child's `SESSION_ID`. Defaults to the extension id. */\n\tsessionId?: string;\n\truntime: CanvasRuntime;\n\t/** Working directory for the child. Defaults to the extension directory's parent. */\n\tcwd?: string;\n\tenv?: NodeJS.ProcessEnv;\n\t/** Per-method overrides merged over {@link CANVAS_REQUEST_TIMEOUT_MS}. 0 disables. */\n\trequestTimeoutMs?: Partial<Record<CanvasProviderMethod, number>>;\n\t/** A `session.log` call from the extension. */\n\tonLog?: (message: string, level: string | undefined, ephemeral: boolean | undefined) => void;\n\t/** A stdout line that was not protocol. Almost always a stray `console.log`. */\n\tonStray?: (line: string) => void;\n\t/** The child's stderr, which extensions use normally. */\n\tonStderr?: (chunk: string) => void;\n\t/** The child exited, expectedly or not. */\n\tonExit?: (code: number | null, signal: NodeJS.Signals | null) => void;\n}\n\n/** A running canvas extension. */\nexport interface CanvasExtensionProcess {\n\treadonly extensionId: string;\n\t/** Resolves with the child's first `ready` message, or rejects if it exits first. */\n\treadonly ready: Promise<CanvasReadyMessage>;\n\t/** Whether the child is still running. */\n\treadonly running: boolean;\n\topen(params: CanvasProviderOpenRequest, options?: CanvasCallOptions): Promise<JsonValue>;\n\tclose(params: CanvasProviderCloseRequest, options?: CanvasCallOptions): Promise<JsonValue>;\n\tinvokeAction(params: CanvasProviderInvokeActionRequest, options?: CanvasCallOptions): Promise<JsonValue>;\n\t/** SIGTERM, then SIGKILL after {@link CANVAS_SHUTDOWN_GRACE_MS}. Resolves on exit. */\n\tterminate(): Promise<void>;\n}\n\ninterface PendingCall {\n\tresolve: (value: JsonValue) => void;\n\treject: (reason: Error) => void;\n\t/** Clear the timer and detach the abort listener. Safe to call twice. */\n\tdispose: () => void;\n}\n\n/** Error carrying the `CanvasError.code` an extension handler threw. */\nexport class CanvasCallError extends Error {\n\treadonly code: string;\n\n\tconstructor(code: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"CanvasCallError\";\n\t\tthis.code = code;\n\t}\n}\n\n/** Fork an extension and return a handle to its provider surface. */\nexport function spawnCanvasExtension(options: CanvasRunnerOptions): CanvasExtensionProcess {\n\tconst timeouts: Record<CanvasProviderMethod, number> = { ...CANVAS_REQUEST_TIMEOUT_MS, ...options.requestTimeoutMs };\n\tconst child = spawn(\n\t\toptions.runtime.execPath,\n\t\t[...options.runtime.execArgv, \"--import\", canvasResolverImportArg(options.runtime.shimUrl), options.entry],\n\t\t{\n\t\t\tcwd: options.cwd,\n\t\t\tenv: {\n\t\t\t\t...(options.env ?? process.env),\n\t\t\t\tHOOCODE_CANVAS_EXTENSION_ID: options.extensionId,\n\t\t\t\t// The real SDK's joinSession throws without SESSION_ID (design doc §11.2).\n\t\t\t\t// Our shim does not read it, but an extension that uses the SDK directly\n\t\t\t\t// would, so setting it is free fidelity. COPILOT_HOME is deliberately left\n\t\t\t\t// alone: a canvas should not be able to tell the hosts apart.\n\t\t\t\tSESSION_ID: options.sessionId ?? options.extensionId,\n\t\t\t},\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t},\n\t) as ChildProcessWithoutNullStreams;\n\n\tconst pending = new Map<number, PendingCall>();\n\tconst decoder = new CanvasMessageDecoder();\n\tlet nextId = 1;\n\tlet exited = false;\n\tlet readyResolve: ((value: CanvasReadyMessage) => void) | undefined;\n\tlet readyReject: ((reason: Error) => void) | undefined;\n\tconst ready = new Promise<CanvasReadyMessage>((resolve, reject) => {\n\t\treadyResolve = resolve;\n\t\treadyReject = reject;\n\t});\n\n\tconst settleAll = (reason: Error): void => {\n\t\tfor (const call of pending.values()) {\n\t\t\tcall.dispose();\n\t\t\tcall.reject(reason);\n\t\t}\n\t\tpending.clear();\n\t};\n\n\tconst handle = (message: CanvasChildToHostMessage): void => {\n\t\tswitch (message.type) {\n\t\t\tcase \"ready\":\n\t\t\t\treadyResolve?.(message);\n\t\t\t\treturn;\n\t\t\tcase \"log\":\n\t\t\t\toptions.onLog?.(message.message, message.level, message.ephemeral);\n\t\t\t\treturn;\n\t\t\tdefault: {\n\t\t\t\t// A response for an id no longer pending is one whose caller stopped\n\t\t\t\t// waiting — aborted or timed out. Dropping it is the whole point.\n\t\t\t\tconst call = pending.get(message.id);\n\t\t\t\tif (!call) return;\n\t\t\t\tpending.delete(message.id);\n\t\t\t\tcall.dispose();\n\t\t\t\tif (message.type === \"response\") call.resolve(message.result);\n\t\t\t\telse call.reject(new CanvasCallError(message.code, message.message));\n\t\t\t}\n\t\t}\n\t};\n\n\tchild.stdout.setEncoding(\"utf8\");\n\tchild.stdout.on(\"data\", (chunk: string) => {\n\t\tconst { values, strays } = decoder.push(chunk);\n\t\tfor (const line of strays) options.onStray?.(line);\n\t\tfor (const value of values) {\n\t\t\tif (isCanvasChildToHostMessage(value)) handle(value);\n\t\t\telse options.onStray?.(JSON.stringify(value));\n\t\t}\n\t});\n\n\tchild.stderr.setEncoding(\"utf8\");\n\tchild.stderr.on(\"data\", (chunk: string) => options.onStderr?.(chunk));\n\n\tchild.on(\"error\", (error: Error) => {\n\t\texited = true;\n\t\treadyReject?.(error);\n\t\tsettleAll(error);\n\t});\n\n\tchild.on(\"exit\", (code, signal) => {\n\t\texited = true;\n\t\tconst reason = new Error(\n\t\t\t`Canvas extension \"${options.extensionId}\" exited (code ${String(code)}, signal ${String(signal)}).`,\n\t\t);\n\t\treadyReject?.(reason);\n\t\tsettleAll(reason);\n\t\toptions.onExit?.(code, signal);\n\t});\n\n\tconst call = (\n\t\tmethod: CanvasProviderMethod,\n\t\tparams: JsonValue,\n\t\tcallOptions?: CanvasCallOptions,\n\t): Promise<JsonValue> => {\n\t\tif (exited) {\n\t\t\treturn Promise.reject(new Error(`Canvas extension \"${options.extensionId}\" is not running.`));\n\t\t}\n\t\tconst signal = callOptions?.signal;\n\t\tif (signal?.aborted) {\n\t\t\t// Nothing is written to the child: a request nobody awaits should not exist.\n\t\t\treturn Promise.reject(\n\t\t\t\tnew CanvasCallError(CANVAS_ERROR_CODE_ABORTED, `Canvas ${method} was cancelled before it started.`),\n\t\t\t);\n\t\t}\n\t\tconst id = nextId;\n\t\tnextId += 1;\n\t\tconst timeoutMs = timeouts[method];\n\t\treturn new Promise<JsonValue>((resolve, reject) => {\n\t\t\tconst abandon = (error: Error): void => {\n\t\t\t\tconst entry = pending.get(id);\n\t\t\t\tif (!entry) return;\n\t\t\t\tpending.delete(id);\n\t\t\t\tentry.dispose();\n\t\t\t\treject(error);\n\t\t\t};\n\t\t\tconst timer =\n\t\t\t\ttimeoutMs > 0\n\t\t\t\t\t? setTimeout(\n\t\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\t\tabandon(\n\t\t\t\t\t\t\t\t\tnew CanvasCallError(\n\t\t\t\t\t\t\t\t\t\tCANVAS_ERROR_CODE_ABORTED,\n\t\t\t\t\t\t\t\t\t\t`Canvas extension \"${options.extensionId}\" did not answer ${method} within ${timeoutMs}ms.`,\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\ttimeoutMs,\n\t\t\t\t\t\t)\n\t\t\t\t\t: undefined;\n\t\t\tconst onAbort = () =>\n\t\t\t\tabandon(new CanvasCallError(CANVAS_ERROR_CODE_ABORTED, `Canvas ${method} was cancelled.`));\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tpending.set(id, {\n\t\t\t\tresolve,\n\t\t\t\treject,\n\t\t\t\tdispose: () => {\n\t\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t},\n\t\t\t});\n\t\t\tchild.stdin.write(\n\t\t\t\tencodeCanvasMessage({ envelope: CANVAS_ENVELOPE_VERSION, type: \"request\", id, method, params }),\n\t\t\t);\n\t\t});\n\t};\n\n\treturn {\n\t\textensionId: options.extensionId,\n\t\tready,\n\t\tget running() {\n\t\t\treturn !exited;\n\t\t},\n\t\topen: (params, callOptions) => call(\"canvas.open\", params as unknown as JsonValue, callOptions),\n\t\tclose: (params, callOptions) => call(\"canvas.close\", params as unknown as JsonValue, callOptions),\n\t\tinvokeAction: (params, callOptions) => call(\"canvas.action.invoke\", params as unknown as JsonValue, callOptions),\n\t\tterminate: () =>\n\t\t\tnew Promise<void>((resolve) => {\n\t\t\t\tif (exited) {\n\t\t\t\t\tresolve();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst kill = setTimeout(() => child.kill(\"SIGKILL\"), CANVAS_SHUTDOWN_GRACE_MS);\n\t\t\t\tchild.once(\"exit\", () => {\n\t\t\t\t\tclearTimeout(kill);\n\t\t\t\t\tresolve();\n\t\t\t\t});\n\t\t\t\tchild.kill(\"SIGTERM\");\n\t\t\t}),\n\t};\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test doubles for the shim's stdio streams, so dispatch can be exercised without
|
|
3
|
+
* forking a child process. Kept beside the shim rather than under `test/` because
|
|
4
|
+
* what these implement is the shim's own stream contract.
|
|
5
|
+
*/
|
|
6
|
+
import type { CanvasShimStreams } from "./index.js";
|
|
7
|
+
/** A readable side whose chunks are pushed by the test. */
|
|
8
|
+
export declare class FakeCanvasInput {
|
|
9
|
+
private listener;
|
|
10
|
+
setEncoding(_encoding: "utf8"): this;
|
|
11
|
+
on(_event: "data", listener: (chunk: string) => void): this;
|
|
12
|
+
/** Deliver a chunk as if the host had written it. */
|
|
13
|
+
feed(chunk: string): void;
|
|
14
|
+
}
|
|
15
|
+
/** Streams plus the lines the shim wrote, for assertions. */
|
|
16
|
+
export interface FakeCanvasStreams extends CanvasShimStreams {
|
|
17
|
+
input: FakeCanvasInput;
|
|
18
|
+
/** Every line written by the shim, parsed. */
|
|
19
|
+
written: unknown[];
|
|
20
|
+
}
|
|
21
|
+
/** Build injectable streams whose writes are collected instead of sent to stdout. */
|
|
22
|
+
export declare function fakeCanvasStreams(extensionId?: string): FakeCanvasStreams;
|
|
23
|
+
//# sourceMappingURL=dispatch.test-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.test-helpers.d.ts","sourceRoot":"","sources":["../../../../src/core/canvas/sdk-shim/dispatch.test-helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,2DAA2D;AAC3D,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAwC;IAExD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEnC;IAED,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAG1D;IAED,qDAAqD;IACrD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAExB;CACD;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC3D,KAAK,EAAE,eAAe,CAAC;IACvB,8CAA8C;IAC9C,OAAO,EAAE,OAAO,EAAE,CAAC;CACnB;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,WAAW,SAAmB,GAAG,iBAAiB,CAYnF","sourcesContent":["/**\n * Test doubles for the shim's stdio streams, so dispatch can be exercised without\n * forking a child process. Kept beside the shim rather than under `test/` because\n * what these implement is the shim's own stream contract.\n */\n\nimport type { CanvasShimStreams } from \"./index.js\";\n\n/** A readable side whose chunks are pushed by the test. */\nexport class FakeCanvasInput {\n\tprivate listener: ((chunk: string) => void) | undefined;\n\n\tsetEncoding(_encoding: \"utf8\"): this {\n\t\treturn this;\n\t}\n\n\ton(_event: \"data\", listener: (chunk: string) => void): this {\n\t\tthis.listener = listener;\n\t\treturn this;\n\t}\n\n\t/** Deliver a chunk as if the host had written it. */\n\tfeed(chunk: string): void {\n\t\tthis.listener?.(chunk);\n\t}\n}\n\n/** Streams plus the lines the shim wrote, for assertions. */\nexport interface FakeCanvasStreams extends CanvasShimStreams {\n\tinput: FakeCanvasInput;\n\t/** Every line written by the shim, parsed. */\n\twritten: unknown[];\n}\n\n/** Build injectable streams whose writes are collected instead of sent to stdout. */\nexport function fakeCanvasStreams(extensionId = \"test-extension\"): FakeCanvasStreams {\n\tconst written: unknown[] = [];\n\treturn {\n\t\tinput: new FakeCanvasInput(),\n\t\twrite: (chunk: string) => {\n\t\t\tfor (const line of chunk.split(\"\\n\")) {\n\t\t\t\tif (line.trim().length > 0) written.push(JSON.parse(line));\n\t\t\t}\n\t\t},\n\t\textensionId,\n\t\twritten,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test doubles for the shim's stdio streams, so dispatch can be exercised without
|
|
3
|
+
* forking a child process. Kept beside the shim rather than under `test/` because
|
|
4
|
+
* what these implement is the shim's own stream contract.
|
|
5
|
+
*/
|
|
6
|
+
/** A readable side whose chunks are pushed by the test. */
|
|
7
|
+
export class FakeCanvasInput {
|
|
8
|
+
listener;
|
|
9
|
+
setEncoding(_encoding) {
|
|
10
|
+
return this;
|
|
11
|
+
}
|
|
12
|
+
on(_event, listener) {
|
|
13
|
+
this.listener = listener;
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
/** Deliver a chunk as if the host had written it. */
|
|
17
|
+
feed(chunk) {
|
|
18
|
+
this.listener?.(chunk);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Build injectable streams whose writes are collected instead of sent to stdout. */
|
|
22
|
+
export function fakeCanvasStreams(extensionId = "test-extension") {
|
|
23
|
+
const written = [];
|
|
24
|
+
return {
|
|
25
|
+
input: new FakeCanvasInput(),
|
|
26
|
+
write: (chunk) => {
|
|
27
|
+
for (const line of chunk.split("\n")) {
|
|
28
|
+
if (line.trim().length > 0)
|
|
29
|
+
written.push(JSON.parse(line));
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
extensionId,
|
|
33
|
+
written,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=dispatch.test-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.test-helpers.js","sourceRoot":"","sources":["../../../../src/core/canvas/sdk-shim/dispatch.test-helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,2DAA2D;AAC3D,MAAM,OAAO,eAAe;IACnB,QAAQ,CAAwC;IAExD,WAAW,CAAC,SAAiB,EAAQ;QACpC,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,EAAE,CAAC,MAAc,EAAE,QAAiC,EAAQ;QAC3D,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,qDAAqD;IACrD,IAAI,CAAC,KAAa,EAAQ;QACzB,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;IAAA,CACvB;CACD;AASD,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,WAAW,GAAG,gBAAgB,EAAqB;IACpF,MAAM,OAAO,GAAc,EAAE,CAAC;IAC9B,OAAO;QACN,KAAK,EAAE,IAAI,eAAe,EAAE;QAC5B,KAAK,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC;YACzB,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;oBAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5D,CAAC;QAAA,CACD;QACD,WAAW;QACX,OAAO;KACP,CAAC;AAAA,CACF","sourcesContent":["/**\n * Test doubles for the shim's stdio streams, so dispatch can be exercised without\n * forking a child process. Kept beside the shim rather than under `test/` because\n * what these implement is the shim's own stream contract.\n */\n\nimport type { CanvasShimStreams } from \"./index.js\";\n\n/** A readable side whose chunks are pushed by the test. */\nexport class FakeCanvasInput {\n\tprivate listener: ((chunk: string) => void) | undefined;\n\n\tsetEncoding(_encoding: \"utf8\"): this {\n\t\treturn this;\n\t}\n\n\ton(_event: \"data\", listener: (chunk: string) => void): this {\n\t\tthis.listener = listener;\n\t\treturn this;\n\t}\n\n\t/** Deliver a chunk as if the host had written it. */\n\tfeed(chunk: string): void {\n\t\tthis.listener?.(chunk);\n\t}\n}\n\n/** Streams plus the lines the shim wrote, for assertions. */\nexport interface FakeCanvasStreams extends CanvasShimStreams {\n\tinput: FakeCanvasInput;\n\t/** Every line written by the shim, parsed. */\n\twritten: unknown[];\n}\n\n/** Build injectable streams whose writes are collected instead of sent to stdout. */\nexport function fakeCanvasStreams(extensionId = \"test-extension\"): FakeCanvasStreams {\n\tconst written: unknown[] = [];\n\treturn {\n\t\tinput: new FakeCanvasInput(),\n\t\twrite: (chunk: string) => {\n\t\t\tfor (const line of chunk.split(\"\\n\")) {\n\t\t\t\tif (line.trim().length > 0) written.push(JSON.parse(line));\n\t\t\t}\n\t\t},\n\t\textensionId,\n\t\twritten,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-side `@github/copilot-sdk/extension` implementation.
|
|
3
|
+
*
|
|
4
|
+
* Design: `docs/canvas-extensions-design.md` §4, §4.1. A canvas extension from
|
|
5
|
+
* `github/awesome-copilot` is a directory of plain `.mjs` files whose only
|
|
6
|
+
* external import is `@github/copilot-sdk/extension` — GitHub's own
|
|
7
|
+
* `docs/extensions.md` states the import "is resolved automatically — you don't
|
|
8
|
+
* install it", and none of the 23 catalog extensions ships `node_modules`. This
|
|
9
|
+
* module is what hoocode resolves that specifier to inside the forked child, so a
|
|
10
|
+
* catalog canvas runs byte-identical to upstream with nothing written into its
|
|
11
|
+
* directory.
|
|
12
|
+
*
|
|
13
|
+
* What this is not: the SDK's `joinSession` resolves to a full `CopilotSession`
|
|
14
|
+
* (messaging, events, RPC passthrough, factories). This shim implements the
|
|
15
|
+
* canvas surface and `log` only. `tools`, `hooks`, and `factories` are accepted
|
|
16
|
+
* and reported as unsupported so an extension that uses them fails visibly rather
|
|
17
|
+
* than half-working (design doc §6.2). The canvas types themselves are held
|
|
18
|
+
* structurally identical to the SDK's by
|
|
19
|
+
* `test/canvas-protocol-conformance.test.ts`.
|
|
20
|
+
*/
|
|
21
|
+
import { type CanvasDeclaration, type CanvasJsonSchema, type CanvasLogLevel, type CanvasProviderCloseRequest, type CanvasProviderInvokeActionRequest, type CanvasProviderOpenRequest, type CanvasProviderOpenResult } from "../protocol.js";
|
|
22
|
+
/**
|
|
23
|
+
* A single agent-callable action contributed by a canvas. Mirrors the SDK's
|
|
24
|
+
* `CanvasAction`: the `handler` closure is stripped before the declaration is
|
|
25
|
+
* sent and dispatched in-process here.
|
|
26
|
+
*/
|
|
27
|
+
export interface CanvasAction {
|
|
28
|
+
/** Action identifier, unique within the canvas. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Description shown to the model when picking an action. */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Optional JSON Schema for the action's `input` payload. */
|
|
33
|
+
inputSchema?: CanvasJsonSchema;
|
|
34
|
+
/** Required per-action dispatch handler. */
|
|
35
|
+
handler: (ctx: CanvasProviderInvokeActionRequest) => Promise<unknown> | unknown;
|
|
36
|
+
}
|
|
37
|
+
/** Options accepted by {@link createCanvas}. Mirrors the SDK's `CanvasOptions`. */
|
|
38
|
+
export interface CanvasOptions {
|
|
39
|
+
/** Canvas id, unique within the declaring connection. */
|
|
40
|
+
id: string;
|
|
41
|
+
/** Human-readable label shown in discovery and host UI chrome. */
|
|
42
|
+
displayName: string;
|
|
43
|
+
/** Short, single-sentence description shown to the agent in canvas catalogs. */
|
|
44
|
+
description: string;
|
|
45
|
+
/** Optional JSON Schema for the `input` payload accepted by `canvas.open`. */
|
|
46
|
+
inputSchema?: CanvasJsonSchema;
|
|
47
|
+
/** Agent-invocable actions, each carrying its own required handler. */
|
|
48
|
+
actions?: CanvasAction[];
|
|
49
|
+
/** Required. Open a new canvas instance. */
|
|
50
|
+
open: (ctx: CanvasProviderOpenRequest) => Promise<CanvasProviderOpenResult> | CanvasProviderOpenResult;
|
|
51
|
+
/**
|
|
52
|
+
* Optional. Notified when a canvas instance is closed. Fire-and-forget: the
|
|
53
|
+
* return value is ignored and errors are logged but not surfaced.
|
|
54
|
+
*/
|
|
55
|
+
onClose?: (ctx: CanvasProviderCloseRequest) => Promise<void> | void;
|
|
56
|
+
}
|
|
57
|
+
/** Structured error returned from canvas handlers. Mirrors the SDK's `CanvasError`. */
|
|
58
|
+
export declare class CanvasError extends Error {
|
|
59
|
+
readonly code: string;
|
|
60
|
+
constructor(code: string, message: string);
|
|
61
|
+
/** Default error when an action is declared but no `handler` is wired. */
|
|
62
|
+
static noHandler(): CanvasError;
|
|
63
|
+
}
|
|
64
|
+
/** A registered canvas: declarative metadata plus in-process handler closures. */
|
|
65
|
+
export declare class Canvas {
|
|
66
|
+
readonly declaration: CanvasDeclaration;
|
|
67
|
+
readonly open: NonNullable<CanvasOptions["open"]>;
|
|
68
|
+
readonly onClose?: CanvasOptions["onClose"];
|
|
69
|
+
/** Handlers by action name, kept out of the declaration that crosses the wire. */
|
|
70
|
+
readonly handlers: ReadonlyMap<string, CanvasAction["handler"]>;
|
|
71
|
+
constructor(options: CanvasOptions);
|
|
72
|
+
}
|
|
73
|
+
/** Create a canvas declaration with bound in-process handlers. */
|
|
74
|
+
export declare function createCanvas(options: CanvasOptions): Canvas;
|
|
75
|
+
/** Configuration accepted by {@link joinSession}. */
|
|
76
|
+
export interface JoinSessionConfig {
|
|
77
|
+
canvases?: Canvas[];
|
|
78
|
+
/** Accepted and reported as unsupported (design doc §6.2). */
|
|
79
|
+
tools?: unknown;
|
|
80
|
+
/** Accepted and reported as unsupported (design doc §6.2). */
|
|
81
|
+
hooks?: unknown;
|
|
82
|
+
/** Accepted and reported as unsupported (design doc §6.2). */
|
|
83
|
+
factories?: unknown;
|
|
84
|
+
/** Accepted and reported as unsupported (design doc §6.2). */
|
|
85
|
+
onPermissionRequest?: unknown;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The subset of the SDK's `CopilotSession` this shim provides. Deliberately small:
|
|
89
|
+
* see the module header.
|
|
90
|
+
*/
|
|
91
|
+
export interface CanvasShimSession {
|
|
92
|
+
/** Log a message to the session timeline. */
|
|
93
|
+
log(message: string, options?: {
|
|
94
|
+
level?: CanvasLogLevel;
|
|
95
|
+
ephemeral?: boolean;
|
|
96
|
+
}): Promise<void>;
|
|
97
|
+
}
|
|
98
|
+
/** Streams the shim reads from and writes to. Injectable so tests need no child process. */
|
|
99
|
+
export interface CanvasShimStreams {
|
|
100
|
+
input: {
|
|
101
|
+
setEncoding(encoding: "utf8"): unknown;
|
|
102
|
+
on(event: "data", listener: (chunk: string) => void): unknown;
|
|
103
|
+
};
|
|
104
|
+
write(chunk: string): unknown;
|
|
105
|
+
/** Provider identifier; the runner passes it in the environment. */
|
|
106
|
+
extensionId: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Join the host session: announce the declared canvases, then serve provider
|
|
110
|
+
* callbacks until the input stream ends.
|
|
111
|
+
*/
|
|
112
|
+
export declare function joinSession(config?: JoinSessionConfig, streams?: CanvasShimStreams): Promise<CanvasShimSession>;
|
|
113
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/core/canvas/sdk-shim/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAYN,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EAEnB,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACtC,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAI7B,MAAM,gBAAgB,CAAC;AAExB;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,4CAA4C;IAC5C,OAAO,EAAE,CAAC,GAAG,EAAE,iCAAiC,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CAChF;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC7B,yDAAyD;IACzD,EAAE,EAAE,MAAM,CAAC;IACX,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,uEAAuE;IACvE,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,4CAA4C;IAC5C,IAAI,EAAE,CAAC,GAAG,EAAE,yBAAyB,KAAK,OAAO,CAAC,wBAAwB,CAAC,GAAG,wBAAwB,CAAC;IACvG;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,0BAA0B,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACpE;AAED,uFAAuF;AACvF,qBAAa,WAAY,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAIxC;IAED,0EAA0E;IAC1E,MAAM,CAAC,SAAS,IAAI,WAAW,CAE9B;CACD;AAED,kFAAkF;AAClF,qBAAa,MAAM;IAClB,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IAC5C,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhE,YAAY,OAAO,EAAE,aAAa,EA0BjC;CACD;AAED,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAE3D;AAKD,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,+DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,+DAA8D;IAC9D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+DAA8D;IAC9D,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IACjC,6CAA6C;IAC7C,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/F;AAED,4FAA4F;AAC5F,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE;QAAE,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAA;KAAE,CAAC;IACjH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;CACpB;AAyBD;;;GAGG;AACH,wBAAsB,WAAW,CAChC,MAAM,GAAE,iBAAsB,EAC9B,OAAO,GAAE,iBAAoC,GAC3C,OAAO,CAAC,iBAAiB,CAAC,CA0C5B","sourcesContent":["/**\n * Child-side `@github/copilot-sdk/extension` implementation.\n *\n * Design: `docs/canvas-extensions-design.md` §4, §4.1. A canvas extension from\n * `github/awesome-copilot` is a directory of plain `.mjs` files whose only\n * external import is `@github/copilot-sdk/extension` — GitHub's own\n * `docs/extensions.md` states the import \"is resolved automatically — you don't\n * install it\", and none of the 23 catalog extensions ships `node_modules`. This\n * module is what hoocode resolves that specifier to inside the forked child, so a\n * catalog canvas runs byte-identical to upstream with nothing written into its\n * directory.\n *\n * What this is not: the SDK's `joinSession` resolves to a full `CopilotSession`\n * (messaging, events, RPC passthrough, factories). This shim implements the\n * canvas surface and `log` only. `tools`, `hooks`, and `factories` are accepted\n * and reported as unsupported so an extension that uses them fails visibly rather\n * than half-working (design doc §6.2). The canvas types themselves are held\n * structurally identical to the SDK's by\n * `test/canvas-protocol-conformance.test.ts`.\n */\n\nimport {\n\tCANVAS_ENVELOPE_VERSION,\n\tCANVAS_ERROR_CODE_INTERNAL,\n\tCANVAS_ERROR_CODE_UNKNOWN_TARGET,\n\tCANVAS_METHOD_CLOSE,\n\tCANVAS_METHOD_INVOKE_ACTION,\n\tCANVAS_METHOD_OPEN,\n\tCANVAS_RESERVED_ACTION_PREFIX,\n\tCANVAS_SDK_PROTOCOL_VERSION,\n\ttype CanvasActionDeclaration,\n\ttype CanvasActionInvokeResult,\n\ttype CanvasChildToHostMessage,\n\ttype CanvasDeclaration,\n\ttype CanvasJsonSchema,\n\ttype CanvasLogLevel,\n\tCanvasMessageDecoder,\n\ttype CanvasProviderCloseRequest,\n\ttype CanvasProviderInvokeActionRequest,\n\ttype CanvasProviderOpenRequest,\n\ttype CanvasProviderOpenResult,\n\tencodeCanvasMessage,\n\tisCanvasHostToChildMessage,\n\ttype JsonValue,\n} from \"../protocol.js\";\n\n/**\n * A single agent-callable action contributed by a canvas. Mirrors the SDK's\n * `CanvasAction`: the `handler` closure is stripped before the declaration is\n * sent and dispatched in-process here.\n */\nexport interface CanvasAction {\n\t/** Action identifier, unique within the canvas. */\n\tname: string;\n\t/** Description shown to the model when picking an action. */\n\tdescription?: string;\n\t/** Optional JSON Schema for the action's `input` payload. */\n\tinputSchema?: CanvasJsonSchema;\n\t/** Required per-action dispatch handler. */\n\thandler: (ctx: CanvasProviderInvokeActionRequest) => Promise<unknown> | unknown;\n}\n\n/** Options accepted by {@link createCanvas}. Mirrors the SDK's `CanvasOptions`. */\nexport interface CanvasOptions {\n\t/** Canvas id, unique within the declaring connection. */\n\tid: string;\n\t/** Human-readable label shown in discovery and host UI chrome. */\n\tdisplayName: string;\n\t/** Short, single-sentence description shown to the agent in canvas catalogs. */\n\tdescription: string;\n\t/** Optional JSON Schema for the `input` payload accepted by `canvas.open`. */\n\tinputSchema?: CanvasJsonSchema;\n\t/** Agent-invocable actions, each carrying its own required handler. */\n\tactions?: CanvasAction[];\n\t/** Required. Open a new canvas instance. */\n\topen: (ctx: CanvasProviderOpenRequest) => Promise<CanvasProviderOpenResult> | CanvasProviderOpenResult;\n\t/**\n\t * Optional. Notified when a canvas instance is closed. Fire-and-forget: the\n\t * return value is ignored and errors are logged but not surfaced.\n\t */\n\tonClose?: (ctx: CanvasProviderCloseRequest) => Promise<void> | void;\n}\n\n/** Structured error returned from canvas handlers. Mirrors the SDK's `CanvasError`. */\nexport class CanvasError extends Error {\n\treadonly code: string;\n\n\tconstructor(code: string, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"CanvasError\";\n\t\tthis.code = code;\n\t}\n\n\t/** Default error when an action is declared but no `handler` is wired. */\n\tstatic noHandler(): CanvasError {\n\t\treturn new CanvasError(\"no_handler\", \"Canvas action has no handler.\");\n\t}\n}\n\n/** A registered canvas: declarative metadata plus in-process handler closures. */\nexport class Canvas {\n\treadonly declaration: CanvasDeclaration;\n\treadonly open: NonNullable<CanvasOptions[\"open\"]>;\n\treadonly onClose?: CanvasOptions[\"onClose\"];\n\t/** Handlers by action name, kept out of the declaration that crosses the wire. */\n\treadonly handlers: ReadonlyMap<string, CanvasAction[\"handler\"]>;\n\n\tconstructor(options: CanvasOptions) {\n\t\tconst actions: CanvasActionDeclaration[] = [];\n\t\tconst handlers = new Map<string, CanvasAction[\"handler\"]>();\n\t\tfor (const action of options.actions ?? []) {\n\t\t\tif (action.name.startsWith(CANVAS_RESERVED_ACTION_PREFIX)) {\n\t\t\t\tthrow new CanvasError(\n\t\t\t\t\t\"invalid_action_name\",\n\t\t\t\t\t`Action \"${action.name}\" must not start with \"${CANVAS_RESERVED_ACTION_PREFIX}\" — that prefix is reserved for lifecycle verbs.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (handlers.has(action.name)) {\n\t\t\t\tthrow new CanvasError(\"duplicate_action\", `Action \"${action.name}\" is declared more than once.`);\n\t\t\t}\n\t\t\tactions.push({ name: action.name, description: action.description, inputSchema: action.inputSchema });\n\t\t\thandlers.set(action.name, action.handler);\n\t\t}\n\t\tthis.declaration = {\n\t\t\tid: options.id,\n\t\t\tdisplayName: options.displayName,\n\t\t\tdescription: options.description,\n\t\t\tinputSchema: options.inputSchema,\n\t\t\tactions: actions.length > 0 ? actions : undefined,\n\t\t};\n\t\tthis.open = options.open;\n\t\tthis.onClose = options.onClose;\n\t\tthis.handlers = handlers;\n\t}\n}\n\n/** Create a canvas declaration with bound in-process handlers. */\nexport function createCanvas(options: CanvasOptions): Canvas {\n\treturn new Canvas(options);\n}\n\n/** Surfaces a Copilot extension may declare that this shim does not implement. */\nconst UNSUPPORTED_KEYS = [\"tools\", \"hooks\", \"factories\", \"onPermissionRequest\"] as const;\n\n/** Configuration accepted by {@link joinSession}. */\nexport interface JoinSessionConfig {\n\tcanvases?: Canvas[];\n\t/** Accepted and reported as unsupported (design doc §6.2). */\n\ttools?: unknown;\n\t/** Accepted and reported as unsupported (design doc §6.2). */\n\thooks?: unknown;\n\t/** Accepted and reported as unsupported (design doc §6.2). */\n\tfactories?: unknown;\n\t/** Accepted and reported as unsupported (design doc §6.2). */\n\tonPermissionRequest?: unknown;\n}\n\n/**\n * The subset of the SDK's `CopilotSession` this shim provides. Deliberately small:\n * see the module header.\n */\nexport interface CanvasShimSession {\n\t/** Log a message to the session timeline. */\n\tlog(message: string, options?: { level?: CanvasLogLevel; ephemeral?: boolean }): Promise<void>;\n}\n\n/** Streams the shim reads from and writes to. Injectable so tests need no child process. */\nexport interface CanvasShimStreams {\n\tinput: { setEncoding(encoding: \"utf8\"): unknown; on(event: \"data\", listener: (chunk: string) => void): unknown };\n\twrite(chunk: string): unknown;\n\t/** Provider identifier; the runner passes it in the environment. */\n\textensionId: string;\n}\n\nfunction defaultStreams(): CanvasShimStreams {\n\treturn {\n\t\tinput: process.stdin,\n\t\twrite: (chunk: string) => process.stdout.write(chunk),\n\t\textensionId: process.env.HOOCODE_CANVAS_EXTENSION_ID ?? \"unknown-extension\",\n\t};\n}\n\nfunction toJsonValue(value: unknown): JsonValue {\n\t// Handler return values are author-supplied. Round-tripping through JSON is what\n\t// the wire does anyway, so do it here where a cycle or a BigInt fails loudly with\n\t// the action's name attached rather than mid-write.\n\treturn value === undefined ? null : (JSON.parse(JSON.stringify(value)) as JsonValue);\n}\n\nfunction errorOf(cause: unknown): { code: string; message: string } {\n\tif (cause instanceof CanvasError) return { code: cause.code, message: cause.message };\n\treturn {\n\t\tcode: CANVAS_ERROR_CODE_INTERNAL,\n\t\tmessage: cause instanceof Error ? cause.message : String(cause),\n\t};\n}\n\n/**\n * Join the host session: announce the declared canvases, then serve provider\n * callbacks until the input stream ends.\n */\nexport async function joinSession(\n\tconfig: JoinSessionConfig = {},\n\tstreams: CanvasShimStreams = defaultStreams(),\n): Promise<CanvasShimSession> {\n\tconst canvases = new Map<string, Canvas>();\n\tfor (const canvas of config.canvases ?? []) {\n\t\tif (canvases.has(canvas.declaration.id)) {\n\t\t\tthrow new CanvasError(\"duplicate_canvas\", `Canvas \"${canvas.declaration.id}\" is declared more than once.`);\n\t\t}\n\t\tcanvases.set(canvas.declaration.id, canvas);\n\t}\n\n\tconst send = (message: CanvasChildToHostMessage): void => {\n\t\tstreams.write(encodeCanvasMessage(message));\n\t};\n\n\tsend({\n\t\tenvelope: CANVAS_ENVELOPE_VERSION,\n\t\ttype: \"ready\",\n\t\tprotocolVersion: CANVAS_SDK_PROTOCOL_VERSION,\n\t\textensionId: streams.extensionId,\n\t\tcanvases: [...canvases.values()].map((canvas) => canvas.declaration),\n\t\tunsupported: UNSUPPORTED_KEYS.filter((key) => config[key] !== undefined),\n\t});\n\n\tconst decoder = new CanvasMessageDecoder();\n\tstreams.input.setEncoding(\"utf8\");\n\tstreams.input.on(\"data\", (chunk: string) => {\n\t\tfor (const value of decoder.push(chunk).values) {\n\t\t\tif (!isCanvasHostToChildMessage(value)) continue;\n\t\t\tvoid dispatch(canvases, value.id, value.method, value.params, send);\n\t\t}\n\t});\n\n\treturn {\n\t\tlog: async (message, options) => {\n\t\t\tsend({\n\t\t\t\tenvelope: CANVAS_ENVELOPE_VERSION,\n\t\t\t\ttype: \"log\",\n\t\t\t\tmessage,\n\t\t\t\tlevel: options?.level,\n\t\t\t\tephemeral: options?.ephemeral,\n\t\t\t});\n\t\t},\n\t};\n}\n\nasync function dispatch(\n\tcanvases: ReadonlyMap<string, Canvas>,\n\tid: number,\n\tmethod: string,\n\tparams: JsonValue,\n\tsend: (message: CanvasChildToHostMessage) => void,\n): Promise<void> {\n\ttry {\n\t\tsend({\n\t\t\tenvelope: CANVAS_ENVELOPE_VERSION,\n\t\t\ttype: \"response\",\n\t\t\tid,\n\t\t\tresult: await invoke(canvases, method, params),\n\t\t});\n\t} catch (cause) {\n\t\tconst { code, message } = errorOf(cause);\n\t\tsend({ envelope: CANVAS_ENVELOPE_VERSION, type: \"error\", id, code, message });\n\t}\n}\n\nasync function invoke(canvases: ReadonlyMap<string, Canvas>, method: string, params: JsonValue): Promise<JsonValue> {\n\tif (method === CANVAS_METHOD_OPEN) {\n\t\tconst ctx = params as unknown as CanvasProviderOpenRequest;\n\t\treturn toJsonValue(await resolve(canvases, ctx.canvasId).open(ctx));\n\t}\n\tif (method === CANVAS_METHOD_CLOSE) {\n\t\tconst ctx = params as unknown as CanvasProviderCloseRequest;\n\t\t// Fire-and-forget per the SDK contract: a failing onClose must not fail the close.\n\t\tawait resolve(canvases, ctx.canvasId).onClose?.(ctx);\n\t\treturn null;\n\t}\n\tif (method === CANVAS_METHOD_INVOKE_ACTION) {\n\t\tconst ctx = params as unknown as CanvasProviderInvokeActionRequest;\n\t\tconst canvas = resolve(canvases, ctx.canvasId);\n\t\tconst handler = canvas.handlers.get(ctx.actionName);\n\t\tif (!handler) {\n\t\t\tthrow new CanvasError(\n\t\t\t\tCANVAS_ERROR_CODE_UNKNOWN_TARGET,\n\t\t\t\t`Canvas \"${ctx.canvasId}\" does not declare an action named \"${ctx.actionName}\".`,\n\t\t\t);\n\t\t}\n\t\tconst result: CanvasActionInvokeResult = toJsonValue(await handler(ctx));\n\t\treturn result;\n\t}\n\t// Unreachable while the host only sends methods `isCanvasHostToChildMessage`\n\t// accepts, but naming it keeps this function total if the protocol grows.\n\tthrow new CanvasError(CANVAS_ERROR_CODE_UNKNOWN_TARGET, `Unsupported provider method \"${method}\".`);\n}\n\nfunction resolve(canvases: ReadonlyMap<string, Canvas>, canvasId: string): Canvas {\n\tconst canvas = canvases.get(canvasId);\n\tif (!canvas) {\n\t\tthrow new CanvasError(CANVAS_ERROR_CODE_UNKNOWN_TARGET, `No canvas is registered with id \"${canvasId}\".`);\n\t}\n\treturn canvas;\n}\n"]}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-side `@github/copilot-sdk/extension` implementation.
|
|
3
|
+
*
|
|
4
|
+
* Design: `docs/canvas-extensions-design.md` §4, §4.1. A canvas extension from
|
|
5
|
+
* `github/awesome-copilot` is a directory of plain `.mjs` files whose only
|
|
6
|
+
* external import is `@github/copilot-sdk/extension` — GitHub's own
|
|
7
|
+
* `docs/extensions.md` states the import "is resolved automatically — you don't
|
|
8
|
+
* install it", and none of the 23 catalog extensions ships `node_modules`. This
|
|
9
|
+
* module is what hoocode resolves that specifier to inside the forked child, so a
|
|
10
|
+
* catalog canvas runs byte-identical to upstream with nothing written into its
|
|
11
|
+
* directory.
|
|
12
|
+
*
|
|
13
|
+
* What this is not: the SDK's `joinSession` resolves to a full `CopilotSession`
|
|
14
|
+
* (messaging, events, RPC passthrough, factories). This shim implements the
|
|
15
|
+
* canvas surface and `log` only. `tools`, `hooks`, and `factories` are accepted
|
|
16
|
+
* and reported as unsupported so an extension that uses them fails visibly rather
|
|
17
|
+
* than half-working (design doc §6.2). The canvas types themselves are held
|
|
18
|
+
* structurally identical to the SDK's by
|
|
19
|
+
* `test/canvas-protocol-conformance.test.ts`.
|
|
20
|
+
*/
|
|
21
|
+
import { CANVAS_ENVELOPE_VERSION, CANVAS_ERROR_CODE_INTERNAL, CANVAS_ERROR_CODE_UNKNOWN_TARGET, CANVAS_METHOD_CLOSE, CANVAS_METHOD_INVOKE_ACTION, CANVAS_METHOD_OPEN, CANVAS_RESERVED_ACTION_PREFIX, CANVAS_SDK_PROTOCOL_VERSION, CanvasMessageDecoder, encodeCanvasMessage, isCanvasHostToChildMessage, } from "../protocol.js";
|
|
22
|
+
/** Structured error returned from canvas handlers. Mirrors the SDK's `CanvasError`. */
|
|
23
|
+
export class CanvasError extends Error {
|
|
24
|
+
code;
|
|
25
|
+
constructor(code, message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "CanvasError";
|
|
28
|
+
this.code = code;
|
|
29
|
+
}
|
|
30
|
+
/** Default error when an action is declared but no `handler` is wired. */
|
|
31
|
+
static noHandler() {
|
|
32
|
+
return new CanvasError("no_handler", "Canvas action has no handler.");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** A registered canvas: declarative metadata plus in-process handler closures. */
|
|
36
|
+
export class Canvas {
|
|
37
|
+
declaration;
|
|
38
|
+
open;
|
|
39
|
+
onClose;
|
|
40
|
+
/** Handlers by action name, kept out of the declaration that crosses the wire. */
|
|
41
|
+
handlers;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
const actions = [];
|
|
44
|
+
const handlers = new Map();
|
|
45
|
+
for (const action of options.actions ?? []) {
|
|
46
|
+
if (action.name.startsWith(CANVAS_RESERVED_ACTION_PREFIX)) {
|
|
47
|
+
throw new CanvasError("invalid_action_name", `Action "${action.name}" must not start with "${CANVAS_RESERVED_ACTION_PREFIX}" — that prefix is reserved for lifecycle verbs.`);
|
|
48
|
+
}
|
|
49
|
+
if (handlers.has(action.name)) {
|
|
50
|
+
throw new CanvasError("duplicate_action", `Action "${action.name}" is declared more than once.`);
|
|
51
|
+
}
|
|
52
|
+
actions.push({ name: action.name, description: action.description, inputSchema: action.inputSchema });
|
|
53
|
+
handlers.set(action.name, action.handler);
|
|
54
|
+
}
|
|
55
|
+
this.declaration = {
|
|
56
|
+
id: options.id,
|
|
57
|
+
displayName: options.displayName,
|
|
58
|
+
description: options.description,
|
|
59
|
+
inputSchema: options.inputSchema,
|
|
60
|
+
actions: actions.length > 0 ? actions : undefined,
|
|
61
|
+
};
|
|
62
|
+
this.open = options.open;
|
|
63
|
+
this.onClose = options.onClose;
|
|
64
|
+
this.handlers = handlers;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Create a canvas declaration with bound in-process handlers. */
|
|
68
|
+
export function createCanvas(options) {
|
|
69
|
+
return new Canvas(options);
|
|
70
|
+
}
|
|
71
|
+
/** Surfaces a Copilot extension may declare that this shim does not implement. */
|
|
72
|
+
const UNSUPPORTED_KEYS = ["tools", "hooks", "factories", "onPermissionRequest"];
|
|
73
|
+
function defaultStreams() {
|
|
74
|
+
return {
|
|
75
|
+
input: process.stdin,
|
|
76
|
+
write: (chunk) => process.stdout.write(chunk),
|
|
77
|
+
extensionId: process.env.HOOCODE_CANVAS_EXTENSION_ID ?? "unknown-extension",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function toJsonValue(value) {
|
|
81
|
+
// Handler return values are author-supplied. Round-tripping through JSON is what
|
|
82
|
+
// the wire does anyway, so do it here where a cycle or a BigInt fails loudly with
|
|
83
|
+
// the action's name attached rather than mid-write.
|
|
84
|
+
return value === undefined ? null : JSON.parse(JSON.stringify(value));
|
|
85
|
+
}
|
|
86
|
+
function errorOf(cause) {
|
|
87
|
+
if (cause instanceof CanvasError)
|
|
88
|
+
return { code: cause.code, message: cause.message };
|
|
89
|
+
return {
|
|
90
|
+
code: CANVAS_ERROR_CODE_INTERNAL,
|
|
91
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Join the host session: announce the declared canvases, then serve provider
|
|
96
|
+
* callbacks until the input stream ends.
|
|
97
|
+
*/
|
|
98
|
+
export async function joinSession(config = {}, streams = defaultStreams()) {
|
|
99
|
+
const canvases = new Map();
|
|
100
|
+
for (const canvas of config.canvases ?? []) {
|
|
101
|
+
if (canvases.has(canvas.declaration.id)) {
|
|
102
|
+
throw new CanvasError("duplicate_canvas", `Canvas "${canvas.declaration.id}" is declared more than once.`);
|
|
103
|
+
}
|
|
104
|
+
canvases.set(canvas.declaration.id, canvas);
|
|
105
|
+
}
|
|
106
|
+
const send = (message) => {
|
|
107
|
+
streams.write(encodeCanvasMessage(message));
|
|
108
|
+
};
|
|
109
|
+
send({
|
|
110
|
+
envelope: CANVAS_ENVELOPE_VERSION,
|
|
111
|
+
type: "ready",
|
|
112
|
+
protocolVersion: CANVAS_SDK_PROTOCOL_VERSION,
|
|
113
|
+
extensionId: streams.extensionId,
|
|
114
|
+
canvases: [...canvases.values()].map((canvas) => canvas.declaration),
|
|
115
|
+
unsupported: UNSUPPORTED_KEYS.filter((key) => config[key] !== undefined),
|
|
116
|
+
});
|
|
117
|
+
const decoder = new CanvasMessageDecoder();
|
|
118
|
+
streams.input.setEncoding("utf8");
|
|
119
|
+
streams.input.on("data", (chunk) => {
|
|
120
|
+
for (const value of decoder.push(chunk).values) {
|
|
121
|
+
if (!isCanvasHostToChildMessage(value))
|
|
122
|
+
continue;
|
|
123
|
+
void dispatch(canvases, value.id, value.method, value.params, send);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
log: async (message, options) => {
|
|
128
|
+
send({
|
|
129
|
+
envelope: CANVAS_ENVELOPE_VERSION,
|
|
130
|
+
type: "log",
|
|
131
|
+
message,
|
|
132
|
+
level: options?.level,
|
|
133
|
+
ephemeral: options?.ephemeral,
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function dispatch(canvases, id, method, params, send) {
|
|
139
|
+
try {
|
|
140
|
+
send({
|
|
141
|
+
envelope: CANVAS_ENVELOPE_VERSION,
|
|
142
|
+
type: "response",
|
|
143
|
+
id,
|
|
144
|
+
result: await invoke(canvases, method, params),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (cause) {
|
|
148
|
+
const { code, message } = errorOf(cause);
|
|
149
|
+
send({ envelope: CANVAS_ENVELOPE_VERSION, type: "error", id, code, message });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function invoke(canvases, method, params) {
|
|
153
|
+
if (method === CANVAS_METHOD_OPEN) {
|
|
154
|
+
const ctx = params;
|
|
155
|
+
return toJsonValue(await resolve(canvases, ctx.canvasId).open(ctx));
|
|
156
|
+
}
|
|
157
|
+
if (method === CANVAS_METHOD_CLOSE) {
|
|
158
|
+
const ctx = params;
|
|
159
|
+
// Fire-and-forget per the SDK contract: a failing onClose must not fail the close.
|
|
160
|
+
await resolve(canvases, ctx.canvasId).onClose?.(ctx);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (method === CANVAS_METHOD_INVOKE_ACTION) {
|
|
164
|
+
const ctx = params;
|
|
165
|
+
const canvas = resolve(canvases, ctx.canvasId);
|
|
166
|
+
const handler = canvas.handlers.get(ctx.actionName);
|
|
167
|
+
if (!handler) {
|
|
168
|
+
throw new CanvasError(CANVAS_ERROR_CODE_UNKNOWN_TARGET, `Canvas "${ctx.canvasId}" does not declare an action named "${ctx.actionName}".`);
|
|
169
|
+
}
|
|
170
|
+
const result = toJsonValue(await handler(ctx));
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
// Unreachable while the host only sends methods `isCanvasHostToChildMessage`
|
|
174
|
+
// accepts, but naming it keeps this function total if the protocol grows.
|
|
175
|
+
throw new CanvasError(CANVAS_ERROR_CODE_UNKNOWN_TARGET, `Unsupported provider method "${method}".`);
|
|
176
|
+
}
|
|
177
|
+
function resolve(canvases, canvasId) {
|
|
178
|
+
const canvas = canvases.get(canvasId);
|
|
179
|
+
if (!canvas) {
|
|
180
|
+
throw new CanvasError(CANVAS_ERROR_CODE_UNKNOWN_TARGET, `No canvas is registered with id "${canvasId}".`);
|
|
181
|
+
}
|
|
182
|
+
return canvas;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=index.js.map
|