@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
@@ -12,6 +12,7 @@
12
12
  * (e.g. `surface_id is required`, `mode must be one of "replace", "append"`).
13
13
  */
14
14
 
15
+ import { parseJsonSafe } from "../util/json.js";
15
16
  import { isPlainObject } from "../util/object.js";
16
17
 
17
18
  export interface InputValidationSuccess {
@@ -67,22 +68,27 @@ function quoteList(values: readonly string[]): string {
67
68
  }
68
69
 
69
70
  /**
70
- * Coerce string-encoded booleans (`"true"`/`"false"`) to real booleans for
71
- * properties the schema declares as `type: "boolean"`.
71
+ * Walk the properties a schema declares as `declaredType`, offering each
72
+ * present value to `coerceValue`. A converter returns `{ value }` to replace
73
+ * the input's value, or `undefined` to leave it alone.
72
74
  *
73
- * Some providers' models serialize booleans as JSON strings. Rejecting those
74
- * loses the caller's intent: the model's typical recovery is to drop the field
75
- * and retry, at which point the field's default silently inverts what it asked
76
- * for (e.g. `app_create` with `auto_open: "false"` → retry omits the field →
77
- * default `true` opens a half-built app). Accepting the unambiguous string
78
- * forms preserves intent.
75
+ * The shared traversal is what keeps the coercions below agreeing on the
76
+ * details a caller depends on: union types (`["boolean", "null"]`) are skipped
77
+ * rather than coerced, an absent property is never introduced, and the input
78
+ * object is cloned lazily so an input with nothing to coerce comes back by
79
+ * reference.
79
80
  *
80
81
  * Pure: returns a new object when a coercion applies, otherwise returns
81
82
  * `input` unchanged. Never mutates `input` or `schema`.
82
83
  */
83
- export function coerceStringBooleans(
84
+ function coercePropertiesOfType(
84
85
  input: Record<string, unknown>,
85
86
  schema: Record<string, unknown> | undefined,
87
+ declaredType: string,
88
+ coerceValue: (
89
+ value: unknown,
90
+ subSchema: Record<string, unknown>,
91
+ ) => { value: unknown } | undefined,
86
92
  ): Record<string, unknown> {
87
93
  if (!schema) {
88
94
  return input;
@@ -94,24 +100,47 @@ export function coerceStringBooleans(
94
100
 
95
101
  let coerced: Record<string, unknown> | undefined;
96
102
  for (const [key, rawSubSchema] of Object.entries(properties)) {
97
- if (!isPlainObject(rawSubSchema)) {
103
+ if (!isPlainObject(rawSubSchema) || rawSubSchema.type !== declaredType) {
98
104
  continue;
99
105
  }
100
- if (rawSubSchema.type !== "boolean") {
106
+ const result = coerceValue(input[key], rawSubSchema);
107
+ if (!result) {
101
108
  continue;
102
109
  }
103
- const value = input[key];
110
+ coerced ??= { ...input };
111
+ coerced[key] = result.value;
112
+ }
113
+ return coerced ?? input;
114
+ }
115
+
116
+ /**
117
+ * Coerce string-encoded booleans (`"true"`/`"false"`) to real booleans for
118
+ * properties the schema declares as `type: "boolean"`.
119
+ *
120
+ * Some providers' models serialize booleans as JSON strings. Rejecting those
121
+ * loses the caller's intent: the model's typical recovery is to drop the field
122
+ * and retry, at which point the field's default silently inverts what it asked
123
+ * for (e.g. `app_create` with `auto_open: "false"` → retry omits the field →
124
+ * default `true` opens a half-built app). Accepting the unambiguous string
125
+ * forms preserves intent.
126
+ *
127
+ * Pure: returns a new object when a coercion applies, otherwise returns
128
+ * `input` unchanged. Never mutates `input` or `schema`.
129
+ */
130
+ export function coerceStringBooleans(
131
+ input: Record<string, unknown>,
132
+ schema: Record<string, unknown> | undefined,
133
+ ): Record<string, unknown> {
134
+ return coercePropertiesOfType(input, schema, "boolean", (value) => {
104
135
  if (typeof value !== "string") {
105
- continue;
136
+ return undefined;
106
137
  }
107
138
  const normalized = value.trim().toLowerCase();
108
139
  if (normalized !== "true" && normalized !== "false") {
109
- continue;
140
+ return undefined;
110
141
  }
111
- coerced ??= { ...input };
112
- coerced[key] = normalized === "true";
113
- }
114
- return coerced ?? input;
142
+ return { value: normalized === "true" };
143
+ });
115
144
  }
116
145
 
117
146
  /**
@@ -141,36 +170,135 @@ export function coerceStringNumbers(
141
170
  input: Record<string, unknown>,
142
171
  schema: Record<string, unknown> | undefined,
143
172
  ): Record<string, unknown> {
144
- if (!schema) {
145
- return input;
146
- }
147
- const properties = schema.properties;
148
- if (!isPlainObject(properties)) {
149
- return input;
150
- }
151
-
152
- let coerced: Record<string, unknown> | undefined;
153
- for (const [key, rawSubSchema] of Object.entries(properties)) {
154
- if (!isPlainObject(rawSubSchema)) {
155
- continue;
156
- }
157
- if (rawSubSchema.type !== "string") {
158
- continue;
159
- }
160
- const value = input[key];
173
+ return coercePropertiesOfType(input, schema, "string", (value) => {
161
174
  if (typeof value !== "number" || !Number.isFinite(value)) {
162
- continue;
175
+ return undefined;
163
176
  }
164
177
  // An integer beyond the safe range was already rounded by JSON.parse;
165
178
  // coercing it would lock in a corrupted identifier. Skip it so validation
166
179
  // fails and the model retries with a lossless quoted string.
167
180
  if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
168
- continue;
181
+ return undefined;
169
182
  }
170
- coerced ??= { ...input };
171
- coerced[key] = String(value);
183
+ return { value: String(value) };
184
+ });
185
+ }
186
+
187
+ /**
188
+ * Repair the shapes a model sends for a property the schema declares as
189
+ * `type: "array"`, when the intent is unambiguous.
190
+ *
191
+ * Two shapes dominate what models actually send, and neither is recoverable
192
+ * from the model's side: it cannot see the difference between what it meant
193
+ * and what arrived, so the observed recovery is to drop the field and retry.
194
+ * That is how a skill gets scaffolded with no `activation_hints` (losing the
195
+ * intent-routing signal that makes it findable later) or with its companion
196
+ * `files` folded into the body instead.
197
+ *
198
+ * - **The JSON text of the array** (`"[\"a\",\"b\"]"`) rather than the array.
199
+ * Decoded back into the array it spells.
200
+ * - **A single element where a list was expected**: a bare phrase for a
201
+ * string-item array (`avoid_when: "when the repo is dirty"`), or one object
202
+ * for an object-item array. Wrapped into a one-element array.
203
+ *
204
+ * A string is wrapped only when the schema declares string items and the text
205
+ * does not open a JSON structure it failed to close, and an object only when
206
+ * every key it carries is a declared property of the item schema. A shape that
207
+ * means something else (a truncated array, a map keyed by something the item
208
+ * schema does not name) is left for the validator to reject rather than
209
+ * silently reinterpreted. Per-element `items.type` checks still run on the
210
+ * repaired array, so a genuinely wrong element keeps its own error, and each
211
+ * element an array gains here is coerced against the item schema so a repaired
212
+ * element cannot carry a shape a natively-sent one could not.
213
+ *
214
+ * Pure: returns a new object when a repair applies, otherwise returns `input`
215
+ * unchanged. Never mutates `input` or `schema`.
216
+ */
217
+ export function coerceArrayShapes(
218
+ input: Record<string, unknown>,
219
+ schema: Record<string, unknown> | undefined,
220
+ ): Record<string, unknown> {
221
+ return coercePropertiesOfType(input, schema, "array", (value, subSchema) => {
222
+ const items = isPlainObject(subSchema.items) ? subSchema.items : undefined;
223
+ const itemType = typeof items?.type === "string" ? items.type : undefined;
224
+
225
+ if (typeof value === "string") {
226
+ const decoded = parseJsonSafe(value);
227
+ if (Array.isArray(decoded)) {
228
+ return { value: coerceElements(decoded, items) };
229
+ }
230
+ if (itemType !== "string") {
231
+ return undefined;
232
+ }
233
+ if (typeof decoded === "string" && decoded.trim()) {
234
+ return { value: [decoded] };
235
+ }
236
+ // Text that opens a JSON structure and then fails to parse is a
237
+ // truncated or malformed array, not a phrase. Wrapping it would store
238
+ // the broken JSON as if it were the element the caller wrote.
239
+ const trimmed = value.trim();
240
+ if (!trimmed || trimmed.startsWith("[") || trimmed.startsWith("{")) {
241
+ return undefined;
242
+ }
243
+ return { value: [value] };
244
+ }
245
+
246
+ if (
247
+ itemType === "object" &&
248
+ isPlainObject(value) &&
249
+ matchesItemProperties(value, items)
250
+ ) {
251
+ return { value: coerceElements([value], items) };
252
+ }
253
+
254
+ return undefined;
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Apply the scalar coercions to each object element against the item schema.
260
+ *
261
+ * Only top-level properties are coerced when a call arrives already in the
262
+ * declared shape, because that is the only level the validator type-checks.
263
+ * An array this function builds has no such level yet: its elements were text
264
+ * a moment ago, and their properties carry whatever the model serialized,
265
+ * including the string-encoded booleans this module exists to read. Coercing
266
+ * them here keeps a repaired element from reaching an executor in a shape a
267
+ * natively-sent one could not.
268
+ */
269
+ function coerceElements(
270
+ elements: unknown[],
271
+ itemSchema: Record<string, unknown> | undefined,
272
+ ): unknown[] {
273
+ if (!isPlainObject(itemSchema?.properties)) {
274
+ return elements;
172
275
  }
173
- return coerced ?? input;
276
+ return elements.map((element) =>
277
+ isPlainObject(element)
278
+ ? coerceStringNumbers(
279
+ coerceStringBooleans(element, itemSchema),
280
+ itemSchema,
281
+ )
282
+ : element,
283
+ );
284
+ }
285
+
286
+ /**
287
+ * Whether `value` reads as one element of `itemSchema`: every key it carries
288
+ * is a property that schema declares. An object that fails this is some other
289
+ * shape (most often a map keyed by data rather than by field name), which is
290
+ * not a single element and must not be wrapped as one.
291
+ */
292
+ function matchesItemProperties(
293
+ value: Record<string, unknown>,
294
+ itemSchema: Record<string, unknown> | undefined,
295
+ ): boolean {
296
+ const properties = itemSchema?.properties;
297
+ if (!isPlainObject(properties)) {
298
+ return false;
299
+ }
300
+ const keys = Object.keys(value);
301
+ return keys.length > 0 && keys.every((key) => key in properties);
174
302
  }
175
303
 
176
304
  /**
@@ -241,6 +369,15 @@ export function validateInputAgainstSchema(
241
369
  }
242
370
  if (typeof declaredType === "string" && SUPPORTED_TYPES.has(declaredType)) {
243
371
  const type = declaredType as SupportedType;
372
+ if (
373
+ type === "array" &&
374
+ (typeof value === "string" || isPlainObject(value))
375
+ ) {
376
+ errors.push(
377
+ `${key} must be an array: pass a JSON array, not ${typeof value === "string" ? "a string" : "an object"}`,
378
+ );
379
+ continue;
380
+ }
244
381
  if (!matchesType(value, type)) {
245
382
  errors.push(
246
383
  type === "boolean" && typeof value === "string"
@@ -50,10 +50,9 @@ const sources = {
50
50
  // A path that does not exist, so the workspace-tree section stays empty and
51
51
  // the assertions isolate the gated surfaces.
52
52
  workingDir: "/tmp/does-not-exist-consult-gating",
53
- // A remote, non-guardian per-turn snapshot: the case the live-state read
54
- // could have wrongly elevated.
53
+ // A non-guardian per-turn snapshot: the case the live-state read could have
54
+ // wrongly elevated.
55
55
  trustClass: "unknown" as const,
56
- sourceChannel: "telegram",
57
56
  allowedToolNames: new Set<string>(),
58
57
  };
59
58
 
@@ -87,12 +86,9 @@ describe("advisor context pack: personal-memory gating", () => {
87
86
  test("feeds the gate the per-turn trust snapshot, not live conversation state", async () => {
88
87
  personalAllowed = true;
89
88
  await buildAdvisorContext(sources);
90
- // The gate must see exactly the snapshot threaded from ToolContext
91
- // (trustClass + executionChannel) so a concurrent live-trust change can't
92
- // elevate this invocation.
93
- expect(gateArg).toEqual({
94
- sourceChannel: "telegram",
95
- trustClass: "unknown",
96
- });
89
+ // The gate must see exactly the class threaded from the ToolContext
90
+ // snapshot, so a concurrent live-trust change cannot elevate this
91
+ // invocation.
92
+ expect(gateArg).toEqual({ trustClass: "unknown" });
97
93
  });
98
94
  });
@@ -33,7 +33,6 @@
33
33
  import { readdir } from "node:fs/promises";
34
34
  import { join } from "node:path";
35
35
 
36
- import type { ChannelId } from "../channels/types.js";
37
36
  import type { SkillSummary } from "../config/skills.js";
38
37
  import type { TrustContext } from "../daemon/trust-context-types.js";
39
38
  import type { TrustClass } from "../runtime/actor-trust-resolver.js";
@@ -46,16 +45,11 @@ export interface AdvisorContextSources {
46
45
  allowedToolNames?: ReadonlySet<string>;
47
46
  /**
48
47
  * Trust class of the turn's actor, from the per-turn `ToolContext.trustClass`
49
- * snapshot. Gates (with {@link sourceChannel}) the personal-memory surfaces.
48
+ * snapshot. Gates the personal-memory surfaces, evaluated exactly as the
49
+ * injectors do and off the same per-turn snapshot rather than the mutable
50
+ * live conversation trust.
50
51
  */
51
52
  trustClass: TrustClass;
52
- /**
53
- * Channel the turn originates on, from the per-turn `ToolContext.executionChannel`
54
- * snapshot. Combined with {@link trustClass} to evaluate personal-memory
55
- * access exactly as the injectors do, off the same per-turn snapshot rather
56
- * than the mutable live conversation trust.
57
- */
58
- sourceChannel?: string;
59
53
  /**
60
54
  * Per-chat plugin scope from `ToolContext.enabledPluginSet`: `null` means no
61
55
  * restriction; otherwise plugin-owned skills outside the set are omitted
@@ -263,18 +257,13 @@ export async function buildWorkspaceTree(
263
257
  */
264
258
  async function personalMemoryAllowedForAdvisor(
265
259
  trustClass: TrustClass,
266
- sourceChannel: string | undefined,
267
260
  ): Promise<boolean> {
268
261
  try {
269
262
  const { isPersonalMemoryAllowed } =
270
263
  await import("../daemon/trust-context.js");
271
- // `isPersonalMemoryAllowed` reads only `sourceChannel` + `trustClass`; build
272
- // a minimal trust context from the per-turn snapshot. The channel may be
273
- // absent (local/internal turns), which the gate treats as non-remote.
274
- const snapshot = {
275
- sourceChannel: sourceChannel as ChannelId | undefined,
276
- trustClass,
277
- } as TrustContext;
264
+ // The gate decides from the trust class alone, so the per-turn snapshot's
265
+ // class is the whole input.
266
+ const snapshot = { trustClass } as TrustContext;
278
267
  return isPersonalMemoryAllowed(snapshot);
279
268
  } catch {
280
269
  return false;
@@ -318,12 +307,7 @@ async function buildWorkspaceSection(
318
307
  // toggle) the runtime injectors use, evaluated off the per-turn trust
319
308
  // snapshot, so a low-risk advisor consult cannot forward private content the
320
309
  // main agent would never receive.
321
- if (
322
- await personalMemoryAllowedForAdvisor(
323
- sources.trustClass,
324
- sources.sourceChannel,
325
- )
326
- ) {
310
+ if (await personalMemoryAllowedForAdvisor(sources.trustClass)) {
327
311
  try {
328
312
  const [{ readNowScratchpad }, { getConfig }] = await Promise.all([
329
313
  import("../daemon/now-scratchpad.js"),
@@ -18,37 +18,17 @@ import type {
18
18
  import { isDomainAllowed } from "./domain-policy.js";
19
19
  import { getCredentialMetadata } from "./metadata-store.js";
20
20
  import { resolveById } from "./resolve.js";
21
- import { isToolAllowed } from "./tool-policy.js";
21
+ import {
22
+ isToolAllowed,
23
+ serverUseDenialReason,
24
+ toolNotAllowedReason,
25
+ } from "./tool-policy.js";
22
26
 
23
27
  const log = getLogger("credential-broker");
24
28
 
25
29
  /** Tokens expire after 5 minutes to limit the window for using stale/revoked credentials. */
26
30
  const TOKEN_TTL_MS = 5 * 60 * 1000;
27
31
 
28
- /**
29
- * Remediation for a credential whose allowed_tools list is empty. Points at
30
- * `credentials prompt` (not inline `credentials set`, which agent shells
31
- * refuse): the secure prompt re-collects the value and sets allowed_tools.
32
- */
33
- const NO_TOOLS_ALLOWED_REMEDIATION =
34
- "No tools are currently allowed - grant access via `assistant credentials prompt --service <service> --field <field> --label <label> --allowed-tools <tools>` (re-collects the value securely and sets allowed_tools).";
35
-
36
- /** Denial reason for a tool that is not in a credential's allowed_tools list. */
37
- function toolNotAllowedReason(
38
- toolName: string,
39
- service: string,
40
- field: string,
41
- allowedTools: string[] | undefined,
42
- ): string {
43
- const tools = allowedTools ?? [];
44
- return (
45
- `Tool "${toolName}" is not allowed to use credential ${service}/${field}. ` +
46
- (tools.length === 0
47
- ? NO_TOOLS_ALLOWED_REMEDIATION
48
- : `Allowed tools: ${tools.join(", ")}.`)
49
- );
50
- }
51
-
52
32
  /**
53
33
  * Credential broker that issues single-use tokens for policy-checked credential access.
54
34
  *
@@ -292,36 +272,14 @@ export class CredentialBroker {
292
272
  request: ServerUseRequest<T>,
293
273
  ): Promise<ServerUseResult<T>> {
294
274
  const metadata = getCredentialMetadata(request.service, request.field);
295
- if (!metadata) {
296
- return {
297
- success: false,
298
- reason: `No credential found for ${request.service}/${request.field}`,
299
- };
300
- }
301
-
302
- if (!isToolAllowed(request.toolName, metadata.allowedTools)) {
303
- return {
304
- success: false,
305
- reason: toolNotAllowedReason(
306
- request.toolName,
307
- request.service,
308
- request.field,
309
- metadata.allowedTools,
310
- ),
311
- };
312
- }
313
-
314
- // Domain policy enforcement - credentials with domain restrictions are
315
- // scoped to browser use on those domains and cannot be used server-side.
316
- const serverDomains = metadata.allowedDomains ?? [];
317
- if (serverDomains.length > 0) {
318
- return {
319
- success: false,
320
- reason:
321
- `Credential ${request.service}/${request.field} has domain restrictions ` +
322
- `(${serverDomains.join(", ")}) and cannot be used server-side. ` +
323
- "Remove domain restrictions or use a separate credential without domain policy.",
324
- };
275
+ const denialReason = serverUseDenialReason(
276
+ metadata,
277
+ request.toolName,
278
+ request.service,
279
+ request.field,
280
+ );
281
+ if (denialReason) {
282
+ return { success: false, reason: denialReason };
325
283
  }
326
284
 
327
285
  const storageKey = credentialKey(request.service, request.field);
@@ -381,30 +339,17 @@ export class CredentialBroker {
381
339
 
382
340
  const { metadata } = resolved;
383
341
 
384
- // Tool policy enforcement
385
- if (!isToolAllowed(request.requestingTool, metadata.allowedTools)) {
386
- return {
387
- success: false,
388
- reason: toolNotAllowedReason(
389
- request.requestingTool,
390
- metadata.service,
391
- metadata.field,
392
- metadata.allowedTools,
393
- ),
394
- };
395
- }
396
-
397
- // Domain policy enforcement - credentials with domain restrictions are
398
- // scoped to browser use on those domains and cannot be used server-side.
399
- const domains = metadata.allowedDomains ?? [];
400
- if (domains.length > 0) {
401
- return {
402
- success: false,
403
- reason:
404
- `Credential ${metadata.service}/${metadata.field} has domain restrictions ` +
405
- `(${domains.join(", ")}) and cannot be used server-side. ` +
406
- "Remove domain restrictions or use a separate credential without domain policy.",
407
- };
342
+ // Shared server-use policy: the ID lookup above is the only by-ID-specific
343
+ // gate, so tool and domain enforcement come from the same helper serverUse
344
+ // uses.
345
+ const denialReason = serverUseDenialReason(
346
+ metadata,
347
+ request.requestingTool,
348
+ metadata.service,
349
+ metadata.field,
350
+ );
351
+ if (denialReason) {
352
+ return { success: false, reason: denialReason };
408
353
  }
409
354
 
410
355
  // Fail-closed: verify the secret value actually exists in secure storage.
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Credential reference parsing. Pure string work, no store access.
3
+ *
4
+ * `"service/field"` is the shared vocabulary of every credential entry point:
5
+ * the CLI, the reveal and set routes, and the plugin-facing read and write
6
+ * APIs all name credentials this way. The rules for what counts as a valid
7
+ * reference live here so each path applies the same ones.
8
+ *
9
+ * Deliberately separate from {@link ./resolve}, which reads the metadata store:
10
+ * a caller that only needs to split a reference should not import a module
11
+ * whose behavior depends on what is stored.
12
+ */
13
+
14
+ /**
15
+ * Parse a `"service/field"` credential reference into its parts.
16
+ *
17
+ * Returns undefined for anything that is not exactly one non-empty segment on
18
+ * each side of a single slash: no slash, a leading or trailing slash, or more
19
+ * than one slash as in `"fal/api/key"`.
20
+ */
21
+ export function parseServiceFieldRef(
22
+ ref: string,
23
+ ): { service: string; field: string } | undefined {
24
+ const slashIndex = ref.indexOf("/");
25
+ if (slashIndex <= 0 || slashIndex >= ref.length - 1) {
26
+ return undefined;
27
+ }
28
+ if (ref.indexOf("/", slashIndex + 1) !== -1) {
29
+ return undefined;
30
+ }
31
+ return {
32
+ service: ref.slice(0, slashIndex),
33
+ field: ref.slice(slashIndex + 1),
34
+ };
35
+ }
@@ -16,6 +16,7 @@ import {
16
16
  listCredentialMetadata,
17
17
  } from "./metadata-store.js";
18
18
  import type { CredentialInjectionTemplate } from "./policy-types.js";
19
+ import { parseServiceFieldRef } from "./ref-parse.js";
19
20
 
20
21
  export interface ResolvedCredential {
21
22
  credentialId: string;
@@ -95,18 +96,11 @@ export function resolveCredentialRef(
95
96
  }
96
97
 
97
98
  // Try as service/field
98
- const slashIndex = ref.indexOf("/");
99
- if (slashIndex <= 0 || slashIndex >= ref.length - 1) {
99
+ const parsed = parseServiceFieldRef(ref);
100
+ if (!parsed) {
100
101
  return undefined;
101
102
  }
102
- // Reject refs with more than one slash (e.g. "fal/api/key")
103
- if (ref.indexOf("/", slashIndex + 1) !== -1) {
104
- return undefined;
105
- }
106
-
107
- const service = ref.slice(0, slashIndex);
108
- const field = ref.slice(slashIndex + 1);
109
- return resolveByServiceField(service, field);
103
+ return resolveByServiceField(parsed.service, parsed.field);
110
104
  }
111
105
 
112
106
  /**