@vellumai/assistant 0.11.6 → 0.11.7-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 (478) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  21. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
  22. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  23. package/node_modules/@vellumai/environments/package.json +2 -1
  24. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  25. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  27. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  28. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  29. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  30. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  32. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  33. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  34. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  35. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  36. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  37. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  38. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  39. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  40. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  41. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  42. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  43. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  44. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  45. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  46. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  47. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  48. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  49. package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  50. package/openapi.yaml +419 -55
  51. package/package.json +4 -1
  52. package/scripts/sync-llm-catalog.ts +6 -0
  53. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  54. package/src/__tests__/approval-card-edit.test.ts +153 -0
  55. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  56. package/src/__tests__/available-skills.test.ts +54 -0
  57. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  58. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  59. package/src/__tests__/channel-policy.test.ts +1 -3
  60. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  61. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  63. package/src/__tests__/config-schema.test.ts +2 -3
  64. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  65. package/src/__tests__/conversation-attachments.test.ts +0 -1
  66. package/src/__tests__/conversation-error.test.ts +44 -0
  67. package/src/__tests__/conversation-evictor.test.ts +19 -0
  68. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  69. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  70. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  71. package/src/__tests__/conversation-slash.test.ts +9 -0
  72. package/src/__tests__/conversation-title-service.test.ts +97 -0
  73. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  74. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  75. package/src/__tests__/credential-broker.test.ts +43 -420
  76. package/src/__tests__/credential-record-write-through.test.ts +78 -0
  77. package/src/__tests__/credential-routes.test.ts +17 -1
  78. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  79. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  80. package/src/__tests__/delete-propagation.test.ts +92 -2
  81. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  82. package/src/__tests__/discord-channel-config.test.ts +237 -0
  83. package/src/__tests__/edit-propagation.test.ts +42 -2
  84. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  85. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  86. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  87. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  88. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  89. package/src/__tests__/image-conversion.test.ts +38 -4
  90. package/src/__tests__/install-skill-routing.test.ts +25 -0
  91. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  92. package/src/__tests__/llm-resolver.test.ts +74 -0
  93. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  94. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  95. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  96. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  97. package/src/__tests__/mcp-cli.test.ts +23 -1
  98. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  99. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  100. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  101. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  102. package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
  103. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  104. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  105. package/src/__tests__/platform.test.ts +19 -1
  106. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  107. package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
  108. package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
  109. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  110. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  111. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  112. package/src/__tests__/schedule-retry.test.ts +183 -4
  113. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  115. package/src/__tests__/skill-load-tool.test.ts +34 -0
  116. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  117. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  118. package/src/__tests__/skills.test.ts +25 -0
  119. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  120. package/src/__tests__/terminal-tools.test.ts +2 -0
  121. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  122. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  123. package/src/__tests__/tool-audit.test.ts +111 -40
  124. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  125. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  126. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  127. package/src/__tests__/voice-config-update.test.ts +43 -0
  128. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  129. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  130. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  131. package/src/agent/loop.ts +31 -2
  132. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  133. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  134. package/src/avatar/avatar-manifest.ts +48 -100
  135. package/src/avatar/avatar-store.ts +7 -7
  136. package/src/avatar/ensure-raster.ts +146 -0
  137. package/src/avatar/resvg-lazy.test.ts +0 -1
  138. package/src/avatar/traits-png-sync.ts +9 -10
  139. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  140. package/src/backup/__tests__/paths.test.ts +6 -2
  141. package/src/backup/paths.ts +6 -4
  142. package/src/bundler/app-compiler.ts +2 -2
  143. package/src/bundler/compiler-tools.test.ts +36 -0
  144. package/src/bundler/compiler-tools.ts +54 -18
  145. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  146. package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
  147. package/src/calls/media-stream-stt-session.ts +2 -1
  148. package/src/calls/telephony-synthesis-language.ts +13 -5
  149. package/src/calls/voice-session-bridge.ts +19 -4
  150. package/src/channels/config.ts +23 -12
  151. package/src/cli/AGENTS.md +3 -1
  152. package/src/cli/commands/backup.help.ts +10 -13
  153. package/src/cli/commands/contacts.help.ts +16 -4
  154. package/src/cli/commands/contacts.ts +5 -1
  155. package/src/cli/commands/credentials.help.ts +1 -1
  156. package/src/cli/commands/inference-providers.ts +18 -53
  157. package/src/cli/commands/mcp.help.ts +6 -5
  158. package/src/cli/commands/mcp.ts +1 -1
  159. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  160. package/src/cli/commands/stt.help.ts +2 -2
  161. package/src/config/__tests__/backup-schema.test.ts +1 -1
  162. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  163. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  164. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  165. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  166. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  167. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  168. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  169. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  170. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  171. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  172. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  173. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  174. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  175. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  176. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  177. package/src/config/default-profile-catalog.ts +195 -10
  178. package/src/config/default-profile-names.ts +41 -0
  179. package/src/config/feature-flag-registry.json +34 -2
  180. package/src/config/inference-profile-validation.ts +11 -2
  181. package/src/config/llm-resolver.ts +25 -0
  182. package/src/config/loader.ts +3 -0
  183. package/src/config/managed-speech-defaults.ts +160 -10
  184. package/src/config/schema.ts +2 -0
  185. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  186. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  187. package/src/config/schemas/backup.ts +1 -1
  188. package/src/config/schemas/channels.ts +32 -0
  189. package/src/config/schemas/live-voice.ts +13 -4
  190. package/src/config/schemas/llm.ts +263 -15
  191. package/src/config/schemas/mcp.ts +10 -2
  192. package/src/config/schemas/memory-retrospective.ts +1 -19
  193. package/src/config/schemas/services.ts +24 -0
  194. package/src/config/schemas/stt.ts +128 -5
  195. package/src/config/seed-inference-profiles.ts +32 -3
  196. package/src/config/skill-state.ts +5 -0
  197. package/src/config/skills.ts +21 -0
  198. package/src/contacts/notify-contacts-changed.ts +5 -2
  199. package/src/context/compactor.ts +74 -1
  200. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  201. package/src/daemon/config-watcher.ts +2 -1
  202. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  203. package/src/daemon/conversation-agent-loop.ts +86 -10
  204. package/src/daemon/conversation-error.ts +25 -0
  205. package/src/daemon/conversation-evictor.ts +2 -4
  206. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  207. package/src/daemon/conversation-skill-tools.ts +5 -1
  208. package/src/daemon/conversation-slash.ts +2 -2
  209. package/src/daemon/conversation-store.ts +8 -1
  210. package/src/daemon/conversation-tool-setup.ts +37 -0
  211. package/src/daemon/conversation.ts +0 -4
  212. package/src/daemon/handlers/channel-config-result.ts +30 -0
  213. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  214. package/src/daemon/handlers/config-ingress.ts +9 -0
  215. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  216. package/src/daemon/handlers/config-telegram.ts +2 -5
  217. package/src/daemon/handlers/conversations.ts +0 -2
  218. package/src/daemon/handlers/skills.ts +25 -7
  219. package/src/daemon/lifecycle.ts +3 -1
  220. package/src/daemon/message-types/messages.ts +0 -2
  221. package/src/daemon/tool-setup-types.ts +5 -3
  222. package/src/daemon/trust-context-types.test.ts +33 -0
  223. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  224. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  225. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  226. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  227. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  228. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
  229. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  230. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  231. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  232. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  233. package/src/live-voice/live-voice-session-manager.ts +39 -2
  234. package/src/live-voice/live-voice-session.ts +224 -9
  235. package/src/live-voice/protocol.ts +187 -1
  236. package/src/messaging/providers/discord/api.ts +2 -1
  237. package/src/messaging/providers/discord/withdraw.ts +62 -0
  238. package/src/messaging/read-provider-metadata.ts +32 -0
  239. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  240. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  241. package/src/monitoring/db-integrity-sample.ts +1 -0
  242. package/src/monitoring/file-descriptors.ts +39 -6
  243. package/src/monitoring/process-memory.ts +37 -5
  244. package/src/monitoring/resource-sampler.ts +25 -8
  245. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  246. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  247. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  248. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  249. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  250. package/src/notifications/activity-failed-dedupe.ts +95 -0
  251. package/src/notifications/adapters/discord.ts +132 -0
  252. package/src/notifications/adapters/shared.ts +16 -0
  253. package/src/notifications/adapters/slack.ts +6 -1
  254. package/src/notifications/adapters/telegram.ts +5 -8
  255. package/src/notifications/approval-card-data.ts +94 -1
  256. package/src/notifications/background-failure-signal.ts +116 -0
  257. package/src/notifications/broadcaster.ts +3 -9
  258. package/src/notifications/copy-composer.ts +41 -8
  259. package/src/notifications/decision-engine.ts +55 -0
  260. package/src/notifications/destination-resolver.ts +45 -5
  261. package/src/notifications/emit-signal.ts +19 -5
  262. package/src/notifications/guardian-question-mode.ts +57 -0
  263. package/src/notifications/types.ts +2 -8
  264. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  265. package/src/oauth/connect-orchestrator.ts +16 -0
  266. package/src/oauth/seed-providers.ts +262 -0
  267. package/src/permissions/checker.test.ts +21 -0
  268. package/src/permissions/checker.ts +6 -0
  269. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  270. package/src/permissions/gateway-threshold-reader.ts +144 -10
  271. package/src/permissions/prompter.ts +0 -6
  272. package/src/permissions/types.ts +5 -4
  273. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  274. package/src/persistence/bulk-write-gate.ts +9 -10
  275. package/src/persistence/conversation-crud.ts +88 -134
  276. package/src/persistence/conversation-title-service.ts +43 -29
  277. package/src/persistence/embeddings/embedding-local.ts +14 -85
  278. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  279. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  280. package/src/persistence/lifecycle-events-store.ts +75 -5
  281. package/src/platform/platform-patch-queue.test.ts +368 -0
  282. package/src/platform/platform-patch-queue.ts +190 -0
  283. package/src/platform/sync-avatar.test.ts +527 -0
  284. package/src/platform/sync-avatar.ts +209 -0
  285. package/src/platform/sync-identity.test.ts +75 -0
  286. package/src/platform/sync-identity.ts +17 -110
  287. package/src/plugin-api/model-profiles.test.ts +2 -2
  288. package/src/plugin-api/model-profiles.ts +7 -7
  289. package/src/plugin-api/transcription-session.ts +1 -1
  290. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  291. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  292. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  293. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  294. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  295. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  296. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  297. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  298. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  299. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  300. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  301. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  302. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  303. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  304. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  305. package/src/plugins/defaults/memory/worker.ts +7 -0
  306. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  307. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  308. package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
  309. package/src/providers/connection-resolution.ts +2 -17
  310. package/src/providers/fallback-breaker.ts +389 -0
  311. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  312. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  313. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  314. package/src/providers/inference/adapter-factory.ts +246 -3
  315. package/src/providers/inference/auth.ts +15 -14
  316. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  317. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  318. package/src/providers/inference/resolve-auth.ts +1 -1
  319. package/src/providers/media-resolve.ts +2 -3
  320. package/src/providers/model-catalog.ts +100 -0
  321. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  322. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  323. package/src/providers/openai/api-error-normalization.ts +13 -0
  324. package/src/providers/openai/chat-completions-provider.ts +84 -0
  325. package/src/providers/opencode/client.test.ts +150 -0
  326. package/src/providers/opencode/client.ts +61 -0
  327. package/src/providers/platform-proxy/constants.ts +5 -0
  328. package/src/providers/provider-resolvability.ts +20 -0
  329. package/src/providers/provider-secret-catalog.ts +3 -2
  330. package/src/providers/retry.ts +956 -68
  331. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  332. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  333. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  334. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  335. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  336. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  337. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  338. package/src/providers/speech-to-text/resolve.ts +132 -24
  339. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  340. package/src/providers/types.ts +39 -0
  341. package/src/providers/usage-tracking.ts +12 -1
  342. package/src/providers/vellum/client.ts +29 -0
  343. package/src/providers/vellum-model-routing.test.ts +2 -0
  344. package/src/providers/vellum-model-routing.ts +3 -3
  345. package/src/runtime/AGENTS.md +1 -2
  346. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  347. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  348. package/src/runtime/agent-wake.ts +44 -5
  349. package/src/runtime/background-job-runner.ts +68 -50
  350. package/src/runtime/channel-readiness-service.ts +0 -39
  351. package/src/runtime/http-server.ts +12 -4
  352. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  353. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  354. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  355. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  356. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  357. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  358. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  359. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  360. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  361. package/src/runtime/routes/approval-routes.ts +0 -12
  362. package/src/runtime/routes/avatar-routes.ts +17 -30
  363. package/src/runtime/routes/backup-routes.ts +2 -2
  364. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  365. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  366. package/src/runtime/routes/contact-routes.ts +20 -15
  367. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  368. package/src/runtime/routes/conversation-routes.ts +2 -2
  369. package/src/runtime/routes/credential-routes.ts +2 -1
  370. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  371. package/src/runtime/routes/inbound-message-handler.ts +47 -48
  372. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  373. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  374. package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
  375. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  376. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  377. package/src/runtime/routes/index.ts +2 -0
  378. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  379. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  380. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  381. package/src/runtime/routes/inference-send-routes.ts +2 -2
  382. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  383. package/src/runtime/routes/integrations/discord.ts +119 -0
  384. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  385. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  386. package/src/runtime/routes/migration-routes.ts +3 -1
  387. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  388. package/src/runtime/routes/secret-routes.ts +7 -0
  389. package/src/runtime/routes/stt-routes.ts +38 -6
  390. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  391. package/src/runtime/routes/watch-routes.ts +9 -5
  392. package/src/runtime/sync/resource-sync-events.ts +2 -0
  393. package/src/runtime/trust-verdict-consumer.ts +3 -3
  394. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  395. package/src/schedule/retry-policy.ts +8 -8
  396. package/src/schedule/run-script.ts +14 -23
  397. package/src/schedule/schedule-recovery.ts +0 -1
  398. package/src/schedule/scheduler.ts +89 -51
  399. package/src/schedule/worker.ts +57 -0
  400. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  401. package/src/security/ces-rpc-record-backend.ts +123 -0
  402. package/src/security/oauth2.ts +7 -0
  403. package/src/security/openai-device-auth.ts +364 -0
  404. package/src/security/secure-keys.ts +15 -0
  405. package/src/skills/available-skills.ts +10 -1
  406. package/src/skills/catalog-install.ts +19 -1
  407. package/src/skills/inline-command-runner.ts +6 -2
  408. package/src/skills/platform-compatibility.test.ts +49 -0
  409. package/src/skills/platform-compatibility.ts +65 -0
  410. package/src/skills/tool-manifest.ts +20 -0
  411. package/src/stt/__tests__/roles.test.ts +152 -0
  412. package/src/stt/daemon-batch-transcriber.ts +4 -1
  413. package/src/stt/roles.ts +180 -0
  414. package/src/stt/stt-stream-session.ts +3 -2
  415. package/src/stt/types.ts +33 -3
  416. package/src/subagent/manager.ts +12 -0
  417. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  418. package/src/telemetry/live-voice-funnel.ts +12 -2
  419. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  420. package/src/telemetry/tool-audit.ts +83 -30
  421. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  422. package/src/telemetry/turn-outcome.ts +75 -4
  423. package/src/tools/client-os.ts +115 -0
  424. package/src/tools/computer-use/definitions.ts +11 -7
  425. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  426. package/src/tools/credentials/broker-types.ts +0 -71
  427. package/src/tools/credentials/broker.ts +6 -201
  428. package/src/tools/credentials/metadata-store.ts +65 -16
  429. package/src/tools/credentials/store.ts +2 -0
  430. package/src/tools/executor.ts +2 -8
  431. package/src/tools/host-shell.test.ts +75 -0
  432. package/src/tools/host-shell.ts +24 -0
  433. package/src/tools/host-terminal/host-shell.ts +15 -44
  434. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  435. package/src/tools/permission-checker.ts +73 -21
  436. package/src/tools/policy-context.ts +1 -0
  437. package/src/tools/shared/filesystem/image-read.ts +1 -1
  438. package/src/tools/skills/find-similar-skills.ts +10 -2
  439. package/src/tools/skills/load.ts +41 -6
  440. package/src/tools/skills/sandbox-runner.ts +8 -16
  441. package/src/tools/skills/skill-tool-factory.ts +1 -0
  442. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  443. package/src/tools/terminal/safe-env.ts +39 -9
  444. package/src/tools/terminal/shell.ts +12 -31
  445. package/src/tools/tool-approval-handler.ts +64 -10
  446. package/src/tools/tool-types.ts +10 -0
  447. package/src/tools/types.ts +19 -3
  448. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  449. package/src/tools/workflows/manage-workflows.ts +2 -2
  450. package/src/tools/workflows/run-workflow.test.ts +2 -1
  451. package/src/util/__tests__/file-use.test.ts +61 -0
  452. package/src/util/__tests__/process-table.test.ts +66 -0
  453. package/src/util/__tests__/process-tree.test.ts +46 -0
  454. package/src/util/errors.ts +1 -0
  455. package/src/util/file-use.ts +91 -0
  456. package/src/util/host-process.test.ts +17 -0
  457. package/src/util/host-process.ts +75 -0
  458. package/src/util/image-conversion.ts +39 -7
  459. package/src/util/platform.ts +32 -13
  460. package/src/util/process-table.ts +249 -0
  461. package/src/util/process-tree.ts +85 -129
  462. package/src/util/provider-error-patterns.ts +20 -0
  463. package/src/util/spawn.ts +9 -2
  464. package/src/watch/__tests__/watch-retro.test.ts +48 -1
  465. package/src/watch/watch-retro.ts +46 -4
  466. package/src/workspace/git-service.ts +4 -14
  467. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  468. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  469. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  470. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  471. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  472. package/src/workspace/migrations/registry.ts +8 -0
  473. package/src/__tests__/permission-types.test.ts +0 -17
  474. package/src/channels/gateway-discord-admission.ts +0 -32
  475. package/src/persistence/fork-message-copy.ts +0 -239
  476. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  477. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  478. package/src/security/oauth2-device-code.ts +0 -312
