@jini-ai/agent-plugins 0.6.15-rc.2 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +21 -50
  3. package/dist/admin-surfaces/action-tools.js +1 -1
  4. package/dist/admin-surfaces/external-links.js +2 -1
  5. package/dist/admin-surfaces/host-validator.js +2 -2
  6. package/dist/admin-surfaces/settings-value.js +3 -1
  7. package/dist/admin-surfaces/surface-client.js +13 -5
  8. package/dist/core/plugin-name.d.ts +11 -0
  9. package/dist/core/plugin-name.js +19 -0
  10. package/dist/folder-zip-node.d.ts +21 -0
  11. package/dist/folder-zip-node.js +85 -3
  12. package/dist/lifecycle/activation.js +2 -2
  13. package/dist/lifecycle/automatic-use.js +12 -7
  14. package/dist/lifecycle/bundled-digests.js +3 -8
  15. package/dist/lifecycle/bundled-source-archive.d.ts +8 -2
  16. package/dist/lifecycle/bundled-source-archive.js +17 -8
  17. package/dist/lifecycle/contribution-loader.js +4 -5
  18. package/dist/lifecycle/index.d.ts +1 -2
  19. package/dist/lifecycle/install.js +59 -53
  20. package/dist/lifecycle/installed-package-index.js +4 -2
  21. package/dist/lifecycle/manifest.d.ts +1 -9
  22. package/dist/lifecycle/manifest.js +6 -21
  23. package/dist/lifecycle/node.js +4 -2
  24. package/dist/lifecycle/resolve-agent-plugin-refs.js +1 -3
  25. package/dist/lifecycle/search-projection.js +7 -6
  26. package/dist/lifecycle/seed-bundled.js +1 -3
  27. package/dist/lifecycle/skill-file-reader.js +15 -5
  28. package/dist/lifecycle/tools/capabilities.js +5 -3
  29. package/dist/lifecycle/tools/projection.js +3 -4
  30. package/dist/lifecycle/update.js +4 -2
  31. package/dist/lifecycle/yauzl-archive-reader.js +21 -23
  32. package/package.json +11 -8
  33. package/dist/admin-surfaces/action-dispatcher.d.ts.map +0 -1
  34. package/dist/admin-surfaces/action-dispatcher.js.map +0 -1
  35. package/dist/admin-surfaces/action-schema.d.ts.map +0 -1
  36. package/dist/admin-surfaces/action-schema.js.map +0 -1
  37. package/dist/admin-surfaces/action-tools.d.ts.map +0 -1
  38. package/dist/admin-surfaces/action-tools.js.map +0 -1
  39. package/dist/admin-surfaces/callback-field-inbox.d.ts.map +0 -1
  40. package/dist/admin-surfaces/callback-field-inbox.js.map +0 -1
  41. package/dist/admin-surfaces/capability-declarations.d.ts.map +0 -1
  42. package/dist/admin-surfaces/capability-declarations.js.map +0 -1
  43. package/dist/admin-surfaces/contribution.d.ts.map +0 -1
  44. package/dist/admin-surfaces/contribution.js.map +0 -1
  45. package/dist/admin-surfaces/delivery-bindings.d.ts.map +0 -1
  46. package/dist/admin-surfaces/delivery-bindings.js.map +0 -1
  47. package/dist/admin-surfaces/descriptor.d.ts.map +0 -1
  48. package/dist/admin-surfaces/descriptor.js.map +0 -1
  49. package/dist/admin-surfaces/external-links.d.ts.map +0 -1
  50. package/dist/admin-surfaces/external-links.js.map +0 -1
  51. package/dist/admin-surfaces/host-validator.d.ts.map +0 -1
  52. package/dist/admin-surfaces/host-validator.js.map +0 -1
  53. package/dist/admin-surfaces/index.d.ts.map +0 -1
  54. package/dist/admin-surfaces/index.js.map +0 -1
  55. package/dist/admin-surfaces/protocol.d.ts.map +0 -1
  56. package/dist/admin-surfaces/protocol.js.map +0 -1
  57. package/dist/admin-surfaces/sandbox.d.ts.map +0 -1
  58. package/dist/admin-surfaces/sandbox.js.map +0 -1
  59. package/dist/admin-surfaces/settings-value.d.ts.map +0 -1
  60. package/dist/admin-surfaces/settings-value.js.map +0 -1
  61. package/dist/admin-surfaces/surface-capabilities.d.ts.map +0 -1
  62. package/dist/admin-surfaces/surface-capabilities.js.map +0 -1
  63. package/dist/admin-surfaces/surface-client.d.ts.map +0 -1
  64. package/dist/admin-surfaces/surface-client.js.map +0 -1
  65. package/dist/admin-surfaces/surface-handlers.d.ts.map +0 -1
  66. package/dist/admin-surfaces/surface-handlers.js.map +0 -1
  67. package/dist/admin-surfaces/theme.d.ts.map +0 -1
  68. package/dist/admin-surfaces/theme.js.map +0 -1
  69. package/dist/contracts/admin-surface-action-schema.d.ts.map +0 -1
  70. package/dist/contracts/admin-surface-action-schema.js.map +0 -1
  71. package/dist/folder-zip-node.d.ts.map +0 -1
  72. package/dist/folder-zip-node.js.map +0 -1
  73. package/dist/folder-zip.d.ts.map +0 -1
  74. package/dist/folder-zip.js.map +0 -1
  75. package/dist/index.d.ts.map +0 -1
  76. package/dist/index.js.map +0 -1
  77. package/dist/lifecycle/activation.d.ts.map +0 -1
  78. package/dist/lifecycle/activation.js.map +0 -1
  79. package/dist/lifecycle/automatic-use.d.ts.map +0 -1
  80. package/dist/lifecycle/automatic-use.js.map +0 -1
  81. package/dist/lifecycle/bundled-digests.d.ts.map +0 -1
  82. package/dist/lifecycle/bundled-digests.js.map +0 -1
  83. package/dist/lifecycle/bundled-source-archive.d.ts.map +0 -1
  84. package/dist/lifecycle/bundled-source-archive.js.map +0 -1
  85. package/dist/lifecycle/capability-projection.d.ts.map +0 -1
  86. package/dist/lifecycle/capability-projection.js.map +0 -1
  87. package/dist/lifecycle/compatibility-metadata.d.ts.map +0 -1
  88. package/dist/lifecycle/compatibility-metadata.js.map +0 -1
  89. package/dist/lifecycle/compatibility.d.ts.map +0 -1
  90. package/dist/lifecycle/compatibility.js.map +0 -1
  91. package/dist/lifecycle/contribution-loader.d.ts.map +0 -1
  92. package/dist/lifecycle/contribution-loader.js.map +0 -1
  93. package/dist/lifecycle/exports.d.ts.map +0 -1
  94. package/dist/lifecycle/exports.js.map +0 -1
  95. package/dist/lifecycle/fetch-archive.d.ts.map +0 -1
  96. package/dist/lifecycle/fetch-archive.js.map +0 -1
  97. package/dist/lifecycle/index.d.ts.map +0 -1
  98. package/dist/lifecycle/index.js.map +0 -1
  99. package/dist/lifecycle/install-errors.d.ts.map +0 -1
  100. package/dist/lifecycle/install-errors.js.map +0 -1
  101. package/dist/lifecycle/install-from-url.d.ts.map +0 -1
  102. package/dist/lifecycle/install-from-url.js.map +0 -1
  103. package/dist/lifecycle/install.d.ts.map +0 -1
  104. package/dist/lifecycle/install.js.map +0 -1
  105. package/dist/lifecycle/installed-package-index.d.ts.map +0 -1
  106. package/dist/lifecycle/installed-package-index.js.map +0 -1
  107. package/dist/lifecycle/installed-package-roots.d.ts.map +0 -1
  108. package/dist/lifecycle/installed-package-roots.js.map +0 -1
  109. package/dist/lifecycle/layout.d.ts.map +0 -1
  110. package/dist/lifecycle/layout.js.map +0 -1
  111. package/dist/lifecycle/manifest.d.ts.map +0 -1
  112. package/dist/lifecycle/manifest.js.map +0 -1
  113. package/dist/lifecycle/mcp-provisioning.d.ts.map +0 -1
  114. package/dist/lifecycle/mcp-provisioning.js.map +0 -1
  115. package/dist/lifecycle/node.d.ts.map +0 -1
  116. package/dist/lifecycle/node.js.map +0 -1
  117. package/dist/lifecycle/package-paths.d.ts.map +0 -1
  118. package/dist/lifecycle/package-paths.js.map +0 -1
  119. package/dist/lifecycle/persistent-state.d.ts.map +0 -1
  120. package/dist/lifecycle/persistent-state.js.map +0 -1
  121. package/dist/lifecycle/ports.d.ts.map +0 -1
  122. package/dist/lifecycle/ports.js.map +0 -1
  123. package/dist/lifecycle/provider-contribution.d.ts.map +0 -1
  124. package/dist/lifecycle/provider-contribution.js.map +0 -1
  125. package/dist/lifecycle/resolve-agent-plugin-refs.d.ts.map +0 -1
  126. package/dist/lifecycle/resolve-agent-plugin-refs.js.map +0 -1
  127. package/dist/lifecycle/retire-bundled.d.ts.map +0 -1
  128. package/dist/lifecycle/retire-bundled.js.map +0 -1
  129. package/dist/lifecycle/search-projection.d.ts.map +0 -1
  130. package/dist/lifecycle/search-projection.js.map +0 -1
  131. package/dist/lifecycle/search.d.ts.map +0 -1
  132. package/dist/lifecycle/search.js.map +0 -1
  133. package/dist/lifecycle/seed-bundled.d.ts.map +0 -1
  134. package/dist/lifecycle/seed-bundled.js.map +0 -1
  135. package/dist/lifecycle/set-enabled.d.ts.map +0 -1
  136. package/dist/lifecycle/set-enabled.js.map +0 -1
  137. package/dist/lifecycle/skill-file-reader.d.ts.map +0 -1
  138. package/dist/lifecycle/skill-file-reader.js.map +0 -1
  139. package/dist/lifecycle/tools/capabilities.d.ts.map +0 -1
  140. package/dist/lifecycle/tools/capabilities.js.map +0 -1
  141. package/dist/lifecycle/tools/definition.d.ts.map +0 -1
  142. package/dist/lifecycle/tools/definition.js.map +0 -1
  143. package/dist/lifecycle/tools/index.d.ts.map +0 -1
  144. package/dist/lifecycle/tools/index.js.map +0 -1
  145. package/dist/lifecycle/tools/manifest.d.ts.map +0 -1
  146. package/dist/lifecycle/tools/manifest.js.map +0 -1
  147. package/dist/lifecycle/tools/projection.d.ts.map +0 -1
  148. package/dist/lifecycle/tools/projection.js.map +0 -1
  149. package/dist/lifecycle/trusted-plugin-files.d.ts.map +0 -1
  150. package/dist/lifecycle/trusted-plugin-files.js.map +0 -1
  151. package/dist/lifecycle/types.d.ts.map +0 -1
  152. package/dist/lifecycle/types.js.map +0 -1
  153. package/dist/lifecycle/uninstall.d.ts.map +0 -1
  154. package/dist/lifecycle/uninstall.js.map +0 -1
  155. package/dist/lifecycle/update-plan.d.ts.map +0 -1
  156. package/dist/lifecycle/update-plan.js.map +0 -1
  157. package/dist/lifecycle/update-store.d.ts.map +0 -1
  158. package/dist/lifecycle/update-store.js.map +0 -1
  159. package/dist/lifecycle/update.d.ts.map +0 -1
  160. package/dist/lifecycle/update.js.map +0 -1
  161. package/dist/lifecycle/yauzl-archive-reader.d.ts.map +0 -1
  162. package/dist/lifecycle/yauzl-archive-reader.js.map +0 -1
  163. package/dist/manifest.d.ts.map +0 -1
  164. package/dist/manifest.js.map +0 -1
  165. package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +0 -4
  166. package/ui-ux-design/mcp.json +0 -4
  167. package/ui-ux-design/plugin.json +0 -25
  168. package/ui-ux-design/skills/frontend-accessibility/SKILL.md +0 -60
  169. package/ui-ux-design/skills/gstack-design/SKILL.md +0 -53
  170. package/ui-ux-design/skills/gstack-design/references/consultation.md +0 -30
  171. package/ui-ux-design/skills/gstack-design/references/html.md +0 -29
  172. package/ui-ux-design/skills/gstack-design/references/review.md +0 -28
  173. package/ui-ux-design/skills/gstack-design/references/shotgun.md +0 -33
  174. package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +0 -23
  175. package/ui-ux-design/skills/interface-design/ORIGINAL.md +0 -16
  176. package/ui-ux-design/skills/interface-design/SKILL.md +0 -41
  177. package/ui-ux-design/skills/interface-design/references/commands/audit.md +0 -40
  178. package/ui-ux-design/skills/interface-design/references/commands/critique.md +0 -21
  179. package/ui-ux-design/skills/interface-design/references/commands/extract.md +0 -34
  180. package/ui-ux-design/skills/interface-design/references/commands/init.md +0 -31
  181. package/ui-ux-design/skills/interface-design/references/commands/status.md +0 -22
  182. package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +0 -29
  183. package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +0 -29
  184. package/ui-ux-design/skills/interface-design/references/system-template.md +0 -57
  185. package/ui-ux-design/skills/shadcn-ui/README.md +0 -244
  186. package/ui-ux-design/skills/shadcn-ui/SKILL.md +0 -326
  187. package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +0 -177
  188. package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +0 -313
  189. package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +0 -177
  190. package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +0 -481
  191. package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +0 -516
  192. package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +0 -463
  193. package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +0 -412
  194. package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +0 -134
  195. package/ui-ux-design/skills/ui-ux-design/SKILL.md +0 -77
  196. package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +0 -72
  197. package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +0 -107
  198. package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +0 -65
  199. package/ui-ux-design/skills/ui-ux-design/references/foundations.md +0 -144
  200. package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +0 -57
  201. package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +0 -102
  202. package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +0 -139
  203. package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +0 -63
  204. package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +0 -78
  205. package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +0 -93
  206. package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +0 -123
  207. package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +0 -57
  208. package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +0 -39
  209. package/ui-ux-design/skills/web-compliance/SKILL.md +0 -129
  210. package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +0 -59
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.15
4
+
5
+ ### Minor Changes
6
+
7
+ - Add opt-in plugin source suffix exclusions and test-file inclusion. Explicit allow-paths and their ancestors now override name exclusions, while excluded descendants and suffix exclusions still skip. Default packing stays unchanged when the new options and conflicting allow/name rules are absent.
8
+
9
+ Export ResponseLimitExceededError with limitBytes from platform/fetch-with-timeout and the platform root. Guarded response-text overflow throws this RangeError subclass; invalid caps keep plain RangeError and existing truncation behavior is preserved.
10
+
11
+ Accept jsdom 26 and 27 as optional CMS peers alongside 25; media uses DOM APIs shared across these versions.
12
+
13
+ - Breaking package-surface change for the fixed 0.6.15 release: remove the bundled
14
+ ui-ux-design files and wildcard export from @jini-ai/agent-plugins. Hosts must
15
+ explicitly install @jini-ai/plugin-ui-ux-design and resolve its ./plugin.json
16
+ export, stage that payload and seed through the existing Agent Plugin lifecycle.
17
+ The lifecycle implementation remains in @jini-ai/agent-plugins. Inner plugin
18
+ version 0.4.6, deterministic packing, digest selection and operator activation
19
+ decisions are preserved. Tovu wording reconciliation and cutover follow later.
20
+
3
21
  ## 0.6.13
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -29,7 +29,7 @@ decides its own install mechanism, permission model, and sandboxing.
29
29
 
