@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/config.d.ts +11 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +18 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/agent-log.d.ts +44 -0
  7. package/dist/core/agent-log.d.ts.map +1 -0
  8. package/dist/core/agent-log.js +74 -0
  9. package/dist/core/agent-log.js.map +1 -0
  10. package/dist/core/canvas/discovery.d.ts +60 -0
  11. package/dist/core/canvas/discovery.d.ts.map +1 -0
  12. package/dist/core/canvas/discovery.js +83 -0
  13. package/dist/core/canvas/discovery.js.map +1 -0
  14. package/dist/core/canvas/launch.d.ts +90 -0
  15. package/dist/core/canvas/launch.d.ts.map +1 -0
  16. package/dist/core/canvas/launch.js +176 -0
  17. package/dist/core/canvas/launch.js.map +1 -0
  18. package/dist/core/canvas/protocol.d.ts +229 -0
  19. package/dist/core/canvas/protocol.d.ts.map +1 -0
  20. package/dist/core/canvas/protocol.js +129 -0
  21. package/dist/core/canvas/protocol.js.map +1 -0
  22. package/dist/core/canvas/registry.d.ts +139 -0
  23. package/dist/core/canvas/registry.d.ts.map +1 -0
  24. package/dist/core/canvas/registry.js +275 -0
  25. package/dist/core/canvas/registry.js.map +1 -0
  26. package/dist/core/canvas/resolver.d.ts +26 -0
  27. package/dist/core/canvas/resolver.d.ts.map +1 -0
  28. package/dist/core/canvas/resolver.js +57 -0
  29. package/dist/core/canvas/resolver.js.map +1 -0
  30. package/dist/core/canvas/runner.d.ts +101 -0
  31. package/dist/core/canvas/runner.d.ts.map +1 -0
  32. package/dist/core/canvas/runner.js +196 -0
  33. package/dist/core/canvas/runner.js.map +1 -0
  34. package/dist/core/canvas/sdk-shim/dispatch.test-helpers.d.ts +23 -0
  35. package/dist/core/canvas/sdk-shim/dispatch.test-helpers.d.ts.map +1 -0
  36. package/dist/core/canvas/sdk-shim/dispatch.test-helpers.js +36 -0
  37. package/dist/core/canvas/sdk-shim/dispatch.test-helpers.js.map +1 -0
  38. package/dist/core/canvas/sdk-shim/index.d.ts +113 -0
  39. package/dist/core/canvas/sdk-shim/index.d.ts.map +1 -0
  40. package/dist/core/canvas/sdk-shim/index.js +184 -0
  41. package/dist/core/canvas/sdk-shim/index.js.map +1 -0
  42. package/dist/core/canvas/session.d.ts +112 -0
  43. package/dist/core/canvas/session.d.ts.map +1 -0
  44. package/dist/core/canvas/session.js +197 -0
  45. package/dist/core/canvas/session.js.map +1 -0
  46. package/dist/core/canvas/trust.d.ts +71 -0
  47. package/dist/core/canvas/trust.d.ts.map +1 -0
  48. package/dist/core/canvas/trust.js +88 -0
  49. package/dist/core/canvas/trust.js.map +1 -0
  50. package/dist/core/extensions/loader.d.ts.map +1 -1
  51. package/dist/core/extensions/loader.js +7 -10
  52. package/dist/core/extensions/loader.js.map +1 -1
  53. package/dist/core/extensions/plugins/trust.d.ts +21 -0
  54. package/dist/core/extensions/plugins/trust.d.ts.map +1 -1
  55. package/dist/core/extensions/plugins/trust.js +26 -0
  56. package/dist/core/extensions/plugins/trust.js.map +1 -1
  57. package/dist/core/lifeguard.d.ts.map +1 -1
  58. package/dist/core/lifeguard.js +2 -1
  59. package/dist/core/lifeguard.js.map +1 -1
  60. package/dist/core/subagent-pool.d.ts.map +1 -1
  61. package/dist/core/subagent-pool.js +6 -4
  62. package/dist/core/subagent-pool.js.map +1 -1
  63. package/dist/core/tools/canvas.d.ts +55 -0
  64. package/dist/core/tools/canvas.d.ts.map +1 -0
  65. package/dist/core/tools/canvas.js +159 -0
  66. package/dist/core/tools/canvas.js.map +1 -0
  67. package/dist/core/tools/subagent.d.ts.map +1 -1
  68. package/dist/core/tools/subagent.js +3 -2
  69. package/dist/core/tools/subagent.js.map +1 -1
  70. package/dist/extensions/core/canvas.d.ts +20 -0
  71. package/dist/extensions/core/canvas.d.ts.map +1 -0
  72. package/dist/extensions/core/canvas.js +192 -0
  73. package/dist/extensions/core/canvas.js.map +1 -0
  74. package/dist/extensions/core/hoo-core.d.ts +1 -0
  75. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  76. package/dist/extensions/core/hoo-core.js +3 -0
  77. package/dist/extensions/core/hoo-core.js.map +1 -1
  78. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.js +7 -0
  80. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  81. package/dist/utils/paths.d.ts +9 -0
  82. package/dist/utils/paths.d.ts.map +1 -1
  83. package/dist/utils/paths.js +16 -0
  84. package/dist/utils/paths.js.map +1 -1
  85. package/docs/routing.md +1 -1
  86. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  87. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  88. package/examples/extensions/sandbox/package.json +1 -1
  89. package/examples/extensions/with-deps/package.json +1 -1
  90. package/package.json +5 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Canvas wire protocol — the entire GitHub-facing surface.\n *\n * Design: `docs/canvas-extensions-design.md` §2. The short version: GitHub's own\n * types (`@github/copilot-sdk` `dist/canvas.d.ts`) state that the Node\n * `createCanvas`/`joinSession` API is one of five language wrappers over the same\n * JSON-RPC wire protocol, and that \"the divergence is API ergonomics only\". So\n * hoocode binds to the wire protocol, not to the Node sugar, and the drift\n * surface is the three provider methods below plus one version integer.\n *\n * Everything a third-party canvas can observe lives in this file. If GitHub\n * moves the protocol, this file and `sdk-shim/` move; nothing else does.\n *\n * Two layers are deliberately separated here:\n *\n * 1. **Provider contract** (`CanvasProvider*`, `CANVAS_METHOD_*`) — GitHub's.\n * These payload shapes reach extension code as the `ctx` argument of\n * `open`, `onClose`, and action handlers, so they must stay structurally\n * identical to the SDK's. `test/canvas-protocol-conformance.test.ts` fails\n * the build if they drift.\n * 2. **Host envelope** (`Canvas*Message`) — ours. It carries provider calls\n * between hoocode's runner and the child-side shim. In the Copilot case the\n * equivalent layer sits between their CLI and their SDK and an extension\n * never sees it, so we are free to keep it simple: newline-delimited JSON.\n */\n\n/** JSON value, mirroring the SDK's `JsonValue`. */\nexport type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };\n\n/** JSON Schema carried as data, mirroring the SDK's `CanvasJsonSchema`. */\nexport type CanvasJsonSchema = JsonValue;\n\n/** Result of an action invocation, mirroring the SDK's `CanvasActionInvokeResult`. */\nexport type CanvasActionInvokeResult = JsonValue;\n\n/**\n * SDK protocol version this implementation targets, mirroring the SDK's\n * `SDK_PROTOCOL_VERSION`. The conformance test asserts the two are equal, so a\n * protocol bump surfaces as a failing test rather than a runtime mismatch.\n */\nexport const CANVAS_SDK_PROTOCOL_VERSION = 3;\n\n/** Provider callback: open a canvas instance. */\nexport const CANVAS_METHOD_OPEN = \"canvas.open\";\n/** Provider callback: a canvas instance was closed. */\nexport const CANVAS_METHOD_CLOSE = \"canvas.close\";\n/** Provider callback: invoke an agent-callable action. */\nexport const CANVAS_METHOD_INVOKE_ACTION = \"canvas.action.invoke\";\n\n/** The three provider callbacks, in the order the design doc lists them. */\nexport const CANVAS_PROVIDER_METHODS = [CANVAS_METHOD_OPEN, CANVAS_METHOD_CLOSE, CANVAS_METHOD_INVOKE_ACTION] as const;\n\n/** One of the three provider callback names. */\nexport type CanvasProviderMethod = (typeof CANVAS_PROVIDER_METHODS)[number];\n\n/** Host capabilities advertised to the provider. */\nexport interface CanvasHostContextCapabilities {\n\t/** Whether canvas rendering is supported. */\n\tcanvases?: boolean;\n}\n\n/** Host context supplied by the runtime. */\nexport interface CanvasHostContext {\n\tcapabilities?: CanvasHostContextCapabilities;\n}\n\n/** Session context supplied by the runtime. */\nexport interface CanvasSessionContext {\n\t/** Active session working directory, when known. */\n\tworkingDirectory?: string;\n}\n\n/** Fields shared by every provider callback payload. */\ninterface CanvasProviderRequestBase {\n\t/** Target session identifier. */\n\tsessionId: string;\n\t/** Owning provider identifier. */\n\textensionId: string;\n\t/** Provider-local canvas identifier. */\n\tcanvasId: string;\n\t/** Canvas instance identifier. */\n\tinstanceId: string;\n\thost?: CanvasHostContext;\n\tsession?: CanvasSessionContext;\n}\n\n/** `canvas.open` payload. */\nexport interface CanvasProviderOpenRequest extends CanvasProviderRequestBase {\n\t/** Canvas open input. */\n\tinput?: JsonValue;\n}\n\n/** `canvas.open` result. A web-rendered canvas returns the URL the host loads. */\nexport interface CanvasProviderOpenResult {\n\t/** URL for web-rendered canvases. */\n\turl?: string;\n\t/** Provider-supplied title. */\n\ttitle?: string;\n\t/** Provider-supplied status text. */\n\tstatus?: string;\n}\n\n/** `canvas.close` payload. */\nexport interface CanvasProviderCloseRequest extends CanvasProviderRequestBase {}\n\n/** `canvas.action.invoke` payload. */\nexport interface CanvasProviderInvokeActionRequest extends CanvasProviderRequestBase {\n\t/** Action name to invoke. */\n\tactionName: string;\n\t/** Action input. */\n\tinput?: JsonValue;\n}\n\n/**\n * Action metadata as it crosses the wire. The SDK strips each action's `handler`\n * closure before sending the declaration, so this is `CanvasAction` minus the\n * handler.\n */\nexport interface CanvasActionDeclaration {\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}\n\n/** Declarative metadata for a single canvas. */\nexport interface CanvasDeclaration {\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. */\n\tactions?: CanvasActionDeclaration[];\n}\n\n/**\n * Reserved action-name prefix. The SDK is explicit: \"Names MUST NOT start with\n * `canvas.` — that prefix is reserved for lifecycle verbs.\"\n */\nexport const CANVAS_RESERVED_ACTION_PREFIX = \"canvas.\";\n\n/** Log levels accepted by `session.log`. */\nexport type CanvasLogLevel = \"info\" | \"warning\" | \"error\";\n\n/**\n * Host envelope version. Ours, not GitHub's — bumped only when the\n * runner↔shim framing changes.\n */\nexport const CANVAS_ENVELOPE_VERSION = 1;\n\n/** Child announces itself and its canvases. Always the first message. */\nexport interface CanvasReadyMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"ready\";\n\t/** The `CANVAS_SDK_PROTOCOL_VERSION` the child was built against. */\n\tprotocolVersion: number;\n\t/** Provider identifier, derived by the runner from the extension directory. */\n\textensionId: string;\n\tcanvases: CanvasDeclaration[];\n\t/**\n\t * Surfaces declared by the extension that this shim does not implement, so the\n\t * runner can warn once instead of letting the extension half-work\n\t * (design doc §6.2).\n\t */\n\tunsupported?: string[];\n}\n\n/** Child forwards a `session.log` call. */\nexport interface CanvasLogMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"log\";\n\tmessage: string;\n\tlevel?: CanvasLogLevel;\n\tephemeral?: boolean;\n}\n\n/** Host asks the child to run one provider callback. */\nexport interface CanvasRequestMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"request\";\n\tid: number;\n\tmethod: CanvasProviderMethod;\n\tparams: JsonValue;\n}\n\n/** Child returns a provider callback result. */\nexport interface CanvasResponseMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"response\";\n\tid: number;\n\tresult: JsonValue;\n}\n\n/**\n * Child returns a failure. `code` carries `CanvasError.code` when the handler threw\n * one, so the agent gets a machine-readable code rather than a string to parse.\n */\nexport interface CanvasErrorMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"error\";\n\tid: number;\n\tcode: string;\n\tmessage: string;\n}\n\n/** Anything the host may send to a child. */\nexport type CanvasHostToChildMessage = CanvasRequestMessage;\n\n/** Anything a child may send to the host. */\nexport type CanvasChildToHostMessage =\n\t| CanvasReadyMessage\n\t| CanvasLogMessage\n\t| CanvasResponseMessage\n\t| CanvasErrorMessage;\n\n/** Error code used when a handler throws something that is not a `CanvasError`. */\nexport const CANVAS_ERROR_CODE_INTERNAL = \"internal_error\";\n/** Error code used when the host asks for a canvas or action the child does not declare. */\nexport const CANVAS_ERROR_CODE_UNKNOWN_TARGET = \"unknown_target\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasCurrentEnvelope(value: Record<string, unknown>): boolean {\n\treturn value.envelope === CANVAS_ENVELOPE_VERSION;\n}\n\n/** Whether `value` is a provider callback name. */\nexport function isCanvasProviderMethod(value: unknown): value is CanvasProviderMethod {\n\treturn typeof value === \"string\" && (CANVAS_PROVIDER_METHODS as readonly string[]).includes(value);\n}\n\n/** Narrow a decoded value to a host→child message. */\nexport function isCanvasHostToChildMessage(value: unknown): value is CanvasHostToChildMessage {\n\tif (!isRecord(value) || !hasCurrentEnvelope(value)) return false;\n\treturn value.type === \"request\" && typeof value.id === \"number\" && isCanvasProviderMethod(value.method);\n}\n\n/** Narrow a decoded value to a child→host message. */\nexport function isCanvasChildToHostMessage(value: unknown): value is CanvasChildToHostMessage {\n\tif (!isRecord(value) || !hasCurrentEnvelope(value)) return false;\n\tswitch (value.type) {\n\t\tcase \"ready\":\n\t\t\treturn typeof value.extensionId === \"string\" && Array.isArray(value.canvases);\n\t\tcase \"log\":\n\t\t\treturn typeof value.message === \"string\";\n\t\tcase \"response\":\n\t\t\treturn typeof value.id === \"number\";\n\t\tcase \"error\":\n\t\t\treturn typeof value.id === \"number\" && typeof value.code === \"string\" && typeof value.message === \"string\";\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/** Serialize one message as a single NDJSON line, newline included. */\nexport function encodeCanvasMessage(message: CanvasHostToChildMessage | CanvasChildToHostMessage): string {\n\treturn `${JSON.stringify(message)}\\n`;\n}\n\n/** One decode pass: parsed JSON values plus any lines that were not JSON at all. */\nexport interface CanvasDecodeResult {\n\tvalues: unknown[];\n\t/**\n\t * Lines that failed to parse. In the Copilot CLI stdout is the JSON-RPC channel,\n\t * so a stray `console.log` corrupts it; we keep the same discipline but report\n\t * the stray text so the runner can tell the author to use `session.log`\n\t * instead of leaving them with a silent protocol error.\n\t */\n\tstrays: string[];\n}\n\n/**\n * Incremental NDJSON decoder. Chunk boundaries do not respect line boundaries, so\n * a partial trailing line is held until the rest arrives.\n */\nexport class CanvasMessageDecoder {\n\tprivate buffer = \"\";\n\n\t/** Decode everything complete in `chunk`, buffering any partial trailing line. */\n\tpush(chunk: string): CanvasDecodeResult {\n\t\tthis.buffer += chunk;\n\t\tconst result: CanvasDecodeResult = { values: [], strays: [] };\n\t\tlet newline = this.buffer.indexOf(\"\\n\");\n\t\twhile (newline !== -1) {\n\t\t\tthis.take(this.buffer.slice(0, newline), result);\n\t\t\tthis.buffer = this.buffer.slice(newline + 1);\n\t\t\tnewline = this.buffer.indexOf(\"\\n\");\n\t\t}\n\t\treturn result;\n\t}\n\n\t/** Decode whatever is left, for use when the stream ends without a trailing newline. */\n\tflush(): CanvasDecodeResult {\n\t\tconst result: CanvasDecodeResult = { values: [], strays: [] };\n\t\tconst rest = this.buffer;\n\t\tthis.buffer = \"\";\n\t\tthis.take(rest, result);\n\t\treturn result;\n\t}\n\n\tprivate take(line: string, result: CanvasDecodeResult): void {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed.length === 0) return;\n\t\ttry {\n\t\t\tresult.values.push(JSON.parse(trimmed));\n\t\t} catch {\n\t\t\tresult.strays.push(trimmed);\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Canvas wire protocol — the entire GitHub-facing surface.
