@kolisachint/hoocode-agent 0.5.24 → 0.5.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/dist/core/brand.d.ts +1 -0
- package/dist/core/brand.d.ts.map +1 -1
- package/dist/core/brand.js +1 -0
- package/dist/core/brand.js.map +1 -1
- package/dist/core/canvas/discovery.d.ts +11 -3
- package/dist/core/canvas/discovery.d.ts.map +1 -1
- package/dist/core/canvas/discovery.js +18 -3
- package/dist/core/canvas/discovery.js.map +1 -1
- package/dist/core/canvas/plugin-canvases.d.ts +30 -0
- package/dist/core/canvas/plugin-canvases.d.ts.map +1 -0
- package/dist/core/canvas/plugin-canvases.js +51 -0
- package/dist/core/canvas/plugin-canvases.js.map +1 -0
- package/dist/core/canvas/session.d.ts +7 -0
- package/dist/core/canvas/session.d.ts.map +1 -1
- package/dist/core/canvas/session.js +8 -1
- package/dist/core/canvas/session.js.map +1 -1
- package/dist/core/canvas/trust.d.ts +4 -5
- package/dist/core/canvas/trust.d.ts.map +1 -1
- package/dist/core/canvas/trust.js +23 -8
- package/dist/core/canvas/trust.js.map +1 -1
- package/dist/core/extensions/plugins/formats/copilot.d.ts +10 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/copilot.js +36 -4
- package/dist/core/extensions/plugins/formats/copilot.js.map +1 -1
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/jsonManifest.js +15 -2
- package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.d.ts +30 -2
- package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
- package/dist/core/extensions/plugins/formats/shared.js +72 -0
- package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
- package/dist/core/extensions/plugins/install.d.ts +31 -0
- package/dist/core/extensions/plugins/install.d.ts.map +1 -1
- package/dist/core/extensions/plugins/install.js +102 -12
- package/dist/core/extensions/plugins/install.js.map +1 -1
- package/dist/core/extensions/plugins/listing.d.ts.map +1 -1
- package/dist/core/extensions/plugins/listing.js +1 -0
- package/dist/core/extensions/plugins/listing.js.map +1 -1
- package/dist/core/extensions/plugins/manifest.d.ts +18 -0
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/manifest.js.map +1 -1
- package/dist/core/extensions/plugins/packaging.d.ts.map +1 -1
- package/dist/core/extensions/plugins/packaging.js +1 -0
- package/dist/core/extensions/plugins/packaging.js.map +1 -1
- package/dist/extensions/core/canvas.d.ts.map +1 -1
- package/dist/extensions/core/canvas.js +1 -1
- package/dist/extensions/core/canvas.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +28 -1
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/extensions/core/scaffold.d.ts +1 -1
- package/dist/extensions/core/scaffold.d.ts.map +1 -1
- package/dist/extensions/core/scaffold.js +185 -1
- package/dist/extensions/core/scaffold.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +11 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +40 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +11 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +20 -1
- package/dist/modes/interactive/resource-display.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.25] - 2026-08-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `/new-canvas <name>` — scaffold a canvas extension. Writes
|
|
8
|
+
`.agents/extensions/<name>/extension.mjs` (or `.github/extensions/` with
|
|
9
|
+
`--platform github`), pre-wired with a declared canvas, one agent-callable
|
|
10
|
+
action, and a token-gated loopback server. It runs as scaffolded, so
|
|
11
|
+
`/canvas open <name>` works immediately — no `/reload`, because canvases are
|
|
12
|
+
discovered when `/canvas` runs rather than loaded at session start.
|
|
13
|
+
|
|
14
|
+
Scaffolding trusts the workspace, the same grant `/plugin install --scope
|
|
15
|
+
project` makes and for the same reason: a canvas lands in the working tree,
|
|
16
|
+
which is where the trust gate looks, so without it the canvas you just asked
|
|
17
|
+
for would be refused with "came with this repository" seconds after you
|
|
18
|
+
created it. The grant is wider than the one canvas — plugins committed here
|
|
19
|
+
may then run hooks and MCP servers — so it is stated in the output and
|
|
20
|
+
`/plugin untrust` reverses it.
|
|
21
|
+
|
|
22
|
+
- Plugins can ship canvases, and `/canvas` now finds them. Both real catalog
|
|
23
|
+
layouts are read: the Copilot manifest's `"extensions": "<dir>"` path key
|
|
24
|
+
(`Redth/mobile-canvas-ghcp`) and the Agent Plugins vendor content namespace
|
|
25
|
+
`com.github.copilot/extensions/<id>/` (`github/awesome-copilot`). A plugin
|
|
26
|
+
whose root itself carries `extension.mjs` counts as one canvas named for the
|
|
27
|
+
plugin. Installed canvases are listed and opened exactly like hand-placed
|
|
28
|
+
ones, and a canvas from a project-scoped plugin goes through the same
|
|
29
|
+
workspace-trust gate as one in `.github/extensions/` — it arrives in every
|
|
30
|
+
collaborator's clone the same way.
|
|
31
|
+
|
|
32
|
+
- Subagents in the `com.github.copilot/agents/` namespace load. This is where
|
|
33
|
+
`github/awesome-copilot` publishes all 125 of its plugin agents; they were
|
|
34
|
+
previously read as nothing.
|
|
35
|
+
|
|
36
|
+
- Canvases appear in the startup and `/reload` surface — a counted cell, and a
|
|
37
|
+
detail row per canvas carrying either `/canvas open <id>` or the reason it is
|
|
38
|
+
withheld. Installing a canvas plugin used to report "1 plugin" and give no
|
|
39
|
+
sign a canvas existed, which is a large part of why the gap below went
|
|
40
|
+
unnoticed.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- `github/awesome-copilot` plugins install with their content. hoocode cloned
|
|
45
|
+
the repository's default branch, where a `plugins/<name>/` directory holds
|
|
46
|
+
only `plugin.json` and `README.md` — the real content lives in top-level
|
|
47
|
+
trees and is materialized into each plugin directory by CI on the
|
|
48
|
+
`marketplace` branch, which is the branch the vendor's own installer reads.
|
|
49
|
+
Every entry therefore installed as an empty shell and reported that it
|
|
50
|
+
contributed no capabilities. The marketplace is now pinned to that branch,
|
|
51
|
+
and a cache left on the wrong ref is re-cloned rather than kept up to date
|
|
52
|
+
forever. `/plugin install arcade-canvas@awesome-copilot` now lands a working
|
|
53
|
+
canvas and says so.
|
|
54
|
+
|
|
55
|
+
The pin names a branch hoocode does not control, so a retired one falls back
|
|
56
|
+
to the default branch with a note saying entries there may be unbuilt, rather
|
|
57
|
+
than dropping the marketplace outright — and a fallback clone stays put
|
|
58
|
+
instead of being discarded and re-downloaded on every run.
|
|
59
|
+
|
|
60
|
+
- `/plugin list` and `/plugin install` refresh the curated marketplace indices
|
|
61
|
+
before reading them, as `SearchPlugins` already did. The human path never did,
|
|
62
|
+
so a stale — or, after the fix below, a *wrong-ref* — cache could persist
|
|
63
|
+
indefinitely: someone who only ever typed `/plugin install` would have kept
|
|
64
|
+
installing unbuilt stubs with no way to know why. TTL-respecting, so it is
|
|
65
|
+
free when the cache is fresh, and it says so before a fetch that will take a
|
|
66
|
+
moment.
|
|
67
|
+
|
|
68
|
+
- Starting a new session, resuming one, or forking one shows the same loaded
|
|
69
|
+
resources `/reload` does. The listing was rendered — extensions rebind before
|
|
70
|
+
the transcript is reset — and then wiped a moment later by that reset, so
|
|
71
|
+
every session change but `/reload` reported nothing.
|
|
72
|
+
|
|
3
73
|
## [0.5.24] - 2026-08-20
|
|
4
74
|
|
|
5
75
|
### Fixed
|
package/dist/core/brand.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export declare const CATEGORY_GLYPH: {
|
|
|
34
34
|
readonly themes: "◒";
|
|
35
35
|
readonly context: "❯";
|
|
36
36
|
readonly extensions: "⊹";
|
|
37
|
+
readonly canvases: "▤";
|
|
37
38
|
};
|
|
38
39
|
export type CategoryKey = keyof typeof CATEGORY_GLYPH;
|
|
39
40
|
/** A soft dot separator used between footer/summary segments. */
|
package/dist/core/brand.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brand.d.ts","sourceRoot":"","sources":["../../src/core/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA0E;AAC1E,eAAO,MAAM,UAAU,WAAM,CAAC;AAE9B;;;;GAIG;AACH,eAAO,MAAM,cAAc
|
|
1
|
+
{"version":3,"file":"brand.d.ts","sourceRoot":"","sources":["../../src/core/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA0E;AAC1E,eAAO,MAAM,UAAU,WAAM,CAAC;AAE9B;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;CAWjB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,cAAc,CAAC;AAEtD,iEAAiE;AACjE,eAAO,MAAM,WAAW,WAAM,CAAC;AAE/B,yCAAyC;AACzC,eAAO,MAAM,gBAAgB,WAAM,CAAC","sourcesContent":["/**\n * HooCode brand identity — the single source of truth for the product mark and\n * the glyph vocabulary the interactive surfaces share.\n *\n * The brand *colour* is the theme's `accent` token (see theme/dark.json,\n * theme/light.json) so it stays theme-aware and swappable; this module owns the\n * non-colour identity: the mark and the category glyphs used by the footer,\n * the startup resource summary, and anywhere a capability class is labelled.\n * Keeping them here means a future rebrand touches one file, not twenty.\n *\n * Glyphs are chosen to be single terminal cell, widely supported, and distinct\n * from the task-panel's own status/owner glyphs (◐ ◆ ◇ ▸ ⧉ ✓ ✗ ○) so the two\n * vocabularies never read as the same signal.\n *\n * This lives in `core/` rather than `modes/interactive/` because the `/plugin`\n * command extension labels the same capability classes, and extensions must not\n * reach into a mode's internals. The module is pure data — no theme, no\n * rendering — so it is safe for every surface.\n */\n\n/** The HooCode mark — a filled hexagon, rendered in the accent colour. */\nexport const BRAND_MARK = \"⬢\";\n\n/**\n * Glyphs for the capability classes a session can load. Used by the startup\n * \"resources ready\" summary and reusable anywhere a class needs a label. Each is\n * a single cell so counts and columns stay aligned.\n */\nexport const CATEGORY_GLYPH = {\n\tskills: \"✦\",\n\tcommands: \"⌘\",\n\tagents: \"◈\",\n\tmcp: \"⧉\",\n\tplugins: \"⬡\",\n\tmarketplaces: \"⊞\",\n\tthemes: \"◒\",\n\tcontext: \"❯\",\n\textensions: \"⊹\",\n\tcanvases: \"▤\",\n} as const;\n\nexport type CategoryKey = keyof typeof CATEGORY_GLYPH;\n\n/** A soft dot separator used between footer/summary segments. */\nexport const SEGMENT_SEP = \"·\";\n\n/** Fork glyph preceding a git branch. */\nexport const GIT_BRANCH_GLYPH = \"⑂\";\n"]}
|
package/dist/core/brand.js
CHANGED
package/dist/core/brand.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brand.js","sourceRoot":"","sources":["../../src/core/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA0E;AAC1E,MAAM,CAAC,MAAM,UAAU,GAAG,KAAG,CAAC;AAE9B;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,MAAM,EAAE,KAAG;IACX,QAAQ,EAAE,KAAG;IACb,MAAM,EAAE,KAAG;IACX,GAAG,EAAE,KAAG;IACR,OAAO,EAAE,KAAG;IACZ,YAAY,EAAE,KAAG;IACjB,MAAM,EAAE,KAAG;IACX,OAAO,EAAE,KAAG;IACZ,UAAU,EAAE,KAAG;
|
|
1
|
+
{"version":3,"file":"brand.js","sourceRoot":"","sources":["../../src/core/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA0E;AAC1E,MAAM,CAAC,MAAM,UAAU,GAAG,KAAG,CAAC;AAE9B;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,MAAM,EAAE,KAAG;IACX,QAAQ,EAAE,KAAG;IACb,MAAM,EAAE,KAAG;IACX,GAAG,EAAE,KAAG;IACR,OAAO,EAAE,KAAG;IACZ,YAAY,EAAE,KAAG;IACjB,MAAM,EAAE,KAAG;IACX,OAAO,EAAE,KAAG;IACZ,UAAU,EAAE,KAAG;IACf,QAAQ,EAAE,KAAG;CACJ,CAAC;AAIX,iEAAiE;AACjE,MAAM,CAAC,MAAM,WAAW,GAAG,IAAG,CAAC;AAE/B,yCAAyC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAG,CAAC","sourcesContent":["/**\n * HooCode brand identity — the single source of truth for the product mark and\n * the glyph vocabulary the interactive surfaces share.\n *\n * The brand *colour* is the theme's `accent` token (see theme/dark.json,\n * theme/light.json) so it stays theme-aware and swappable; this module owns the\n * non-colour identity: the mark and the category glyphs used by the footer,\n * the startup resource summary, and anywhere a capability class is labelled.\n * Keeping them here means a future rebrand touches one file, not twenty.\n *\n * Glyphs are chosen to be single terminal cell, widely supported, and distinct\n * from the task-panel's own status/owner glyphs (◐ ◆ ◇ ▸ ⧉ ✓ ✗ ○) so the two\n * vocabularies never read as the same signal.\n *\n * This lives in `core/` rather than `modes/interactive/` because the `/plugin`\n * command extension labels the same capability classes, and extensions must not\n * reach into a mode's internals. The module is pure data — no theme, no\n * rendering — so it is safe for every surface.\n */\n\n/** The HooCode mark — a filled hexagon, rendered in the accent colour. */\nexport const BRAND_MARK = \"⬢\";\n\n/**\n * Glyphs for the capability classes a session can load. Used by the startup\n * \"resources ready\" summary and reusable anywhere a class needs a label. Each is\n * a single cell so counts and columns stay aligned.\n */\nexport const CATEGORY_GLYPH = {\n\tskills: \"✦\",\n\tcommands: \"⌘\",\n\tagents: \"◈\",\n\tmcp: \"⧉\",\n\tplugins: \"⬡\",\n\tmarketplaces: \"⊞\",\n\tthemes: \"◒\",\n\tcontext: \"❯\",\n\textensions: \"⊹\",\n\tcanvases: \"▤\",\n} as const;\n\nexport type CategoryKey = keyof typeof CATEGORY_GLYPH;\n\n/** A soft dot separator used between footer/summary segments. */\nexport const SEGMENT_SEP = \"·\";\n\n/** Fork glyph preceding a git branch. */\nexport const GIT_BRANCH_GLYPH = \"⑂\";\n"]}
|
|
@@ -53,8 +53,16 @@ export interface DiscoveredCanvasExtension {
|
|
|
53
53
|
*/
|
|
54
54
|
export declare function canvasSearchRoots(cwd: string, homeDir: string): CanvasSearchRoot[];
|
|
55
55
|
/**
|
|
56
|
-
* Discover canvas extensions across `roots`. First
|
|
57
|
-
* so a project-local extension shadows a same-named user-scope
|
|
56
|
+
* Discover canvas extensions across `roots`, then append `extra`. First wins on
|
|
57
|
+
* an id collision, so a project-local extension shadows a same-named user-scope
|
|
58
|
+
* one — and anything a person placed in a search root by hand shadows a
|
|
59
|
+
* same-named canvas that arrived inside a package.
|
|
60
|
+
*
|
|
61
|
+
* `extra` is how plugin-shipped canvases join the same list (see
|
|
62
|
+
* `plugin-canvases.ts`): a plugin's canvas directories are named by its manifest
|
|
63
|
+
* rather than by their position under a search root, so they cannot be expressed
|
|
64
|
+
* as a root — but once resolved they are ordinary discovered extensions, and
|
|
65
|
+
* every consumer downstream is better off not knowing the difference.
|
|
58
66
|
*/
|
|
59
|
-
export declare function discoverCanvasExtensions(roots: CanvasSearchRoot[]): DiscoveredCanvasExtension[];
|
|
67
|
+
export declare function discoverCanvasExtensions(roots: CanvasSearchRoot[], extra?: readonly DiscoveredCanvasExtension[]): DiscoveredCanvasExtension[];
|
|
60
68
|
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,gCAAgC;AAChC,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAExD,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;CACnB;AAED,gCAAgC;AAChC,MAAM,WAAW,yBAAyB;IACzC,uDAAuD;IACvD,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAMlF;AAqBD
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,gCAAgC;AAChC,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD,+DAA+D;AAC/D,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAExD,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,WAAW,CAAC;CACnB;AAED,gCAAgC;AAChC,MAAM,WAAW,yBAAyB;IACzC,uDAAuD;IACvD,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAMlF;AAqBD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACvC,KAAK,EAAE,gBAAgB,EAAE,EACzB,KAAK,GAAE,SAAS,yBAAyB,EAAO,GAC9C,yBAAyB,EAAE,CAgB7B","sourcesContent":["/**\n * Canvas extension discovery.\n *\n * Design: `docs/canvas-extensions-design.md` §4.1, §4.3. GitHub's\n * `docs/extensions.md` is specific: the CLI \"scans `.github/extensions/`\n * (project) and the user's copilot config extensions directory for\n * subdirectories containing `extension.mjs`\", the entry file is required and must\n * be named `extension.mjs`, and only ES modules are supported. All 23 extensions\n * in `github/awesome-copilot` comply.\n *\n * So discovery keys off `<dir>/extension.mjs` and nothing else. In particular it\n * does not read `package.json`: 3 of those 23 extensions ship without one, none\n * ships `node_modules`, and where a `package.json` does exist its `main` is\n * always `extension.mjs` anyway. Keying off it would add a failure mode without\n * adding information.\n *\n * The extension id is the directory name, matching how the catalog and the\n * Copilot app refer to extensions (`pr-artifact-explorer`).\n *\n * Some extensions also carry a `copilot-extension.json`, but it is neither\n * required nor informative: 8 of the 23 catalog extensions have one, every\n * instance holds exactly `{ name, version }`, and `name` always equals the\n * directory name. Reading it would add a parse and a disagreement case without\n * telling us anything the directory name does not.\n */\n\nimport { readdirSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\n\n/** Required entry file name. */\nexport const CANVAS_ENTRY_FILE = \"extension.mjs\";\n\n/** Where a canvas extension came from, in precedence order. */\nexport type CanvasScope = \"agents\" | \"project\" | \"user\";\n\n/** Directory searched for canvas extensions. */\nexport interface CanvasSearchRoot {\n\tdir: string;\n\tscope: CanvasScope;\n}\n\n/** One discovered extension. */\nexport interface DiscoveredCanvasExtension {\n\t/** Directory name, used as the provider identifier. */\n\tid: string;\n\t/** Absolute path to the extension directory. */\n\tdir: string;\n\t/** Absolute path to `extension.mjs`. */\n\tentry: string;\n\tscope: CanvasScope;\n}\n\n/**\n * Search roots in precedence order. `.agents/` first per\n * `docs/plugin-format-mapping.md` §0; the Copilot conventions follow as\n * compatibility inputs.\n *\n * `.github/extensions/` travels with a clone, so anything found there is subject\n * to the workspace-trust gate before it is ever forked (design doc §5). Discovery\n * itself is read-only and always safe to run.\n */\nexport function canvasSearchRoots(cwd: string, homeDir: string): CanvasSearchRoot[] {\n\treturn [\n\t\t{ dir: path.join(cwd, \".agents\", \"extensions\"), scope: \"agents\" },\n\t\t{ dir: path.join(cwd, \".github\", \"extensions\"), scope: \"project\" },\n\t\t{ dir: path.join(homeDir, \".copilot\", \"extensions\"), scope: \"user\" },\n\t];\n}\n\nfunction subdirectories(dir: string): string[] {\n\ttry {\n\t\treturn readdirSync(dir, { withFileTypes: true })\n\t\t\t.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())\n\t\t\t.map((entry) => entry.name)\n\t\t\t.sort();\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nfunction hasEntryFile(dir: string): boolean {\n\ttry {\n\t\treturn statSync(path.join(dir, CANVAS_ENTRY_FILE)).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Discover canvas extensions across `roots`, then append `extra`. First wins on\n * an id collision, so a project-local extension shadows a same-named user-scope\n * one — and anything a person placed in a search root by hand shadows a\n * same-named canvas that arrived inside a package.\n *\n * `extra` is how plugin-shipped canvases join the same list (see\n * `plugin-canvases.ts`): a plugin's canvas directories are named by its manifest\n * rather than by their position under a search root, so they cannot be expressed\n * as a root — but once resolved they are ordinary discovered extensions, and\n * every consumer downstream is better off not knowing the difference.\n */\nexport function discoverCanvasExtensions(\n\troots: CanvasSearchRoot[],\n\textra: readonly DiscoveredCanvasExtension[] = [],\n): DiscoveredCanvasExtension[] {\n\tconst found = new Map<string, DiscoveredCanvasExtension>();\n\tfor (const root of roots) {\n\t\tfor (const id of subdirectories(root.dir)) {\n\t\t\tif (found.has(id)) continue;\n\t\t\tconst dir = path.join(root.dir, id);\n\t\t\tif (!hasEntryFile(dir)) continue;\n\t\t\tfound.set(id, { id, dir, entry: path.join(dir, CANVAS_ENTRY_FILE), scope: root.scope });\n\t\t}\n\t}\n\tfor (const extension of extra) {\n\t\tif (found.has(extension.id)) continue;\n\t\tif (!hasEntryFile(extension.dir)) continue;\n\t\tfound.set(extension.id, extension);\n\t}\n\treturn [...found.values()];\n}\n"]}
|
|
@@ -63,10 +63,18 @@ function hasEntryFile(dir) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
* Discover canvas extensions across `roots`. First
|
|
67
|
-
* so a project-local extension shadows a same-named user-scope
|
|
66
|
+
* Discover canvas extensions across `roots`, then append `extra`. First wins on
|
|
67
|
+
* an id collision, so a project-local extension shadows a same-named user-scope
|
|
68
|
+
* one — and anything a person placed in a search root by hand shadows a
|
|
69
|
+
* same-named canvas that arrived inside a package.
|
|
70
|
+
*
|
|
71
|
+
* `extra` is how plugin-shipped canvases join the same list (see
|
|
72
|
+
* `plugin-canvases.ts`): a plugin's canvas directories are named by its manifest
|
|
73
|
+
* rather than by their position under a search root, so they cannot be expressed
|
|
74
|
+
* as a root — but once resolved they are ordinary discovered extensions, and
|
|
75
|
+
* every consumer downstream is better off not knowing the difference.
|
|
68
76
|
*/
|
|
69
|
-
export function discoverCanvasExtensions(roots) {
|
|
77
|
+
export function discoverCanvasExtensions(roots, extra = []) {
|
|
70
78
|
const found = new Map();
|
|
71
79
|
for (const root of roots) {
|
|
72
80
|
for (const id of subdirectories(root.dir)) {
|
|
@@ -78,6 +86,13 @@ export function discoverCanvasExtensions(roots) {
|
|
|
78
86
|
found.set(id, { id, dir, entry: path.join(dir, CANVAS_ENTRY_FILE), scope: root.scope });
|
|
79
87
|
}
|
|
80
88
|
}
|
|
89
|
+
for (const extension of extra) {
|
|
90
|
+
if (found.has(extension.id))
|
|
91
|
+
continue;
|
|
92
|
+
if (!hasEntryFile(extension.dir))
|
|
93
|
+
continue;
|
|
94
|
+
found.set(extension.id, extension);
|
|
95
|
+
}
|
|
81
96
|
return [...found.values()];
|
|
82
97
|
}
|
|
83
98
|
//# sourceMappingURL=discovery.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/core/canvas/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,gCAAgC;AAChC,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAsBjD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,OAAe,EAAsB;IACnF,OAAO;QACN,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE;QACjE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE;QAClE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;KACpE,CAAC;AAAA,CACF;AAED,SAAS,cAAc,CAAC,GAAW,EAAY;IAC9C,IAAI,CAAC;QACJ,OAAO,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aAC9C,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;aAChE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;aAC1B,IAAI,EAAE,CAAC;IACV,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AAAA,CACD;AAED,SAAS,YAAY,CAAC,GAAW,EAAW;IAC3C,IAAI,CAAC;QACJ,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AAAA,CACD;AAED
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/core/canvas/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,gCAAgC;AAChC,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAsBjD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,OAAe,EAAsB;IACnF,OAAO;QACN,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE;QACjE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE;QAClE,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;KACpE,CAAC;AAAA,CACF;AAED,SAAS,cAAc,CAAC,GAAW,EAAY;IAC9C,IAAI,CAAC;QACJ,OAAO,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aAC9C,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;aAChE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;aAC1B,IAAI,EAAE,CAAC;IACV,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AAAA,CACD;AAED,SAAS,YAAY,CAAC,GAAW,EAAW;IAC3C,IAAI,CAAC;QACJ,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;AAAA,CACD;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,wBAAwB,CACvC,KAAyB,EACzB,KAAK,GAAyC,EAAE,EAClB;IAC9B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAqC,CAAC;IAC3D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,KAAK,MAAM,EAAE,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3C,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,SAAS;YAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YACpC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;gBAAE,SAAS;YACjC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QACzF,CAAC;IACF,CAAC;IACD,KAAK,MAAM,SAAS,IAAI,KAAK,EAAE,CAAC;QAC/B,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAAE,SAAS;QACtC,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,SAAS;QAC3C,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,CAC3B","sourcesContent":["/**\n * Canvas extension discovery.\n *\n * Design: `docs/canvas-extensions-design.md` §4.1, §4.3. GitHub's\n * `docs/extensions.md` is specific: the CLI \"scans `.github/extensions/`\n * (project) and the user's copilot config extensions directory for\n * subdirectories containing `extension.mjs`\", the entry file is required and must\n * be named `extension.mjs`, and only ES modules are supported. All 23 extensions\n * in `github/awesome-copilot` comply.\n *\n * So discovery keys off `<dir>/extension.mjs` and nothing else. In particular it\n * does not read `package.json`: 3 of those 23 extensions ship without one, none\n * ships `node_modules`, and where a `package.json` does exist its `main` is\n * always `extension.mjs` anyway. Keying off it would add a failure mode without\n * adding information.\n *\n * The extension id is the directory name, matching how the catalog and the\n * Copilot app refer to extensions (`pr-artifact-explorer`).\n *\n * Some extensions also carry a `copilot-extension.json`, but it is neither\n * required nor informative: 8 of the 23 catalog extensions have one, every\n * instance holds exactly `{ name, version }`, and `name` always equals the\n * directory name. Reading it would add a parse and a disagreement case without\n * telling us anything the directory name does not.\n */\n\nimport { readdirSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\n\n/** Required entry file name. */\nexport const CANVAS_ENTRY_FILE = \"extension.mjs\";\n\n/** Where a canvas extension came from, in precedence order. */\nexport type CanvasScope = \"agents\" | \"project\" | \"user\";\n\n/** Directory searched for canvas extensions. */\nexport interface CanvasSearchRoot {\n\tdir: string;\n\tscope: CanvasScope;\n}\n\n/** One discovered extension. */\nexport interface DiscoveredCanvasExtension {\n\t/** Directory name, used as the provider identifier. */\n\tid: string;\n\t/** Absolute path to the extension directory. */\n\tdir: string;\n\t/** Absolute path to `extension.mjs`. */\n\tentry: string;\n\tscope: CanvasScope;\n}\n\n/**\n * Search roots in precedence order. `.agents/` first per\n * `docs/plugin-format-mapping.md` §0; the Copilot conventions follow as\n * compatibility inputs.\n *\n * `.github/extensions/` travels with a clone, so anything found there is subject\n * to the workspace-trust gate before it is ever forked (design doc §5). Discovery\n * itself is read-only and always safe to run.\n */\nexport function canvasSearchRoots(cwd: string, homeDir: string): CanvasSearchRoot[] {\n\treturn [\n\t\t{ dir: path.join(cwd, \".agents\", \"extensions\"), scope: \"agents\" },\n\t\t{ dir: path.join(cwd, \".github\", \"extensions\"), scope: \"project\" },\n\t\t{ dir: path.join(homeDir, \".copilot\", \"extensions\"), scope: \"user\" },\n\t];\n}\n\nfunction subdirectories(dir: string): string[] {\n\ttry {\n\t\treturn readdirSync(dir, { withFileTypes: true })\n\t\t\t.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())\n\t\t\t.map((entry) => entry.name)\n\t\t\t.sort();\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nfunction hasEntryFile(dir: string): boolean {\n\ttry {\n\t\treturn statSync(path.join(dir, CANVAS_ENTRY_FILE)).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Discover canvas extensions across `roots`, then append `extra`. First wins on\n * an id collision, so a project-local extension shadows a same-named user-scope\n * one — and anything a person placed in a search root by hand shadows a\n * same-named canvas that arrived inside a package.\n *\n * `extra` is how plugin-shipped canvases join the same list (see\n * `plugin-canvases.ts`): a plugin's canvas directories are named by its manifest\n * rather than by their position under a search root, so they cannot be expressed\n * as a root — but once resolved they are ordinary discovered extensions, and\n * every consumer downstream is better off not knowing the difference.\n */\nexport function discoverCanvasExtensions(\n\troots: CanvasSearchRoot[],\n\textra: readonly DiscoveredCanvasExtension[] = [],\n): DiscoveredCanvasExtension[] {\n\tconst found = new Map<string, DiscoveredCanvasExtension>();\n\tfor (const root of roots) {\n\t\tfor (const id of subdirectories(root.dir)) {\n\t\t\tif (found.has(id)) continue;\n\t\t\tconst dir = path.join(root.dir, id);\n\t\t\tif (!hasEntryFile(dir)) continue;\n\t\t\tfound.set(id, { id, dir, entry: path.join(dir, CANVAS_ENTRY_FILE), scope: root.scope });\n\t\t}\n\t}\n\tfor (const extension of extra) {\n\t\tif (found.has(extension.id)) continue;\n\t\tif (!hasEntryFile(extension.dir)) continue;\n\t\tfound.set(extension.id, extension);\n\t}\n\treturn [...found.values()];\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas extensions that arrive inside an installed plugin.
|
|
3
|
+
*
|
|
4
|
+
* Design: `docs/canvas-extensions-design.md` §4.3. Discovery's three search roots
|
|
5
|
+
* (`.agents/extensions`, `.github/extensions`, `~/.copilot/extensions`) describe
|
|
6
|
+
* where a person *puts* a canvas by hand. They say nothing about where a package
|
|
7
|
+
* manager puts one, and a plugin install lands in `~/.agents/plugins/<name>/`
|
|
8
|
+
* instead — so a canvas plugin installed exactly as instructed was copied to disk
|
|
9
|
+
* intact and then never listed, because nothing looked there.
|
|
10
|
+
*
|
|
11
|
+
* The plugin readers already resolve each plugin's canvas directories
|
|
12
|
+
* (`NormalizedPlugin.canvasExtensions`), so this module is only the join: read
|
|
13
|
+
* the plugins on the discovery path, and present their canvases in the shape
|
|
14
|
+
* `discovery.ts` produces for everything else. Keeping the shape identical is
|
|
15
|
+
* what lets the trust gate, the listing and `open` stay unaware that a plugin was
|
|
16
|
+
* involved at all.
|
|
17
|
+
*/
|
|
18
|
+
import { type DiscoveredCanvasExtension } from "./discovery.js";
|
|
19
|
+
/**
|
|
20
|
+
* Canvases shipped by every plugin on the discovery path.
|
|
21
|
+
*
|
|
22
|
+
* Scope is derived from where the plugin lives, not from the canvas directory:
|
|
23
|
+
* a plugin installed at project scope (or committed into the working tree) is
|
|
24
|
+
* repository-supplied whichever subdirectory its canvas sits in, and that is the
|
|
25
|
+
* question the trust gate asks. `pluginScopeOf`'s third answer, `repo`, folds
|
|
26
|
+
* into `project` here — for a canvas the two mean the same thing, since neither
|
|
27
|
+
* can distinguish "I put this here" from "this arrived in the clone".
|
|
28
|
+
*/
|
|
29
|
+
export declare function pluginCanvasExtensions(cwd: string, agentDir?: string): DiscoveredCanvasExtension[];
|
|
30
|
+
//# sourceMappingURL=plugin-canvases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-canvases.d.ts","sourceRoot":"","sources":["../../../src/core/canvas/plugin-canvases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,OAAO,EAAqB,KAAK,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAEnF;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,yBAAyB,EAAE,CAejH","sourcesContent":["/**\n * Canvas extensions that arrive inside an installed plugin.\n *\n * Design: `docs/canvas-extensions-design.md` §4.3. Discovery's three search roots\n * (`.agents/extensions`, `.github/extensions`, `~/.copilot/extensions`) describe\n * where a person *puts* a canvas by hand. They say nothing about where a package\n * manager puts one, and a plugin install lands in `~/.agents/plugins/<name>/`\n * instead — so a canvas plugin installed exactly as instructed was copied to disk\n * intact and then never listed, because nothing looked there.\n *\n * The plugin readers already resolve each plugin's canvas directories\n * (`NormalizedPlugin.canvasExtensions`), so this module is only the join: read\n * the plugins on the discovery path, and present their canvases in the shape\n * `discovery.ts` produces for everything else. Keeping the shape identical is\n * what lets the trust gate, the listing and `open` stay unaware that a plugin was\n * involved at all.\n */\n\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { defaultPluginDirs } from \"../extensions/loader.js\";\nimport { discoverPlugins } from \"../extensions/plugins/index.js\";\nimport { pluginScopeOf } from \"../extensions/plugins/locations.js\";\nimport { CANVAS_ENTRY_FILE, type DiscoveredCanvasExtension } from \"./discovery.js\";\n\n/**\n * Canvases shipped by every plugin on the discovery path.\n *\n * Scope is derived from where the plugin lives, not from the canvas directory:\n * a plugin installed at project scope (or committed into the working tree) is\n * repository-supplied whichever subdirectory its canvas sits in, and that is the\n * question the trust gate asks. `pluginScopeOf`'s third answer, `repo`, folds\n * into `project` here — for a canvas the two mean the same thing, since neither\n * can distinguish \"I put this here\" from \"this arrived in the clone\".\n */\nexport function pluginCanvasExtensions(cwd: string, agentDir: string = getAgentDir()): DiscoveredCanvasExtension[] {\n\tconst found: DiscoveredCanvasExtension[] = [];\n\tfor (const plugin of discoverPlugins(defaultPluginDirs(cwd, agentDir))) {\n\t\tif (!plugin.canvasExtensions?.length) continue;\n\t\tconst scope = pluginScopeOf(plugin.root, cwd) === \"user\" ? \"user\" : \"project\";\n\t\tfor (const extension of plugin.canvasExtensions) {\n\t\t\tfound.push({\n\t\t\t\tid: extension.id,\n\t\t\t\tdir: extension.dir,\n\t\t\t\tentry: path.join(extension.dir, CANVAS_ENTRY_FILE),\n\t\t\t\tscope,\n\t\t\t});\n\t\t}\n\t}\n\treturn found;\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas extensions that arrive inside an installed plugin.
|
|
3
|
+
*
|
|
4
|
+
* Design: `docs/canvas-extensions-design.md` §4.3. Discovery's three search roots
|
|
5
|
+
* (`.agents/extensions`, `.github/extensions`, `~/.copilot/extensions`) describe
|
|
6
|
+
* where a person *puts* a canvas by hand. They say nothing about where a package
|
|
7
|
+
* manager puts one, and a plugin install lands in `~/.agents/plugins/<name>/`
|
|
8
|
+
* instead — so a canvas plugin installed exactly as instructed was copied to disk
|
|
9
|
+
* intact and then never listed, because nothing looked there.
|
|
10
|
+
*
|
|
11
|
+
* The plugin readers already resolve each plugin's canvas directories
|
|
12
|
+
* (`NormalizedPlugin.canvasExtensions`), so this module is only the join: read
|
|
13
|
+
* the plugins on the discovery path, and present their canvases in the shape
|
|
14
|
+
* `discovery.ts` produces for everything else. Keeping the shape identical is
|
|
15
|
+
* what lets the trust gate, the listing and `open` stay unaware that a plugin was
|
|
16
|
+
* involved at all.
|
|
17
|
+
*/
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { getAgentDir } from "../../config.js";
|
|
20
|
+
import { defaultPluginDirs } from "../extensions/loader.js";
|
|
21
|
+
import { discoverPlugins } from "../extensions/plugins/index.js";
|
|
22
|
+
import { pluginScopeOf } from "../extensions/plugins/locations.js";
|
|
23
|
+
import { CANVAS_ENTRY_FILE } from "./discovery.js";
|
|
24
|
+
/**
|
|
25
|
+
* Canvases shipped by every plugin on the discovery path.
|
|
26
|
+
*
|
|
27
|
+
* Scope is derived from where the plugin lives, not from the canvas directory:
|
|
28
|
+
* a plugin installed at project scope (or committed into the working tree) is
|
|
29
|
+
* repository-supplied whichever subdirectory its canvas sits in, and that is the
|
|
30
|
+
* question the trust gate asks. `pluginScopeOf`'s third answer, `repo`, folds
|
|
31
|
+
* into `project` here — for a canvas the two mean the same thing, since neither
|
|
32
|
+
* can distinguish "I put this here" from "this arrived in the clone".
|
|
33
|
+
*/
|
|
34
|
+
export function pluginCanvasExtensions(cwd, agentDir = getAgentDir()) {
|
|
35
|
+
const found = [];
|
|
36
|
+
for (const plugin of discoverPlugins(defaultPluginDirs(cwd, agentDir))) {
|
|
37
|
+
if (!plugin.canvasExtensions?.length)
|
|
38
|
+
continue;
|
|
39
|
+
const scope = pluginScopeOf(plugin.root, cwd) === "user" ? "user" : "project";
|
|
40
|
+
for (const extension of plugin.canvasExtensions) {
|
|
41
|
+
found.push({
|
|
42
|
+
id: extension.id,
|
|
43
|
+
dir: extension.dir,
|
|
44
|
+
entry: path.join(extension.dir, CANVAS_ENTRY_FILE),
|
|
45
|
+
scope,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=plugin-canvases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-canvases.js","sourceRoot":"","sources":["../../../src/core/canvas/plugin-canvases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAkC,MAAM,gBAAgB,CAAC;AAEnF;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAW,EAAE,QAAQ,GAAW,WAAW,EAAE,EAA+B;IAClH,MAAM,KAAK,GAAgC,EAAE,CAAC;IAC9C,KAAK,MAAM,MAAM,IAAI,eAAe,CAAC,iBAAiB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;QACxE,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM;YAAE,SAAS;QAC/C,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9E,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;YACjD,KAAK,CAAC,IAAI,CAAC;gBACV,EAAE,EAAE,SAAS,CAAC,EAAE;gBAChB,GAAG,EAAE,SAAS,CAAC,GAAG;gBAClB,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,iBAAiB,CAAC;gBAClD,KAAK;aACL,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb","sourcesContent":["/**\n * Canvas extensions that arrive inside an installed plugin.\n *\n * Design: `docs/canvas-extensions-design.md` §4.3. Discovery's three search roots\n * (`.agents/extensions`, `.github/extensions`, `~/.copilot/extensions`) describe\n * where a person *puts* a canvas by hand. They say nothing about where a package\n * manager puts one, and a plugin install lands in `~/.agents/plugins/<name>/`\n * instead — so a canvas plugin installed exactly as instructed was copied to disk\n * intact and then never listed, because nothing looked there.\n *\n * The plugin readers already resolve each plugin's canvas directories\n * (`NormalizedPlugin.canvasExtensions`), so this module is only the join: read\n * the plugins on the discovery path, and present their canvases in the shape\n * `discovery.ts` produces for everything else. Keeping the shape identical is\n * what lets the trust gate, the listing and `open` stay unaware that a plugin was\n * involved at all.\n */\n\nimport * as path from \"node:path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { defaultPluginDirs } from \"../extensions/loader.js\";\nimport { discoverPlugins } from \"../extensions/plugins/index.js\";\nimport { pluginScopeOf } from \"../extensions/plugins/locations.js\";\nimport { CANVAS_ENTRY_FILE, type DiscoveredCanvasExtension } from \"./discovery.js\";\n\n/**\n * Canvases shipped by every plugin on the discovery path.\n *\n * Scope is derived from where the plugin lives, not from the canvas directory:\n * a plugin installed at project scope (or committed into the working tree) is\n * repository-supplied whichever subdirectory its canvas sits in, and that is the\n * question the trust gate asks. `pluginScopeOf`'s third answer, `repo`, folds\n * into `project` here — for a canvas the two mean the same thing, since neither\n * can distinguish \"I put this here\" from \"this arrived in the clone\".\n */\nexport function pluginCanvasExtensions(cwd: string, agentDir: string = getAgentDir()): DiscoveredCanvasExtension[] {\n\tconst found: DiscoveredCanvasExtension[] = [];\n\tfor (const plugin of discoverPlugins(defaultPluginDirs(cwd, agentDir))) {\n\t\tif (!plugin.canvasExtensions?.length) continue;\n\t\tconst scope = pluginScopeOf(plugin.root, cwd) === \"user\" ? \"user\" : \"project\";\n\t\tfor (const extension of plugin.canvasExtensions) {\n\t\t\tfound.push({\n\t\t\t\tid: extension.id,\n\t\t\t\tdir: extension.dir,\n\t\t\t\tentry: path.join(extension.dir, CANVAS_ENTRY_FILE),\n\t\t\t\tscope,\n\t\t\t});\n\t\t}\n\t}\n\treturn found;\n}\n"]}
|
|
@@ -45,6 +45,12 @@ export interface CanvasSessionOptions extends CanvasRegistryEvents {
|
|
|
45
45
|
agentDir?: string;
|
|
46
46
|
/** Override the search roots; defaults to {@link canvasSearchRoots}. */
|
|
47
47
|
roots?: CanvasSearchRoot[];
|
|
48
|
+
/**
|
|
49
|
+
* Override how plugin-shipped canvases are found; defaults to
|
|
50
|
+
* {@link pluginCanvasExtensions}. Pass `() => []` to look at the search roots
|
|
51
|
+
* and nothing else.
|
|
52
|
+
*/
|
|
53
|
+
pluginExtensions?: () => DiscoveredCanvasExtension[];
|
|
48
54
|
/** Override availability resolution, for tests and for hosts that already know. */
|
|
49
55
|
resolveRuntime?: () => Promise<CanvasAvailability>;
|
|
50
56
|
}
|
|
@@ -63,6 +69,7 @@ export declare function parseCanvasRef(input: string): CanvasRef | undefined;
|
|
|
63
69
|
export declare class CanvasSession {
|
|
64
70
|
private readonly options;
|
|
65
71
|
private readonly roots;
|
|
72
|
+
private readonly pluginExtensions;
|
|
66
73
|
/** Cached because resolving can spawn `node --version` and cannot change mid-session. */
|
|
67
74
|
private availabilityPromise;
|
|
68
75
|
/** Created on first successful open, not at construction: listing must not fork. */
|
|
@@ -1 +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"]}
|
|
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 { pluginCanvasExtensions } from \"./plugin-canvases.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/**\n\t * Override how plugin-shipped canvases are found; defaults to\n\t * {@link pluginCanvasExtensions}. Pass `() => []` to look at the search roots\n\t * and nothing else.\n\t */\n\tpluginExtensions?: () => DiscoveredCanvasExtension[];\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\tprivate readonly pluginExtensions: () => DiscoveredCanvasExtension[];\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\t// Re-read on every discover rather than cached: /plugin install can add a\n\t\t// canvas mid-session, and a listing that cannot see it is the bug this\n\t\t// exists to fix.\n\t\tthis.pluginExtensions =\n\t\t\toptions.pluginExtensions ?? (() => pluginCanvasExtensions(options.cwd, options.agentDir ?? getAgentDir()));\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, this.pluginExtensions()),\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"]}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { getAgentDir } from "../../config.js";
|
|
17
17
|
import { canvasSearchRoots, discoverCanvasExtensions } from "./discovery.js";
|
|
18
18
|
import { resolveCanvasRuntime } from "./launch.js";
|
|
19
|
+
import { pluginCanvasExtensions } from "./plugin-canvases.js";
|
|
19
20
|
import { CanvasRegistry } from "./registry.js";
|
|
20
21
|
import { gateCanvasExtensions } from "./trust.js";
|
|
21
22
|
/**
|
|
@@ -40,6 +41,7 @@ export function parseCanvasRef(input) {
|
|
|
40
41
|
export class CanvasSession {
|
|
41
42
|
options;
|
|
42
43
|
roots;
|
|
44
|
+
pluginExtensions;
|
|
43
45
|
/** Cached because resolving can spawn `node --version` and cannot change mid-session. */
|
|
44
46
|
availabilityPromise;
|
|
45
47
|
/** Created on first successful open, not at construction: listing must not fork. */
|
|
@@ -47,10 +49,15 @@ export class CanvasSession {
|
|
|
47
49
|
constructor(options) {
|
|
48
50
|
this.options = options;
|
|
49
51
|
this.roots = options.roots ?? canvasSearchRoots(options.cwd, options.homeDir);
|
|
52
|
+
// Re-read on every discover rather than cached: /plugin install can add a
|
|
53
|
+
// canvas mid-session, and a listing that cannot see it is the bug this
|
|
54
|
+
// exists to fix.
|
|
55
|
+
this.pluginExtensions =
|
|
56
|
+
options.pluginExtensions ?? (() => pluginCanvasExtensions(options.cwd, options.agentDir ?? getAgentDir()));
|
|
50
57
|
}
|
|
51
58
|
/** Discovered extensions, partitioned by the trust gate. Read-only and always safe. */
|
|
52
59
|
discover() {
|
|
53
|
-
const gated = gateCanvasExtensions(discoverCanvasExtensions(this.roots), this.options.cwd, this.options.agentDir ?? getAgentDir());
|
|
60
|
+
const gated = gateCanvasExtensions(discoverCanvasExtensions(this.roots, this.pluginExtensions()), this.options.cwd, this.options.agentDir ?? getAgentDir());
|
|
54
61
|
return { runnable: gated.runnable, withheld: gated.withheld.map((entry) => entry.extension) };
|
|
55
62
|
}
|
|
56
63
|
/** Whether canvases can run here. Resolved once per session and cached. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../../src/core/canvas/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAyB,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AACpG,OAAO,EAA2B,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAuB,cAAc,EAA6B,MAAM,eAAe,CAAC;AAE/F,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAyClD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa,EAAyB;IACpE,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAClD,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;IACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACjD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACxE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AAAA,CACjC;AAED,MAAM,OAAO,aAAa;IACR,OAAO,CAAuB;IAC9B,KAAK,CAAqB;IAC3C,yFAAyF;IACjF,mBAAmB,CAA0C;IACrE,oFAAoF;IAC5E,QAAQ,CAA6B;IAE7C,YAAY,OAA6B,EAAE;QAC1C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,iBAAiB,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IAAA,CAC9E;IAED,uFAAuF;IACvF,QAAQ,GAAqF;QAC5F,MAAM,KAAK,GAAG,oBAAoB,CACjC,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,EACpC,IAAI,CAAC,OAAO,CAAC,GAAG,EAChB,IAAI,CAAC,OAAO,CAAC,QAAQ,IAAI,WAAW,EAAE,CACtC,CAAC;QACF,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;IAAA,CAC9F;IAED,2EAA2E;IAC3E,KAAK,CAAC,YAAY,GAAgC;QACjD,IAAI,CAAC,mBAAmB,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,oBAAoB,CAAC,EAAE,CAAC;QACrF,OAAO,IAAI,CAAC,mBAAmB,CAAC;IAAA,CAChC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,IAAI,GAA4B;QACrC,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAC/C,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,mBAAmB,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;QAE/D,MAAM,QAAQ,GAAoB,EAAE,CAAC;QACrC,KAAK,MAAM,SAAS,IAAI,QAAQ,EAAE,CAAC;YAClC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,WAAW,KAAK,SAAS,CAAC,EAAE,CAAC,CAAC;YACnF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC5B,QAAQ,CAAC,IAAI,CAAC;oBACb,WAAW,EAAE,SAAS,CAAC,EAAE;oBACzB,QAAQ,EAAE,SAAS;oBACnB,WAAW,EAAE,SAAS;oBACtB,KAAK,EAAE,SAAS,CAAC,KAAK;oBACtB,QAAQ,EAAE,SAAS;oBACnB,IAAI,EAAE,EAAE;iBACR,CAAC,CAAC;gBACH,SAAS;YACV,CAAC;YACD,KAAK,MAAM,QAAQ,IAAI,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;gBAChF,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;gBACjF,QAAQ,CAAC,IAAI,CAAC;oBACb,WAAW,EAAE,SAAS,CAAC,EAAE;oBACzB,QAAQ;oBACR,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK;oBAChC,KAAK,EAAE,SAAS,CAAC,KAAK;oBACtB,QAAQ,EAAE,SAAS;oBACnB,IAAI,EAAE,SAAS;iBACf,CAAC,CAAC;YACJ,CAAC;QACF,CAAC;QACD,KAAK,MAAM,SAAS,IAAI,QAAQ,EAAE,CAAC;YAClC,QAAQ,CAAC,IAAI,CAAC;gBACb,WAAW,EAAE,SAAS,CAAC,EAAE;gBACzB,QAAQ,EAAE,SAAS;gBACnB,WAAW,EAAE,SAAS;gBACtB,KAAK,EAAE,SAAS,CAAC,KAAK;gBACtB,QAAQ,EAAE,qBAAqB;gBAC/B,IAAI,EAAE,EAAE;aACR,CAAC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IAAA,CAClE;IAED;;;;;OAKG;IACH,KAAK,CAAC,IAAI,CAAC,GAAc,EAAE,OAA2B,EAA2B;QAChF,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAC/C,IAAI,CAAC,YAAY,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QAElE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC/C,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,GAAG,CAAC,WAAW,CAAC,CAAC;QACjF,IAAI,CAAC,SAAS,EAAE,CAAC;YAChB,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;gBACpE,MAAM,IAAI,KAAK,CACd,qBAAqB,GAAG,CAAC,WAAW,iEAAiE;oBACpG,iEAAiE,CAClE,CAAC;YACH,CAAC;YACD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC;YAC7E,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,CAAC,WAAW,aAAa,KAAK,IAAI,CAAC,CAAC;QAChF,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;QACnD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC;QAChF,OAAO,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAAA,CAC9D;IAED,sFAAsF;IACtF,KAAK,CAAC,KAAK,CAAC,UAAkB,EAAuC;QACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC5C,MAAM,QAAQ,GAAG,QAAQ,EAAE,aAAa,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,KAAK,UAAU,CAAC,CAAC;QAC1F,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ;YAAE,OAAO,SAAS,CAAC;QAC7C,MAAM,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC/B,OAAO,QAAQ,CAAC;IAAA,CAChB;IAED,2BAA2B;IAC3B,SAAS,GAAqB;QAC7B,OAAO,IAAI,CAAC,mBAAmB,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;IAAA,CACzD;IAED;;;;;OAKG;IACH,mBAAmB,GAA+B;QACjD,OAAO,IAAI,CAAC,QAAQ,CAAC;IAAA,CACrB;IAED,kEAAkE;IAClE,KAAK,CAAC,QAAQ,GAAsB;QACnC,OAAO,CAAC,MAAM,IAAI,CAAC,mBAAmB,EAAE,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC;IAAA,CAC5D;IAED,iEAAiE;IACjE,KAAK,CAAC,OAAO,GAAkB;QAC9B,MAAM,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,CAAC;QAChC,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;IAAA,CAC1B;IAEO,cAAc,CAAC,YAA8D,EAAkB;QACtG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpB,IAAI,CAAC,QAAQ,GAAG,IAAI,cAAc,CAAC;gBAClC,OAAO,EAAE,YAAY,CAAC,OAAO;gBAC7B,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG;gBACrB,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;gBAC/B,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK;gBACzB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO;gBAC7B,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;gBAC/B,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY;aACvC,CAAC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC;IAAA,CACrB;IAED;;;;;;OAMG;IACK,KAAK,CAAC,YAAY,CAAC,QAAwB,EAAE,SAAoC,EAAmB;QAC3G,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QAC5D,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,YAAY,CAAC,CAAC,CAAC,EAAE,EAAY,CAAC;QACpE,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,SAAS,CAAC,EAAE,yBAAyB,CAAC,CAAC;QAC3G,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,GAAG,SAAS,CAAC,EAAE,IAAI,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9F,MAAM,IAAI,KAAK,CAAC,qBAAqB,SAAS,CAAC,EAAE,6CAA6C,GAAG,GAAG,CAAC,CAAC;IAAA,CACtG;CACD","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"]}
|
|
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 { pluginCanvasExtensions } from \"./plugin-canvases.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/**\n\t * Override how plugin-shipped canvases are found; defaults to\n\t * {@link pluginCanvasExtensions}. Pass `() => []` to look at the search roots\n\t * and nothing else.\n\t */\n\tpluginExtensions?: () => DiscoveredCanvasExtension[];\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\tprivate readonly pluginExtensions: () => DiscoveredCanvasExtension[];\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\t// Re-read on every discover rather than cached: /plugin install can add a\n\t\t// canvas mid-session, and a listing that cannot see it is the bug this\n\t\t// exists to fix.\n\t\tthis.pluginExtensions =\n\t\t\toptions.pluginExtensions ?? (() => pluginCanvasExtensions(options.cwd, options.agentDir ?? getAgentDir()));\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, this.pluginExtensions()),\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"]}
|
|
@@ -35,11 +35,10 @@ export declare class CanvasTrustError extends Error {
|
|
|
35
35
|
* Whether an extension sits in the working tree, and therefore arrived with the
|
|
36
36
|
* repository as far as anyone but its author can tell.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* question being asked; it only decides *whether* to ask about trust.
|
|
38
|
+
* Every project-scope home counts — see {@link canvasProjectScopeRoots}. None of
|
|
39
|
+
* them can distinguish "I put this here" from "this arrived in the clone", which
|
|
40
|
+
* is exactly why location is not the question being asked; it only decides
|
|
41
|
+
* *whether* to ask about trust.
|
|
43
42
|
*
|
|
44
43
|
* User scope (`~/.copilot/extensions/`) is not project-supplied: it is outside any
|
|
45
44
|
* repository and got there by a deliberate local act.
|
|
@@ -1 +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
|
|
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;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,yBAAyB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAElG;AAuBD;;;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 { CONFIG_DIR_NAME, 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 * Every project-scope home counts — see {@link canvasProjectScopeRoots}. None of\n * them can distinguish \"I put this here\" from \"this arrived in the clone\", which\n * is exactly why location is not the question being asked; it only decides\n * *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/**\n * A canvas extension's project-scope homes — the locations that travel with a\n * clone.\n *\n * The plugin homes belong here for the same reason `loader.ts` counts them for\n * plugins: a plugin installed at project scope, or committed into the working\n * tree, arrives in every collaborator's clone, and a canvas it ships is a\n * process with a listening socket exactly like one dropped in `.github/`. The\n * person who ran the install knows they chose it; nobody who clones the result\n * does, and location cannot tell those two apart.\n */\nfunction canvasProjectScopeRoots(cwd: string): string[] {\n\treturn [\n\t\tpath.join(cwd, \".agents\", \"extensions\"),\n\t\tpath.join(cwd, \".github\", \"extensions\"),\n\t\tpath.join(cwd, \".agents\", \"plugins\"),\n\t\tpath.join(cwd, CONFIG_DIR_NAME, \"plugins\"),\n\t\tpath.join(cwd, \".claude\", \"skills\"),\n\t];\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"]}
|