30
30
  ## What a plugin looks like on disk
31
31
 
32
- A plugin is a directory. Example (this package's own bundled plugin, `ui-ux-design/`):
32
+ A plugin is a directory. Example (`@jini-ai/plugin-ui-ux-design` publishes this payload):
33
33
 
34
34
  ```
35
35
  ui-ux-design/
@@ -104,13 +104,13 @@ Catalog format) that this package does not implement.
104
104
  | `.` (root) | universal | `PluginManifest`/`McpManifest` types, `validatePluginManifest({ value })`/`validateMcpManifest({ value })` structural validators (plus compatible `isPluginManifest(value)`/`isMcpManifest(value)` wrappers), and the known path constants (`PLUGIN_MANIFEST_FILENAME`, `PLUGIN_SKILLS_DIRNAME`, `PLUGIN_MCP_MANIFEST_FILENAME`). No filesystem access, no DOM. |
105
105
  | `./manifest` | universal | Strict manifest/MCP parsing and typed namespace readers; object structural validators are also re-exported here. |
106
106
  | `./lifecycle` | node | Host-injected installation, activation, locks, digests, bundles, trusted files, reference resolution, ranking and MCP provisioning helpers. |
107
+ | `./lifecycle` packing | node | `createAgentPluginLifecycle(ports).packAgentPluginDirectory({ sourceDir }, { allowPaths, excludeNames, excludeSuffixes, includeTests })`; explicit allow-paths and their ancestors beat name exclusions; excluded descendants still skip. Suffix exclusions always win; test skips require `includeTests: true` to disable. |
107
108
  | `./lifecycle/node` | node | `createNodeAgentPluginEffects({})` opt-in native effects. |
108
109
  | `./lifecycle/yauzl` | node | `createYauzlAgentPluginArchiveReader({ yauzl })`; the host supplies the optional peer library. |
109
110
  | `./folder-zip` | universal | `zipFolderFiles({ files, maxBytes })`: packs picked folder `File`s into one stored-mode, binary-safe `.zip` `File` (the admin "Upload a folder" encoder). |
110
111
  | `./folder-zip/node` | node | `zipLocalFolder({ path, maxBytes })`: the same encoder over a local folder; refuses symlinks/special files, bounds entries and bytes. |
111
112
  | `./admin-surfaces` | universal | Plugin-owned admin surfaces: the contribution definition (host-named file), sandboxed-iframe/CSP/bootstrap helpers, the host-page message gate and the server-side action dispatcher. See "Admin surfaces" below. |
112
113
  | `./admin-surfaces/client` | universal | `createAdminSurfaceClient(...)`: the self-contained surface-side protocol client a plugin's UI imports or copies. |
113
- | `./ui-ux-design/*` | — | Raw files of the bundled `ui-ux-design` plugin — static JSON/Markdown, not run through the TS build. |
114
114
 
115
115
  The universal root handles structural validation. The separate Node lifecycle entry implements
116
116
  local archive installation and host-owned activation policy; it does not implement remote catalog discovery.
@@ -150,42 +150,19 @@ package supports. Do not confuse the two — "agent plugins" (this package, a
150
150
  public spec) and "Jini plugins" (`@jini-ai/plugins`'s `./host`, Jini's own unbuilt format) share
151
151
  the word "plugin" and nothing else.
152
152
 
153
- ## Bundled plugins
154
-
155
- Real, install-ready plugin directories shipped alongside the code — not illustrative examples.
156
- Each lives at the package root under **its own name** (`ui-ux-design/`).
157
- Plugins live at the package root rather than inside `src/` because `src/` is the TypeScript
158
- compile root (`rootDir: "src"`, `include: ["src"]`) — a plugin's skill trees can carry files
159
- (`.tsx` examples, etc.) that are illustrative content, not package code, and putting them under
160
- `src/` makes `tsc` try to compile them (measured, on `ui-ux-design`: 207 errors from the three
161
- `shadcn-ui/examples/*.tsx` files alone). Static content and compiler input are kept in separate
162
- trees on purpose.
163
-
164
- ### `ui-ux-design/`
165
-
166
- Bundles the AI-Dev-Shop Web Design agent's full skill set (per its `agents/web-design/skills.md`
167
- persona and the `framework/routing/skills-registry.md` ownership mapping) into one portable plugin:
168
-
169
- - `ui-ux-design` — design-foundations (tokens, typography, spacing, breakpoints, component state
170
- matrix) *and* first-impression polish, scanning hierarchy, conversion-focused visual signals —
171
- one skill, not two. AI-Dev-Shop merged its former separate `ux-design` and `premium-ui` skills
172
- into this single `ui-ux-design` skill; this sample tracks that merge instead of carrying the two
173
- predecessor skills as stale copies (see git history for the 2026-08-12 consolidation).
174
- - `interface-design` — repeatable, memory-consistent visual systems for dashboards/admin panels/apps
175
- - `gstack-design` — manual four-mode workflow (consultation, shotgun, html, review)
176
- - `frontend-accessibility` — WCAG 2.1 AA checklist
177
- - `vercel-web-design-guidelines` — Vercel Web Interface Guidelines auditor
178
- - `shadcn-ui` — shadcn/ui (Radix + Tailwind) component discovery/integration guidance
179
- - `web-compliance` — legal/compliance checkpoints for public-facing UX flows
180
-
181
- Each skill directory is a verbatim copy of the corresponding `AI-Dev-Shop/skills/<name>/` tree —
182
- copied rather than referenced, because the whole point of a plugin is that it is self-contained and
183
- portable to a host that has never heard of AI-Dev-Shop. `AI-Dev-Shop/agents/web-design/skills.md`
184
- itself (the persona that composes these skills into one role) is not bundled — it is ADS-specific
185
- routing glue, not a portable skill.
186
-
187
- Consumers may read bundled raw assets through the public wildcard subpath. Product-specific
188
- plugin assets are owned and shipped by their product, outside this library.
153
+ ## Independently packaged plugins
154
+
155
+ Starting with the breaking 0.6.16 release, this library ships manifest and lifecycle code only.
156
+ Install `@jini-ai/plugin-ui-ux-design` explicitly for the design payload. Resolve
157
+ `@jini-ai/plugin-ui-ux-design/plugin.json`; its containing directory holds the portable assets.
158
+ Stage that directory beneath the host's bundled-source root, then call the existing
159
+ `seedBundledAgentPlugins({ layout, workspaceId, sourceRoot })`. Read contributions through trusted
160
+ installed-package readers. Seeding preserves digest selection and operator activation decisions.
161
+
162
+ The editable design source remains at `ui-ux-design/` here, outside `src/`: its `.tsx` examples
163
+ are illustrative skill content and must not enter the TypeScript compile root. Assembly generates
164
+ only the standalone package's payload. The old `@jini-ai/agent-plugins/ui-ux-design/*` export is
165
+ removed; product-specific assets remain owned by their product.
189
166
 
190
167
  ## Scripts
191
168
 
@@ -198,18 +175,12 @@ pnpm --filter @jini-ai/agent-plugins test
198
175
  The build and typecheck include lifecycle test sources. Copied Node test options are
199
176
  translated to Vitest registration, preserving per-test timeouts and platform skip conditions.
200
177
 
201
- ## Adding another bundled plugin
202
-
203
- 1. Create `<plugin-name>/plugin.json` at the package root with at least
204
- `{ "$schema": AGENT_PLUGINS_SCHEMA_URL, "name": "<plugin-name>" }`. Do **not** put it under
205
- `src/` — see "Bundled plugins" above for why.
206
- 2. Add `<plugin-name>/skills/<skill-name>/SKILL.md` per skill (plus any `references/`,
207
- `examples/`, `scripts/` the skill needs).
208
- 3. Register it in `package.json`: an `exports` key `"./<plugin-name>/*": "./<plugin-name>/*"`, a
209
- matching `jini.entries` key (the R8 guard requires every export to have one), and a
210
- `files` entry.
211
- 4. Add coverage in `src/__tests__/manifest.test.ts` following the `ui-ux-design plugin` block — assert the manifest validates and the expected skill
212
- directories exist.
178
+ ## Adding another plugin
179
+
180
+ Create a separate `@jini-ai/plugin-<id>` workspace package with its own reviewed manifest,
181
+ README, LICENSE and NOTICE. Generate only that plugin's payload; do not add plugin assets or
182
+ plugin dependencies to this lifecycle library. Keep skill trees outside TypeScript compile roots,
183
+ validate the plugin manifest and test installation through this package's existing lifecycle.
213
184
 
214
185
  ## Lifecycle API and ports
215
186
 
@@ -40,7 +40,7 @@ export function buildAdminSurfaceActionTools(required, optional = {}) {
40
40
  await required.authorize({ context });
41
41
  const payload = checkedPayload({ input, action });
42
42
  const dispatcher = createAdminSurfaceActionDispatcher({ descriptor, handlers: required.handlersFor({ pluginId, surfaceId: descriptor.id }) });
43
- const dispatch = () => dispatcher.dispatch({ action: action.name, payload, context });
43
+ const dispatch = dispatcher.dispatch.bind(dispatcher, { action: action.name, payload, context });
44
44
  return optional.run ? optional.run({ dispatch }) : dispatch();
45
45
  },
46
46
  },
@@ -20,7 +20,8 @@ function isOpenLinkEnvelope(data) {
20
20
  return false;
21
21
  const message = data;
22
22
  const isProtocol = message.protocol === ADMIN_SURFACE_PROTOCOL && message.version === ADMIN_SURFACE_PROTOCOL_VERSION && message.kind === 'open-link';
23
- return isProtocol && typeof message.mountId === 'string' && typeof message.requestId === 'string' && REQUEST_ID_PATTERN.test(message.requestId) && typeof message.url === 'string';
23
+ const hasRequestId = typeof message.requestId === 'string' && REQUEST_ID_PATTERN.test(message.requestId);
24
+ return isProtocol && typeof message.mountId === 'string' && hasRequestId && typeof message.url === 'string';
24
25
  }
25
26
  /** The normalized URL when `raw` may be opened, or why not. Messages never echo the URL. @complexity O(n) in the URL length. */
26
27
  function checkLink(raw, origins) {
@@ -18,8 +18,8 @@ function isRequestEnvelope(data) {
18
18
  return isProtocol && hasRequestFields(message);
19
19
  }
20
20
  function hasRequestFields(message) {
21
- return typeof message.mountId === 'string' && typeof message.requestId === 'string' && REQUEST_ID_PATTERN.test(message.requestId)
22
- && typeof message.action === 'string' && Object.hasOwn(message, 'payload');
21
+ const hasRequestId = typeof message.requestId === 'string' && REQUEST_ID_PATTERN.test(message.requestId);
22
+ return typeof message.mountId === 'string' && hasRequestId && typeof message.action === 'string' && Object.hasOwn(message, 'payload');
23
23
  }
24
24
  /** Character length of the payload as JSON, or Infinity when it is not JSON-serializable. */
25
25
  /** Whether `event` comes from `mount`'s frame at all (origin and window), for every inbound message kind.
@@ -10,7 +10,9 @@ import { PLUGIN_STATE_MAX_KEY_LENGTH } from '../lifecycle/tools/capabilities.js'
10
10
  export const ADMIN_SURFACE_STATE_LIMITS = Object.freeze({ maxKeyLength: PLUGIN_STATE_MAX_KEY_LENGTH, maxValueBytes: 16 * 1024, maxDepth: 16 });
11
11
  export const STATE_CREDENTIAL_MESSAGE = 'The plugin tried to save a connection credential in its settings; nothing was saved.';
12
12
  function isStateScalar(value) {
13
- return value === null || typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value));
13
+ if (typeof value === 'number')
14
+ return Number.isFinite(value);
15
+ return value === null || typeof value === 'string' || typeof value === 'boolean';
14
16
  }
15
17
  /** True for plain JSON data only: no undefined, functions, NaN, class instances or deep nesting. @complexity O(n) nodes. */
16
18
  function isStateValue(value, depth) {
@@ -43,6 +43,12 @@ function isFailure(value) {
43
43
  const error = value;
44
44
  return typeof error.code === 'string' && typeof error.message === 'string' && typeof error.retryable === 'boolean';
45
45
  }
46
+ /** A non-empty, bounded token value with nothing that could escape a CSS custom property. */
47
+ function isSafeThemeValue(token) {
48
+ if (typeof token !== 'string')
49
+ return false;
50
+ return token.length > 0 && token.length <= 200 && !UNSAFE_THEME_VALUE.test(token);
51
+ }
46
52
  /** The host's theme with every unknown name or unsafe value dropped; undefined when it is not a theme. */
47
53
  function acceptedTheme(value) {
48
54
  if (typeof value !== 'object' || value === null)
@@ -50,8 +56,7 @@ function acceptedTheme(value) {
50
56
  const { scheme, tokens } = value;
51
57
  if ((scheme !== 'light' && scheme !== 'dark') || typeof tokens !== 'object' || tokens === null)
52
58
  return undefined;
53
- const safe = Object.entries(tokens).slice(0, MAX_THEME_TOKENS).filter(([name, token]) => THEME_TOKEN.test(name)
54
- && typeof token === 'string' && token.length > 0 && token.length <= 200 && !UNSAFE_THEME_VALUE.test(token));
59
+ const safe = Object.entries(tokens).slice(0, MAX_THEME_TOKENS).filter(([name, token]) => THEME_TOKEN.test(name) && isSafeThemeValue(token));
55
60
  return { scheme, tokens: Object.fromEntries(safe) };
56
61
  }
57
62
  /** The browser root when there is a document (none in a server-side test). */
@@ -158,13 +163,16 @@ export function createAdminSurfaceClient(required, optional = {}) {
158
163
  const unsubscribe = required.listen((event) => {
159
164
  if (event.source !== required.parent)
160
165
  return;
161
- if (isMessage(event.data, 'init'))
166
+ if (isMessage(event.data, 'init')) {
162
167
  onInit(event, event.data);
163
- else if (isMessage(event.data, 'theme')) {
168
+ return;
169
+ }
170
+ if (isMessage(event.data, 'theme')) {
164
171
  if (event.origin === init?.origin && event.data.mountId === init.mountId)
165
172
  applyTheme(event.data.theme);
173
+ return;
166
174
  }
167
- else if (isMessage(event.data, 'action-response'))
175
+ if (isMessage(event.data, 'action-response'))
168
176
  onResponse(event, event.data);
169
177
  });
170
178
  /** Posts one request of `kind` to the host origin and waits for its `action-response`. */
@@ -0,0 +1,11 @@
1
+ export declare const MAX_NAME_LENGTH = 64;
2
+ /**
3
+ * True when `value` is a valid Agent Plugin id: `plugin.json`'s `name` grammar (§5.5, see
4
+ * `NAME_PATTERN`), 1-64 characters. The one owner of that grammar: a host checks an id taken from a
5
+ * URL or a tool input here before it names anything on disk, instead of restating the pattern.
6
+ * @complexity O(n) in the value's length.
7
+ */
8
+ export declare function isAgentPluginName({ value }: {
9
+ readonly value: unknown;
10
+ }, _optional?: Record<string, never>): boolean;
11
+ //# sourceMappingURL=plugin-name.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * §5.5's grammar as one regex: alnum runs separated by single `-`/`.` delimiters. This shape alone
3
+ * guarantees start/end are alphanumeric (the regex can only start and end on `[a-z0-9]+`) AND that
4
+ * no two delimiters are ever adjacent (a delimiter is always followed by at least one alnum char
5
+ * before the next one can appear) — so "no leading/trailing hyphen" and "no consecutive delimiters"
6
+ * fall out of one pattern rather than needing separate checks that could disagree.
7
+ */
8
+ const NAME_PATTERN = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
9
+ export const MAX_NAME_LENGTH = 64;
10
+ /**
11
+ * True when `value` is a valid Agent Plugin id: `plugin.json`'s `name` grammar (§5.5, see
12
+ * `NAME_PATTERN`), 1-64 characters. The one owner of that grammar: a host checks an id taken from a
13
+ * URL or a tool input here before it names anything on disk, instead of restating the pattern.
14
+ * @complexity O(n) in the value's length.
15
+ */
16
+ export function isAgentPluginName({ value }, _optional = {}) {
17
+ return typeof value === "string" && value.length <= MAX_NAME_LENGTH && NAME_PATTERN.test(value);
18
+ }
19
+ //# sourceMappingURL=plugin-name.js.map
@@ -8,4 +8,25 @@ export declare function zipLocalFolder(required: {
8
8
  path: string;
9
9
  maxBytes: number;
10
10
  }, _optional?: Record<string, never>): Promise<Uint8Array>;
11
+ export interface CustomizeAgentPluginFolderInput {
12
+ readonly path: string;
13
+ readonly maxBytes: number;
14
+ readonly name: string;
15
+ readonly identityNamespace: string;
16
+ readonly edit: {
17
+ readonly path: string;
18
+ readonly oldString: string;
19
+ readonly newString: string;
20
+ };
21
+ }
22
+ /**
23
+ * Fork a captured Agent Plugin package with one exact text edit and a fresh identity.
24
+ * Uses the folder upload reader/encoder; never writes into an immutable installed package.
25
+ * @param required Source folder, byte ceiling, new local id, host namespace and literal edit.
26
+ * @returns ZIP bytes for the existing upload preview/installer. Does not grant bundled trust.
27
+ * @throws {FolderZipError} Unsafe/missing paths, links, non-unique text, identity or size refusal.
28
+ * @complexity O(e log e + b) time and O(e + b) space, bounded by upload entry/byte ceilings.
29
+ * @example zipCustomizedAgentPluginFolder({ path, maxBytes, name: 'my-plugin', identityNamespace: 'host', edit })
30
+ */
31
+ export declare function zipCustomizedAgentPluginFolder(required: CustomizeAgentPluginFolderInput, _optional?: Record<string, never>): Promise<Uint8Array>;
11
32
  //# sourceMappingURL=folder-zip-node.d.ts.map
@@ -2,6 +2,13 @@ import * as fs from "node:fs/promises";
2
2
  import { constants } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { FOLDER_MAX_FILES, FOLDER_UNREADABLE, FolderZipError, zipFolderFiles } from "./folder-zip.js";
5
+ import { isAgentPluginName } from './core/plugin-name.js';
6
+ /** Code-unit order, independent of locale. */
7
+ function compareCodeUnits(a, b) {
8
+ if (a === b)
9
+ return 0;
10
+ return a < b ? -1 : 1;
11
+ }
5
12
  async function readLocalFile(required, _optional = {}) {
6
13
  const { filesystem, root, filename, name, maxBytes } = required;
7
14
  if (await filesystem.realpath(filename) !== filename)
@@ -41,7 +48,8 @@ async function collectDirectory(required, _optional = {}) {
41
48
  const handle = await filesystem.opendir(directory);
42
49
  // Mutate the traversal's owned buffers so visiting directories does not copy accumulated files.
43
50
  for await (const entry of handle) {
44
- if (++collection.entries > FOLDER_MAX_FILES)
51
+ collection.entries += 1;
52
+ if (collection.entries > FOLDER_MAX_FILES)
45
53
  throw new FolderZipError("too-many", `More than ${FOLDER_MAX_FILES} entries.`);
46
54
  const filename = path.join(directory, entry.name);
47
55
  if (entry.isDirectory()) {
@@ -62,6 +70,10 @@ async function collectDirectory(required, _optional = {}) {
62
70
  * bounded to 4096 entries and maxBytes. No network I/O.
63
71
  */
64
72
  export async function zipLocalFolder(required, _optional = {}) {
73
+ return encodeLocalFiles({ files: await collectLocalFiles(required), maxBytes: required.maxBytes });
74
+ }
75
+ /** Shared bounded, no-link directory reader for uploads and customized copies. No writes. */
76
+ async function collectLocalFiles(required) {
65
77
  const collection = { files: [], pending: [], entries: 0, total: 0 };
66
78
  try {
67
79
  const root = await fs.realpath(required.path);
@@ -72,8 +84,8 @@ export async function zipLocalFolder(required, _optional = {}) {
72
84
  }
73
85
  const { files } = collection;
74
86
  // Stable order gives repeat local-folder installs identical ZIP digests.
75
- files.sort((a, b) => a.webkitRelativePath < b.webkitRelativePath ? -1 : a.webkitRelativePath > b.webkitRelativePath ? 1 : 0);
76
- return new Uint8Array(await (await zipFolderFiles({ files, maxBytes: required.maxBytes })).arrayBuffer());
87
+ files.sort((a, b) => compareCodeUnits(a.webkitRelativePath, b.webkitRelativePath));
88
+ return files;
77
89
  }
78
90
  catch (error) {
79
91
  if (error instanceof FolderZipError)
@@ -81,4 +93,74 @@ export async function zipLocalFolder(required, _optional = {}) {
81
93
  throw new FolderZipError("unreadable", FOLDER_UNREADABLE);
82
94
  }
83
95
  }
96
+ /** Encode through the upload owner, rechecking the byte budget after any transformation. */
97
+ async function encodeLocalFiles({ files, maxBytes }) {
98
+ return new Uint8Array(await (await zipFolderFiles({ files, maxBytes })).arrayBuffer());
99
+ }
100
+ /** Replace captured bytes while preserving the upload encoder's relative path. */
101
+ function replaceCapturedFile({ file, text }) {
102
+ const next = new File([text], file.name);
103
+ Object.defineProperty(next, 'webkitRelativePath', { value: file.webkitRelativePath });
104
+ return next;
105
+ }
106
+ /** Non-UTF-8 or NUL-bearing bytes are binary; refuse them as a named error, not a decoder TypeError. */
107
+ function decodeEditableText(bytes) {
108
+ let text;
109
+ try {
110
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
111
+ }
112
+ catch {
113
+ text = '\0';
114
+ }
115
+ if (text.includes('\0'))
116
+ throw new FolderZipError('unreadable', 'Binary files cannot be edited as text.');
117
+ return text;
118
+ }
119
+ /** Require exactly one literal match; `$&` and other replacement syntax remain plain text. */
120
+ function customizeText({ text, edit }) {
121
+ if (!edit.oldString)
122
+ throw new FolderZipError('unreadable', 'oldString must not be empty.');
123
+ const index = text.indexOf(edit.oldString);
124
+ if (index < 0 || text.indexOf(edit.oldString, index + edit.oldString.length) >= 0) {
125
+ throw new FolderZipError('unreadable', 'oldString must match exactly once. Read the file and send more context.');
126
+ }
127
+ return text.slice(0, index) + edit.newString + text.slice(index + edit.oldString.length);
128
+ }
129
+ /** A fork must not retain its publisher identity or historical aliases and supersession claims. */
130
+ function customizeManifest({ text, name, identityNamespace }) {
131
+ const manifest = JSON.parse(text);
132
+ if (!isAgentPluginName({ value: name }) || manifest.name === name) {
133
+ throw new FolderZipError('unreadable', 'Choose a valid new Agent Plugin id different from the source.');
134
+ }
135
+ manifest.name = name;
136
+ const extension = manifest.extensions?.[identityNamespace];
137
+ if (extension) {
138
+ extension.id = `local/${name}`;
139
+ delete extension.renamedFrom;
140
+ delete extension.supersedes;
141
+ }
142
+ return JSON.stringify(manifest, null, 2) + '\n';
143
+ }
144
+ /**
145
+ * Fork a captured Agent Plugin package with one exact text edit and a fresh identity.
146
+ * Uses the folder upload reader/encoder; never writes into an immutable installed package.
147
+ * @param required Source folder, byte ceiling, new local id, host namespace and literal edit.
148
+ * @returns ZIP bytes for the existing upload preview/installer. Does not grant bundled trust.
149
+ * @throws {FolderZipError} Unsafe/missing paths, links, non-unique text, identity or size refusal.
150
+ * @complexity O(e log e + b) time and O(e + b) space, bounded by upload entry/byte ceilings.
151
+ * @example zipCustomizedAgentPluginFolder({ path, maxBytes, name: 'my-plugin', identityNamespace: 'host', edit })
152
+ */
153
+ export async function zipCustomizedAgentPluginFolder(required, _optional = {}) {
154
+ const files = await collectLocalFiles(required);
155
+ // Match captured package paths, never join a model-supplied edit path onto the filesystem.
156
+ const target = files.findIndex(file => file.webkitRelativePath === `folder/${required.edit.path}`);
157
+ const manifest = files.findIndex(file => file.webkitRelativePath === 'folder/plugin.json');
158
+ if (target < 0 || manifest < 0 || target === manifest) {
159
+ throw new FolderZipError('unreadable', 'Choose an existing package-relative text file other than plugin.json.');
160
+ }
161
+ const text = decodeEditableText(new Uint8Array(await files[target].arrayBuffer()));
162
+ files[target] = replaceCapturedFile({ file: files[target], text: customizeText({ text, edit: required.edit }) });
163
+ files[manifest] = replaceCapturedFile({ file: files[manifest], text: customizeManifest({ text: await files[manifest].text(), name: required.name, identityNamespace: required.identityNamespace }) });
164
+ return encodeLocalFiles({ files, maxBytes: required.maxBytes });
165
+ }
84
166
  //# sourceMappingURL=folder-zip-node.js.map
@@ -87,10 +87,10 @@ export class AgentPluginActivationsBusyError extends Error {
87
87
  holder;
88
88
  lost;
89
89
  constructor({ lockPath, holder, lost }) {
90
+ const holderNote = holder !== undefined ? ` — held by pid ${holder.pid} on ${holder.hostname} since ${holder.acquiredAt}` : " — holder unknown";
90
91
  super((lost
91
92
  ? `agent-plugin activation: lost the write lock (${lockPath}) before committing — another process judged it stale`
92
- : `agent-plugin activation: could not take the write lock (${lockPath}) within 15s` +
93
- (holder !== undefined ? ` — held by pid ${holder.pid} on ${holder.hostname} since ${holder.acquiredAt}` : " — holder unknown")) +
93
+ : `agent-plugin activation: could not take the write lock (${lockPath}) within 15s` + holderNote) +
94
94
  ". Nothing was written, so no recorded enable/disable decision was changed. This clears by itself when the other " +
95
95
  "host process (server, agent daemon, or the agent-plugin:activation CLI) finishes; a lock left by a crashed " +
96
96
  "process is removed automatically once that process is gone or the lock is older than 10s. If it persists, stop " +
@@ -116,20 +116,25 @@ export function createAutomaticAgentPluginUse({ mode, state: initialState, ports
116
116
  */
117
117
  async function resolveGuidance({ matches }, { signal } = {}) {
118
118
  const guidance = [];
119
- for (const match of matches) {
120
- if (signal?.aborted)
121
- break;
119
+ // Unloaded plugins contribute nothing; oversized guidance becomes a pointer instead of the text.
120
+ const guidanceFor = async (match) => {
122
121
  const result = await resolveOneGuidance({ plugin: match }, { signal });
123
122
  if (result.status !== 'loaded')
124
- continue;
123
+ return undefined;
125
124
  if (new TextEncoder().encode(result.guidance).length > MAX_AUTOMATIC_GUIDANCE_BYTES) {
126
125
  const pointer = ports.formatPluginToolPointer?.({ pluginId: match.pluginId, skill: match.pluginId, file: 'SKILL.md' });
127
126
  const howToLoad = pointer === undefined ? '' : ` Load the full instructions with ${pointer}; use the same call with file '<relative path>' for supporting references.`;
128
- guidance.push(`Agent Plugin '${match.pluginId}' guidance is deferred because it exceeds the automatic byte limit.${howToLoad}`);
129
- continue;
127
+ return `Agent Plugin '${match.pluginId}' guidance is deferred because it exceeds the automatic byte limit.${howToLoad}`;
130
128
  }
131
129
  recordDelivery({ plugin: match, result });
132
- guidance.push(result.guidance);
130
+ return result.guidance;
131
+ };
132
+ for (const match of matches) {
133
+ if (signal?.aborted)
134
+ break;
135
+ const text = await guidanceFor(match);
136
+ if (text !== undefined)
137
+ guidance.push(text);
133
138
  }
134
139
  return guidance;
135
140
  }
@@ -115,14 +115,9 @@ function buildModule(ports) {
115
115
  return EMPTY_LEDGER;
116
116
  const digests = new Map();
117
117
  for (const [pluginId, entry] of Object.entries(value.plugins)) {
118
- if (!SAFE_PLUGIN_ID_PATTERN.test(pluginId))
119
- continue;
120
- if (!isPlainObject(entry))
121
- continue;
122
- const { archiveDigest } = entry;
123
- if (typeof archiveDigest !== "string" || !SHA256_DIGEST_PATTERN.test(archiveDigest))
124
- continue;
125
- digests.set(pluginId, archiveDigest);
118
+ const archiveDigest = SAFE_PLUGIN_ID_PATTERN.test(pluginId) && isPlainObject(entry) ? entry.archiveDigest : undefined;
119
+ if (typeof archiveDigest === "string" && SHA256_DIGEST_PATTERN.test(archiveDigest))
120
+ digests.set(pluginId, archiveDigest);
126
121
  }
127
122
  return digests;
128
123
  }
@@ -13,10 +13,16 @@ export interface PackedAgentPluginArchive {
13
13
  }
14
14
  /** Opt-in source selection, resource bounds and archive identity. Omission preserves bundled-v1. */
15
15
  export interface PackAgentPluginDirectoryOptional {
16
- /** Exact package-relative files or directory subtrees. Empty selects nothing; exclusions win. */
16
+ /** Exact package-relative files or directory subtrees. Empty selects nothing. An explicit path
17
+ * and its ancestors override excludeNames; excluded descendants of a selected subtree still skip.
18
+ * Suffix exclusions and default test skips win even over explicit paths. */
17
19
  readonly allowPaths?: readonly string[];
18
- /** Entry basenames excluded at every depth, before entering directories. */
20
+ /** Entry basenames excluded at every depth unless explicitly allowed (or needed to reach one). */
19
21
  readonly excludeNames?: readonly string[];
22
+ /** Case-sensitive path suffixes excluded at every depth, including directories and explicit paths. */
23
+ readonly excludeSuffixes?: readonly string[];
24
+ /** Include __tests__ directories and .test./.spec. files. Defaults to false; other filters still apply. */
25
+ readonly includeTests?: boolean;
20
26
  /** Nonnegative safe integers; root depth is zero. Supplied bounds fail rather than truncate. */
21
27
  readonly limits?: {
22
28
  readonly maxEntries: number;
@@ -262,10 +262,11 @@ function buildModule(ports) {
262
262
  }
263
263
  /**
264
264
  * Lists runtime regular files under `dir`, as POSIX-style package-relative paths.
265
- * Test directories and test/spec files are excluded so test-only edits cannot change installed
265
+ * By default, test directories and test/spec files are excluded so test-only edits cannot change installed
266
266
  * runtime contents or their content-addressed digest.
267
267
  *
268
- * @complexity O(e * a) selection checks for e enumerated entries and a allowlist paths (one when omitted).
268
+ * @complexity O(e * (a + s)) selection checks for e entries, a allowlist paths and s suffixes.
269
+ * Path comparison cost scales with path length; selection uses O(1) auxiliary space per entry.
269
270
  */
270
271
  async function listRegularFiles({ dir, prefix, depth, state }) {
271
272
  const entries = await readdir(dir, { withFileTypes: true });
@@ -274,9 +275,8 @@ function buildModule(ports) {
274
275
  const files = [];
275
276
  for (const entry of entries) {
276
277
  const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
277
- if (state.excluded.has(entry.name))
278
- continue;
279
- if (!selectPath({ relative, directory: entry.isDirectory(), state }))
278
+ const directory = entry.isDirectory();
279
+ if (isNameExcluded({ name: entry.name, relative, directory, state }) || !selectPath({ relative, directory, state }))
280
280
  continue;
281
281
  if (entry.isDirectory()) {
282
282
  files.push(...await directoryEntryFiles({ name: entry.name, dir, relative, depth, state }));
@@ -289,13 +289,13 @@ function buildModule(ports) {
289
289
  return files;
290
290
  }
291
291
  async function directoryEntryFiles({ name, dir, relative, depth, state }) {
292
- if (name === '__tests__')
292
+ if (name === '__tests__' && !state.options.includeTests)
293
293
  return [];
294
294
  assertPackingLimit({ state, name: 'maxDepth', value: depth + 1 });
295
295
  return listRegularFiles({ dir: path.join(dir, name), prefix: relative, depth: depth + 1, state });
296
296
  }
297
297
  function regularEntryFiles({ entry, relative, state }) {
298
- if (entry.isFile() && !/\.(test|spec)\./.test(entry.name)) {
298
+ if (entry.isFile() && (state.options.includeTests || !/\.(test|spec)\./.test(entry.name))) {
299
299
  recordFile({ relative, state });
300
300
  return [relative];
301
301
  }
@@ -307,8 +307,17 @@ function buildModule(ports) {
307
307
  state.files.add(relative);
308
308
  assertPackingLimit({ state, name: 'maxFiles', value: state.files.size });
309
309
  }
310
- /** Ancestors of an allowed path must be traversed, but their unrelated siblings are pruned. */
310
+ /** An exact allow-path reopens excluded ancestors without reopening unrelated siblings.
311
+ * A selected subtree does not implicitly override name exclusions farther down that subtree.
312
+ * @complexity O(a * p) time for a allow-paths of length p, O(1) space. No I/O or mutation. */
313
+ function isNameExcluded({ name, relative, directory, state }) {
314
+ return state.excluded.has(name) && !state.allowPaths?.some(allowed => relative === allowed || (directory && allowed.startsWith(`${relative}/`)));
315
+ }
316
+ /** Ancestors of an allowed path must be traversed, but their unrelated siblings are pruned.
317
+ * Suffix exclusions always win. O((a + s) * p) time, O(1) space; no I/O or mutation. */
311
318
  function selectPath({ relative, directory, state }) {
319
+ if (state.options.excludeSuffixes?.some(suffix => relative.endsWith(suffix)))
320
+ return false;
312
321
  return state.allowPaths === undefined || state.allowPaths.some(allowed => relative === allowed || relative.startsWith(`${allowed}/`) || (directory && allowed.startsWith(`${relative}/`)));
313
322
  }
314
323
  function refuseEntry({ state, entryPath, symlink }) {
@@ -49,12 +49,11 @@ export function createPluginContributionLoader(required, _optional = {}) {
49
49
  refusals.push(verdict.refusal);
50
50
  continue;
51
51
  }
52
+ // A host package policy refusal stands in for the plugin's contributions.
52
53
  const refusal = packageRefusal?.({ plugin: verdict.trusted });
53
- if (refusal !== undefined) {
54
- refusals.push(refusal);
55
- continue;
56
- }
57
- const loaded = await loadPluginContributionsFromSource({ plugin: verdict.trusted, definition });
54
+ const loaded = refusal === undefined
55
+ ? await loadPluginContributionsFromSource({ plugin: verdict.trusted, definition })
56
+ : { items: [], refusals: [refusal] };
58
57
  items.push(...loaded.items);
59
58
  refusals.push(...loaded.refusals);
60
59
  }
@@ -7,7 +7,6 @@ import { maxAgentPluginArchiveBytes } from './fetch-archive.js';
7
7
  import type { FetchAgentPluginArchiveOptional } from './fetch-archive.js';
8
8
  import type { PackAgentPluginDirectoryOptional } from './bundled-source-archive.js';
9
9
  import type { AgentPluginLifecycleRequired, AgentPluginLifecycleOptional } from './ports.js';
10
- import type { AgentPluginDeliveryMode } from './resolve-agent-plugin-refs.js';
11
10
  import type { InstalledDigestIdentity } from './bundled-digests.js';
12
11
  export type { SeededAgentPluginOutcome, SeedBundledAgentPluginsRequired, SeedBundledAgentPluginsOptional, SeedBundledAgentPluginsResult, PackingForSource } from './seed-bundled.js';
13
12
  export type { RetireBundledAgentPluginsRequired, RetireBundledAgentPluginsOptional, RetiredAgentPluginOutcome, RetiredAgentPluginSuccessorOutcome } from './retire-bundled.js';
@@ -167,7 +166,7 @@ export declare function createAgentPluginLifecycle(required: AgentPluginLifecycl
167
166
  pluginRefIds: readonly string[];
168
167
  workspaceLayout: Pick<import("./layout.js").AgentPluginWorkspaceLayout, "packages" | "root">;
169
168
  }, { deliveryMode, ...request }?: import("./resolve-agent-plugin-refs.js").AgentPluginSkillRequest & {
170
- deliveryMode?: AgentPluginDeliveryMode;
169
+ deliveryMode?: import("./types.js").AgentPluginDeliveryMode;
171
170
  }) => Promise<import("./resolve-agent-plugin-refs.js").ResolveAgentPluginRefsResult>;
172
171
  pluginMemory: (input: {
173
172
  readonly workspaceId: string;