@vellumai/assistant 0.11.4 → 0.11.5-staging.2

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 (367) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -6,21 +6,8 @@ export const skillsHelp: CliCommandHelp = {
6
6
  name: "skills",
7
7
  description: "Browse and install skills from the Vellum catalog",
8
8
  helpText: `
9
- Manage skills from the Vellum catalog. Skills extend the assistant's
10
- capabilities with pre-built workflows and tools.
11
-
12
- Examples:
13
- $ assistant skills list
14
- $ assistant skills list --json
15
- $ assistant skills inspect slack
16
- $ assistant skills inspect resend-setup --json
17
- $ assistant skills search react
18
- $ assistant skills search react --limit 5 --json
19
- $ assistant skills install weather
20
- $ assistant skills install weather --overwrite
21
- $ assistant skills uninstall weather
22
- $ assistant skills add vercel-labs/skills@find-skills
23
- $ assistant skills add vercel-labs/skills/find-skills --overwrite`,
9
+ Manage skills from the Vellum catalog. Skills are instructions of
10
+ workflows and tools that teach the assistant when dynamically relevant.`,
24
11
  subcommands: [
25
12
  {
26
13
  name: "list",
@@ -70,15 +57,25 @@ Examples:
70
57
  { flags: "--json", description: "Machine-readable JSON output" },
71
58
  ],
72
59
  helpText: `
60
+ When to use:
61
+ Second stop after plugin search when the user asks to set up, install, or
62
+ add a capability (for example "Setup <app> for me", "add a skill for file management").
63
+ Search here before searching the web.
64
+
73
65
  Arguments:
74
66
  query Free-text search term matched against skill names, descriptions,
75
- and tags. Searches the Vellum catalog, the skills.sh community
67
+ and tags. Use the product or capability name, not the full user
68
+ sentence. Searches the Vellum catalog, the skills.sh community
76
69
  registry, and the clawhub registry.
77
70
 
78
71
  Displays results from all sources with clear labels. When a skill ID
79
72
  exists in both the Vellum catalog and a community registry, a conflict
80
73
  note is shown with guidance on which install command to use.
81
74
 
75
+ If a catalog match is found, install it with 'assistant skills install <id>'.
76
+ If a community match is found, add it with 'assistant skills add <source>'.
77
+ If nothing matches, try web search.
78
+
82
79
  Examples:
83
80
  $ assistant skills search react
84
81
  $ assistant skills search "file management" --limit 3
@@ -5,6 +5,7 @@
5
5
  * IPC route (`trust_rules_list`).
6
6
  */
7
7
 
8
+ import type { TrustRulesListIpcResponse } from "@vellumai/gateway-client";
8
9
  import type { Command } from "commander";
9
10
 
10
11
  import { cliIpcCall } from "../../ipc/cli-client.js";
@@ -13,18 +14,6 @@ import { registerCommand } from "../lib/register-command.js";
13
14
  import { log } from "../logger.js";
14
15
  import { trustHelp } from "./trust.help.js";
15
16
 
16
- // -- Types --------------------------------------------------------------------
17
-
18
- interface TrustRule {
19
- id: string;
20
- tool: string;
21
- pattern: string;
22
- risk: string;
23
- origin: string;
24
- userModified: boolean;
25
- updatedAt: string;
26
- }
27
-
28
17
  // -- Registration -------------------------------------------------------------
29
18
 
30
19
  export function registerTrustCommand(program: Command): void {
@@ -44,7 +33,7 @@ export function registerTrustCommand(program: Command): void {
44
33
  ...(opts.tool ? { tool: opts.tool } : {}),
45
34
  };
46
35
 
47
- const result = await cliIpcCall<{ rules: TrustRule[] }>(
36
+ const result = await cliIpcCall<TrustRulesListIpcResponse>(
48
37
  "trust_rules_list",
49
38
  { body: params },
50
39
  );
@@ -86,7 +75,7 @@ export function registerTrustCommand(program: Command): void {
86
75
  "MODIFIED",
87
76
  ];
88
77
 
89
- const rows: string[][] = rules.map((r: TrustRule) => [
78
+ const rows: string[][] = rules.map((r) => [
90
79
  r.id.slice(0, 16),
91
80
  r.tool,
92
81
  r.pattern,
@@ -303,6 +303,44 @@ describe("installPlugin — install lifecycle", () => {
303
303
  expect(existsSync(join(target, "README.md"))).toBe(true);
304
304
  });
305
305
 
306
+ test("--force keeps live config.json instead of a pin-shipped default", async () => {
307
+ // GIVEN an install with user config, and a clone that materializes a default
308
+ const target = join(pluginsDir, "caveman");
309
+ mkdirSync(target, { recursive: true });
310
+ writeFileSync(join(target, "package.json"), '{"name":"caveman"}');
311
+ writeFileSync(
312
+ join(target, "config.json"),
313
+ '{"provider":"photon","ingressMode":"live"}\n',
314
+ );
315
+ mkdirSync(join(target, "data"), { recursive: true });
316
+ writeFileSync(join(target, "data", "cursor.json"), '{"n":1}\n');
317
+
318
+ const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
319
+ const runGit = fakeGitRunner({
320
+ tree: {
321
+ "package.json": '{"name":"caveman"}',
322
+ "README.md": "# caveman",
323
+ "config.json": '{"provider":"comms","ingressMode":"webhook"}\n',
324
+ },
325
+ commit: CAVEMAN_SHA,
326
+ });
327
+
328
+ // WHEN we reinstall with --force
329
+ await installPlugin(
330
+ { name: "caveman", force: true, ref: "main" },
331
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
332
+ );
333
+
334
+ // THEN user-owned state survives and the pin's default config does not win
335
+ expect(readFileSync(join(target, "config.json"), "utf-8")).toBe(
336
+ '{"provider":"photon","ingressMode":"live"}\n',
337
+ );
338
+ expect(readFileSync(join(target, "data", "cursor.json"), "utf-8")).toBe(
339
+ '{"n":1}\n',
340
+ );
341
+ expect(existsSync(join(target, "README.md"))).toBe(true);
342
+ });
343
+
306
344
  test("commitOverride materializes a specific commit, keeping repo from the manifest", async () => {
307
345
  // GIVEN a manifest pinning the current SHA, but an override to an older one
308
346
  const OLD_SHA = "1".repeat(40);
@@ -440,4 +440,38 @@ describe("mergePluginTree", () => {
440
440
  expect(result.conflicts).toEqual(["f.txt"]);
441
441
  expect(result.binaryConflicts).toEqual([]);
442
442
  });
443
+
444
+ test("copies live config.json, data/, and .disabled from ours, ignoring the pin", async () => {
445
+ // GIVEN user-owned state on the live install and different defaults at the pin
446
+ writeTree(baseDir, { "package.json": '{"name":"p"}\n' });
447
+ writeTree(oursDir, {
448
+ "package.json": '{"name":"p"}\n',
449
+ "config.json": '{"provider":"photon","ingressMode":"live"}\n',
450
+ "data/cursor.json": '{"n":1}\n',
451
+ ".disabled": "",
452
+ });
453
+ writeTree(theirsDir, {
454
+ "package.json": '{"name":"p"}\n',
455
+ "config.json": '{"provider":"comms","ingressMode":"webhook"}\n',
456
+ "data/cursor.json": '{"n":0}\n',
457
+ });
458
+
459
+ // WHEN merged with theirs (the strategy that would otherwise take the pin)
460
+ const result = await mergePluginTree({
461
+ baseDir,
462
+ oursDir,
463
+ theirsDir,
464
+ destDir,
465
+ strategy: "theirs",
466
+ });
467
+
468
+ // THEN user-owned state is the live copy, not the pin, and is not counted
469
+ // as a merged source file
470
+ expect(read("config.json")).toBe(
471
+ '{"provider":"photon","ingressMode":"live"}\n',
472
+ );
473
+ expect(read("data/cursor.json")).toBe('{"n":1}\n');
474
+ expect(read(".disabled")).toBe("");
475
+ expect(result.fileCount).toBe(1);
476
+ });
443
477
  });
@@ -17,7 +17,11 @@ import { tmpdir } from "node:os";
17
17
  import { join } from "node:path";
18
18
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
19
19
 
20
- import { detectPluginSurfaces } from "../plugin-surfaces.js";
20
+ import {
21
+ detectPluginSurfaces,
22
+ findPluginSetupSkill,
23
+ formatPluginSetupHint,
24
+ } from "../plugin-surfaces.js";
21
25
 
22
26
  let pluginDir: string;
23
27
 
@@ -188,3 +192,30 @@ describe("detectPluginSurfaces", () => {
188
192
  expect(detectPluginSurfaces(pluginDir).schedules).toEqual([]);
189
193
  });
190
194
  });
195
+
196
+ describe("findPluginSetupSkill", () => {
197
+ test("prefers the generic setup id when both conventions are present", () => {
198
+ expect(
199
+ findPluginSetupSkill("imessage", ["setup", "imessage-setup", "other"]),
200
+ ).toBe("setup");
201
+ });
202
+
203
+ test("falls back to <plugin-name>-setup", () => {
204
+ expect(findPluginSetupSkill("imessage", ["imessage-setup"])).toBe(
205
+ "imessage-setup",
206
+ );
207
+ });
208
+
209
+ test("returns undefined when the plugin ships no setup skill", () => {
210
+ expect(findPluginSetupSkill("imessage", ["imessage-send"])).toBeUndefined();
211
+ expect(findPluginSetupSkill("imessage", [])).toBeUndefined();
212
+ });
213
+ });
214
+
215
+ describe("formatPluginSetupHint", () => {
216
+ test("names the skill in the install-command copy", () => {
217
+ expect(formatPluginSetupHint("imessage-setup")).toBe(
218
+ "Load the imessage-setup skill to help set up this plugin",
219
+ );
220
+ });
221
+ });
@@ -5,6 +5,7 @@ import {
5
5
  rmSync,
6
6
  writeFileSync,
7
7
  } from "node:fs";
8
+ import { tmpdir } from "node:os";
8
9
  import { join } from "node:path";
9
10
  import { afterEach, beforeEach, describe, expect, it } from "bun:test";
10
11
 
@@ -18,7 +19,7 @@ import {
18
19
 
19
20
  // VELLUM_WORKSPACE_DIR is set to this; getWorkspacePluginsDir() returns
20
21
  // <workspace>/plugins, so the actual plugins directory is TMP_WS_DIR/plugins.
21
- const TMP_WS_DIR = join(import.meta.dir, "__tmp__toggle-plugin-test");
22
+ const TMP_WS_DIR = join(tmpdir(), `toggle-plugin-test-${process.pid}`);
22
23
  const TMP_PLUGINS_DIR = join(TMP_WS_DIR, "plugins");
23
24
 
24
25
  beforeEach(() => {
@@ -920,6 +920,70 @@ describe("upgradePlugin --strategy", () => {
920
920
  expect(installedFile("level-up", "local-only.txt")).toBeNull();
921
921
  });
922
922
 
923
+ test("keeps live config.json, data/, and .disabled under overwrite, theirs, and ours", async () => {
924
+ // GIVEN user-owned state on the live install, and a pin that ships defaults
925
+ const userConfig = '{"provider":"photon","ingressMode":"live"}\n';
926
+ const pinConfig = '{"provider":"comms","ingressMode":"webhook"}\n';
927
+ const liveOurs: Tree = {
928
+ ...OURS,
929
+ "config.json": userConfig,
930
+ "data/cursor.json": '{"n":1}\n',
931
+ ".disabled": "",
932
+ };
933
+ const pinTheirs: Tree = {
934
+ ...THEIRS,
935
+ "config.json": pinConfig,
936
+ "data/cursor.json": '{"n":0}\n',
937
+ };
938
+
939
+ for (const strategy of ["overwrite", "theirs", "ours"] as const) {
940
+ rmSync(join(pluginsDir, "level-up"), { recursive: true, force: true });
941
+ installMergeCopy("level-up", liveOurs, SHA_A, BASE);
942
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
943
+ const runGit = treeGitRunner({
944
+ [SHA_A]: BASE,
945
+ [SHA_B]: pinTheirs,
946
+ });
947
+
948
+ // WHEN the plugin is upgraded
949
+ const result = await upgradePlugin(
950
+ { name: "level-up", strategy },
951
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
952
+ );
953
+
954
+ // THEN user-owned state is unchanged, including under `theirs` and
955
+ // overwrite, which would otherwise take the pin wholesale
956
+ expect(result.outcome).toBe("upgraded");
957
+ expect(installedFile("level-up", "config.json")).toBe(userConfig);
958
+ expect(installedFile("level-up", "data/cursor.json")).toBe('{"n":1}\n');
959
+ expect(existsSync(join(pluginsDir, "level-up", ".disabled"))).toBe(true);
960
+ }
961
+ });
962
+
963
+ test("does not seed config.json from the pin when the live install has none", async () => {
964
+ // GIVEN an install with no config.json, and a pin that ships defaults
965
+ installMergeCopy("level-up", OURS, SHA_A, BASE);
966
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
967
+ const runGit = treeGitRunner({
968
+ [SHA_A]: BASE,
969
+ [SHA_B]: {
970
+ ...THEIRS,
971
+ "config.json": '{"provider":"comms","ingressMode":"webhook"}\n',
972
+ },
973
+ });
974
+
975
+ // WHEN upgraded (overwrite materializes the pin tree directly)
976
+ const result = await upgradePlugin(
977
+ { name: "level-up", strategy: "overwrite" },
978
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
979
+ );
980
+
981
+ // THEN the pin's config.json does not land: host-owned config is created
982
+ // by the plugin at runtime, not by the installer
983
+ expect(result.outcome).toBe("upgraded");
984
+ expect(installedFile("level-up", "config.json")).toBeNull();
985
+ });
986
+
923
987
  test("defaults to overwrite when no strategy is given", async () => {
924
988
  // GIVEN an install with a local-only file and an advanced pin
925
989
  installMergeCopy("level-up", OURS, SHA_A, BASE);
@@ -292,6 +292,19 @@
292
292
  "license": "MIT",
293
293
  "icon": "🎬"
294
294
  },
295
+ {
296
+ "name": "natural",
297
+ "source": {
298
+ "source": "github",
299
+ "repo": "vellum-ai/natural",
300
+ "ref": "3cfc8c97c14df311fc64992ab8b172c1b2d64eb0"
301
+ },
302
+ "description": "Hosted MCP access to Natural payment operations for Vellum assistants.",
303
+ "category": "finance",
304
+ "homepage": "https://www.natural.com",
305
+ "license": "MIT",
306
+ "icon": "💳"
307
+ },
295
308
  {
296
309
  "name": "originkit",
297
310
  "source": {
@@ -31,6 +31,8 @@ export interface DeleteSecureKeyResult {
31
31
  result: DeleteResult;
32
32
  /** Human-readable error reason when result="error". */
33
33
  error?: string;
34
+ /** Daemon error code when result="error" (e.g. `CREDENTIAL_IN_USE`). */
35
+ code?: string;
34
36
  }
35
37
 
36
38
  // ---------------------------------------------------------------------------
@@ -104,13 +106,18 @@ export async function setSecureKeyViaDaemon(
104
106
  /**
105
107
  * Delete a secret via the daemon IPC socket. Falls back to direct
106
108
  * `deleteSecureKeyAsync()` when the daemon is not running.
109
+ *
110
+ * `force` carries the caller's confirmation past the daemon's in-use guard,
111
+ * which refuses a delete while an LLM provider connection resolves its auth
112
+ * through the credential.
107
113
  */
108
114
  export async function deleteSecureKeyViaDaemon(
109
115
  type: string,
110
116
  name: string,
117
+ force = false,
111
118
  ): Promise<DeleteSecureKeyResult> {
112
119
  const ipc = await cliIpcCall<{ success: boolean }>("secrets_delete", {
113
- body: { type, name },
120
+ body: force ? { type, name, force } : { type, name },
114
121
  });
115
122
 
116
123
  if (ipc.ok && ipc.result?.success) {
@@ -122,7 +129,9 @@ export async function deleteSecureKeyViaDaemon(
122
129
  if (ipc.error.includes("not found") || ipc.error.includes("404")) {
123
130
  return { result: "not-found" };
124
131
  }
125
- return { result: "error", error: ipc.error };
132
+ return ipc.errorCode !== undefined
133
+ ? { result: "error", error: ipc.error, code: ipc.errorCode }
134
+ : { result: "error", error: ipc.error };
126
135
  }
127
136
 
128
137
  // Daemon returned success=false
@@ -133,7 +142,20 @@ export async function deleteSecureKeyViaDaemon(
133
142
  };
134
143
  }
135
144
 
136
- // Daemon unreachable — fall back to direct delete.
145
+ // Daemon unreachable. The in-use guard runs daemon-side, next to the
146
+ // provider connection records it reads, so a direct delete here would
147
+ // remove a credential an LLM connection depends on with no check at all.
148
+ // The delete is refused until the caller either starts the assistant or
149
+ // states the intent explicitly.
150
+ if (!force) {
151
+ return {
152
+ result: "error",
153
+ code: "IN_USE_CHECK_UNAVAILABLE",
154
+ error:
155
+ "Cannot check whether LLM provider connections depend on this credential while the assistant is not running. Start the assistant, or re-run with --force to delete without the check.",
156
+ };
157
+ }
158
+
137
159
  if (type === "api_key") {
138
160
  // Delete from both locations; during migration overlap both may exist.
139
161
  const credResult = await deleteSecureKeyAsync(
@@ -26,7 +26,6 @@
26
26
  import { execFile } from "node:child_process";
27
27
  import {
28
28
  copyFileSync,
29
- cpSync,
30
29
  existsSync,
31
30
  mkdirSync,
32
31
  readdirSync,
@@ -40,7 +39,12 @@ import { tmpdir } from "node:os";
40
39
  import { dirname, join, resolve, sep } from "node:path";
41
40
  import { promisify } from "node:util";
42
41
 
43
- import { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
42
+ import {
43
+ copyPreservedUserState,
44
+ isPluginUserStateEntry,
45
+ PRESERVED_ENTRIES,
46
+ stripPreservedUserState,
47
+ } from "../../plugins/plugin-tree-walk.js";
44
48
  import { ensureBun } from "../../util/bun-runtime.js";
45
49
  import { getWorkspacePluginsDir } from "../../util/platform.js";
46
50
  import type { FetchLike } from "./fetch-like.js";
@@ -687,27 +691,13 @@ export async function finalizeStagedInstall(
687
691
  }
688
692
  }
689
693
 
690
- // Copy preserved entries (config.json, data/, .disabled) from the existing
691
- // install into the staging dir before the swap so user-owned state survives
692
- // upgrades and reinstalls. Without this, the rm+rename below would destroy
693
- // user config and runtime data.
694
+ // User-owned state (config.json, data/, .disabled) is not plugin source.
695
+ // Drop any copy the pin materialized into staging, then copy the live
696
+ // install's bytes over so an upgrade cannot reset user config to pin
697
+ // defaults. Without the live copy, the rm+rename below would destroy it.
698
+ stripPreservedUserState(stagingDir);
694
699
  if (existsSync(target)) {
695
- for (const entry of PRESERVED_ENTRIES) {
696
- if (entry === INSTALL_META_FILENAME) {
697
- continue;
698
- } // sidecar is rewritten above
699
- const src = join(target, entry);
700
- if (!existsSync(src)) {
701
- continue;
702
- }
703
- const dest = join(stagingDir, entry);
704
- const stat = statSync(src);
705
- if (stat.isDirectory()) {
706
- cpSync(src, dest, { recursive: true });
707
- } else {
708
- copyFileSync(src, dest);
709
- }
710
- }
700
+ copyPreservedUserState(target, stagingDir);
711
701
  rmSync(target, { recursive: true, force: true });
712
702
  }
713
703
  renameSync(stagingDir, target);
@@ -1287,7 +1277,8 @@ function pluginPostinstallEnv(bun: string): NodeJS.ProcessEnv {
1287
1277
 
1288
1278
  /**
1289
1279
  * Recursively copy regular files from `srcRoot` into `destDir`, skipping the
1290
- * top-level `.git` directory, a top-level `bunfig.toml` (see below), and any
1280
+ * top-level `.git` directory, a top-level `bunfig.toml` (see below), top-level
1281
+ * user-state entries (`config.json`, `data/`, `.disabled`), and any
1291
1282
  * symlinks. Returns the file count.
1292
1283
  */
1293
1284
  function copyTreeSkippingGit(srcRoot: string, destDir: string): number {
@@ -1300,6 +1291,12 @@ function copyTreeSkippingGit(srcRoot: string, destDir: string): number {
1300
1291
  if (relDir === "" && entry.name === ".git") {
1301
1292
  continue;
1302
1293
  }
1294
+ // Runtime-owned state is not plugin source. A pin that ships
1295
+ // `config.json` or `data/` must not land in the staged tree, or an
1296
+ // upgrade would reset user config to whatever the revision committed.
1297
+ if (relDir === "" && isPluginUserStateEntry(entry.name)) {
1298
+ continue;
1299
+ }
1303
1300
  // Drop a top-level `bunfig.toml`. The adapter postinstall runs `bun` with
1304
1301
  // its cwd at the staged root, and Bun auto-loads `$cwd/bunfig.toml` as
1305
1302
  // project config — including a `preload` list it executes before the
@@ -1712,6 +1709,7 @@ async function copyDir(
1712
1709
  ref: string,
1713
1710
  destDir: string,
1714
1711
  fetchFn: FetchLike,
1712
+ atPluginRoot = true,
1715
1713
  ): Promise<number> {
1716
1714
  const entries = await listDir(owner, repo, apiPath, ref, fetchFn);
1717
1715
  if (entries === null) {
@@ -1725,11 +1723,24 @@ async function copyDir(
1725
1723
  continue;
1726
1724
  }
1727
1725
  assertSafeFilename("entry name", entry.name);
1726
+ // Adapter stubs overlay the plugin root. User-state names at that root
1727
+ // are host-owned, not adapter input.
1728
+ if (atPluginRoot && isPluginUserStateEntry(entry.name)) {
1729
+ continue;
1730
+ }
1728
1731
 
1729
1732
  if (entry.type === "dir") {
1730
1733
  const subDest = join(destDir, entry.name);
1731
1734
  mkdirSync(subDest, { recursive: true });
1732
- count += await copyDir(owner, repo, entry.path, ref, subDest, fetchFn);
1735
+ count += await copyDir(
1736
+ owner,
1737
+ repo,
1738
+ entry.path,
1739
+ ref,
1740
+ subDest,
1741
+ fetchFn,
1742
+ false,
1743
+ );
1733
1744
  continue;
1734
1745
  }
1735
1746
 
@@ -23,6 +23,13 @@
23
23
  * one side) is resolved here before any line merge, since `git merge-file`
24
24
  * operates on three existing blobs.
25
25
  *
26
+ * Runtime-owned user state (`config.json`, `data/`, `.disabled`) is never
27
+ * merged. Those paths are excluded from the fingerprints and copied from the
28
+ * live install (`ours`) into the destination after the source merge, so a
29
+ * pin that ships default config cannot reset user settings. The provenance
30
+ * sidecar is excluded on every side and rewritten by the caller after the swap.
31
+ *
32
+ *
26
33
  * Binary files cannot be line-merged, so a binary file that diverged on both
27
34
  * sides is resolved whole-file by the strategy (`ours`/`theirs`); under
28
35
  * `assistant` the local copy is kept and the path is reported as a binary
@@ -44,7 +51,10 @@ import { tmpdir } from "node:os";
44
51
  import { dirname, join } from "node:path";
45
52
  import { promisify } from "node:util";
46
53
 
47
- import { PRESERVED_ENTRIES } from "../../plugins/plugin-tree-walk.js";
54
+ import {
55
+ copyPreservedUserState,
56
+ PRESERVED_ENTRIES,
57
+ } from "../../plugins/plugin-tree-walk.js";
48
58
  import { computeFingerprint } from "./plugin-fingerprint.js";
49
59
 
50
60
  const execFileAsync = promisify(execFile);
@@ -230,9 +240,11 @@ async function threeWayMergeFile(
230
240
 
231
241
  /**
232
242
  * Three-way merge `oursDir`/`theirsDir` against `baseDir` into `destDir` per
233
- * `strategy`. The provenance sidecar is excluded on every side — it is
234
- * rewritten by the caller after the swap and must not be carried through the
235
- * merge.
243
+ * `strategy`. User-owned state (`config.json`, `data/`, `.disabled`) is
244
+ * excluded from the line merge and copied from `oursDir` afterward, so the
245
+ * pin cannot reset it. The provenance sidecar is excluded on every side — it
246
+ * is rewritten by the caller after the swap and must not be carried through
247
+ * the merge.
236
248
  *
237
249
  * Returns the file count plus, for the `assistant` strategy, the paths left for
238
250
  * the assistant to resolve (text files with conflict markers and modify/delete
@@ -332,5 +344,11 @@ export async function mergePluginTree({
332
344
  // Present only in the base: removed on both sides, so it stays removed.
333
345
  }
334
346
 
347
+ // User-owned state is not plugin source, so it is never a merge input.
348
+ // Copy it from the live install so `--strategy theirs` cannot take a
349
+ // pin-shipped `config.json` (or drop an untracked one the fingerprint
350
+ // walk never saw).
351
+ copyPreservedUserState(oursDir, destDir);
352
+
335
353
  return { fileCount, conflicts, binaryConflicts };
336
354
  }
@@ -216,6 +216,33 @@ function listSchedules(schedulesDir: string): PluginScheduleSurface[] {
216
216
  );
217
217
  }
218
218
 
219
+ /**
220
+ * Skill id a plugin ships for first-run setup, if any.
221
+ *
222
+ * Convention: `skills/setup/SKILL.md` or `skills/<plugin-name>-setup/SKILL.md`.
223
+ * The directory name is the id `skill_load` uses. `setup` wins when both exist
224
+ * so the generic name is the standard; `<plugin-name>-setup` covers plugins
225
+ * that already ship that form.
226
+ */
227
+ export function findPluginSetupSkill(
228
+ pluginName: string,
229
+ skillIds: readonly string[],
230
+ ): string | undefined {
231
+ if (skillIds.includes("setup")) {
232
+ return "setup";
233
+ }
234
+ const named = `${pluginName}-setup`;
235
+ if (skillIds.includes(named)) {
236
+ return named;
237
+ }
238
+ return undefined;
239
+ }
240
+
241
+ /** Install-command copy pointing at a plugin's setup skill. */
242
+ export function formatPluginSetupHint(skillId: string): string {
243
+ return `Load the ${skillId} skill to help set up this plugin`;
244
+ }
245
+
219
246
  /**
220
247
  * Detect the {@link PluginSurfaces} an installed plugin contributes by walking
221
248
  * its install tree at `pluginDir`. Surface types with no contributions come
@@ -31,8 +31,8 @@ import {
31
31
  // profile that runs another.
32
32
 
33
33
  const FLAG = BALANCED_MODEL_EXPERIMENT_FLAG_KEY;
34
- const SHIPPED_MODEL = "gpt-5.6-luna";
35
- const GLM_MODEL = "accounts/fireworks/models/glm-5p2";
34
+ const SHIPPED_MODEL = "accounts/fireworks/models/glm-5p2";
35
+ const GLM_MODEL = SHIPPED_MODEL;
36
36
 
37
37
  function setArm(value: boolean | string): void {
38
38
  setOverridesForTesting({ [FLAG]: value });
@@ -79,7 +79,7 @@ describe("balanced-model experiment arms", () => {
79
79
  expect(entry?.source).toBe("managed");
80
80
  });
81
81
 
82
- test("glm-5p2 repoints mainAgent at GLM 5.2 within the model's output cap", () => {
82
+ test("glm-5p2 resolves mainAgent to GLM 5.2 within the model's output cap", () => {
83
83
  setArm("glm-5p2");
84
84
  const resolved = resolveCallSiteConfig(
85
85
  "mainAgent",
@@ -93,7 +93,7 @@ describe("balanced-model experiment arms", () => {
93
93
  const cap = catalogMaxOutputTokens(upstream as string, GLM_MODEL);
94
94
  expect(cap).toBeDefined();
95
95
  expect(resolved.maxTokens).toBeLessThanOrEqual(cap as number);
96
- // Only the model moves: the shipped token budget stands.
96
+ // The shipped token budget stands.
97
97
  expect(resolved.maxTokens).toBe(
98
98
  CODE_DEFAULT_PROFILE_ENTRIES.balanced.maxTokens as number,
99
99
  );
@@ -111,7 +111,7 @@ describe("balanced-model experiment arms", () => {
111
111
  });
112
112
 
113
113
  test("the other default profiles are untouched by an arm", () => {
114
- setArm("glm-5p2");
114
+ setArm("terra");
115
115
  for (const key of [
116
116
  "quality-optimized",
117
117
  "cost-optimized",
@@ -239,10 +239,10 @@ describe("balanced-model experiment boundaries", () => {
239
239
  });
240
240
 
241
241
  test("an install with no defaultProvider still picks up the arm", () => {
242
- setArm("glm-5p2");
242
+ setArm("terra");
243
243
  expect(
244
244
  resolveDefaultProfileForProvider(undefined, "balanced", null)?.model,
245
- ).toBe(GLM_MODEL);
245
+ ).toBe("gpt-5.6-terra");
246
246
  });
247
247
  });
248
248
 
@@ -57,12 +57,12 @@ describe("getEffectiveProfiles", () => {
57
57
  }
58
58
  });
59
59
 
60
- test("the managed Balanced profile routes GPT-5.6 Luna through OpenAI", () => {
60
+ test("the managed Balanced profile routes GLM 5.2 through Fireworks", () => {
61
61
  const balanced = CODE_DEFAULT_PROFILE_ENTRIES.balanced;
62
- expect(balanced.model).toBe("gpt-5.6-luna");
62
+ expect(balanced.model).toBe("accounts/fireworks/models/glm-5p2");
63
63
  expect(resolveRoutingIdentity(balanced.provider, balanced.model)).toEqual({
64
64
  connectionName: "vellum",
65
- expectedProvider: "openai",
65
+ expectedProvider: "fireworks",
66
66
  });
67
67
  });
68
68