@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":"index.js","sourceRoot":"","sources":["../../../../src/core/canvas/sdk-shim/index.ts"],"names":[],"mappings":"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","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,112 @@
1
+ /**
2
+ * Session-scoped canvas facade: everything the TUI needs, with no TUI in it.
3
+ *
4
+ * Design: `docs/canvas-extensions-design.md` §11. The pieces underneath — discovery,
5
+ * the trust gate, availability, the registry — are each small and separately tested.
6
+ * This is what stitches them into the four questions a user surface actually asks:
7
+ * what is there, can it run, open this one, close that one.
8
+ *
9
+ * It holds no TUI types on purpose. `extensions/core/canvas.ts` renders and supplies
10
+ * an `AbortSignal` from a cancellable loader; everything decided here stays testable
11
+ * without a terminal.
12
+ *
13
+ * Availability is resolved once and cached, because resolving can spawn
14
+ * `node --version` (§11.1) and the answer cannot change within a session.
15
+ */
16
+ import type { DiscoveredCanvasExtension } from "./discovery.js";
17
+ import { type CanvasSearchRoot } from "./discovery.js";
18
+ import { type CanvasAvailability } from "./launch.js";
19
+ import { type CanvasInstance, CanvasRegistry, type CanvasRegistryEvents } from "./registry.js";
20
+ import type { CanvasCallOptions } from "./runner.js";
21
+ /** One canvas a person could open, or has open. */
22
+ export interface CanvasListing {
23
+ extensionId: string;
24
+ /** Undefined until the extension has been forked, since declarations come from it. */
25
+ canvasId: string | undefined;
26
+ displayName: string | undefined;
27
+ scope: DiscoveredCanvasExtension["scope"];
28
+ /** Why it cannot be opened, if it cannot. */
29
+ withheld: "untrusted-workspace" | undefined;
30
+ /** Instances of this canvas that are currently open. */
31
+ open: CanvasInstance[];
32
+ }
33
+ /** What `list()` reports. */
34
+ export interface CanvasOverview {
35
+ /** Absent `reason` means canvases can run here. */
36
+ availability: CanvasAvailability;
37
+ listings: CanvasListing[];
38
+ /** Extensions withheld by the trust gate — surfaced, never hidden (§5.1). */
39
+ withheldCount: number;
40
+ }
41
+ /** Configuration for a session's canvas facade. */
42
+ export interface CanvasSessionOptions extends CanvasRegistryEvents {
43
+ cwd: string;
44
+ homeDir: string;
45
+ agentDir?: string;
46
+ /** Override the search roots; defaults to {@link canvasSearchRoots}. */
47
+ roots?: CanvasSearchRoot[];
48
+ /** Override availability resolution, for tests and for hosts that already know. */
49
+ resolveRuntime?: () => Promise<CanvasAvailability>;
50
+ }
51
+ /** Reference to a canvas: an extension id, optionally narrowed to one of its canvases. */
52
+ export interface CanvasRef {
53
+ extensionId: string;
54
+ canvasId?: string;
55
+ }
56
+ /**
57
+ * Parse `extension` or `extension:canvas`.
58
+ *
59
+ * Extension ids are directory names and canvas ids are provider-local, so a single
60
+ * colon is unambiguous and needs no quoting.
61
+ */
62
+ export declare function parseCanvasRef(input: string): CanvasRef | undefined;
63
+ export declare class CanvasSession {
64
+ private readonly options;
65
+ private readonly roots;
66
+ /** Cached because resolving can spawn `node --version` and cannot change mid-session. */
67
+ private availabilityPromise;
68
+ /** Created on first successful open, not at construction: listing must not fork. */
69
+ private registry;
70
+ constructor(options: CanvasSessionOptions);
71
+ /** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */
72
+ discover(): {
73
+ runnable: DiscoveredCanvasExtension[];
74
+ withheld: DiscoveredCanvasExtension[];
75
+ };
76
+ /** Whether canvases can run here. Resolved once per session and cached. */
77
+ availability(): Promise<CanvasAvailability>;
78
+ /**
79
+ * What is installed, what is open, and what is being withheld.
80
+ *
81
+ * Deliberately does not fork anything: listing must stay free and safe, so a
82
+ * `canvasId` is only known for extensions already running. That is the visible
83
+ * consequence of a canvas having no passive half (§5.1) — even its name comes from
84
+ * running its code.
85
+ */
86
+ list(): Promise<CanvasOverview>;
87
+ /**
88
+ * Open a canvas.
89
+ *
90
+ * `options.signal` comes from the caller's cancellable loader, so a person's Esc
91
+ * reaches the registry's abandon path (§11.6) rather than merely hiding a spinner.
92
+ */
93
+ open(ref: CanvasRef, options?: CanvasCallOptions): Promise<CanvasInstance>;
94
+ /** Close one open instance. Unknown ids are a no-op, so closing twice is harmless. */
95
+ close(instanceId: string): Promise<CanvasInstance | undefined>;
96
+ /** Every open instance. */
97
+ instances(): CanvasInstance[];
98
+ /**
99
+ * The live registry, or undefined if nothing has been opened yet.
100
+ *
101
+ * Exposed so the host can hand it to the canvas tools, which read
102
+ * `listInstances()` and `activeActions()` from it.
103
+ */
104
+ registryOrUndefined(): CanvasRegistry | undefined;
105
+ /** Advisory cleanup, driven by whoever owns the session clock. */
106
+ reapIdle(): Promise<string[]>;
107
+ /** Close everything and stop every child. Safe to call twice. */
108
+ dispose(): Promise<void>;
109
+ private ensureRegistry;
110
+ private soleCanvasId;
111
+ }
112
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session-scoped canvas facade: everything the TUI needs, with no TUI in it.\n *\n * Design: `docs/canvas-extensions-design.md` §11. The pieces underneath — discovery,\n * the trust gate, availability, the registry — are each small and separately tested.\n * This is what stitches them into the four questions a user surface actually asks:\n * what is there, can it run, open this one, close that one.\n *\n * It holds no TUI types on purpose. `extensions/core/canvas.ts` renders and supplies\n * an `AbortSignal` from a cancellable loader; everything decided here stays testable\n * without a terminal.\n *\n * Availability is resolved once and cached, because resolving can spawn\n * `node --version` (§11.1) and the answer cannot change within a session.\n */\n\nimport { getAgentDir } from \"../../config.js\";\nimport type { DiscoveredCanvasExtension } from \"./discovery.js\";\nimport { type CanvasSearchRoot, canvasSearchRoots, discoverCanvasExtensions } from \"./discovery.js\";\nimport { type CanvasAvailability, resolveCanvasRuntime } from \"./launch.js\";\nimport { type CanvasInstance, CanvasRegistry, type CanvasRegistryEvents } from \"./registry.js\";\nimport type { CanvasCallOptions } from \"./runner.js\";\nimport { gateCanvasExtensions } from \"./trust.js\";\n\n/** One canvas a person could open, or has open. */\nexport interface CanvasListing {\n\textensionId: string;\n\t/** Undefined until the extension has been forked, since declarations come from it. */\n\tcanvasId: string | undefined;\n\tdisplayName: string | undefined;\n\tscope: DiscoveredCanvasExtension[\"scope\"];\n\t/** Why it cannot be opened, if it cannot. */\n\twithheld: \"untrusted-workspace\" | undefined;\n\t/** Instances of this canvas that are currently open. */\n\topen: CanvasInstance[];\n}\n\n/** What `list()` reports. */\nexport interface CanvasOverview {\n\t/** Absent `reason` means canvases can run here. */\n\tavailability: CanvasAvailability;\n\tlistings: CanvasListing[];\n\t/** Extensions withheld by the trust gate — surfaced, never hidden (§5.1). */\n\twithheldCount: number;\n}\n\n/** Configuration for a session's canvas facade. */\nexport interface CanvasSessionOptions extends CanvasRegistryEvents {\n\tcwd: string;\n\thomeDir: string;\n\tagentDir?: string;\n\t/** Override the search roots; defaults to {@link canvasSearchRoots}. */\n\troots?: CanvasSearchRoot[];\n\t/** Override availability resolution, for tests and for hosts that already know. */\n\tresolveRuntime?: () => Promise<CanvasAvailability>;\n}\n\n/** Reference to a canvas: an extension id, optionally narrowed to one of its canvases. */\nexport interface CanvasRef {\n\textensionId: string;\n\tcanvasId?: string;\n}\n\n/**\n * Parse `extension` or `extension:canvas`.\n *\n * Extension ids are directory names and canvas ids are provider-local, so a single\n * colon is unambiguous and needs no quoting.\n */\nexport function parseCanvasRef(input: string): CanvasRef | undefined {\n\tconst trimmed = input.trim();\n\tif (trimmed.length === 0) return undefined;\n\tconst colon = trimmed.indexOf(\":\");\n\tif (colon === -1) return { extensionId: trimmed };\n\tconst extensionId = trimmed.slice(0, colon).trim();\n\tconst canvasId = trimmed.slice(colon + 1).trim();\n\tif (extensionId.length === 0 || canvasId.length === 0) return undefined;\n\treturn { extensionId, canvasId };\n}\n\nexport class CanvasSession {\n\tprivate readonly options: CanvasSessionOptions;\n\tprivate readonly roots: CanvasSearchRoot[];\n\t/** Cached because resolving can spawn `node --version` and cannot change mid-session. */\n\tprivate availabilityPromise: Promise<CanvasAvailability> | undefined;\n\t/** Created on first successful open, not at construction: listing must not fork. */\n\tprivate registry: CanvasRegistry | undefined;\n\n\tconstructor(options: CanvasSessionOptions) {\n\t\tthis.options = options;\n\t\tthis.roots = options.roots ?? canvasSearchRoots(options.cwd, options.homeDir);\n\t}\n\n\t/** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */\n\tdiscover(): { runnable: DiscoveredCanvasExtension[]; withheld: DiscoveredCanvasExtension[] } {\n\t\tconst gated = gateCanvasExtensions(\n\t\t\tdiscoverCanvasExtensions(this.roots),\n\t\t\tthis.options.cwd,\n\t\t\tthis.options.agentDir ?? getAgentDir(),\n\t\t);\n\t\treturn { runnable: gated.runnable, withheld: gated.withheld.map((entry) => entry.extension) };\n\t}\n\n\t/** Whether canvases can run here. Resolved once per session and cached. */\n\tasync availability(): Promise<CanvasAvailability> {\n\t\tthis.availabilityPromise ??= (this.options.resolveRuntime ?? resolveCanvasRuntime)();\n\t\treturn this.availabilityPromise;\n\t}\n\n\t/**\n\t * What is installed, what is open, and what is being withheld.\n\t *\n\t * Deliberately does not fork anything: listing must stay free and safe, so a\n\t * `canvasId` is only known for extensions already running. That is the visible\n\t * consequence of a canvas having no passive half (§5.1) — even its name comes from\n\t * running its code.\n\t */\n\tasync list(): Promise<CanvasOverview> {\n\t\tconst availability = await this.availability();\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst open = this.registryOrUndefined()?.listInstances() ?? [];\n\n\t\tconst listings: CanvasListing[] = [];\n\t\tfor (const extension of runnable) {\n\t\t\tconst instances = open.filter((instance) => instance.extensionId === extension.id);\n\t\t\tif (instances.length === 0) {\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId: undefined,\n\t\t\t\t\tdisplayName: undefined,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: [],\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const canvasId of new Set(instances.map((instance) => instance.canvasId))) {\n\t\t\t\tconst forCanvas = instances.filter((instance) => instance.canvasId === canvasId);\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId,\n\t\t\t\t\tdisplayName: forCanvas[0]?.title,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: forCanvas,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const extension of withheld) {\n\t\t\tlistings.push({\n\t\t\t\textensionId: extension.id,\n\t\t\t\tcanvasId: undefined,\n\t\t\t\tdisplayName: undefined,\n\t\t\t\tscope: extension.scope,\n\t\t\t\twithheld: \"untrusted-workspace\",\n\t\t\t\topen: [],\n\t\t\t});\n\t\t}\n\t\treturn { availability, listings, withheldCount: withheld.length };\n\t}\n\n\t/**\n\t * Open a canvas.\n\t *\n\t * `options.signal` comes from the caller's cancellable loader, so a person's Esc\n\t * reaches the registry's abandon path (§11.6) rather than merely hiding a spinner.\n\t */\n\tasync open(ref: CanvasRef, options?: CanvasCallOptions): Promise<CanvasInstance> {\n\t\tconst availability = await this.availability();\n\t\tif (!availability.available) throw new Error(availability.reason);\n\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst extension = runnable.find((candidate) => candidate.id === ref.extensionId);\n\t\tif (!extension) {\n\t\t\tif (withheld.some((candidate) => candidate.id === ref.extensionId)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Canvas extension \"${ref.extensionId}\" came with this repository, which is not a trusted workspace. ` +\n\t\t\t\t\t\t\"Run /plugin trust to allow this directory to run code it ships.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst known = runnable.map((candidate) => candidate.id).join(\", \") || \"none\";\n\t\t\tthrow new Error(`No canvas extension \"${ref.extensionId}\" (found: ${known}).`);\n\t\t}\n\n\t\tconst registry = this.ensureRegistry(availability);\n\t\tconst canvasId = ref.canvasId ?? (await this.soleCanvasId(registry, extension));\n\t\treturn registry.open(extension, canvasId, undefined, options);\n\t}\n\n\t/** Close one open instance. Unknown ids are a no-op, so closing twice is harmless. */\n\tasync close(instanceId: string): Promise<CanvasInstance | undefined> {\n\t\tconst registry = this.registryOrUndefined();\n\t\tconst instance = registry?.listInstances().find((open) => open.instanceId === instanceId);\n\t\tif (!registry || !instance) return undefined;\n\t\tawait registry.close(instance);\n\t\treturn instance;\n\t}\n\n\t/** Every open instance. */\n\tinstances(): CanvasInstance[] {\n\t\treturn this.registryOrUndefined()?.listInstances() ?? [];\n\t}\n\n\t/**\n\t * The live registry, or undefined if nothing has been opened yet.\n\t *\n\t * Exposed so the host can hand it to the canvas tools, which read\n\t * `listInstances()` and `activeActions()` from it.\n\t */\n\tregistryOrUndefined(): CanvasRegistry | undefined {\n\t\treturn this.registry;\n\t}\n\n\t/** Advisory cleanup, driven by whoever owns the session clock. */\n\tasync reapIdle(): Promise<string[]> {\n\t\treturn (await this.registryOrUndefined()?.reapIdle()) ?? [];\n\t}\n\n\t/** Close everything and stop every child. Safe to call twice. */\n\tasync dispose(): Promise<void> {\n\t\tawait this.registry?.shutdown();\n\t\tthis.registry = undefined;\n\t}\n\n\tprivate ensureRegistry(availability: Extract<CanvasAvailability, { available: true }>): CanvasRegistry {\n\t\tif (!this.registry) {\n\t\t\tthis.registry = new CanvasRegistry({\n\t\t\t\truntime: availability.runtime,\n\t\t\t\tcwd: this.options.cwd,\n\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\tonLog: this.options.onLog,\n\t\t\t\tonStray: this.options.onStray,\n\t\t\t\tonStderr: this.options.onStderr,\n\t\t\t\tonDiagnostic: this.options.onDiagnostic,\n\t\t\t});\n\t\t}\n\t\treturn this.registry;\n\t}\n\n\t/**\n\t * Pick the canvas when the caller named only an extension.\n\t *\n\t * Forking to read the declarations is unavoidable: they arrive in the child's\n\t * `ready` message. A multi-canvas extension must be named explicitly rather than\n\t * guessed at.\n\t */\n\tprivate async soleCanvasId(registry: CanvasRegistry, extension: DiscoveredCanvasExtension): Promise<string> {\n\t\tconst declarations = await registry.declarations(extension);\n\t\tif (declarations.length === 1) return declarations[0]?.id as string;\n\t\tif (declarations.length === 0) throw new Error(`Canvas extension \"${extension.id}\" declares no canvases.`);\n\t\tconst ids = declarations.map((declaration) => `${extension.id}:${declaration.id}`).join(\", \");\n\t\tthrow new Error(`Canvas extension \"${extension.id}\" declares several canvases; name one of: ${ids}.`);\n\t}\n}\n"]}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Session-scoped canvas facade: everything the TUI needs, with no TUI in it.
3
+ *
4
+ * Design: `docs/canvas-extensions-design.md` §11. The pieces underneath — discovery,
5
+ * the trust gate, availability, the registry — are each small and separately tested.
6
+ * This is what stitches them into the four questions a user surface actually asks:
7
+ * what is there, can it run, open this one, close that one.
8
+ *
9
+ * It holds no TUI types on purpose. `extensions/core/canvas.ts` renders and supplies
10
+ * an `AbortSignal` from a cancellable loader; everything decided here stays testable
11
+ * without a terminal.
12
+ *
13
+ * Availability is resolved once and cached, because resolving can spawn
14
+ * `node --version` (§11.1) and the answer cannot change within a session.
15
+ */
16
+ import { getAgentDir } from "../../config.js";
17
+ import { canvasSearchRoots, discoverCanvasExtensions } from "./discovery.js";
18
+ import { resolveCanvasRuntime } from "./launch.js";
19
+ import { CanvasRegistry } from "./registry.js";
20
+ import { gateCanvasExtensions } from "./trust.js";
21
+ /**
22
+ * Parse `extension` or `extension:canvas`.
23
+ *
24
+ * Extension ids are directory names and canvas ids are provider-local, so a single
25
+ * colon is unambiguous and needs no quoting.
26
+ */
27
+ export function parseCanvasRef(input) {
28
+ const trimmed = input.trim();
29
+ if (trimmed.length === 0)
30
+ return undefined;
31
+ const colon = trimmed.indexOf(":");
32
+ if (colon === -1)
33
+ return { extensionId: trimmed };
34
+ const extensionId = trimmed.slice(0, colon).trim();
35
+ const canvasId = trimmed.slice(colon + 1).trim();
36
+ if (extensionId.length === 0 || canvasId.length === 0)
37
+ return undefined;
38
+ return { extensionId, canvasId };
39
+ }
40
+ export class CanvasSession {
41
+ options;
42
+ roots;
43
+ /** Cached because resolving can spawn `node --version` and cannot change mid-session. */
44
+ availabilityPromise;
45
+ /** Created on first successful open, not at construction: listing must not fork. */
46
+ registry;
47
+ constructor(options) {
48
+ this.options = options;
49
+ this.roots = options.roots ?? canvasSearchRoots(options.cwd, options.homeDir);
50
+ }
51
+ /** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */
52
+ discover() {
53
+ const gated = gateCanvasExtensions(discoverCanvasExtensions(this.roots), this.options.cwd, this.options.agentDir ?? getAgentDir());
54
+ return { runnable: gated.runnable, withheld: gated.withheld.map((entry) => entry.extension) };
55
+ }
56
+ /** Whether canvases can run here. Resolved once per session and cached. */
57
+ async availability() {
58
+ this.availabilityPromise ??= (this.options.resolveRuntime ?? resolveCanvasRuntime)();
59
+ return this.availabilityPromise;
60
+ }
61
+ /**
62
+ * What is installed, what is open, and what is being withheld.
63
+ *
64
+ * Deliberately does not fork anything: listing must stay free and safe, so a
65
+ * `canvasId` is only known for extensions already running. That is the visible
66
+ * consequence of a canvas having no passive half (§5.1) — even its name comes from
67
+ * running its code.
68
+ */
69
+ async list() {
70
+ const availability = await this.availability();
71
+ const { runnable, withheld } = this.discover();
72
+ const open = this.registryOrUndefined()?.listInstances() ?? [];
73
+ const listings = [];
74
+ for (const extension of runnable) {
75
+ const instances = open.filter((instance) => instance.extensionId === extension.id);
76
+ if (instances.length === 0) {
77
+ listings.push({
78
+ extensionId: extension.id,
79
+ canvasId: undefined,
80
+ displayName: undefined,
81
+ scope: extension.scope,
82
+ withheld: undefined,
83
+ open: [],
84
+ });
85
+ continue;
86
+ }
87
+ for (const canvasId of new Set(instances.map((instance) => instance.canvasId))) {
88
+ const forCanvas = instances.filter((instance) => instance.canvasId === canvasId);
89
+ listings.push({
90
+ extensionId: extension.id,
91
+ canvasId,
92
+ displayName: forCanvas[0]?.title,
93
+ scope: extension.scope,
94
+ withheld: undefined,
95
+ open: forCanvas,
96
+ });
97
+ }
98
+ }
99
+ for (const extension of withheld) {
100
+ listings.push({
101
+ extensionId: extension.id,
102
+ canvasId: undefined,
103
+ displayName: undefined,
104
+ scope: extension.scope,
105
+ withheld: "untrusted-workspace",
106
+ open: [],
107
+ });
108
+ }
109
+ return { availability, listings, withheldCount: withheld.length };
110
+ }
111
+ /**
112
+ * Open a canvas.
113
+ *
114
+ * `options.signal` comes from the caller's cancellable loader, so a person's Esc
115
+ * reaches the registry's abandon path (§11.6) rather than merely hiding a spinner.
116
+ */
117
+ async open(ref, options) {
118
+ const availability = await this.availability();
119
+ if (!availability.available)
120
+ throw new Error(availability.reason);
121
+ const { runnable, withheld } = this.discover();
122
+ const extension = runnable.find((candidate) => candidate.id === ref.extensionId);
123
+ if (!extension) {
124
+ if (withheld.some((candidate) => candidate.id === ref.extensionId)) {
125
+ throw new Error(`Canvas extension "${ref.extensionId}" came with this repository, which is not a trusted workspace. ` +
126
+ "Run /plugin trust to allow this directory to run code it ships.");
127
+ }
128
+ const known = runnable.map((candidate) => candidate.id).join(", ") || "none";
129
+ throw new Error(`No canvas extension "${ref.extensionId}" (found: ${known}).`);
130
+ }
131
+ const registry = this.ensureRegistry(availability);
132
+ const canvasId = ref.canvasId ?? (await this.soleCanvasId(registry, extension));
133
+ return registry.open(extension, canvasId, undefined, options);
134
+ }
135
+ /** Close one open instance. Unknown ids are a no-op, so closing twice is harmless. */
136
+ async close(instanceId) {
137
+ const registry = this.registryOrUndefined();
138
+ const instance = registry?.listInstances().find((open) => open.instanceId === instanceId);
139
+ if (!registry || !instance)
140
+ return undefined;
141
+ await registry.close(instance);
142
+ return instance;
143
+ }
144
+ /** Every open instance. */
145
+ instances() {
146
+ return this.registryOrUndefined()?.listInstances() ?? [];
147
+ }
148
+ /**
149
+ * The live registry, or undefined if nothing has been opened yet.
150
+ *
151
+ * Exposed so the host can hand it to the canvas tools, which read
152
+ * `listInstances()` and `activeActions()` from it.
153
+ */
154
+ registryOrUndefined() {
155
+ return this.registry;
156
+ }
157
+ /** Advisory cleanup, driven by whoever owns the session clock. */
158
+ async reapIdle() {
159
+ return (await this.registryOrUndefined()?.reapIdle()) ?? [];
160
+ }
161
+ /** Close everything and stop every child. Safe to call twice. */
162
+ async dispose() {
163
+ await this.registry?.shutdown();
164
+ this.registry = undefined;
165
+ }
166
+ ensureRegistry(availability) {
167
+ if (!this.registry) {
168
+ this.registry = new CanvasRegistry({
169
+ runtime: availability.runtime,
170
+ cwd: this.options.cwd,
171
+ agentDir: this.options.agentDir,
172
+ onLog: this.options.onLog,
173
+ onStray: this.options.onStray,
174
+ onStderr: this.options.onStderr,
175
+ onDiagnostic: this.options.onDiagnostic,
176
+ });
177
+ }
178
+ return this.registry;
179
+ }
180
+ /**
181
+ * Pick the canvas when the caller named only an extension.
182
+ *
183
+ * Forking to read the declarations is unavoidable: they arrive in the child's
184
+ * `ready` message. A multi-canvas extension must be named explicitly rather than
185
+ * guessed at.
186
+ */
187
+ async soleCanvasId(registry, extension) {
188
+ const declarations = await registry.declarations(extension);
189
+ if (declarations.length === 1)
190
+ return declarations[0]?.id;
191
+ if (declarations.length === 0)
192
+ throw new Error(`Canvas extension "${extension.id}" declares no canvases.`);
193
+ const ids = declarations.map((declaration) => `${extension.id}:${declaration.id}`).join(", ");
194
+ throw new Error(`Canvas extension "${extension.id}" declares several canvases; name one of: ${ids}.`);
195
+ }
196
+ }
197
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/core/canvas/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Session-scoped canvas facade: everything the TUI needs, with no TUI in it.\n *\n * Design: `docs/canvas-extensions-design.md` §11. The pieces underneath — discovery,\n * the trust gate, availability, the registry — are each small and separately tested.\n * This is what stitches them into the four questions a user surface actually asks:\n * what is there, can it run, open this one, close that one.\n *\n * It holds no TUI types on purpose. `extensions/core/canvas.ts` renders and supplies\n * an `AbortSignal` from a cancellable loader; everything decided here stays testable\n * without a terminal.\n *\n * Availability is resolved once and cached, because resolving can spawn\n * `node --version` (§11.1) and the answer cannot change within a session.\n */\n\nimport { getAgentDir } from \"../../config.js\";\nimport type { DiscoveredCanvasExtension } from \"./discovery.js\";\nimport { type CanvasSearchRoot, canvasSearchRoots, discoverCanvasExtensions } from \"./discovery.js\";\nimport { type CanvasAvailability, resolveCanvasRuntime } from \"./launch.js\";\nimport { type CanvasInstance, CanvasRegistry, type CanvasRegistryEvents } from \"./registry.js\";\nimport type { CanvasCallOptions } from \"./runner.js\";\nimport { gateCanvasExtensions } from \"./trust.js\";\n\n/** One canvas a person could open, or has open. */\nexport interface CanvasListing {\n\textensionId: string;\n\t/** Undefined until the extension has been forked, since declarations come from it. */\n\tcanvasId: string | undefined;\n\tdisplayName: string | undefined;\n\tscope: DiscoveredCanvasExtension[\"scope\"];\n\t/** Why it cannot be opened, if it cannot. */\n\twithheld: \"untrusted-workspace\" | undefined;\n\t/** Instances of this canvas that are currently open. */\n\topen: CanvasInstance[];\n}\n\n/** What `list()` reports. */\nexport interface CanvasOverview {\n\t/** Absent `reason` means canvases can run here. */\n\tavailability: CanvasAvailability;\n\tlistings: CanvasListing[];\n\t/** Extensions withheld by the trust gate — surfaced, never hidden (§5.1). */\n\twithheldCount: number;\n}\n\n/** Configuration for a session's canvas facade. */\nexport interface CanvasSessionOptions extends CanvasRegistryEvents {\n\tcwd: string;\n\thomeDir: string;\n\tagentDir?: string;\n\t/** Override the search roots; defaults to {@link canvasSearchRoots}. */\n\troots?: CanvasSearchRoot[];\n\t/** Override availability resolution, for tests and for hosts that already know. */\n\tresolveRuntime?: () => Promise<CanvasAvailability>;\n}\n\n/** Reference to a canvas: an extension id, optionally narrowed to one of its canvases. */\nexport interface CanvasRef {\n\textensionId: string;\n\tcanvasId?: string;\n}\n\n/**\n * Parse `extension` or `extension:canvas`.\n *\n * Extension ids are directory names and canvas ids are provider-local, so a single\n * colon is unambiguous and needs no quoting.\n */\nexport function parseCanvasRef(input: string): CanvasRef | undefined {\n\tconst trimmed = input.trim();\n\tif (trimmed.length === 0) return undefined;\n\tconst colon = trimmed.indexOf(\":\");\n\tif (colon === -1) return { extensionId: trimmed };\n\tconst extensionId = trimmed.slice(0, colon).trim();\n\tconst canvasId = trimmed.slice(colon + 1).trim();\n\tif (extensionId.length === 0 || canvasId.length === 0) return undefined;\n\treturn { extensionId, canvasId };\n}\n\nexport class CanvasSession {\n\tprivate readonly options: CanvasSessionOptions;\n\tprivate readonly roots: CanvasSearchRoot[];\n\t/** Cached because resolving can spawn `node --version` and cannot change mid-session. */\n\tprivate availabilityPromise: Promise<CanvasAvailability> | undefined;\n\t/** Created on first successful open, not at construction: listing must not fork. */\n\tprivate registry: CanvasRegistry | undefined;\n\n\tconstructor(options: CanvasSessionOptions) {\n\t\tthis.options = options;\n\t\tthis.roots = options.roots ?? canvasSearchRoots(options.cwd, options.homeDir);\n\t}\n\n\t/** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */\n\tdiscover(): { runnable: DiscoveredCanvasExtension[]; withheld: DiscoveredCanvasExtension[] } {\n\t\tconst gated = gateCanvasExtensions(\n\t\t\tdiscoverCanvasExtensions(this.roots),\n\t\t\tthis.options.cwd,\n\t\t\tthis.options.agentDir ?? getAgentDir(),\n\t\t);\n\t\treturn { runnable: gated.runnable, withheld: gated.withheld.map((entry) => entry.extension) };\n\t}\n\n\t/** Whether canvases can run here. Resolved once per session and cached. */\n\tasync availability(): Promise<CanvasAvailability> {\n\t\tthis.availabilityPromise ??= (this.options.resolveRuntime ?? resolveCanvasRuntime)();\n\t\treturn this.availabilityPromise;\n\t}\n\n\t/**\n\t * What is installed, what is open, and what is being withheld.\n\t *\n\t * Deliberately does not fork anything: listing must stay free and safe, so a\n\t * `canvasId` is only known for extensions already running. That is the visible\n\t * consequence of a canvas having no passive half (§5.1) — even its name comes from\n\t * running its code.\n\t */\n\tasync list(): Promise<CanvasOverview> {\n\t\tconst availability = await this.availability();\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst open = this.registryOrUndefined()?.listInstances() ?? [];\n\n\t\tconst listings: CanvasListing[] = [];\n\t\tfor (const extension of runnable) {\n\t\t\tconst instances = open.filter((instance) => instance.extensionId === extension.id);\n\t\t\tif (instances.length === 0) {\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId: undefined,\n\t\t\t\t\tdisplayName: undefined,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: [],\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tfor (const canvasId of new Set(instances.map((instance) => instance.canvasId))) {\n\t\t\t\tconst forCanvas = instances.filter((instance) => instance.canvasId === canvasId);\n\t\t\t\tlistings.push({\n\t\t\t\t\textensionId: extension.id,\n\t\t\t\t\tcanvasId,\n\t\t\t\t\tdisplayName: forCanvas[0]?.title,\n\t\t\t\t\tscope: extension.scope,\n\t\t\t\t\twithheld: undefined,\n\t\t\t\t\topen: forCanvas,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfor (const extension of withheld) {\n\t\t\tlistings.push({\n\t\t\t\textensionId: extension.id,\n\t\t\t\tcanvasId: undefined,\n\t\t\t\tdisplayName: undefined,\n\t\t\t\tscope: extension.scope,\n\t\t\t\twithheld: \"untrusted-workspace\",\n\t\t\t\topen: [],\n\t\t\t});\n\t\t}\n\t\treturn { availability, listings, withheldCount: withheld.length };\n\t}\n\n\t/**\n\t * Open a canvas.\n\t *\n\t * `options.signal` comes from the caller's cancellable loader, so a person's Esc\n\t * reaches the registry's abandon path (§11.6) rather than merely hiding a spinner.\n\t */\n\tasync open(ref: CanvasRef, options?: CanvasCallOptions): Promise<CanvasInstance> {\n\t\tconst availability = await this.availability();\n\t\tif (!availability.available) throw new Error(availability.reason);\n\n\t\tconst { runnable, withheld } = this.discover();\n\t\tconst extension = runnable.find((candidate) => candidate.id === ref.extensionId);\n\t\tif (!extension) {\n\t\t\tif (withheld.some((candidate) => candidate.id === ref.extensionId)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Canvas extension \"${ref.extensionId}\" came with this repository, which is not a trusted workspace. ` +\n\t\t\t\t\t\t\"Run /plugin trust to allow this directory to run code it ships.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst known = runnable.map((candidate) => candidate.id).join(\", \") || \"none\";\n\t\t\tthrow new Error(`No canvas extension \"${ref.extensionId}\" (found: ${known}).`);\n\t\t}\n\n\t\tconst registry = this.ensureRegistry(availability);\n\t\tconst canvasId = ref.canvasId ?? (await this.soleCanvasId(registry, extension));\n\t\treturn registry.open(extension, canvasId, undefined, options);\n\t}\n\n\t/** Close one open instance. Unknown ids are a no-op, so closing twice is harmless. */\n\tasync close(instanceId: string): Promise<CanvasInstance | undefined> {\n\t\tconst registry = this.registryOrUndefined();\n\t\tconst instance = registry?.listInstances().find((open) => open.instanceId === instanceId);\n\t\tif (!registry || !instance) return undefined;\n\t\tawait registry.close(instance);\n\t\treturn instance;\n\t}\n\n\t/** Every open instance. */\n\tinstances(): CanvasInstance[] {\n\t\treturn this.registryOrUndefined()?.listInstances() ?? [];\n\t}\n\n\t/**\n\t * The live registry, or undefined if nothing has been opened yet.\n\t *\n\t * Exposed so the host can hand it to the canvas tools, which read\n\t * `listInstances()` and `activeActions()` from it.\n\t */\n\tregistryOrUndefined(): CanvasRegistry | undefined {\n\t\treturn this.registry;\n\t}\n\n\t/** Advisory cleanup, driven by whoever owns the session clock. */\n\tasync reapIdle(): Promise<string[]> {\n\t\treturn (await this.registryOrUndefined()?.reapIdle()) ?? [];\n\t}\n\n\t/** Close everything and stop every child. Safe to call twice. */\n\tasync dispose(): Promise<void> {\n\t\tawait this.registry?.shutdown();\n\t\tthis.registry = undefined;\n\t}\n\n\tprivate ensureRegistry(availability: Extract<CanvasAvailability, { available: true }>): CanvasRegistry {\n\t\tif (!this.registry) {\n\t\t\tthis.registry = new CanvasRegistry({\n\t\t\t\truntime: availability.runtime,\n\t\t\t\tcwd: this.options.cwd,\n\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\tonLog: this.options.onLog,\n\t\t\t\tonStray: this.options.onStray,\n\t\t\t\tonStderr: this.options.onStderr,\n\t\t\t\tonDiagnostic: this.options.onDiagnostic,\n\t\t\t});\n\t\t}\n\t\treturn this.registry;\n\t}\n\n\t/**\n\t * Pick the canvas when the caller named only an extension.\n\t *\n\t * Forking to read the declarations is unavoidable: they arrive in the child's\n\t * `ready` message. A multi-canvas extension must be named explicitly rather than\n\t * guessed at.\n\t */\n\tprivate async soleCanvasId(registry: CanvasRegistry, extension: DiscoveredCanvasExtension): Promise<string> {\n\t\tconst declarations = await registry.declarations(extension);\n\t\tif (declarations.length === 1) return declarations[0]?.id as string;\n\t\tif (declarations.length === 0) throw new Error(`Canvas extension \"${extension.id}\" declares no canvases.`);\n\t\tconst ids = declarations.map((declaration) => `${extension.id}:${declaration.id}`).join(\", \");\n\t\tthrow new Error(`Canvas extension \"${extension.id}\" declares several canvases; name one of: ${ids}.`);\n\t}\n}\n"]}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Trust gating for canvas extensions.
3
+ *
4
+ * Design: `docs/canvas-extensions-design.md` §5. The argument is already written
5
+ * down in `core/extensions/plugins/trust.ts`, for the same reason:
6
+ *
7
+ * > Its skills and commands are text the model reads, which is no worse than
8
+ * > reading the repository itself, but its **hooks and MCP servers are processes**
9
+ * > that start on session load.
10
+ *
11
+ * A canvas extension is a process **that also opens a listening socket**, so it
12
+ * belongs in that record on identical grounds and needs no new mechanism. The gate
13
+ * itself is shared: `isRepositorySupplied` and `shouldWithholdRepositorySupplied`
14
+ * live in `plugins/trust.ts` and are used by the plugin gate too, so this module
15
+ * supplies only the roots that are specific to canvas extensions. The record lives
16
+ * outside the repository, so repository content cannot forge it.
17
+ *
18
+ * One difference from plugins, and it matters. `shouldWithholdExecutables` can
19
+ * withhold a plugin's hooks and MCP servers while still loading its skills,
20
+ * because a plugin has passive capabilities worth having. **A canvas has none.**
21
+ * Its declaration, its actions, and its UI all come from running its code. There
22
+ * is nothing to partially allow, so an untrusted canvas is withheld whole.
23
+ *
24
+ * What stays available is discovery: `discovery.ts` only reads directory entries,
25
+ * so an untrusted canvas can still be listed, named, and offered — the person can
26
+ * see what is on offer and decide to trust the workspace. Listing is not running.
27
+ */
28
+ import type { DiscoveredCanvasExtension } from "./discovery.js";
29
+ /** Thrown when something tries to run a canvas the workspace has not earned. */
30
+ export declare class CanvasTrustError extends Error {
31
+ readonly extensionId: string;
32
+ constructor(extensionId: string, cwd: string);
33
+ }
34
+ /**
35
+ * Whether an extension sits in the working tree, and therefore arrived with the
36
+ * repository as far as anyone but its author can tell.
37
+ *
38
+ * Both project-scope homes count. `.agents/extensions/` is where hoocode would
39
+ * author its own, and `.github/extensions/` is Copilot's project scope — the one
40
+ * that travels in every clone. Neither location can distinguish "I put this here"
41
+ * from "this arrived in the clone", which is exactly why location is not the
42
+ * question being asked; it only decides *whether* to ask about trust.
43
+ *
44
+ * User scope (`~/.copilot/extensions/`) is not project-supplied: it is outside any
45
+ * repository and got there by a deliberate local act.
46
+ */
47
+ export declare function isProjectSuppliedCanvas(extension: DiscoveredCanvasExtension, cwd: string): boolean;
48
+ /**
49
+ * Whether an extension must not be forked: it came with the repository and this
50
+ * machine has not trusted the workspace.
51
+ */
52
+ export declare function shouldWithholdCanvas(extension: DiscoveredCanvasExtension, cwd: string, agentDir?: string): boolean;
53
+ /** A withheld extension, with the reason, so a caller can explain rather than hide. */
54
+ export interface WithheldCanvasExtension {
55
+ extension: DiscoveredCanvasExtension;
56
+ reason: "untrusted-workspace";
57
+ }
58
+ /** Discovered extensions split into what may be forked and what may not. */
59
+ export interface GatedCanvasExtensions {
60
+ runnable: DiscoveredCanvasExtension[];
61
+ withheld: WithheldCanvasExtension[];
62
+ }
63
+ /**
64
+ * Partition discovered extensions by trust.
65
+ *
66
+ * Callers should present `withheld` rather than dropping it: the point of the gate
67
+ * is that the person can see a repository offers a canvas and choose, not that the
68
+ * offer disappears.
69
+ */
70
+ export declare function gateCanvasExtensions(extensions: DiscoveredCanvasExtension[], cwd: string, agentDir?: string): GatedCanvasExtensions;
71
+ //# sourceMappingURL=trust.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/trust.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAEhE,gFAAgF;AAChF,qBAAa,gBAAiB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,YAAY,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAQ3C;CACD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,yBAAyB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAElG;AAOD;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,SAAS,EAAE,yBAAyB,EACpC,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,GAC9B,OAAO,CAET;AAED,uFAAuF;AACvF,MAAM,WAAW,uBAAuB;IACvC,SAAS,EAAE,yBAAyB,CAAC;IACrC,MAAM,EAAE,qBAAqB,CAAC;CAC9B;AAED,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,yBAAyB,EAAE,CAAC;IACtC,QAAQ,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CACnC,UAAU,EAAE,yBAAyB,EAAE,EACvC,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,GAC9B,qBAAqB,CAUvB","sourcesContent":["/**\n * Trust gating for canvas extensions.\n *\n * Design: `docs/canvas-extensions-design.md` §5. The argument is already written\n * down in `core/extensions/plugins/trust.ts`, for the same reason:\n *\n * > Its skills and commands are text the model reads, which is no worse than\n * > reading the repository itself, but its **hooks and MCP servers are processes**\n * > that start on session load.\n *\n * A canvas extension is a process **that also opens a listening socket**, so it\n * belongs in that record on identical grounds and needs no new mechanism. The gate\n * itself is shared: `isRepositorySupplied` and `shouldWithholdRepositorySupplied`\n * live in `plugins/trust.ts` and are used by the plugin gate too, so this module\n * supplies only the roots that are specific to canvas extensions. The record lives\n * outside the repository, so repository content cannot forge it.\n *\n * One difference from plugins, and it matters. `shouldWithholdExecutables` can\n * withhold a plugin's hooks and MCP servers while still loading its skills,\n * because a plugin has passive capabilities worth having. **A canvas has none.**\n * Its declaration, its actions, and its UI all come from running its code. There\n * is nothing to partially allow, so an untrusted canvas is withheld whole.\n *\n * What stays available is discovery: `discovery.ts` only reads directory entries,\n * so an untrusted canvas can still be listed, named, and offered — the person can\n * see what is on offer and decide to trust the workspace. Listing is not running.\n */\n\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { isRepositorySupplied, shouldWithholdRepositorySupplied } from \"../extensions/plugins/trust.js\";\nimport type { DiscoveredCanvasExtension } from \"./discovery.js\";\n\n/** Thrown when something tries to run a canvas the workspace has not earned. */\nexport class CanvasTrustError extends Error {\n\treadonly extensionId: string;\n\n\tconstructor(extensionId: string, cwd: string) {\n\t\tsuper(\n\t\t\t`Canvas extension \"${extensionId}\" came with this repository and \"${cwd}\" is not a trusted workspace. ` +\n\t\t\t\t`Running it would start a process and open a listening socket on your machine. ` +\n\t\t\t\t`Trust the workspace first if that is what you want.`,\n\t\t);\n\t\tthis.name = \"CanvasTrustError\";\n\t\tthis.extensionId = extensionId;\n\t}\n}\n\n/**\n * Whether an extension sits in the working tree, and therefore arrived with the\n * repository as far as anyone but its author can tell.\n *\n * Both project-scope homes count. `.agents/extensions/` is where hoocode would\n * author its own, and `.github/extensions/` is Copilot's project scope — the one\n * that travels in every clone. Neither location can distinguish \"I put this here\"\n * from \"this arrived in the clone\", which is exactly why location is not the\n * question being asked; it only decides *whether* to ask about trust.\n *\n * User scope (`~/.copilot/extensions/`) is not project-supplied: it is outside any\n * repository and got there by a deliberate local act.\n */\nexport function isProjectSuppliedCanvas(extension: DiscoveredCanvasExtension, cwd: string): boolean {\n\treturn isRepositorySupplied(extension.dir, canvasProjectScopeRoots(cwd));\n}\n\n/** A canvas extension's project-scope homes — the locations that travel with a clone. */\nfunction canvasProjectScopeRoots(cwd: string): string[] {\n\treturn [path.join(cwd, \".agents\", \"extensions\"), path.join(cwd, \".github\", \"extensions\")];\n}\n\n/**\n * Whether an extension must not be forked: it came with the repository and this\n * machine has not trusted the workspace.\n */\nexport function shouldWithholdCanvas(\n\textension: DiscoveredCanvasExtension,\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): boolean {\n\treturn shouldWithholdRepositorySupplied(extension.dir, cwd, canvasProjectScopeRoots(cwd), agentDir);\n}\n\n/** A withheld extension, with the reason, so a caller can explain rather than hide. */\nexport interface WithheldCanvasExtension {\n\textension: DiscoveredCanvasExtension;\n\treason: \"untrusted-workspace\";\n}\n\n/** Discovered extensions split into what may be forked and what may not. */\nexport interface GatedCanvasExtensions {\n\trunnable: DiscoveredCanvasExtension[];\n\twithheld: WithheldCanvasExtension[];\n}\n\n/**\n * Partition discovered extensions by trust.\n *\n * Callers should present `withheld` rather than dropping it: the point of the gate\n * is that the person can see a repository offers a canvas and choose, not that the\n * offer disappears.\n */\nexport function gateCanvasExtensions(\n\textensions: DiscoveredCanvasExtension[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): GatedCanvasExtensions {\n\tconst gated: GatedCanvasExtensions = { runnable: [], withheld: [] };\n\tfor (const extension of extensions) {\n\t\tif (shouldWithholdCanvas(extension, cwd, agentDir)) {\n\t\t\tgated.withheld.push({ extension, reason: \"untrusted-workspace\" });\n\t\t} else {\n\t\t\tgated.runnable.push(extension);\n\t\t}\n\t}\n\treturn gated;\n}\n"]}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Trust gating for canvas extensions.
3
+ *
4
+ * Design: `docs/canvas-extensions-design.md` §5. The argument is already written
5
+ * down in `core/extensions/plugins/trust.ts`, for the same reason:
6
+ *
7
+ * > Its skills and commands are text the model reads, which is no worse than
8
+ * > reading the repository itself, but its **hooks and MCP servers are processes**
9
+ * > that start on session load.
10
+ *
11
+ * A canvas extension is a process **that also opens a listening socket**, so it
12
+ * belongs in that record on identical grounds and needs no new mechanism. The gate
13
+ * itself is shared: `isRepositorySupplied` and `shouldWithholdRepositorySupplied`
14
+ * live in `plugins/trust.ts` and are used by the plugin gate too, so this module
15
+ * supplies only the roots that are specific to canvas extensions. The record lives
16
+ * outside the repository, so repository content cannot forge it.
17
+ *
18
+ * One difference from plugins, and it matters. `shouldWithholdExecutables` can
19
+ * withhold a plugin's hooks and MCP servers while still loading its skills,
20
+ * because a plugin has passive capabilities worth having. **A canvas has none.**
21
+ * Its declaration, its actions, and its UI all come from running its code. There
22
+ * is nothing to partially allow, so an untrusted canvas is withheld whole.
23
+ *
24
+ * What stays available is discovery: `discovery.ts` only reads directory entries,
25
+ * so an untrusted canvas can still be listed, named, and offered — the person can
26
+ * see what is on offer and decide to trust the workspace. Listing is not running.
27
+ */
28
+ import * as path from "node:path";
29
+ import { getAgentDir } from "../../config.js";
30
+ import { isRepositorySupplied, shouldWithholdRepositorySupplied } from "../extensions/plugins/trust.js";
31
+ /** Thrown when something tries to run a canvas the workspace has not earned. */
32
+ export class CanvasTrustError extends Error {
33
+ extensionId;
34
+ constructor(extensionId, cwd) {
35
+ super(`Canvas extension "${extensionId}" came with this repository and "${cwd}" is not a trusted workspace. ` +
36
+ `Running it would start a process and open a listening socket on your machine. ` +
37
+ `Trust the workspace first if that is what you want.`);
38
+ this.name = "CanvasTrustError";
39
+ this.extensionId = extensionId;
40
+ }
41
+ }
42
+ /**
43
+ * Whether an extension sits in the working tree, and therefore arrived with the
44
+ * repository as far as anyone but its author can tell.
45
+ *
46
+ * Both project-scope homes count. `.agents/extensions/` is where hoocode would
47
+ * author its own, and `.github/extensions/` is Copilot's project scope — the one
48
+ * that travels in every clone. Neither location can distinguish "I put this here"
49
+ * from "this arrived in the clone", which is exactly why location is not the
50
+ * question being asked; it only decides *whether* to ask about trust.
51
+ *
52
+ * User scope (`~/.copilot/extensions/`) is not project-supplied: it is outside any
53
+ * repository and got there by a deliberate local act.
54
+ */
55
+ export function isProjectSuppliedCanvas(extension, cwd) {
56
+ return isRepositorySupplied(extension.dir, canvasProjectScopeRoots(cwd));
57
+ }
58
+ /** A canvas extension's project-scope homes — the locations that travel with a clone. */
59
+ function canvasProjectScopeRoots(cwd) {
60
+ return [path.join(cwd, ".agents", "extensions"), path.join(cwd, ".github", "extensions")];
61
+ }
62
+ /**
63
+ * Whether an extension must not be forked: it came with the repository and this
64
+ * machine has not trusted the workspace.
65
+ */
66
+ export function shouldWithholdCanvas(extension, cwd, agentDir = getAgentDir()) {
67
+ return shouldWithholdRepositorySupplied(extension.dir, cwd, canvasProjectScopeRoots(cwd), agentDir);
68
+ }
69
+ /**
70
+ * Partition discovered extensions by trust.
71
+ *
72
+ * Callers should present `withheld` rather than dropping it: the point of the gate
73
+ * is that the person can see a repository offers a canvas and choose, not that the
74
+ * offer disappears.
75
+ */
76
+ export function gateCanvasExtensions(extensions, cwd, agentDir = getAgentDir()) {
77
+ const gated = { runnable: [], withheld: [] };
78
+ for (const extension of extensions) {
79
+ if (shouldWithholdCanvas(extension, cwd, agentDir)) {
80
+ gated.withheld.push({ extension, reason: "untrusted-workspace" });
81
+ }
82
+ else {
83
+ gated.runnable.push(extension);
84
+ }
85
+ }
86
+ return gated;
87
+ }
88
+ //# sourceMappingURL=trust.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust.js","sourceRoot":"","sources":["../../../src/core/canvas/trust.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,gCAAgC,EAAE,MAAM,gCAAgC,CAAC;AAGxG,gFAAgF;AAChF,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACjC,WAAW,CAAS;IAE7B,YAAY,WAAmB,EAAE,GAAW,EAAE;QAC7C,KAAK,CACJ,qBAAqB,WAAW,oCAAoC,GAAG,gCAAgC;YACtG,gFAAgF;YAChF,qDAAqD,CACtD,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IAAA,CAC/B;CACD;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB,CAAC,SAAoC,EAAE,GAAW,EAAW;IACnG,OAAO,oBAAoB,CAAC,SAAS,CAAC,GAAG,EAAE,uBAAuB,CAAC,GAAG,CAAC,CAAC,CAAC;AAAA,CACzE;AAED,2FAAyF;AACzF,SAAS,uBAAuB,CAAC,GAAW,EAAY;IACvD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAAA,CAC1F;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CACnC,SAAoC,EACpC,GAAW,EACX,QAAQ,GAAW,WAAW,EAAE,EACtB;IACV,OAAO,gCAAgC,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,uBAAuB,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA,CACpG;AAcD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CACnC,UAAuC,EACvC,GAAW,EACX,QAAQ,GAAW,WAAW,EAAE,EACR;IACxB,MAAM,KAAK,GAA0B,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IACpE,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,oBAAoB,CAAC,SAAS,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC;YACpD,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC,CAAC;QACnE,CAAC;aAAM,CAAC;YACP,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb","sourcesContent":["/**\n * Trust gating for canvas extensions.\n *\n * Design: `docs/canvas-extensions-design.md` §5. The argument is already written\n * down in `core/extensions/plugins/trust.ts`, for the same reason:\n *\n * > Its skills and commands are text the model reads, which is no worse than\n * > reading the repository itself, but its **hooks and MCP servers are processes**\n * > that start on session load.\n *\n * A canvas extension is a process **that also opens a listening socket**, so it\n * belongs in that record on identical grounds and needs no new mechanism. The gate\n * itself is shared: `isRepositorySupplied` and `shouldWithholdRepositorySupplied`\n * live in `plugins/trust.ts` and are used by the plugin gate too, so this module\n * supplies only the roots that are specific to canvas extensions. The record lives\n * outside the repository, so repository content cannot forge it.\n *\n * One difference from plugins, and it matters. `shouldWithholdExecutables` can\n * withhold a plugin's hooks and MCP servers while still loading its skills,\n * because a plugin has passive capabilities worth having. **A canvas has none.**\n * Its declaration, its actions, and its UI all come from running its code. There\n * is nothing to partially allow, so an untrusted canvas is withheld whole.\n *\n * What stays available is discovery: `discovery.ts` only reads directory entries,\n * so an untrusted canvas can still be listed, named, and offered — the person can\n * see what is on offer and decide to trust the workspace. Listing is not running.\n */\n\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { isRepositorySupplied, shouldWithholdRepositorySupplied } from \"../extensions/plugins/trust.js\";\nimport type { DiscoveredCanvasExtension } from \"./discovery.js\";\n\n/** Thrown when something tries to run a canvas the workspace has not earned. */\nexport class CanvasTrustError extends Error {\n\treadonly extensionId: string;\n\n\tconstructor(extensionId: string, cwd: string) {\n\t\tsuper(\n\t\t\t`Canvas extension \"${extensionId}\" came with this repository and \"${cwd}\" is not a trusted workspace. ` +\n\t\t\t\t`Running it would start a process and open a listening socket on your machine. ` +\n\t\t\t\t`Trust the workspace first if that is what you want.`,\n\t\t);\n\t\tthis.name = \"CanvasTrustError\";\n\t\tthis.extensionId = extensionId;\n\t}\n}\n\n/**\n * Whether an extension sits in the working tree, and therefore arrived with the\n * repository as far as anyone but its author can tell.\n *\n * Both project-scope homes count. `.agents/extensions/` is where hoocode would\n * author its own, and `.github/extensions/` is Copilot's project scope — the one\n * that travels in every clone. Neither location can distinguish \"I put this here\"\n * from \"this arrived in the clone\", which is exactly why location is not the\n * question being asked; it only decides *whether* to ask about trust.\n *\n * User scope (`~/.copilot/extensions/`) is not project-supplied: it is outside any\n * repository and got there by a deliberate local act.\n */\nexport function isProjectSuppliedCanvas(extension: DiscoveredCanvasExtension, cwd: string): boolean {\n\treturn isRepositorySupplied(extension.dir, canvasProjectScopeRoots(cwd));\n}\n\n/** A canvas extension's project-scope homes — the locations that travel with a clone. */\nfunction canvasProjectScopeRoots(cwd: string): string[] {\n\treturn [path.join(cwd, \".agents\", \"extensions\"), path.join(cwd, \".github\", \"extensions\")];\n}\n\n/**\n * Whether an extension must not be forked: it came with the repository and this\n * machine has not trusted the workspace.\n */\nexport function shouldWithholdCanvas(\n\textension: DiscoveredCanvasExtension,\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): boolean {\n\treturn shouldWithholdRepositorySupplied(extension.dir, cwd, canvasProjectScopeRoots(cwd), agentDir);\n}\n\n/** A withheld extension, with the reason, so a caller can explain rather than hide. */\nexport interface WithheldCanvasExtension {\n\textension: DiscoveredCanvasExtension;\n\treason: \"untrusted-workspace\";\n}\n\n/** Discovered extensions split into what may be forked and what may not. */\nexport interface GatedCanvasExtensions {\n\trunnable: DiscoveredCanvasExtension[];\n\twithheld: WithheldCanvasExtension[];\n}\n\n/**\n * Partition discovered extensions by trust.\n *\n * Callers should present `withheld` rather than dropping it: the point of the gate\n * is that the person can see a repository offers a canvas and choose, not that the\n * offer disappears.\n */\nexport function gateCanvasExtensions(\n\textensions: DiscoveredCanvasExtension[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n): GatedCanvasExtensions {\n\tconst gated: GatedCanvasExtensions = { runnable: [], withheld: [] };\n\tfor (const extension of extensions) {\n\t\tif (shouldWithholdCanvas(extension, cwd, agentDir)) {\n\t\t\tgated.withheld.push({ extension, reason: \"untrusted-workspace\" });\n\t\t} else {\n\t\t\tgated.runnable.push(extension);\n\t\t}\n\t}\n\treturn gated;\n}\n"]}