3
+ *
4
+ * Design: `docs/canvas-extensions-design.md` §2. The short version: GitHub's own
5
+ * types (`@github/copilot-sdk` `dist/canvas.d.ts`) state that the Node
6
+ * `createCanvas`/`joinSession` API is one of five language wrappers over the same
7
+ * JSON-RPC wire protocol, and that "the divergence is API ergonomics only". So
8
+ * hoocode binds to the wire protocol, not to the Node sugar, and the drift
9
+ * surface is the three provider methods below plus one version integer.
10
+ *
11
+ * Everything a third-party canvas can observe lives in this file. If GitHub
12
+ * moves the protocol, this file and `sdk-shim/` move; nothing else does.
13
+ *
14
+ * Two layers are deliberately separated here:
15
+ *
16
+ * 1. **Provider contract** (`CanvasProvider*`, `CANVAS_METHOD_*`) — GitHub's.
17
+ * These payload shapes reach extension code as the `ctx` argument of
18
+ * `open`, `onClose`, and action handlers, so they must stay structurally
19
+ * identical to the SDK's. `test/canvas-protocol-conformance.test.ts` fails
20
+ * the build if they drift.
21
+ * 2. **Host envelope** (`Canvas*Message`) — ours. It carries provider calls
22
+ * between hoocode's runner and the child-side shim. In the Copilot case the
23
+ * equivalent layer sits between their CLI and their SDK and an extension
24
+ * never sees it, so we are free to keep it simple: newline-delimited JSON.
25
+ */
26
+ /**
27
+ * SDK protocol version this implementation targets, mirroring the SDK's
28
+ * `SDK_PROTOCOL_VERSION`. The conformance test asserts the two are equal, so a
29
+ * protocol bump surfaces as a failing test rather than a runtime mismatch.
30
+ */
31
+ export const CANVAS_SDK_PROTOCOL_VERSION = 3;
32
+ /** Provider callback: open a canvas instance. */
33
+ export const CANVAS_METHOD_OPEN = "canvas.open";
34
+ /** Provider callback: a canvas instance was closed. */
35
+ export const CANVAS_METHOD_CLOSE = "canvas.close";
36
+ /** Provider callback: invoke an agent-callable action. */
37
+ export const CANVAS_METHOD_INVOKE_ACTION = "canvas.action.invoke";
38
+ /** The three provider callbacks, in the order the design doc lists them. */
39
+ export const CANVAS_PROVIDER_METHODS = [CANVAS_METHOD_OPEN, CANVAS_METHOD_CLOSE, CANVAS_METHOD_INVOKE_ACTION];
40
+ /**
41
+ * Reserved action-name prefix. The SDK is explicit: "Names MUST NOT start with
42
+ * `canvas.` — that prefix is reserved for lifecycle verbs."
43
+ */
44
+ export const CANVAS_RESERVED_ACTION_PREFIX = "canvas.";
45
+ /**
46
+ * Host envelope version. Ours, not GitHub's — bumped only when the
47
+ * runner↔shim framing changes.
48
+ */
49
+ export const CANVAS_ENVELOPE_VERSION = 1;
50
+ /** Error code used when a handler throws something that is not a `CanvasError`. */
51
+ export const CANVAS_ERROR_CODE_INTERNAL = "internal_error";
52
+ /** Error code used when the host asks for a canvas or action the child does not declare. */
53
+ export const CANVAS_ERROR_CODE_UNKNOWN_TARGET = "unknown_target";
54
+ function isRecord(value) {
55
+ return typeof value === "object" && value !== null && !Array.isArray(value);
56
+ }
57
+ function hasCurrentEnvelope(value) {
58
+ return value.envelope === CANVAS_ENVELOPE_VERSION;
59
+ }
60
+ /** Whether `value` is a provider callback name. */
61
+ export function isCanvasProviderMethod(value) {
62
+ return typeof value === "string" && CANVAS_PROVIDER_METHODS.includes(value);
63
+ }
64
+ /** Narrow a decoded value to a host→child message. */
65
+ export function isCanvasHostToChildMessage(value) {
66
+ if (!isRecord(value) || !hasCurrentEnvelope(value))
67
+ return false;
68
+ return value.type === "request" && typeof value.id === "number" && isCanvasProviderMethod(value.method);
69
+ }
70
+ /** Narrow a decoded value to a child→host message. */
71
+ export function isCanvasChildToHostMessage(value) {
72
+ if (!isRecord(value) || !hasCurrentEnvelope(value))
73
+ return false;
74
+ switch (value.type) {
75
+ case "ready":
76
+ return typeof value.extensionId === "string" && Array.isArray(value.canvases);
77
+ case "log":
78
+ return typeof value.message === "string";
79
+ case "response":
80
+ return typeof value.id === "number";
81
+ case "error":
82
+ return typeof value.id === "number" && typeof value.code === "string" && typeof value.message === "string";
83
+ default:
84
+ return false;
85
+ }
86
+ }
87
+ /** Serialize one message as a single NDJSON line, newline included. */
88
+ export function encodeCanvasMessage(message) {
89
+ return `${JSON.stringify(message)}\n`;
90
+ }
91
+ /**
92
+ * Incremental NDJSON decoder. Chunk boundaries do not respect line boundaries, so
93
+ * a partial trailing line is held until the rest arrives.
94
+ */
95
+ export class CanvasMessageDecoder {
96
+ buffer = "";
97
+ /** Decode everything complete in `chunk`, buffering any partial trailing line. */
98
+ push(chunk) {
99
+ this.buffer += chunk;
100
+ const result = { values: [], strays: [] };
101
+ let newline = this.buffer.indexOf("\n");
102
+ while (newline !== -1) {
103
+ this.take(this.buffer.slice(0, newline), result);
104
+ this.buffer = this.buffer.slice(newline + 1);
105
+ newline = this.buffer.indexOf("\n");
106
+ }
107
+ return result;
108
+ }
109
+ /** Decode whatever is left, for use when the stream ends without a trailing newline. */
110
+ flush() {
111
+ const result = { values: [], strays: [] };
112
+ const rest = this.buffer;
113
+ this.buffer = "";
114
+ this.take(rest, result);
115
+ return result;
116
+ }
117
+ take(line, result) {
118
+ const trimmed = line.trim();
119
+ if (trimmed.length === 0)
120
+ return;
121
+ try {
122
+ result.values.push(JSON.parse(trimmed));
123
+ }
124
+ catch {
125
+ result.strays.push(trimmed);
126
+ }
127
+ }
128
+ }
129
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/core/canvas/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Canvas wire protocol — the entire GitHub-facing surface.\n *\n * Design: `docs/canvas-extensions-design.md` §2. The short version: GitHub's own\n * types (`@github/copilot-sdk` `dist/canvas.d.ts`) state that the Node\n * `createCanvas`/`joinSession` API is one of five language wrappers over the same\n * JSON-RPC wire protocol, and that \"the divergence is API ergonomics only\". So\n * hoocode binds to the wire protocol, not to the Node sugar, and the drift\n * surface is the three provider methods below plus one version integer.\n *\n * Everything a third-party canvas can observe lives in this file. If GitHub\n * moves the protocol, this file and `sdk-shim/` move; nothing else does.\n *\n * Two layers are deliberately separated here:\n *\n * 1. **Provider contract** (`CanvasProvider*`, `CANVAS_METHOD_*`) — GitHub's.\n * These payload shapes reach extension code as the `ctx` argument of\n * `open`, `onClose`, and action handlers, so they must stay structurally\n * identical to the SDK's. `test/canvas-protocol-conformance.test.ts` fails\n * the build if they drift.\n * 2. **Host envelope** (`Canvas*Message`) — ours. It carries provider calls\n * between hoocode's runner and the child-side shim. In the Copilot case the\n * equivalent layer sits between their CLI and their SDK and an extension\n * never sees it, so we are free to keep it simple: newline-delimited JSON.\n */\n\n/** JSON value, mirroring the SDK's `JsonValue`. */\nexport type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };\n\n/** JSON Schema carried as data, mirroring the SDK's `CanvasJsonSchema`. */\nexport type CanvasJsonSchema = JsonValue;\n\n/** Result of an action invocation, mirroring the SDK's `CanvasActionInvokeResult`. */\nexport type CanvasActionInvokeResult = JsonValue;\n\n/**\n * SDK protocol version this implementation targets, mirroring the SDK's\n * `SDK_PROTOCOL_VERSION`. The conformance test asserts the two are equal, so a\n * protocol bump surfaces as a failing test rather than a runtime mismatch.\n */\nexport const CANVAS_SDK_PROTOCOL_VERSION = 3;\n\n/** Provider callback: open a canvas instance. */\nexport const CANVAS_METHOD_OPEN = \"canvas.open\";\n/** Provider callback: a canvas instance was closed. */\nexport const CANVAS_METHOD_CLOSE = \"canvas.close\";\n/** Provider callback: invoke an agent-callable action. */\nexport const CANVAS_METHOD_INVOKE_ACTION = \"canvas.action.invoke\";\n\n/** The three provider callbacks, in the order the design doc lists them. */\nexport const CANVAS_PROVIDER_METHODS = [CANVAS_METHOD_OPEN, CANVAS_METHOD_CLOSE, CANVAS_METHOD_INVOKE_ACTION] as const;\n\n/** One of the three provider callback names. */\nexport type CanvasProviderMethod = (typeof CANVAS_PROVIDER_METHODS)[number];\n\n/** Host capabilities advertised to the provider. */\nexport interface CanvasHostContextCapabilities {\n\t/** Whether canvas rendering is supported. */\n\tcanvases?: boolean;\n}\n\n/** Host context supplied by the runtime. */\nexport interface CanvasHostContext {\n\tcapabilities?: CanvasHostContextCapabilities;\n}\n\n/** Session context supplied by the runtime. */\nexport interface CanvasSessionContext {\n\t/** Active session working directory, when known. */\n\tworkingDirectory?: string;\n}\n\n/** Fields shared by every provider callback payload. */\ninterface CanvasProviderRequestBase {\n\t/** Target session identifier. */\n\tsessionId: string;\n\t/** Owning provider identifier. */\n\textensionId: string;\n\t/** Provider-local canvas identifier. */\n\tcanvasId: string;\n\t/** Canvas instance identifier. */\n\tinstanceId: string;\n\thost?: CanvasHostContext;\n\tsession?: CanvasSessionContext;\n}\n\n/** `canvas.open` payload. */\nexport interface CanvasProviderOpenRequest extends CanvasProviderRequestBase {\n\t/** Canvas open input. */\n\tinput?: JsonValue;\n}\n\n/** `canvas.open` result. A web-rendered canvas returns the URL the host loads. */\nexport interface CanvasProviderOpenResult {\n\t/** URL for web-rendered canvases. */\n\turl?: string;\n\t/** Provider-supplied title. */\n\ttitle?: string;\n\t/** Provider-supplied status text. */\n\tstatus?: string;\n}\n\n/** `canvas.close` payload. */\nexport interface CanvasProviderCloseRequest extends CanvasProviderRequestBase {}\n\n/** `canvas.action.invoke` payload. */\nexport interface CanvasProviderInvokeActionRequest extends CanvasProviderRequestBase {\n\t/** Action name to invoke. */\n\tactionName: string;\n\t/** Action input. */\n\tinput?: JsonValue;\n}\n\n/**\n * Action metadata as it crosses the wire. The SDK strips each action's `handler`\n * closure before sending the declaration, so this is `CanvasAction` minus the\n * handler.\n */\nexport interface CanvasActionDeclaration {\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}\n\n/** Declarative metadata for a single canvas. */\nexport interface CanvasDeclaration {\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. */\n\tactions?: CanvasActionDeclaration[];\n}\n\n/**\n * Reserved action-name prefix. The SDK is explicit: \"Names MUST NOT start with\n * `canvas.` — that prefix is reserved for lifecycle verbs.\"\n */\nexport const CANVAS_RESERVED_ACTION_PREFIX = \"canvas.\";\n\n/** Log levels accepted by `session.log`. */\nexport type CanvasLogLevel = \"info\" | \"warning\" | \"error\";\n\n/**\n * Host envelope version. Ours, not GitHub's — bumped only when the\n * runner↔shim framing changes.\n */\nexport const CANVAS_ENVELOPE_VERSION = 1;\n\n/** Child announces itself and its canvases. Always the first message. */\nexport interface CanvasReadyMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"ready\";\n\t/** The `CANVAS_SDK_PROTOCOL_VERSION` the child was built against. */\n\tprotocolVersion: number;\n\t/** Provider identifier, derived by the runner from the extension directory. */\n\textensionId: string;\n\tcanvases: CanvasDeclaration[];\n\t/**\n\t * Surfaces declared by the extension that this shim does not implement, so the\n\t * runner can warn once instead of letting the extension half-work\n\t * (design doc §6.2).\n\t */\n\tunsupported?: string[];\n}\n\n/** Child forwards a `session.log` call. */\nexport interface CanvasLogMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"log\";\n\tmessage: string;\n\tlevel?: CanvasLogLevel;\n\tephemeral?: boolean;\n}\n\n/** Host asks the child to run one provider callback. */\nexport interface CanvasRequestMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"request\";\n\tid: number;\n\tmethod: CanvasProviderMethod;\n\tparams: JsonValue;\n}\n\n/** Child returns a provider callback result. */\nexport interface CanvasResponseMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"response\";\n\tid: number;\n\tresult: JsonValue;\n}\n\n/**\n * Child returns a failure. `code` carries `CanvasError.code` when the handler threw\n * one, so the agent gets a machine-readable code rather than a string to parse.\n */\nexport interface CanvasErrorMessage {\n\tenvelope: typeof CANVAS_ENVELOPE_VERSION;\n\ttype: \"error\";\n\tid: number;\n\tcode: string;\n\tmessage: string;\n}\n\n/** Anything the host may send to a child. */\nexport type CanvasHostToChildMessage = CanvasRequestMessage;\n\n/** Anything a child may send to the host. */\nexport type CanvasChildToHostMessage =\n\t| CanvasReadyMessage\n\t| CanvasLogMessage\n\t| CanvasResponseMessage\n\t| CanvasErrorMessage;\n\n/** Error code used when a handler throws something that is not a `CanvasError`. */\nexport const CANVAS_ERROR_CODE_INTERNAL = \"internal_error\";\n/** Error code used when the host asks for a canvas or action the child does not declare. */\nexport const CANVAS_ERROR_CODE_UNKNOWN_TARGET = \"unknown_target\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasCurrentEnvelope(value: Record<string, unknown>): boolean {\n\treturn value.envelope === CANVAS_ENVELOPE_VERSION;\n}\n\n/** Whether `value` is a provider callback name. */\nexport function isCanvasProviderMethod(value: unknown): value is CanvasProviderMethod {\n\treturn typeof value === \"string\" && (CANVAS_PROVIDER_METHODS as readonly string[]).includes(value);\n}\n\n/** Narrow a decoded value to a host→child message. */\nexport function isCanvasHostToChildMessage(value: unknown): value is CanvasHostToChildMessage {\n\tif (!isRecord(value) || !hasCurrentEnvelope(value)) return false;\n\treturn value.type === \"request\" && typeof value.id === \"number\" && isCanvasProviderMethod(value.method);\n}\n\n/** Narrow a decoded value to a child→host message. */\nexport function isCanvasChildToHostMessage(value: unknown): value is CanvasChildToHostMessage {\n\tif (!isRecord(value) || !hasCurrentEnvelope(value)) return false;\n\tswitch (value.type) {\n\t\tcase \"ready\":\n\t\t\treturn typeof value.extensionId === \"string\" && Array.isArray(value.canvases);\n\t\tcase \"log\":\n\t\t\treturn typeof value.message === \"string\";\n\t\tcase \"response\":\n\t\t\treturn typeof value.id === \"number\";\n\t\tcase \"error\":\n\t\t\treturn typeof value.id === \"number\" && typeof value.code === \"string\" && typeof value.message === \"string\";\n\t\tdefault:\n\t\t\treturn false;\n\t}\n}\n\n/** Serialize one message as a single NDJSON line, newline included. */\nexport function encodeCanvasMessage(message: CanvasHostToChildMessage | CanvasChildToHostMessage): string {\n\treturn `${JSON.stringify(message)}\\n`;\n}\n\n/** One decode pass: parsed JSON values plus any lines that were not JSON at all. */\nexport interface CanvasDecodeResult {\n\tvalues: unknown[];\n\t/**\n\t * Lines that failed to parse. In the Copilot CLI stdout is the JSON-RPC channel,\n\t * so a stray `console.log` corrupts it; we keep the same discipline but report\n\t * the stray text so the runner can tell the author to use `session.log`\n\t * instead of leaving them with a silent protocol error.\n\t */\n\tstrays: string[];\n}\n\n/**\n * Incremental NDJSON decoder. Chunk boundaries do not respect line boundaries, so\n * a partial trailing line is held until the rest arrives.\n */\nexport class CanvasMessageDecoder {\n\tprivate buffer = \"\";\n\n\t/** Decode everything complete in `chunk`, buffering any partial trailing line. */\n\tpush(chunk: string): CanvasDecodeResult {\n\t\tthis.buffer += chunk;\n\t\tconst result: CanvasDecodeResult = { values: [], strays: [] };\n\t\tlet newline = this.buffer.indexOf(\"\\n\");\n\t\twhile (newline !== -1) {\n\t\t\tthis.take(this.buffer.slice(0, newline), result);\n\t\t\tthis.buffer = this.buffer.slice(newline + 1);\n\t\t\tnewline = this.buffer.indexOf(\"\\n\");\n\t\t}\n\t\treturn result;\n\t}\n\n\t/** Decode whatever is left, for use when the stream ends without a trailing newline. */\n\tflush(): CanvasDecodeResult {\n\t\tconst result: CanvasDecodeResult = { values: [], strays: [] };\n\t\tconst rest = this.buffer;\n\t\tthis.buffer = \"\";\n\t\tthis.take(rest, result);\n\t\treturn result;\n\t}\n\n\tprivate take(line: string, result: CanvasDecodeResult): void {\n\t\tconst trimmed = line.trim();\n\t\tif (trimmed.length === 0) return;\n\t\ttry {\n\t\t\tresult.values.push(JSON.parse(trimmed));\n\t\t} catch {\n\t\t\tresult.strays.push(trimmed);\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Canvas instance registry — owns children, instances, and reaping.
3
+ *
4
+ * Design: `docs/canvas-extensions-design.md` §4, §6, §7. One child process per
5
+ * extension, many instances per child, keyed by `(extensionId, canvasId,
6
+ * instanceId)` because `joinSession({ canvases: [...] })` takes an array and each
7
+ * canvas can be opened more than once.
8
+ *
9
+ * **Correction to the design doc's §6.** That section called for an "SSE-liveness
10
+ * heartbeat — an instance with no connected client for N seconds is idle". That is
11
+ * not implementable. The SSE endpoint and its client set live inside the
12
+ * extension's own HTTP server (`entry.sseClients` in `pr-artifact-explorer`'s
13
+ * `server.mjs`); the host never sees them. Learning otherwise would take either
14
+ * proxying the canvas URL — which breaks the token, origin and CSP model the
15
+ * extension built — or adding a liveness call to the contract, which breaks tier-2
16
+ * portability. Neither is worth it for a reaper.
17
+ *
18
+ * So idleness here means something narrower and honest: **time since hoocode last
19
+ * touched the instance** (opened it, or invoked an action on it). A person reading
20
+ * a canvas in a browser tab is invisible to us, so a generous timeout is the point
21
+ * rather than a limitation, and `reapIdle` is advisory cleanup — not a claim about
22
+ * whether anybody is watching.
23
+ *
24
+ * The registry starts no timers. `reapIdle()` is driven by the caller and `now` is
25
+ * injectable, so lifetime policy belongs to whoever owns the session clock and the
26
+ * tests do not sleep.
27
+ */
28
+ import type { DiscoveredCanvasExtension } from "./discovery.js";
29
+ import type { CanvasActionDeclaration, CanvasDeclaration, JsonValue } from "./protocol.js";
30
+ import { type CanvasCallOptions, type CanvasRunnerOptions, type CanvasRuntime } from "./runner.js";
31
+ /** Default idle ceiling before an untouched instance is reaped. */
32
+ export declare const CANVAS_INSTANCE_IDLE_MS: number;
33
+ /** Default grace period a child is kept alive after its last instance closes. */
34
+ export declare const CANVAS_CHILD_LINGER_MS: number;
35
+ /** Default cap on concurrent instances of a single canvas. */
36
+ export declare const CANVAS_MAX_INSTANCES_PER_CANVAS = 8;
37
+ /** Stable identity of one open instance. */
38
+ export interface CanvasInstanceKey {
39
+ extensionId: string;
40
+ canvasId: string;
41
+ instanceId: string;
42
+ }
43
+ /** An open canvas instance. */
44
+ export interface CanvasInstance extends CanvasInstanceKey {
45
+ /** URL the host hands to a browser. */
46
+ url: string | undefined;
47
+ title: string | undefined;
48
+ status: string | undefined;
49
+ /** When hoocode last opened this instance or invoked one of its actions. */
50
+ lastTouchedAt: number;
51
+ }
52
+ /**
53
+ * One agent-callable action on an open instance. This is the input the future
54
+ * tool bridge consumes; nothing registers it as a tool yet, deliberately — that
55
+ * makes canvases reachable by the agent and must follow the trust gate (§5).
56
+ */
57
+ export interface CanvasActionBinding extends CanvasInstanceKey {
58
+ action: CanvasActionDeclaration;
59
+ }
60
+ /** Diagnostics the registry emits. The host decides how to surface them. */
61
+ export interface CanvasRegistryEvents {
62
+ /** A `session.log` call from an extension. */
63
+ onLog?: (extensionId: string, message: string, level: string | undefined) => void;
64
+ /** A non-protocol stdout line — almost always a stray `console.log`. */
65
+ onStray?: (extensionId: string, line: string) => void;
66
+ /** The child's stderr. */
67
+ onStderr?: (extensionId: string, chunk: string) => void;
68
+ /** Something the host should tell the user about once. */
69
+ onDiagnostic?: (extensionId: string, message: string) => void;
70
+ }
71
+ /** Registry configuration. */
72
+ export interface CanvasRegistryOptions extends CanvasRegistryEvents {
73
+ runtime: CanvasRuntime;
74
+ /**
75
+ * Working directory the trust gate is evaluated against (§5). Required: forking
76
+ * a canvas that arrived in a clone is exactly what the gate exists to prevent,
77
+ * so there is no sensible default to fall back to.
78
+ */
79
+ cwd: string;
80
+ /** Trust-store location. Defaults to the agent dir; injectable for tests. */
81
+ agentDir?: string;
82
+ /** Clock, injectable so idle policy is testable without sleeping. */
83
+ now?: () => number;
84
+ idleTimeoutMs?: number;
85
+ childLingerMs?: number;
86
+ /**
87
+ * Per-method provider-call ceilings, merged over the runner's defaults.
88
+ *
89
+ * Plumbed through because the registry is the entry point everything real goes
90
+ * via: without this the ceilings in `runner.ts` were only reachable by calling
91
+ * `spawnCanvasExtension` directly, which nothing does.
92
+ */
93
+ requestTimeoutMs?: CanvasRunnerOptions["requestTimeoutMs"];
94
+ maxInstancesPerCanvas?: number;
95
+ /** Instance id generator, injectable for deterministic tests. */
96
+ newInstanceId?: () => string;
97
+ }
98
+ /** Render a key as a stable string, for maps and messages. */
99
+ export declare function canvasInstanceKeyOf(key: CanvasInstanceKey): string;
100
+ export declare class CanvasRegistry {
101
+ private readonly children;
102
+ private readonly instances;
103
+ private readonly options;
104
+ private readonly now;
105
+ private readonly newInstanceId;
106
+ constructor(options: CanvasRegistryOptions);
107
+ /** Canvases an extension declares, forking it if it is not already running. */
108
+ declarations(extension: DiscoveredCanvasExtension): Promise<CanvasDeclaration[]>;
109
+ /** Open a canvas instance and return what the host needs to render it. */
110
+ open(extension: DiscoveredCanvasExtension, canvasId: string, input?: JsonValue, options?: CanvasCallOptions): Promise<CanvasInstance>;
111
+ /** Invoke an action on an open instance. */
112
+ invokeAction(key: CanvasInstanceKey, actionName: string, input?: JsonValue, options?: CanvasCallOptions): Promise<JsonValue>;
113
+ /** Close one instance. Unknown keys are a no-op, so close is idempotent. */
114
+ close(key: CanvasInstanceKey): Promise<void>;
115
+ /** Every open instance. */
116
+ listInstances(): CanvasInstance[];
117
+ /**
118
+ * Actions currently invocable, one entry per open instance per declared action.
119
+ * Empty when nothing is open — which is the point: a canvas that is not open
120
+ * costs the prompt nothing (§7).
121
+ */
122
+ activeActions(): CanvasActionBinding[];
123
+ /**
124
+ * Close instances hoocode has not touched within the idle timeout, then reap
125
+ * children that have had no instances for the linger period. Advisory cleanup:
126
+ * see the module header on what "idle" can and cannot mean here.
127
+ *
128
+ * @returns The instance keys that were closed.
129
+ */
130
+ reapIdle(): Promise<string[]>;
131
+ /** Close everything and terminate every child. Safe to call twice. */
132
+ shutdown(): Promise<void>;
133
+ private abandon;
134
+ private instancesOf;
135
+ private child;
136
+ /** Drop a dead child and its instances, so a crash cannot leave stale entries. */
137
+ private forget;
138
+ }
139
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Canvas instance registry — owns children, instances, and reaping.\n *\n * Design: `docs/canvas-extensions-design.md` §4, §6, §7. One child process per\n * extension, many instances per child, keyed by `(extensionId, canvasId,\n * instanceId)` because `joinSession({ canvases: [...] })` takes an array and each\n * canvas can be opened more than once.\n *\n * **Correction to the design doc's §6.** That section called for an \"SSE-liveness\n * heartbeat — an instance with no connected client for N seconds is idle\". That is\n * not implementable. The SSE endpoint and its client set live inside the\n * extension's own HTTP server (`entry.sseClients` in `pr-artifact-explorer`'s\n * `server.mjs`); the host never sees them. Learning otherwise would take either\n * proxying the canvas URL — which breaks the token, origin and CSP model the\n * extension built — or adding a liveness call to the contract, which breaks tier-2\n * portability. Neither is worth it for a reaper.\n *\n * So idleness here means something narrower and honest: **time since hoocode last\n * touched the instance** (opened it, or invoked an action on it). A person reading\n * a canvas in a browser tab is invisible to us, so a generous timeout is the point\n * rather than a limitation, and `reapIdle` is advisory cleanup — not a claim about\n * whether anybody is watching.\n *\n * The registry starts no timers. `reapIdle()` is driven by the caller and `now` is\n * injectable, so lifetime policy belongs to whoever owns the session clock and the\n * tests do not sleep.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport * as path from \"node:path\";\nimport type { DiscoveredCanvasExtension } from \"./discovery.js\";\nimport type { CanvasActionDeclaration, CanvasDeclaration, CanvasProviderOpenResult, JsonValue } from \"./protocol.js\";\nimport {\n\ttype CanvasCallOptions,\n\ttype CanvasExtensionProcess,\n\ttype CanvasRunnerOptions,\n\ttype CanvasRuntime,\n\tspawnCanvasExtension,\n} from \"./runner.js\";\nimport { CanvasTrustError, shouldWithholdCanvas } from \"./trust.js\";\n\n/** Default idle ceiling before an untouched instance is reaped. */\nexport const CANVAS_INSTANCE_IDLE_MS = 30 * 60 * 1_000;\n\n/** Default grace period a child is kept alive after its last instance closes. */\nexport const CANVAS_CHILD_LINGER_MS = 60 * 1_000;\n\n/** Default cap on concurrent instances of a single canvas. */\nexport const CANVAS_MAX_INSTANCES_PER_CANVAS = 8;\n\n/** Stable identity of one open instance. */\nexport interface CanvasInstanceKey {\n\textensionId: string;\n\tcanvasId: string;\n\tinstanceId: string;\n}\n\n/** An open canvas instance. */\nexport interface CanvasInstance extends CanvasInstanceKey {\n\t/** URL the host hands to a browser. */\n\turl: string | undefined;\n\ttitle: string | undefined;\n\tstatus: string | undefined;\n\t/** When hoocode last opened this instance or invoked one of its actions. */\n\tlastTouchedAt: number;\n}\n\n/**\n * One agent-callable action on an open instance. This is the input the future\n * tool bridge consumes; nothing registers it as a tool yet, deliberately — that\n * makes canvases reachable by the agent and must follow the trust gate (§5).\n */\nexport interface CanvasActionBinding extends CanvasInstanceKey {\n\taction: CanvasActionDeclaration;\n}\n\n/** Diagnostics the registry emits. The host decides how to surface them. */\nexport interface CanvasRegistryEvents {\n\t/** A `session.log` call from an extension. */\n\tonLog?: (extensionId: string, message: string, level: string | undefined) => void;\n\t/** A non-protocol stdout line — almost always a stray `console.log`. */\n\tonStray?: (extensionId: string, line: string) => void;\n\t/** The child's stderr. */\n\tonStderr?: (extensionId: string, chunk: string) => void;\n\t/** Something the host should tell the user about once. */\n\tonDiagnostic?: (extensionId: string, message: string) => void;\n}\n\n/** Registry configuration. */\nexport interface CanvasRegistryOptions extends CanvasRegistryEvents {\n\truntime: CanvasRuntime;\n\t/**\n\t * Working directory the trust gate is evaluated against (§5). Required: forking\n\t * a canvas that arrived in a clone is exactly what the gate exists to prevent,\n\t * so there is no sensible default to fall back to.\n\t */\n\tcwd: string;\n\t/** Trust-store location. Defaults to the agent dir; injectable for tests. */\n\tagentDir?: string;\n\t/** Clock, injectable so idle policy is testable without sleeping. */\n\tnow?: () => number;\n\tidleTimeoutMs?: number;\n\tchildLingerMs?: number;\n\t/**\n\t * Per-method provider-call ceilings, merged over the runner's defaults.\n\t *\n\t * Plumbed through because the registry is the entry point everything real goes\n\t * via: without this the ceilings in `runner.ts` were only reachable by calling\n\t * `spawnCanvasExtension` directly, which nothing does.\n\t */\n\trequestTimeoutMs?: CanvasRunnerOptions[\"requestTimeoutMs\"];\n\tmaxInstancesPerCanvas?: number;\n\t/** Instance id generator, injectable for deterministic tests. */\n\tnewInstanceId?: () => string;\n}\n\ninterface ChildEntry {\n\tprocess: CanvasExtensionProcess;\n\tdeclarations: Map<string, CanvasDeclaration>;\n\t/** When the child's instance count last dropped to zero; undefined while in use. */\n\tidleSince: number | undefined;\n}\n\n/** Render a key as a stable string, for maps and messages. */\nexport function canvasInstanceKeyOf(key: CanvasInstanceKey): string {\n\treturn `${key.extensionId}::${key.canvasId}::${key.instanceId}`;\n}\n\nexport class CanvasRegistry {\n\tprivate readonly children = new Map<string, ChildEntry>();\n\tprivate readonly instances = new Map<string, CanvasInstance>();\n\tprivate readonly options: CanvasRegistryOptions;\n\tprivate readonly now: () => number;\n\tprivate readonly newInstanceId: () => string;\n\n\tconstructor(options: CanvasRegistryOptions) {\n\t\tthis.options = options;\n\t\tthis.now = options.now ?? Date.now;\n\t\tthis.newInstanceId = options.newInstanceId ?? randomUUID;\n\t}\n\n\t/** Canvases an extension declares, forking it if it is not already running. */\n\tasync declarations(extension: DiscoveredCanvasExtension): Promise<CanvasDeclaration[]> {\n\t\tconst child = await this.child(extension);\n\t\treturn [...child.declarations.values()];\n\t}\n\n\t/** Open a canvas instance and return what the host needs to render it. */\n\tasync open(\n\t\textension: DiscoveredCanvasExtension,\n\t\tcanvasId: string,\n\t\tinput?: JsonValue,\n\t\toptions?: CanvasCallOptions,\n\t): Promise<CanvasInstance> {\n\t\tconst child = await this.child(extension);\n\t\tif (!child.declarations.has(canvasId)) {\n\t\t\tconst known = [...child.declarations.keys()].join(\", \") || \"none\";\n\t\t\tthrow new Error(`Extension \"${extension.id}\" declares no canvas \"${canvasId}\" (declares: ${known}).`);\n\t\t}\n\n\t\tconst limit = this.options.maxInstancesPerCanvas ?? CANVAS_MAX_INSTANCES_PER_CANVAS;\n\t\tconst open = this.listInstances().filter(\n\t\t\t(instance) => instance.extensionId === extension.id && instance.canvasId === canvasId,\n\t\t);\n\t\tif (open.length >= limit) {\n\t\t\tthrow new Error(`Canvas \"${canvasId}\" already has ${open.length} open instances (limit ${limit}).`);\n\t\t}\n\n\t\tconst instanceId = this.newInstanceId();\n\t\tlet result: CanvasProviderOpenResult | null;\n\t\ttry {\n\t\t\tresult = (await child.process.open(\n\t\t\t\t{ sessionId: extension.id, extensionId: extension.id, canvasId, instanceId, input },\n\t\t\t\toptions,\n\t\t\t)) as CanvasProviderOpenResult | null;\n\t\t} catch (cause) {\n\t\t\tawait this.abandon(extension.id, canvasId, instanceId);\n\t\t\tthrow cause;\n\t\t}\n\n\t\tconst instance: CanvasInstance = {\n\t\t\textensionId: extension.id,\n\t\t\tcanvasId,\n\t\t\tinstanceId,\n\t\t\turl: result?.url,\n\t\t\ttitle: result?.title,\n\t\t\tstatus: result?.status,\n\t\t\tlastTouchedAt: this.now(),\n\t\t};\n\t\tthis.instances.set(canvasInstanceKeyOf(instance), instance);\n\t\tchild.idleSince = undefined;\n\t\treturn instance;\n\t}\n\n\t/** Invoke an action on an open instance. */\n\tasync invokeAction(\n\t\tkey: CanvasInstanceKey,\n\t\tactionName: string,\n\t\tinput?: JsonValue,\n\t\toptions?: CanvasCallOptions,\n\t): Promise<JsonValue> {\n\t\tconst instance = this.instances.get(canvasInstanceKeyOf(key));\n\t\tif (!instance) throw new Error(`No open canvas instance ${canvasInstanceKeyOf(key)}.`);\n\t\tconst child = this.children.get(key.extensionId);\n\t\tif (!child) throw new Error(`Canvas extension \"${key.extensionId}\" is not running.`);\n\n\t\tconst result = await child.process.invokeAction(\n\t\t\t{\n\t\t\t\tsessionId: key.extensionId,\n\t\t\t\textensionId: key.extensionId,\n\t\t\t\tcanvasId: key.canvasId,\n\t\t\t\tinstanceId: key.instanceId,\n\t\t\t\tactionName,\n\t\t\t\tinput,\n\t\t\t},\n\t\t\toptions,\n\t\t);\n\t\tinstance.lastTouchedAt = this.now();\n\t\treturn result;\n\t}\n\n\t/** Close one instance. Unknown keys are a no-op, so close is idempotent. */\n\tasync close(key: CanvasInstanceKey): Promise<void> {\n\t\tconst id = canvasInstanceKeyOf(key);\n\t\tif (!this.instances.delete(id)) return;\n\t\tconst child = this.children.get(key.extensionId);\n\t\tif (!child) return;\n\t\ttry {\n\t\t\tawait child.process.close({\n\t\t\t\tsessionId: key.extensionId,\n\t\t\t\textensionId: key.extensionId,\n\t\t\t\tcanvasId: key.canvasId,\n\t\t\t\tinstanceId: key.instanceId,\n\t\t\t});\n\t\t} catch (cause) {\n\t\t\t// onClose is fire-and-forget in the SDK contract, so a failure here must not\n\t\t\t// leave the instance half-closed in our table.\n\t\t\tthis.options.onDiagnostic?.(\n\t\t\t\tkey.extensionId,\n\t\t\t\t`Closing canvas instance ${id} failed: ${cause instanceof Error ? cause.message : String(cause)}`,\n\t\t\t);\n\t\t}\n\t\tif (this.instancesOf(key.extensionId).length === 0) child.idleSince = this.now();\n\t}\n\n\t/** Every open instance. */\n\tlistInstances(): CanvasInstance[] {\n\t\treturn [...this.instances.values()];\n\t}\n\n\t/**\n\t * Actions currently invocable, one entry per open instance per declared action.\n\t * Empty when nothing is open — which is the point: a canvas that is not open\n\t * costs the prompt nothing (§7).\n\t */\n\tactiveActions(): CanvasActionBinding[] {\n\t\tconst bindings: CanvasActionBinding[] = [];\n\t\tfor (const instance of this.instances.values()) {\n\t\t\tconst declaration = this.children.get(instance.extensionId)?.declarations.get(instance.canvasId);\n\t\t\tfor (const action of declaration?.actions ?? []) {\n\t\t\t\tbindings.push({\n\t\t\t\t\textensionId: instance.extensionId,\n\t\t\t\t\tcanvasId: instance.canvasId,\n\t\t\t\t\tinstanceId: instance.instanceId,\n\t\t\t\t\taction,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn bindings;\n\t}\n\n\t/**\n\t * Close instances hoocode has not touched within the idle timeout, then reap\n\t * children that have had no instances for the linger period. Advisory cleanup:\n\t * see the module header on what \"idle\" can and cannot mean here.\n\t *\n\t * @returns The instance keys that were closed.\n\t */\n\tasync reapIdle(): Promise<string[]> {\n\t\tconst idleTimeout = this.options.idleTimeoutMs ?? CANVAS_INSTANCE_IDLE_MS;\n\t\tconst linger = this.options.childLingerMs ?? CANVAS_CHILD_LINGER_MS;\n\t\tconst now = this.now();\n\n\t\tconst expired = this.listInstances().filter((instance) => now - instance.lastTouchedAt >= idleTimeout);\n\t\tfor (const instance of expired) await this.close(instance);\n\n\t\tfor (const [extensionId, child] of [...this.children.entries()]) {\n\t\t\tconst unused = this.instancesOf(extensionId).length === 0;\n\t\t\tif (!unused) continue;\n\t\t\tconst since = child.idleSince ?? now;\n\t\t\tchild.idleSince = since;\n\t\t\tif (now - since >= linger) {\n\t\t\t\tthis.children.delete(extensionId);\n\t\t\t\tawait child.process.terminate();\n\t\t\t}\n\t\t}\n\n\t\treturn expired.map((instance) => canvasInstanceKeyOf(instance));\n\t}\n\n\t/** Close everything and terminate every child. Safe to call twice. */\n\tasync shutdown(): Promise<void> {\n\t\tfor (const instance of this.listInstances()) await this.close(instance);\n\t\tconst children = [...this.children.values()];\n\t\tthis.children.clear();\n\t\tawait Promise.all(children.map((child) => child.process.terminate()));\n\t}\n\n\t/**\n\t * Reconcile an instance we asked to open but never saw open — because a person\n\t * cancelled, or the call timed out. One path serves both.\n\t *\n\t * The provider protocol has no cancel verb, so the child may have finished opening\n\t * and be holding a port. `canvas.close` is the only way to tell it to let go, and\n\t * it can be sent because the instance id was generated before the open call.\n\t *\n\t * If the close itself goes unanswered the child is wedged, and the only remaining\n\t * lever is terminating it — but that kills every instance of that extension, so it\n\t * is done only when no other instance is live. When siblings exist the child is left\n\t * alone and the leak is reported, rather than paid for by someone else's open canvas.\n\t */\n\tprivate async abandon(extensionId: string, canvasId: string, instanceId: string): Promise<void> {\n\t\tconst child = this.children.get(extensionId);\n\t\tif (!child) return;\n\t\ttry {\n\t\t\tawait child.process.close({ sessionId: extensionId, extensionId, canvasId, instanceId });\n\t\t\treturn;\n\t\t} catch (cause) {\n\t\t\tconst detail = cause instanceof Error ? cause.message : String(cause);\n\t\t\tif (this.instancesOf(extensionId).length > 0) {\n\t\t\t\tthis.options.onDiagnostic?.(\n\t\t\t\t\textensionId,\n\t\t\t\t\t`Stopped opening canvas \"${canvasId}\" but the extension did not confirm the close (${detail}). ` +\n\t\t\t\t\t\t\"It has other canvases open, so it was left running; a port may stay bound until it exits.\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.children.delete(extensionId);\n\t\t\tawait child.process.terminate();\n\t\t\tthis.options.onDiagnostic?.(\n\t\t\t\textensionId,\n\t\t\t\t`Stopped opening canvas \"${canvasId}\" and the extension did not confirm the close (${detail}); it was stopped.`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate instancesOf(extensionId: string): CanvasInstance[] {\n\t\treturn this.listInstances().filter((instance) => instance.extensionId === extensionId);\n\t}\n\n\tprivate async child(extension: DiscoveredCanvasExtension): Promise<ChildEntry> {\n\t\t// The single choke point: every path that could start a process comes through\n\t\t// here, so the gate is enforced once and cannot be bypassed by a caller that\n\t\t// forgot to filter. Callers should still filter with `gateCanvasExtensions`\n\t\t// so they can explain the refusal; this is the backstop, not the UI.\n\t\tif (shouldWithholdCanvas(extension, this.options.cwd, this.options.agentDir)) {\n\t\t\tthrow new CanvasTrustError(extension.id, this.options.cwd);\n\t\t}\n\n\t\tconst existing = this.children.get(extension.id);\n\t\tif (existing?.process.running) return existing;\n\t\tif (existing) this.children.delete(extension.id);\n\n\t\tconst process = spawnCanvasExtension({\n\t\t\textensionId: extension.id,\n\t\t\tentry: extension.entry,\n\t\t\truntime: this.options.runtime,\n\t\t\trequestTimeoutMs: this.options.requestTimeoutMs,\n\t\t\tcwd: path.dirname(extension.dir),\n\t\t\tonLog: (message, level) => this.options.onLog?.(extension.id, message, level),\n\t\t\tonStray: (line) => this.options.onStray?.(extension.id, line),\n\t\t\tonStderr: (chunk) => this.options.onStderr?.(extension.id, chunk),\n\t\t\tonExit: () => this.forget(extension.id),\n\t\t});\n\n\t\tconst ready = await process.ready;\n\t\tif (ready.unsupported && ready.unsupported.length > 0) {\n\t\t\tthis.options.onDiagnostic?.(\n\t\t\t\textension.id,\n\t\t\t\t`Canvas extension \"${extension.id}\" declares ${ready.unsupported.join(\", \")}, which hoocode does not support; those surfaces are ignored.`,\n\t\t\t);\n\t\t}\n\n\t\tconst entry: ChildEntry = {\n\t\t\tprocess,\n\t\t\tdeclarations: new Map(ready.canvases.map((declaration) => [declaration.id, declaration])),\n\t\t\tidleSince: this.now(),\n\t\t};\n\t\tthis.children.set(extension.id, entry);\n\t\treturn entry;\n\t}\n\n\t/** Drop a dead child and its instances, so a crash cannot leave stale entries. */\n\tprivate forget(extensionId: string): void {\n\t\tthis.children.delete(extensionId);\n\t\tfor (const [id, instance] of [...this.instances.entries()]) {\n\t\t\tif (instance.extensionId === extensionId) this.instances.delete(id);\n\t\t}\n\t}\n}\n"]}