@@ -0,0 +1,364 @@
1
+ /**
2
+ * OpenAI Codex device-code authorization.
3
+ *
4
+ * OpenAI's Codex sign-in uses a bespoke device-authorization API rather than
5
+ * RFC 8628. The daemon asks for a short user code, the user types it at
6
+ * `https://auth.openai.com/codex/device`, and the poll endpoint answers with an
7
+ * OAuth authorization code plus the PKCE pair minted alongside it. The caller
8
+ * exchanges that pair at the normal token endpoint, so the resulting tokens are
9
+ * identical to the browser redirect flow's.
10
+ *
11
+ * The account must have "Enable device code authorization for Codex" turned on
12
+ * in ChatGPT security settings. When it is off the poll never succeeds: it
13
+ * either stays pending until the code expires or answers with a distinct error
14
+ * code, both of which surface as a `DeviceAuthError`.
15
+ */
16
+
17
+ import { getLogger } from "../util/logger.js";
18
+ import type { OAuth2Config, OAuth2FlowResult } from "./oauth2.js";
19
+ import { exchangeCodeForTokens } from "./oauth2.js";
20
+
21
+ const log = getLogger("openai-device-auth");
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Endpoints
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export const OPENAI_DEVICE_AUTH_USERCODE_URL =
28
+ "https://auth.openai.com/api/accounts/deviceauth/usercode";
29
+
30
+ export const OPENAI_DEVICE_AUTH_TOKEN_URL =
31
+ "https://auth.openai.com/api/accounts/deviceauth/token";
32
+
33
+ /** Page where the user types the short code. */
34
+ export const OPENAI_DEVICE_VERIFICATION_URL =
35
+ "https://auth.openai.com/codex/device";
36
+
37
+ /** Redirect URI the device-issued authorization code is bound to. */
38
+ export const OPENAI_DEVICE_REDIRECT_URI =
39
+ "https://auth.openai.com/deviceauth/callback";
40
+
41
+ /** Fallback poll interval when the server omits or garbles `interval`. */
42
+ const DEFAULT_POLL_INTERVAL_SECONDS = 5;
43
+
44
+ /** Hard ceiling on a device authorization, regardless of `expires_at`. */
45
+ export const OPENAI_DEVICE_AUTH_MAX_LIFETIME_MS = 15 * 60 * 1000;
46
+
47
+ /** Error code the poll endpoint returns while the user has not yet approved. */
48
+ const AUTHORIZATION_PENDING_CODE = "deviceauth_authorization_pending";
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Types
52
+ // ---------------------------------------------------------------------------
53
+
54
+ export class DeviceAuthError extends Error {
55
+ /**
56
+ * Machine-readable failure code: the provider's `error.code` when it sends
57
+ * one, otherwise a locally assigned code (`expired_token`, `request_failed`,
58
+ * `aborted`).
59
+ */
60
+ readonly code: string;
61
+
62
+ constructor(message: string, code: string) {
63
+ super(message);
64
+ this.name = "DeviceAuthError";
65
+ this.code = code;
66
+ }
67
+ }
68
+
69
+ /** A minted device authorization, before the user has approved it. */
70
+ export interface DeviceAuthRequest {
71
+ deviceAuthId: string;
72
+ /** Short code the user types at `verificationUrl`. */
73
+ userCode: string;
74
+ verificationUrl: string;
75
+ /** ISO-8601 instant after which the code stops being accepted. */
76
+ expiresAt: string;
77
+ intervalSeconds: number;
78
+ }
79
+
80
+ /** The PKCE-bound authorization code the poll endpoint hands back. */
81
+ export interface DeviceAuthCode {
82
+ authorizationCode: string;
83
+ codeVerifier: string;
84
+ codeChallenge?: string;
85
+ }
86
+
87
+ export interface DeviceAuthOptions {
88
+ /** Injectable `fetch`, for tests. */
89
+ fetchImpl?: typeof fetch;
90
+ /** Injectable delay between polls, for tests. */
91
+ sleep?: (ms: number) => Promise<void>;
92
+ /** Injectable clock, for tests. */
93
+ now?: () => number;
94
+ signal?: AbortSignal;
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Step 1: mint a user code
99
+ // ---------------------------------------------------------------------------
100
+
101
+ export async function requestDeviceCode(
102
+ clientId: string,
103
+ options: DeviceAuthOptions = {},
104
+ ): Promise<DeviceAuthRequest> {
105
+ const doFetch = options.fetchImpl ?? fetch;
106
+
107
+ let response: Response;
108
+ try {
109
+ response = await doFetch(OPENAI_DEVICE_AUTH_USERCODE_URL, {
110
+ method: "POST",
111
+ headers: {
112
+ "Content-Type": "application/json",
113
+ Accept: "application/json",
114
+ },
115
+ body: JSON.stringify({ client_id: clientId }),
116
+ signal: options.signal,
117
+ });
118
+ } catch (err) {
119
+ throw new DeviceAuthError(
120
+ `Could not reach the device authorization endpoint: ${errorMessage(err)}`,
121
+ "request_failed",
122
+ );
123
+ }
124
+
125
+ if (!response.ok) {
126
+ const { code, message } = await readErrorBody(response);
127
+ throw new DeviceAuthError(
128
+ message ??
129
+ `Device authorization request failed (HTTP ${response.status})`,
130
+ code ?? "request_failed",
131
+ );
132
+ }
133
+
134
+ const data = (await response.json()) as Record<string, unknown>;
135
+ const deviceAuthId = asString(data.device_auth_id);
136
+ const userCode = asString(data.user_code);
137
+ if (!deviceAuthId || !userCode) {
138
+ throw new DeviceAuthError(
139
+ "Device authorization response is missing a device id or user code.",
140
+ "request_failed",
141
+ );
142
+ }
143
+
144
+ const expiresAt =
145
+ asString(data.expires_at) ??
146
+ new Date(
147
+ (options.now?.() ?? Date.now()) + OPENAI_DEVICE_AUTH_MAX_LIFETIME_MS,
148
+ ).toISOString();
149
+
150
+ return {
151
+ deviceAuthId,
152
+ userCode,
153
+ verificationUrl: OPENAI_DEVICE_VERIFICATION_URL,
154
+ expiresAt,
155
+ intervalSeconds: parseIntervalSeconds(data.interval),
156
+ };
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Step 2: poll until the user approves
161
+ // ---------------------------------------------------------------------------
162
+
163
+ /**
164
+ * Poll until the user approves the code, the code expires, or the provider
165
+ * reports a non-pending failure.
166
+ *
167
+ * The poll answers 403 (and occasionally 404) while approval is outstanding, so
168
+ * those statuses are only fatal when the body names an error code other than
169
+ * `deviceauth_authorization_pending`. Transport failures and 5xx/429 responses
170
+ * are retried until the deadline.
171
+ */
172
+ export async function pollForAuthorizationCode(
173
+ request: DeviceAuthRequest,
174
+ options: DeviceAuthOptions = {},
175
+ ): Promise<DeviceAuthCode> {
176
+ const doFetch = options.fetchImpl ?? fetch;
177
+ const doSleep = options.sleep ?? defaultSleep;
178
+ const now = options.now ?? (() => Date.now());
179
+
180
+ const intervalMs = request.intervalSeconds * 1000;
181
+ const deadline = resolveDeadline(request.expiresAt, now());
182
+
183
+ while (now() < deadline) {
184
+ assertNotAborted(options.signal);
185
+
186
+ let response: Response | undefined;
187
+ try {
188
+ response = await doFetch(OPENAI_DEVICE_AUTH_TOKEN_URL, {
189
+ method: "POST",
190
+ headers: {
191
+ "Content-Type": "application/json",
192
+ Accept: "application/json",
193
+ },
194
+ body: JSON.stringify({
195
+ device_auth_id: request.deviceAuthId,
196
+ user_code: request.userCode,
197
+ }),
198
+ signal: options.signal,
199
+ });
200
+ } catch (err) {
201
+ assertNotAborted(options.signal);
202
+ log.warn({ err: errorMessage(err) }, "Device auth poll failed, retrying");
203
+ }
204
+
205
+ if (response?.ok) {
206
+ const data = (await response.json()) as Record<string, unknown>;
207
+ const authorizationCode = asString(data.authorization_code);
208
+ const codeVerifier = asString(data.code_verifier);
209
+ if (!authorizationCode || !codeVerifier) {
210
+ throw new DeviceAuthError(
211
+ "Device authorization succeeded but returned no authorization code.",
212
+ "request_failed",
213
+ );
214
+ }
215
+ log.info("OpenAI device authorization approved");
216
+ const result: DeviceAuthCode = { authorizationCode, codeVerifier };
217
+ const codeChallenge = asString(data.code_challenge);
218
+ if (codeChallenge) {
219
+ result.codeChallenge = codeChallenge;
220
+ }
221
+ return result;
222
+ }
223
+
224
+ if (response && !isRetryableStatus(response.status)) {
225
+ const { code, message } = await readErrorBody(response);
226
+ // A pending approval answers 403/404 with the pending code; anything else
227
+ // is terminal, including the error raised when device-code authorization
228
+ // is disabled for the account.
229
+ if (code !== AUTHORIZATION_PENDING_CODE && (code || message)) {
230
+ throw new DeviceAuthError(
231
+ message ?? `Device authorization failed (HTTP ${response.status})`,
232
+ code ?? "request_failed",
233
+ );
234
+ }
235
+ }
236
+
237
+ await doSleep(intervalMs);
238
+ }
239
+
240
+ assertNotAborted(options.signal);
241
+ throw new DeviceAuthError(
242
+ "The device code expired before the sign-in was approved.",
243
+ "expired_token",
244
+ );
245
+ }
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Step 3: exchange the authorization code
249
+ // ---------------------------------------------------------------------------
250
+
251
+ /**
252
+ * Run steps 2 and 3: wait for approval, then exchange the device-issued
253
+ * authorization code and its PKCE verifier for OAuth tokens.
254
+ *
255
+ * The signal reaches the exchange too, and is checked once more after it
256
+ * returns: a cancellation that lands while the tokens are in flight must not
257
+ * hand the caller a set of credentials to store.
258
+ */
259
+ export async function completeDeviceAuth(
260
+ config: OAuth2Config,
261
+ request: DeviceAuthRequest,
262
+ options: DeviceAuthOptions = {},
263
+ ): Promise<OAuth2FlowResult> {
264
+ const { authorizationCode, codeVerifier } = await pollForAuthorizationCode(
265
+ request,
266
+ options,
267
+ );
268
+
269
+ const result = await exchangeCodeForTokens(
270
+ config,
271
+ authorizationCode,
272
+ OPENAI_DEVICE_REDIRECT_URI,
273
+ codeVerifier,
274
+ undefined,
275
+ { signal: options.signal },
276
+ );
277
+ assertNotAborted(options.signal);
278
+ return result;
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Helpers
283
+ // ---------------------------------------------------------------------------
284
+
285
+ /** `interval` arrives as a string of seconds; fall back when it is unusable. */
286
+ function parseIntervalSeconds(raw: unknown): number {
287
+ const parsed = Number(raw);
288
+ if (!Number.isFinite(parsed) || parsed <= 0) {
289
+ return DEFAULT_POLL_INTERVAL_SECONDS;
290
+ }
291
+ return parsed;
292
+ }
293
+
294
+ /** Stop at `expires_at`, never later than the hard lifetime ceiling. */
295
+ function resolveDeadline(expiresAt: string, startedAt: number): number {
296
+ const ceiling = startedAt + OPENAI_DEVICE_AUTH_MAX_LIFETIME_MS;
297
+ const parsed = Date.parse(expiresAt);
298
+ if (!Number.isFinite(parsed)) {
299
+ return ceiling;
300
+ }
301
+ return Math.min(parsed, ceiling);
302
+ }
303
+
304
+ function isRetryableStatus(status: number): boolean {
305
+ return status === 429 || status >= 500;
306
+ }
307
+
308
+ /**
309
+ * Pull `{ error: { code, message } }` off a failure body. Both fields are
310
+ * optional: an unparseable body reads as a transient failure so the caller
311
+ * keeps polling.
312
+ */
313
+ async function readErrorBody(
314
+ response: Response,
315
+ ): Promise<{ code?: string; message?: string }> {
316
+ const raw = await response.text().catch(() => "");
317
+ if (!raw) {
318
+ return {};
319
+ }
320
+ try {
321
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
322
+ const error = parsed.error;
323
+ if (error && typeof error === "object") {
324
+ const nested = error as Record<string, unknown>;
325
+ const result: { code?: string; message?: string } = {};
326
+ const code = asString(nested.code);
327
+ if (code) {
328
+ result.code = code;
329
+ }
330
+ const message = asString(nested.message);
331
+ if (message) {
332
+ result.message = message;
333
+ }
334
+ return result;
335
+ }
336
+ const code = asString(error);
337
+ return code ? { code } : {};
338
+ } catch {
339
+ return {};
340
+ }
341
+ }
342
+
343
+ function asString(value: unknown): string | undefined {
344
+ return typeof value === "string" && value.length > 0 ? value : undefined;
345
+ }
346
+
347
+ function errorMessage(err: unknown): string {
348
+ return err instanceof Error ? err.message : String(err);
349
+ }
350
+
351
+ function assertNotAborted(signal?: AbortSignal): void {
352
+ if (signal?.aborted) {
353
+ throw new DeviceAuthError("Device authorization was cancelled.", "aborted");
354
+ }
355
+ }
356
+
357
+ function defaultSleep(ms: number): Promise<void> {
358
+ return new Promise((resolve) => {
359
+ const timer = setTimeout(resolve, ms);
360
+ if (typeof timer === "object" && "unref" in timer) {
361
+ timer.unref();
362
+ }
363
+ });
364
+ }
@@ -156,6 +156,21 @@ export function setCesClient(client: CesClient | undefined): void {
156
156
  "CES client updated; resetting resolved credential backend cache",
157
157
  );
158
158
  _cesClientListener?.(client);
159
+ void attachCredentialRecordBackend(client);
160
+ }
161
+
162
+ async function attachCredentialRecordBackend(
163
+ client: CesClient | undefined,
164
+ ): Promise<void> {
165
+ const { CesRpcRecordBackend } = await import("./ces-rpc-record-backend.js");
166
+ const { setCredentialRecordBackend } = await import(
167
+ "../tools/credentials/metadata-store.js"
168
+ );
169
+ if (!client) {
170
+ setCredentialRecordBackend(undefined);
171
+ return;
172
+ }
173
+ setCredentialRecordBackend(new CesRpcRecordBackend(client));
159
174
  }
160
175
 
161
176
  /**
@@ -1,6 +1,10 @@
1
1
  import { getConfig } from "../config/loader.js";
2
2
  import type { SkillSource } from "../config/skills.js";
3
3
  import type { SkillInstallMeta } from "./install-meta.js";
4
+ import {
5
+ isSkillCompatibleWithPlatform,
6
+ type SkillPlatform,
7
+ } from "./platform-compatibility.js";
4
8
 
5
9
  /**
6
10
  * Plugin-facing read API over the skill surface: the locally installed
@@ -28,6 +32,8 @@ export interface ResolvedSkillEntry {
28
32
  avoidWhen?: string[];
29
33
  /** True when the skill is pinned into the memory selector pool every turn. */
30
34
  alwaysCandidate?: boolean;
35
+ /** Host operating systems on which this skill may be used. */
36
+ platforms?: SkillPlatform[];
31
37
  /** True for locally installed skills; false for remote catalog entries. */
32
38
  installed: boolean;
33
39
  /** Where the installed skill comes from. Unset for remote catalog entries. */
@@ -77,6 +83,7 @@ export async function listInstalledSkills(): Promise<ResolvedSkillEntry[]> {
77
83
  activationHints: summary.activationHints,
78
84
  avoidWhen: summary.avoidWhen,
79
85
  alwaysCandidate: summary.alwaysCandidate,
86
+ platforms: summary.platforms,
80
87
  installed: true,
81
88
  source: summary.source,
82
89
  state: stateById.get(summary.id) ?? "unavailable",
@@ -121,14 +128,16 @@ export async function listCatalogSkills(): Promise<ResolvedSkillEntry[]> {
121
128
  typeof flagKey === "string" &&
122
129
  flagKey.length > 0 &&
123
130
  !isAssistantFeatureFlagEnabled(flagKey, config);
131
+ const incompatible = !isSkillCompatibleWithPlatform(entry);
124
132
  return {
125
133
  id: entry.id,
126
134
  displayName: entry.metadata?.vellum?.["display-name"] ?? entry.name,
127
135
  description: entry.description,
128
136
  activationHints: entry.metadata?.vellum?.["activation-hints"],
129
137
  avoidWhen: entry.metadata?.vellum?.["avoid-when"],
138
+ platforms: entry.platforms,
130
139
  installed: false,
131
- state: gated ? "unavailable" : "available",
140
+ state: gated || incompatible ? "unavailable" : "available",
132
141
  };
133
142
  });
134
143
  }
@@ -19,6 +19,12 @@ import { loadSkillCatalog } from "../config/skills.js";
19
19
  import { getLogger } from "../util/logger.js";
20
20
  import { getWorkspaceSkillsDir } from "../util/platform.js";
21
21
  import { computeSkillHash, writeInstallMeta } from "./install-meta.js";
22
+ import {
23
+ isSkillCompatibleWithPlatform,
24
+ normalizeSkillPlatforms,
25
+ type SkillPlatform,
26
+ skillPlatformUnavailableMessage,
27
+ } from "./platform-compatibility.js";
22
28
 
23
29
  const log = getLogger("catalog-install");
24
30
 
@@ -33,6 +39,7 @@ export interface CatalogSkill {
33
39
  includes?: string[];
34
40
  version?: string;
35
41
  updatedAt?: string;
42
+ platforms?: SkillPlatform[];
36
43
  metadata?: {
37
44
  icon?: string;
38
45
  emoji?: string;
@@ -42,6 +49,7 @@ export interface CatalogSkill {
42
49
  "avoid-when"?: string[];
43
50
  "feature-flag"?: string;
44
51
  category?: string;
52
+ platforms?: SkillPlatform[];
45
53
  };
46
54
  };
47
55
  }
@@ -111,6 +119,7 @@ interface RawCatalogEntry {
111
119
  updatedAt?: unknown;
112
120
  display_name?: unknown;
113
121
  category?: unknown;
122
+ platforms?: unknown;
114
123
  updated_at?: unknown;
115
124
  metadata?: CatalogSkill["metadata"];
116
125
  }
@@ -144,6 +153,9 @@ function normalizeCatalogEntry(raw: unknown): CatalogSkill | null {
144
153
  const displayName = nested?.["display-name"] ?? asStr(entry.display_name);
145
154
  const icon = asStr(entry.icon) ?? asStr(entry.metadata?.icon);
146
155
  const updatedAt = asStr(entry.updatedAt) ?? asStr(entry.updated_at);
156
+ const platforms = normalizeSkillPlatforms(
157
+ nested?.platforms ?? entry.platforms,
158
+ );
147
159
 
148
160
  return {
149
161
  id,
@@ -154,6 +166,7 @@ function normalizeCatalogEntry(raw: unknown): CatalogSkill | null {
154
166
  ...(entry.includes ? { includes: entry.includes } : {}),
155
167
  ...(entry.version ? { version: entry.version } : {}),
156
168
  ...(updatedAt ? { updatedAt } : {}),
169
+ ...(platforms ? { platforms } : {}),
157
170
  metadata: {
158
171
  ...entry.metadata,
159
172
  ...(icon ? { icon } : {}),
@@ -161,6 +174,7 @@ function normalizeCatalogEntry(raw: unknown): CatalogSkill | null {
161
174
  ...nested,
162
175
  ...(displayName ? { "display-name": displayName } : {}),
163
176
  ...(category ? { category } : {}),
177
+ ...(platforms ? { platforms } : {}),
164
178
  },
165
179
  },
166
180
  };
@@ -610,7 +624,8 @@ export async function resolveCatalog(
610
624
  /**
611
625
  * Attempt to find and install a skill from the first-party catalog.
612
626
  * Returns true if the skill was installed, false if not found in catalog.
613
- * Throws on install failures (network, filesystem, etc).
627
+ * Throws when the skill is unsupported on this host or on install failures
628
+ * (network, filesystem, etc).
614
629
  *
615
630
  * When `catalog` is provided it is used directly, avoiding a redundant
616
631
  * network fetch — pass a pre-resolved catalog when calling in a loop.
@@ -639,6 +654,9 @@ export async function autoInstallFromCatalog(
639
654
  if (!entry) {
640
655
  return false;
641
656
  }
657
+ if (!isSkillCompatibleWithPlatform(entry)) {
658
+ throw new Error(skillPlatformUnavailableMessage(skillId, entry));
659
+ }
642
660
 
643
661
  // If the skill already exists on disk, reuse it instead of attempting a
644
662
  // fresh install that would fail.
@@ -25,6 +25,10 @@ import { spawn } from "node:child_process";
25
25
 
26
26
  import { buildSanitizedEnv } from "../tools/terminal/safe-env.js";
27
27
  import { stripAnsiSequences } from "../util/ansi.js";
28
+ import {
29
+ buildShellInvocation,
30
+ terminateProcessTree,
31
+ } from "../util/host-process.js";
28
32
  import { getLogger } from "../util/logger.js";
29
33
 
30
34
  const log = getLogger("inline-command-runner");
@@ -99,7 +103,7 @@ export async function runInlineCommand(
99
103
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
100
104
  const maxChars = options?.maxOutputChars ?? MAX_OUTPUT_CHARS;
101
105
 
102
- const wrapped = { command: "bash", args: ["-c", "--", command] };
106
+ const wrapped = buildShellInvocation(command);
103
107
 
104
108
  // Build a minimal, sanitized environment. Explicitly exclude gateway URL,
105
109
  // workspace dir, and data dir since inline commands have no business calling
@@ -135,7 +139,7 @@ export async function runInlineCommand(
135
139
 
136
140
  const timer = setTimeout(() => {
137
141
  timedOut = true;
138
- child.kill("SIGKILL");
142
+ terminateProcessTree(child);
139
143
  }, timeoutMs);
140
144
 
141
145
  child.stdout!.on("data", (data: Buffer) => {
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ filterSkillsByPlatform,
5
+ isSkillCompatibleWithPlatform,
6
+ normalizeSkillPlatforms,
7
+ skillPlatformForNodePlatform,
8
+ } from "./platform-compatibility.js";
9
+
10
+ describe("skill platform compatibility", () => {
11
+ test("maps Node platforms to skill platform names", () => {
12
+ expect(skillPlatformForNodePlatform("darwin")).toBe("macos");
13
+ expect(skillPlatformForNodePlatform("win32")).toBe("windows");
14
+ expect(skillPlatformForNodePlatform("linux")).toBe("linux");
15
+ expect(skillPlatformForNodePlatform("freebsd")).toBeNull();
16
+ });
17
+
18
+ test("treats missing platform metadata as portable", () => {
19
+ expect(isSkillCompatibleWithPlatform({}, "win32")).toBe(true);
20
+ expect(isSkillCompatibleWithPlatform({}, "linux")).toBe(true);
21
+ });
22
+
23
+ test("matches declared host platforms", () => {
24
+ const skill = { platforms: ["macos", "linux"] as const };
25
+ expect(isSkillCompatibleWithPlatform(skill, "darwin")).toBe(true);
26
+ expect(isSkillCompatibleWithPlatform(skill, "linux")).toBe(true);
27
+ expect(isSkillCompatibleWithPlatform(skill, "win32")).toBe(false);
28
+ });
29
+
30
+ test("filters incompatible skills from offer surfaces", () => {
31
+ const skills = [
32
+ { id: "portable" },
33
+ { id: "mac-only", platforms: ["macos"] as const },
34
+ { id: "windows-only", platforms: ["windows"] as const },
35
+ ];
36
+
37
+ expect(
38
+ filterSkillsByPlatform(skills, "win32").map((skill) => skill.id),
39
+ ).toEqual(["portable", "windows-only"]);
40
+ });
41
+
42
+ test("normalizes valid unique metadata values", () => {
43
+ expect(normalizeSkillPlatforms(["windows", "windows", "linux"])).toEqual([
44
+ "windows",
45
+ "linux",
46
+ ]);
47
+ expect(normalizeSkillPlatforms(["android", 42])).toBeUndefined();
48
+ });
49
+ });
@@ -0,0 +1,65 @@
1
+ export const SKILL_PLATFORM_VALUES = ["macos", "windows", "linux"] as const;
2
+
3
+ export type SkillPlatform = (typeof SKILL_PLATFORM_VALUES)[number];
4
+
5
+ export interface PlatformScopedSkill {
6
+ platforms?: readonly SkillPlatform[];
7
+ }
8
+
9
+ export function normalizeSkillPlatforms(
10
+ value: unknown,
11
+ ): SkillPlatform[] | undefined {
12
+ if (!Array.isArray(value)) {
13
+ return undefined;
14
+ }
15
+ const platforms = [
16
+ ...new Set(
17
+ value.filter((platform): platform is SkillPlatform =>
18
+ (SKILL_PLATFORM_VALUES as readonly unknown[]).includes(platform),
19
+ ),
20
+ ),
21
+ ];
22
+ return platforms.length > 0 ? platforms : undefined;
23
+ }
24
+
25
+ export function skillPlatformForNodePlatform(
26
+ platform: NodeJS.Platform,
27
+ ): SkillPlatform | null {
28
+ if (platform === "darwin") {
29
+ return "macos";
30
+ }
31
+ if (platform === "win32") {
32
+ return "windows";
33
+ }
34
+ if (platform === "linux") {
35
+ return "linux";
36
+ }
37
+ return null;
38
+ }
39
+
40
+ export function isSkillCompatibleWithPlatform(
41
+ skill: PlatformScopedSkill,
42
+ platform: NodeJS.Platform = process.platform,
43
+ ): boolean {
44
+ if (!skill.platforms || skill.platforms.length === 0) {
45
+ return true;
46
+ }
47
+ const skillPlatform = skillPlatformForNodePlatform(platform);
48
+ return skillPlatform !== null && skill.platforms.includes(skillPlatform);
49
+ }
50
+
51
+ export function filterSkillsByPlatform<T extends PlatformScopedSkill>(
52
+ skills: readonly T[],
53
+ platform: NodeJS.Platform = process.platform,
54
+ ): T[] {
55
+ return skills.filter((skill) =>
56
+ isSkillCompatibleWithPlatform(skill, platform),
57
+ );
58
+ }
59
+
60
+ export function skillPlatformUnavailableMessage(
61
+ skillId: string,
62
+ skill: PlatformScopedSkill,
63
+ ): string {
64
+ return `Skill "${skillId}" is unavailable on this operating system. Supported platforms: ${(skill.platforms ?? []).join(", ")}.`;
65
+ }
@@ -1,9 +1,11 @@
1
1
  import { readFileSync } from "node:fs";
2
2
 
3
+ import { CLIENT_OS_VALUES, type ClientOs } from "../channels/types.js";
3
4
  import type { SkillToolEntry, SkillToolManifest } from "../config/skills.js";
4
5
 
5
6
  const VALID_RISK_LEVELS = new Set(["low", "medium", "high"]);
6
7
  const VALID_EXECUTION_TARGETS = new Set(["host", "sandbox"]);
8
+ const VALID_CLIENT_OS_VALUES = new Set<string>(CLIENT_OS_VALUES);
7
9
 
8
10
  /**
9
11
  * Parse and validate a raw TOOLS.json payload into a typed SkillToolManifest.
@@ -137,6 +139,23 @@ function parseToolEntry(raw: unknown, prefix: string): SkillToolEntry {
137
139
  const execution_target =
138
140
  entry.execution_target as SkillToolEntry["execution_target"];
139
141
 
142
+ let supported_client_os: ClientOs[] | undefined;
143
+ if ("supported_client_os" in entry) {
144
+ if (
145
+ !Array.isArray(entry.supported_client_os) ||
146
+ entry.supported_client_os.length === 0 ||
147
+ entry.supported_client_os.some(
148
+ (value) =>
149
+ typeof value !== "string" || !VALID_CLIENT_OS_VALUES.has(value),
150
+ )
151
+ ) {
152
+ throw new Error(
153
+ `${prefix}: "supported_client_os" must be a non-empty array of known client OS values`,
154
+ );
155
+ }
156
+ supported_client_os = [...new Set(entry.supported_client_os as ClientOs[])];
157
+ }
158
+
140
159
  return {
141
160
  name,
142
161
  description,
@@ -145,6 +164,7 @@ function parseToolEntry(raw: unknown, prefix: string): SkillToolEntry {
145
164
  input_schema,
146
165
  executor,
147
166
  execution_target,
167
+ supported_client_os,
148
168
  };
149
169
  }
150
170