@vellumai/assistant 0.4.53 → 0.4.55

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 (255) hide show
  1. package/bun.lock +62 -349
  2. package/docs/architecture/integrations.md +1 -1
  3. package/docs/architecture/keychain-broker.md +94 -29
  4. package/docs/architecture/security.md +2 -2
  5. package/knip.json +7 -29
  6. package/package.json +2 -9
  7. package/src/__tests__/agent-loop.test.ts +1 -1
  8. package/src/__tests__/app-git-history.test.ts +0 -2
  9. package/src/__tests__/app-git-service.test.ts +1 -6
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/avatar-e2e.test.ts +0 -1
  12. package/src/__tests__/browser-fill-credential.test.ts +1 -6
  13. package/src/__tests__/call-domain.test.ts +0 -1
  14. package/src/__tests__/call-routes-http.test.ts +0 -1
  15. package/src/__tests__/channel-guardian.test.ts +4 -4
  16. package/src/__tests__/channel-readiness-routes.test.ts +0 -1
  17. package/src/__tests__/channel-readiness-service.test.ts +0 -1
  18. package/src/__tests__/checker.test.ts +13 -11
  19. package/src/__tests__/claude-code-skill-regression.test.ts +0 -1
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +1 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +0 -3
  22. package/src/__tests__/config-schema.test.ts +3 -9
  23. package/src/__tests__/config-watcher.test.ts +11 -3
  24. package/src/__tests__/credential-broker-browser-fill.test.ts +27 -24
  25. package/src/__tests__/credential-broker-server-use.test.ts +60 -24
  26. package/src/__tests__/credential-security-e2e.test.ts +1 -6
  27. package/src/__tests__/credential-security-invariants.test.ts +13 -8
  28. package/src/__tests__/credential-vault-unit.test.ts +28 -12
  29. package/src/__tests__/credential-vault.test.ts +40 -28
  30. package/src/__tests__/credentials-cli.test.ts +1 -21
  31. package/src/__tests__/email-invite-adapter.test.ts +0 -1
  32. package/src/__tests__/fixtures/credential-security-fixtures.ts +3 -3
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -79
  34. package/src/__tests__/gateway-only-enforcement.test.ts +1 -21
  35. package/src/__tests__/guardian-action-conversation-turn.test.ts +8 -8
  36. package/src/__tests__/guardian-action-late-reply.test.ts +13 -14
  37. package/src/__tests__/guardian-action-store.test.ts +0 -57
  38. package/src/__tests__/guardian-outbound-http.test.ts +1 -1
  39. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -3
  40. package/src/__tests__/hooks-blocking.test.ts +1 -1
  41. package/src/__tests__/hooks-config.test.ts +5 -29
  42. package/src/__tests__/hooks-discovery.test.ts +1 -1
  43. package/src/__tests__/hooks-integration.test.ts +1 -1
  44. package/src/__tests__/hooks-manager.test.ts +1 -1
  45. package/src/__tests__/hooks-runner.test.ts +1 -23
  46. package/src/__tests__/hooks-settings.test.ts +1 -1
  47. package/src/__tests__/hooks-templates.test.ts +1 -1
  48. package/src/__tests__/integration-status.test.ts +0 -1
  49. package/src/__tests__/invite-routes-http.test.ts +0 -3
  50. package/src/__tests__/list-messages-attachments.test.ts +4 -4
  51. package/src/__tests__/llm-usage-store.test.ts +50 -0
  52. package/src/__tests__/managed-proxy-context.test.ts +41 -41
  53. package/src/__tests__/media-generate-image.test.ts +2 -2
  54. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -6
  55. package/src/__tests__/memory-regressions.experimental.test.ts +4 -4
  56. package/src/__tests__/memory-regressions.test.ts +27 -27
  57. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  58. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -4
  59. package/src/__tests__/notification-decision-fallback.test.ts +1 -1
  60. package/src/__tests__/oauth-cli.test.ts +1 -4
  61. package/src/__tests__/oauth-store.test.ts +1 -3
  62. package/src/__tests__/openai-provider.test.ts +7 -7
  63. package/src/__tests__/platform.test.ts +14 -4
  64. package/src/__tests__/pricing.test.ts +0 -223
  65. package/src/__tests__/provider-commit-message-generator.test.ts +1 -4
  66. package/src/__tests__/provider-fail-open-selection.test.ts +58 -54
  67. package/src/__tests__/provider-managed-proxy-integration.test.ts +63 -63
  68. package/src/__tests__/provider-registry-ollama.test.ts +3 -3
  69. package/src/__tests__/public-ingress-urls.test.ts +1 -1
  70. package/src/__tests__/registry.test.ts +3 -103
  71. package/src/__tests__/script-proxy-injection-runtime.test.ts +2 -7
  72. package/src/__tests__/secret-onetime-send.test.ts +1 -6
  73. package/src/__tests__/secret-routes-managed-proxy.test.ts +6 -13
  74. package/src/__tests__/secure-keys.test.ts +241 -229
  75. package/src/__tests__/session-abort-tool-results.test.ts +0 -1
  76. package/src/__tests__/session-confirmation-signals.test.ts +0 -1
  77. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -7
  78. package/src/__tests__/session-pre-run-repair.test.ts +0 -1
  79. package/src/__tests__/session-provider-retry-repair.test.ts +0 -1
  80. package/src/__tests__/session-queue.test.ts +2 -4
  81. package/src/__tests__/session-slash-known.test.ts +0 -1
  82. package/src/__tests__/session-slash-queue.test.ts +0 -1
  83. package/src/__tests__/session-slash-unknown.test.ts +0 -1
  84. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  85. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  86. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  87. package/src/__tests__/slack-channel-config.test.ts +1 -7
  88. package/src/__tests__/swarm-recursion.test.ts +0 -1
  89. package/src/__tests__/swarm-session-integration.test.ts +0 -1
  90. package/src/__tests__/swarm-tool.test.ts +0 -1
  91. package/src/__tests__/task-compiler.test.ts +1 -1
  92. package/src/__tests__/test-support/browser-skill-harness.ts +0 -18
  93. package/src/__tests__/test-support/computer-use-skill-harness.ts +0 -23
  94. package/src/__tests__/tool-executor.test.ts +1 -1
  95. package/src/__tests__/trust-store.test.ts +3 -82
  96. package/src/__tests__/twilio-config.test.ts +0 -1
  97. package/src/__tests__/twilio-provider.test.ts +0 -5
  98. package/src/__tests__/twilio-routes.test.ts +0 -1
  99. package/src/__tests__/usage-cache-backfill-migration.test.ts +10 -10
  100. package/src/calls/guardian-question-copy.ts +1 -1
  101. package/src/cli/commands/bash.ts +3 -0
  102. package/src/cli/commands/doctor.ts +10 -34
  103. package/src/cli/commands/memory.ts +3 -5
  104. package/src/cli/commands/sessions.ts +1 -1
  105. package/src/cli/commands/usage.ts +359 -0
  106. package/src/cli/http-client.ts +22 -12
  107. package/src/cli/program.ts +2 -0
  108. package/src/cli/reference.ts +1 -0
  109. package/src/cli.ts +251 -181
  110. package/src/config/assistant-feature-flags.ts +0 -7
  111. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  112. package/src/config/bundled-skills/claude-code/SKILL.md +1 -1
  113. package/src/config/bundled-skills/claude-code/TOOLS.json +1 -1
  114. package/src/config/bundled-skills/gmail/SKILL.md +0 -1
  115. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  116. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  117. package/src/config/bundled-skills/messaging/SKILL.md +0 -1
  118. package/src/config/bundled-skills/sequences/SKILL.md +0 -1
  119. package/src/config/env.ts +13 -0
  120. package/src/config/feature-flag-registry.json +9 -41
  121. package/src/config/schemas/security.ts +1 -2
  122. package/src/config/skills.ts +1 -1
  123. package/src/contacts/contact-store.ts +0 -50
  124. package/src/daemon/approved-devices-store.ts +0 -44
  125. package/src/daemon/classifier.ts +1 -1
  126. package/src/daemon/config-watcher.ts +14 -8
  127. package/src/daemon/handlers/config-model.ts +1 -1
  128. package/src/daemon/handlers/sessions.ts +4 -116
  129. package/src/daemon/handlers/skills.ts +1 -1
  130. package/src/daemon/lifecycle.ts +13 -15
  131. package/src/daemon/providers-setup.ts +1 -1
  132. package/src/daemon/server.ts +20 -3
  133. package/src/daemon/session-slash.ts +2 -2
  134. package/src/daemon/shutdown-handlers.ts +15 -0
  135. package/src/daemon/watch-handler.ts +2 -2
  136. package/src/email/guardrails.ts +1 -1
  137. package/src/email/service.ts +0 -5
  138. package/src/hooks/templates.ts +1 -1
  139. package/src/media/app-icon-generator.ts +2 -2
  140. package/src/media/avatar-router.ts +2 -2
  141. package/src/media/gemini-image-service.ts +5 -5
  142. package/src/memory/admin.ts +2 -2
  143. package/src/memory/app-git-service.ts +0 -7
  144. package/src/memory/conversation-crud.ts +1 -1
  145. package/src/memory/conversation-title-service.ts +2 -2
  146. package/src/memory/embedding-backend.ts +30 -26
  147. package/src/memory/external-conversation-store.ts +0 -30
  148. package/src/memory/guardian-action-store.ts +0 -31
  149. package/src/memory/guardian-approvals.ts +1 -56
  150. package/src/memory/indexer.ts +4 -3
  151. package/src/memory/items-extractor.ts +1 -1
  152. package/src/memory/job-handlers/backfill.ts +5 -2
  153. package/src/memory/job-handlers/index-maintenance.ts +2 -2
  154. package/src/memory/job-handlers/media-processing.ts +2 -2
  155. package/src/memory/job-handlers/summarization.ts +1 -1
  156. package/src/memory/job-utils.ts +1 -2
  157. package/src/memory/jobs-worker.ts +2 -2
  158. package/src/memory/llm-usage-store.ts +57 -11
  159. package/src/memory/media-store.ts +4 -535
  160. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +2 -2
  161. package/src/memory/migrations/110-channel-guardian.ts +0 -1
  162. package/src/memory/published-pages-store.ts +0 -83
  163. package/src/memory/qdrant-circuit-breaker.ts +0 -8
  164. package/src/memory/retriever.ts +1 -1
  165. package/src/memory/schema/calls.ts +0 -67
  166. package/src/memory/search/semantic.ts +1 -8
  167. package/src/memory/shared-app-links-store.ts +0 -15
  168. package/src/messaging/registry.ts +0 -5
  169. package/src/messaging/style-analyzer.ts +1 -1
  170. package/src/notifications/copy-composer.ts +5 -13
  171. package/src/notifications/decision-engine.ts +2 -2
  172. package/src/notifications/deliveries-store.ts +0 -39
  173. package/src/notifications/guardian-question-mode.ts +6 -10
  174. package/src/notifications/preference-extractor.ts +1 -1
  175. package/src/oauth/byo-connection.test.ts +29 -20
  176. package/src/oauth/provider-behaviors.ts +1 -1
  177. package/src/permissions/checker.ts +1 -1
  178. package/src/permissions/shell-identity.ts +0 -5
  179. package/src/permissions/trust-store.ts +0 -37
  180. package/src/prompts/system-prompt.ts +4 -4
  181. package/src/prompts/templates/SOUL.md +1 -1
  182. package/src/providers/managed-proxy/constants.ts +8 -10
  183. package/src/providers/managed-proxy/context.ts +14 -9
  184. package/src/providers/provider-send-message.ts +4 -52
  185. package/src/providers/registry.ts +16 -50
  186. package/src/runtime/actor-token-store.ts +0 -23
  187. package/src/runtime/auth/__tests__/guard-tests.test.ts +64 -0
  188. package/src/runtime/http-router.ts +5 -1
  189. package/src/runtime/http-server.ts +101 -4
  190. package/src/runtime/invite-instruction-generator.ts +25 -51
  191. package/src/runtime/invite-service.ts +0 -20
  192. package/src/runtime/routes/attachment-routes.ts +1 -1
  193. package/src/runtime/routes/brain-graph-routes.ts +1 -1
  194. package/src/runtime/routes/call-routes.ts +1 -1
  195. package/src/runtime/routes/conversation-routes.ts +32 -11
  196. package/src/runtime/routes/debug-routes.ts +1 -1
  197. package/src/runtime/routes/diagnostics-routes.ts +2 -2
  198. package/src/runtime/routes/documents-routes.ts +3 -3
  199. package/src/runtime/routes/global-search-routes.ts +1 -1
  200. package/src/runtime/routes/guardian-bootstrap-routes.ts +0 -20
  201. package/src/runtime/routes/guardian-refresh-routes.ts +0 -20
  202. package/src/runtime/routes/secret-routes.ts +4 -4
  203. package/src/runtime/routes/session-management-routes.ts +27 -0
  204. package/src/runtime/routes/trust-rules-routes.ts +1 -1
  205. package/src/security/credential-backend.ts +148 -0
  206. package/src/security/oauth2.ts +1 -1
  207. package/src/security/secret-allowlist.ts +1 -1
  208. package/src/security/secure-keys.ts +98 -160
  209. package/src/security/token-manager.ts +0 -7
  210. package/src/sequence/guardrails.ts +0 -4
  211. package/src/sequence/store.ts +1 -20
  212. package/src/sequence/types.ts +1 -36
  213. package/src/signals/bash.ts +33 -0
  214. package/src/signals/cancel.ts +69 -0
  215. package/src/signals/conversation-undo.ts +127 -0
  216. package/src/signals/trust-rule.ts +174 -0
  217. package/src/skills/clawhub.ts +5 -5
  218. package/src/skills/managed-store.ts +4 -4
  219. package/src/subagent/manager.ts +8 -1
  220. package/src/telemetry/usage-telemetry-reporter.test.ts +366 -0
  221. package/src/telemetry/usage-telemetry-reporter.ts +181 -0
  222. package/src/tools/claude-code/claude-code.ts +2 -2
  223. package/src/tools/credentials/vault.ts +8 -4
  224. package/src/tools/memory/handlers.test.ts +24 -26
  225. package/src/tools/memory/handlers.ts +1 -13
  226. package/src/tools/registry.ts +5 -100
  227. package/src/tools/terminal/parser.ts +34 -4
  228. package/src/tools/tool-manifest.ts +0 -10
  229. package/src/usage/actors.ts +0 -12
  230. package/src/util/canonicalize-identity.ts +0 -9
  231. package/src/util/errors.ts +0 -3
  232. package/src/util/platform.ts +24 -7
  233. package/src/util/pricing.ts +0 -38
  234. package/src/watcher/constants.ts +0 -7
  235. package/src/watcher/providers/linear.ts +1 -1
  236. package/src/work-items/work-item-store.ts +4 -4
  237. package/src/workspace/commit-message-provider.ts +1 -1
  238. package/src/workspace/git-service.ts +44 -1
  239. package/src/workspace/provider-commit-message-generator.ts +1 -1
  240. package/src/__tests__/fixtures/proxy-fixtures.ts +0 -147
  241. package/src/browser-extension-relay/client.ts +0 -155
  242. package/src/contacts/index.ts +0 -18
  243. package/src/daemon/tls-certs.ts +0 -270
  244. package/src/errors.ts +0 -41
  245. package/src/events/index.ts +0 -18
  246. package/src/followups/index.ts +0 -10
  247. package/src/playbooks/index.ts +0 -10
  248. package/src/runtime/auth/index.ts +0 -44
  249. package/src/tasks/candidate-store.ts +0 -95
  250. package/src/tools/browser/api-map.ts +0 -313
  251. package/src/tools/browser/auto-navigate.ts +0 -469
  252. package/src/tools/browser/headless-browser.ts +0 -590
  253. package/src/tools/browser/recording-store.ts +0 -75
  254. package/src/tools/computer-use/registry.ts +0 -21
  255. package/src/tools/tasks/index.ts +0 -27
@@ -466,7 +466,7 @@ function buildToolPermissionSection(): string {
466
466
  "",
467
467
  "### Always-Available Tools (No Approval Required)",
468
468
  "",
469
- "- **file_read** on your workspace directory — You can freely read any file under your `.vellum` workspace at any time. Use this proactively to check files, load context, and inform your responses without asking. **Always use `file_read` for workspace files (IDENTITY.md, USER.md, SOUL.md, etc.), never `host_file_read`.**",
469
+ "- **file_read** on your workspace directory — You can freely read any file under your `.vellum` workspace at any time. Use this proactively to check files, load context, and inform your responses without asking. **Always use `file_read` for workspace files, never `host_file_read`.** Note: your core prompt files (IDENTITY.md, SOUL.md, USER.md) are already loaded into your system prompt — no need to re-read them at the start of a conversation.",
470
470
  "- **web_search** — You can search the web at any time without approval. Use this to look up documentation, current information, or anything you need.",
471
471
  ].join("\n");
472
472
  }
@@ -706,7 +706,7 @@ function buildLearningMemorySection(): string {
706
706
  "",
707
707
  "When you make a mistake, hit a dead end, or discover something non-obvious, save it to memory so you don't repeat it.",
708
708
  "",
709
- 'Use `memory_manage` with `op: "save", kind: "learning"` for:',
709
+ 'Use `memory_manage` with `op: "save", kind: "constraint"` for:',
710
710
  "- **Mistakes and corrections** — wrong assumptions, failed approaches, gotchas you ran into",
711
711
  "- **Discoveries** — undocumented behaviors, surprising API quirks, things that weren't obvious",
712
712
  "- **Working solutions** — the approach that actually worked after trial and error",
@@ -715,8 +715,8 @@ function buildLearningMemorySection(): string {
715
715
  "The statement should capture both what happened and the takeaway. Write it as advice to your future self.",
716
716
  "",
717
717
  "Examples:",
718
- '- `memory_manage({ op: "save", kind: "learning", subject: "macOS Shortcuts CLI", statement: "shortcuts CLI requires full disk access to export shortcuts — if permission is denied, guide the user to grant it in System Settings rather than retrying." })`',
719
- '- `memory_manage({ op: "save", kind: "learning", subject: "Gmail API pagination", statement: "Gmail search returns max 100 results per page. Always check nextPageToken and loop if the user asks for \'all\' messages." })`',
718
+ '- `memory_manage({ op: "save", kind: "constraint", subject: "macOS Shortcuts CLI", statement: "shortcuts CLI requires full disk access to export shortcuts — if permission is denied, guide the user to grant it in System Settings rather than retrying." })`',
719
+ '- `memory_manage({ op: "save", kind: "constraint", subject: "Gmail API pagination", statement: "Gmail search returns max 100 results per page. Always check nextPageToken and loop if the user asks for \'all\' messages." })`',
720
720
  "",
721
721
  "Don't overthink it. If you catch yourself thinking \"I'll remember that for next time,\" save it.",
722
722
  ].join("\n");
@@ -34,7 +34,7 @@ You are your own entity, with an identity that’s still forming. You can have t
34
34
 
35
35
  ## Continuity
36
36
 
37
- Each session, you wake up fresh. Your workspace files and saved memories are your continuity — they're how you persist across conversations. Read them. Update them. Save what matters.
37
+ Each session, you wake up fresh. Your workspace files and saved memories are your continuity — they're how you persist across conversations. Your core prompt files (IDENTITY.md, SOUL.md, USER.md) are already loaded into this conversation — you don't need to read them again. Update them when things change. Save what matters.
38
38
 
39
39
  If you change this file, tell your user — it's your soul, and they should know.
40
40
 
@@ -19,12 +19,16 @@ export interface ManagedProviderMeta {
19
19
  * Explicit provider metadata for all known providers.
20
20
  * Managed providers get a deterministic proxy path; non-managed providers
21
21
  * are marked accordingly and have no proxy path.
22
+ *
23
+ * This table describes managed proxy routing capability only. It does not
24
+ * control which providers auto-bootstrap into the text-model registry when
25
+ * managed credentials are present; that policy lives in the registry/context
26
+ * fallback allowlists.
22
27
  */
23
28
  export const MANAGED_PROVIDER_META: Record<string, ManagedProviderMeta> = {
24
29
  openai: {
25
30
  name: "openai",
26
- managed: true,
27
- proxyPath: "/v1/runtime-proxy/openai",
31
+ managed: false,
28
32
  },
29
33
  anthropic: {
30
34
  name: "anthropic",
@@ -38,13 +42,11 @@ export const MANAGED_PROVIDER_META: Record<string, ManagedProviderMeta> = {
38
42
  },
39
43
  fireworks: {
40
44
  name: "fireworks",
41
- managed: true,
42
- proxyPath: "/v1/runtime-proxy/fireworks",
45
+ managed: false,
43
46
  },
44
47
  openrouter: {
45
48
  name: "openrouter",
46
- managed: true,
47
- proxyPath: "/v1/runtime-proxy/openrouter",
49
+ managed: false,
48
50
  },
49
51
  vertex: {
50
52
  name: "vertex",
@@ -54,7 +56,3 @@ export const MANAGED_PROVIDER_META: Record<string, ManagedProviderMeta> = {
54
56
  ollama: { name: "ollama", managed: false },
55
57
  };
56
58
 
57
- /** Provider names that support managed proxy routing. */
58
- export const MANAGED_PROVIDER_NAMES = Object.entries(MANAGED_PROVIDER_META)
59
- .filter(([, meta]) => meta.managed)
60
- .map(([name]) => name);
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { getPlatformBaseUrl } from "../../config/env.js";
13
13
  import { credentialKey } from "../../security/credential-key.js";
14
- import { getSecureKey } from "../../security/secure-keys.js";
14
+ import { getSecureKeyAsync } from "../../security/secure-keys.js";
15
15
  import { MANAGED_PROVIDER_META } from "./constants.js";
16
16
 
17
17
  /** Storage key for the assistant API key credential. */
@@ -35,9 +35,10 @@ export interface ManagedProxyContext {
35
35
  * Returns an enabled context only when both the platform base URL and
36
36
  * the assistant API key are present. Otherwise returns a disabled context.
37
37
  */
38
- export function resolveManagedProxyContext(): ManagedProxyContext {
38
+ export async function resolveManagedProxyContext(): Promise<ManagedProxyContext> {
39
39
  const platformBaseUrl = getPlatformBaseUrl().replace(/\/+$/, "");
40
- const assistantApiKey = getSecureKey(ASSISTANT_API_KEY_STORAGE_KEY) ?? "";
40
+ const assistantApiKey =
41
+ (await getSecureKeyAsync(ASSISTANT_API_KEY_STORAGE_KEY)) ?? "";
41
42
 
42
43
  const enabled = !!platformBaseUrl && !!assistantApiKey;
43
44
 
@@ -48,8 +49,8 @@ export function resolveManagedProxyContext(): ManagedProxyContext {
48
49
  * Check whether managed proxy prerequisites are available.
49
50
  * Shorthand for checking that both platform URL and assistant API key exist.
50
51
  */
51
- export function hasManagedProxyPrereqs(): boolean {
52
- return resolveManagedProxyContext().enabled;
52
+ export async function hasManagedProxyPrereqs(): Promise<boolean> {
53
+ return (await resolveManagedProxyContext()).enabled;
53
54
  }
54
55
 
55
56
  /**
@@ -58,11 +59,13 @@ export function hasManagedProxyPrereqs(): boolean {
58
59
  * Combines the platform base URL with the provider's deterministic proxy path.
59
60
  * Returns undefined if the provider is not managed or prerequisites are missing.
60
61
  */
61
- export function buildManagedBaseUrl(provider: string): string | undefined {
62
+ export async function buildManagedBaseUrl(
63
+ provider: string,
64
+ ): Promise<string | undefined> {
62
65
  const meta = MANAGED_PROVIDER_META[provider];
63
66
  if (!meta?.managed || !meta.proxyPath) return undefined;
64
67
 
65
- const ctx = resolveManagedProxyContext();
68
+ const ctx = await resolveManagedProxyContext();
66
69
  if (!ctx.enabled) return undefined;
67
70
 
68
71
  return `${ctx.platformBaseUrl}${meta.proxyPath}`;
@@ -74,8 +77,10 @@ export function buildManagedBaseUrl(provider: string): string | undefined {
74
77
  * Returns true when the provider supports managed proxy routing and
75
78
  * all prerequisites (platform URL + assistant API key) are satisfied.
76
79
  */
77
- export function managedFallbackEnabledFor(provider: string): boolean {
80
+ export async function managedFallbackEnabledFor(
81
+ provider: string,
82
+ ): Promise<boolean> {
78
83
  const meta = MANAGED_PROVIDER_META[provider];
79
84
  if (!meta?.managed) return false;
80
- return hasManagedProxyPrereqs();
85
+ return await hasManagedProxyPrereqs();
81
86
  }
@@ -40,12 +40,12 @@ let fallbackWarningLogged = false;
40
40
  *
41
41
  * Returns `null` when no providers are available at all.
42
42
  */
43
- export function resolveConfiguredProvider(): ConfiguredProviderResult | null {
43
+ export async function resolveConfiguredProvider(): Promise<ConfiguredProviderResult | null> {
44
44
  const config = getConfig();
45
45
 
46
46
  if (listProviders().length === 0) {
47
47
  try {
48
- initializeProviders(config);
48
+ await initializeProviders(config);
49
49
  } catch {
50
50
  return null;
51
51
  }
@@ -89,8 +89,8 @@ export function resolveConfiguredProvider(): ConfiguredProviderResult | null {
89
89
  *
90
90
  * Returns `null` when no providers are available.
91
91
  */
92
- export function getConfiguredProvider(): Provider | null {
93
- const result = resolveConfiguredProvider();
92
+ export async function getConfiguredProvider(): Promise<Provider | null> {
93
+ const result = await resolveConfiguredProvider();
94
94
  return result?.provider ?? null;
95
95
  }
96
96
 
@@ -170,51 +170,3 @@ export function extractToolUse(
170
170
  export function userMessage(text: string): Message {
171
171
  return { role: "user", content: [{ type: "text", text }] };
172
172
  }
173
-
174
- /**
175
- * Build a single user message with image + text content.
176
- */
177
- export function userMessageWithImage(
178
- imageBase64: string,
179
- mediaType: string,
180
- text: string,
181
- ): Message {
182
- return {
183
- role: "user",
184
- content: [
185
- {
186
- type: "image",
187
- source: {
188
- type: "base64",
189
- media_type: mediaType,
190
- data: imageBase64,
191
- },
192
- },
193
- { type: "text", text },
194
- ],
195
- };
196
- }
197
-
198
- /**
199
- * Build a single user message with multiple images followed by a text block.
200
- * Each image becomes its own content block; the text block comes last.
201
- */
202
- export function userMessageWithImages(
203
- images: Array<{ base64: string; mediaType: string }>,
204
- text: string,
205
- ): Message {
206
- return {
207
- role: "user",
208
- content: [
209
- ...images.map((img) => ({
210
- type: "image" as const,
211
- source: {
212
- type: "base64" as const,
213
- media_type: img.mediaType,
214
- data: img.base64,
215
- },
216
- })),
217
- { type: "text" as const, text },
218
- ],
219
- };
220
- }
@@ -1,5 +1,5 @@
1
1
  import { wrapWithLogfire } from "../logfire.js";
2
- import { getSecureKey } from "../security/secure-keys.js";
2
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
3
3
  import { ConfigError, ProviderNotConfiguredError } from "../util/errors.js";
4
4
  import { AnthropicProvider } from "./anthropic/client.js";
5
5
  import { FailoverProvider, type ProviderHealthStatus } from "./failover.js";
@@ -202,7 +202,9 @@ export function getProviderDebugStatus(
202
202
  };
203
203
  }
204
204
 
205
- export function initializeProviders(config: ProvidersConfig): void {
205
+ export async function initializeProviders(
206
+ config: ProvidersConfig,
207
+ ): Promise<void> {
206
208
  providers.clear();
207
209
  routingSources.clear();
208
210
  cachedFailoverProvider = null;
@@ -211,7 +213,7 @@ export function initializeProviders(config: ProvidersConfig): void {
211
213
  const streamTimeoutMs =
212
214
  (config.timeouts?.providerStreamTimeoutSec ?? 300) * 1000;
213
215
 
214
- const anthropicKey = getSecureKey("anthropic");
216
+ const anthropicKey = await getSecureKeyAsync("anthropic");
215
217
  if (anthropicKey) {
216
218
  const model = resolveModel(config, "anthropic");
217
219
  registerProvider(
@@ -228,9 +230,9 @@ export function initializeProviders(config: ProvidersConfig): void {
228
230
  routingSources.set("anthropic", "user-key");
229
231
  } else {
230
232
  // No user Anthropic key — route through managed proxy
231
- const managedBaseUrl = buildManagedBaseUrl("anthropic");
233
+ const managedBaseUrl = await buildManagedBaseUrl("anthropic");
232
234
  if (managedBaseUrl) {
233
- const ctx = resolveManagedProxyContext();
235
+ const ctx = await resolveManagedProxyContext();
234
236
  const model = resolveModel(config, "anthropic");
235
237
  registerProvider(
236
238
  "anthropic",
@@ -248,7 +250,7 @@ export function initializeProviders(config: ProvidersConfig): void {
248
250
  routingSources.set("anthropic", "managed-proxy");
249
251
  }
250
252
  }
251
- const openaiKey = getSecureKey("openai");
253
+ const openaiKey = await getSecureKeyAsync("openai");
252
254
  if (openaiKey) {
253
255
  const model = resolveModel(config, "openai");
254
256
  registerProvider(
@@ -261,9 +263,9 @@ export function initializeProviders(config: ProvidersConfig): void {
261
263
  );
262
264
  routingSources.set("openai", "user-key");
263
265
  } else {
264
- const managedBaseUrl = buildManagedBaseUrl("openai");
266
+ const managedBaseUrl = await buildManagedBaseUrl("openai");
265
267
  if (managedBaseUrl) {
266
- const ctx = resolveManagedProxyContext();
268
+ const ctx = await resolveManagedProxyContext();
267
269
  const model = resolveModel(config, "openai");
268
270
  registerProvider(
269
271
  "openai",
@@ -279,7 +281,7 @@ export function initializeProviders(config: ProvidersConfig): void {
279
281
  routingSources.set("openai", "managed-proxy");
280
282
  }
281
283
  }
282
- const geminiKey = getSecureKey("gemini");
284
+ const geminiKey = await getSecureKeyAsync("gemini");
283
285
  if (geminiKey) {
284
286
  const model = resolveModel(config, "gemini");
285
287
  registerProvider(
@@ -293,9 +295,9 @@ export function initializeProviders(config: ProvidersConfig): void {
293
295
  routingSources.set("gemini", "user-key");
294
296
  } else {
295
297
  // No user Gemini key — route through Vertex managed proxy
296
- const managedBaseUrl = buildManagedBaseUrl("vertex");
298
+ const managedBaseUrl = await buildManagedBaseUrl("vertex");
297
299
  if (managedBaseUrl) {
298
- const ctx = resolveManagedProxyContext();
300
+ const ctx = await resolveManagedProxyContext();
299
301
  const model = resolveModel(config, "gemini");
300
302
  registerProvider(
301
303
  "gemini",
@@ -311,7 +313,7 @@ export function initializeProviders(config: ProvidersConfig): void {
311
313
  routingSources.set("gemini", "managed-proxy");
312
314
  }
313
315
  }
314
- const ollamaKey = getSecureKey("ollama");
316
+ const ollamaKey = await getSecureKeyAsync("ollama");
315
317
  if (config.provider === "ollama" || ollamaKey) {
316
318
  const model = resolveModel(config, "ollama");
317
319
  registerProvider(
@@ -327,7 +329,7 @@ export function initializeProviders(config: ProvidersConfig): void {
327
329
  );
328
330
  routingSources.set("ollama", "user-key");
329
331
  }
330
- const fireworksKey = getSecureKey("fireworks");
332
+ const fireworksKey = await getSecureKeyAsync("fireworks");
331
333
  if (fireworksKey) {
332
334
  const model = resolveModel(config, "fireworks");
333
335
  registerProvider(
@@ -341,26 +343,8 @@ export function initializeProviders(config: ProvidersConfig): void {
341
343
  ),
342
344
  );
343
345
  routingSources.set("fireworks", "user-key");
344
- } else {
345
- const managedBaseUrl = buildManagedBaseUrl("fireworks");
346
- if (managedBaseUrl) {
347
- const ctx = resolveManagedProxyContext();
348
- const model = resolveModel(config, "fireworks");
349
- registerProvider(
350
- "fireworks",
351
- new RetryProvider(
352
- wrapWithLogfire(
353
- new FireworksProvider(ctx.assistantApiKey, model, {
354
- baseURL: managedBaseUrl,
355
- streamTimeoutMs,
356
- }),
357
- ),
358
- ),
359
- );
360
- routingSources.set("fireworks", "managed-proxy");
361
- }
362
346
  }
363
- const openrouterKey = getSecureKey("openrouter");
347
+ const openrouterKey = await getSecureKeyAsync("openrouter");
364
348
  if (openrouterKey) {
365
349
  const model = resolveModel(config, "openrouter");
366
350
  registerProvider(
@@ -374,23 +358,5 @@ export function initializeProviders(config: ProvidersConfig): void {
374
358
  ),
375
359
  );
376
360
  routingSources.set("openrouter", "user-key");
377
- } else {
378
- const managedBaseUrl = buildManagedBaseUrl("openrouter");
379
- if (managedBaseUrl) {
380
- const ctx = resolveManagedProxyContext();
381
- const model = resolveModel(config, "openrouter");
382
- registerProvider(
383
- "openrouter",
384
- new RetryProvider(
385
- wrapWithLogfire(
386
- new OpenRouterProvider(ctx.assistantApiKey, model, {
387
- baseURL: managedBaseUrl,
388
- streamTimeoutMs,
389
- }),
390
- ),
391
- ),
392
- );
393
- routingSources.set("openrouter", "managed-proxy");
394
- }
395
361
  }
396
362
  }
@@ -155,29 +155,6 @@ export function revokeByDeviceBinding(
155
155
  return matching.length;
156
156
  }
157
157
 
158
- /**
159
- * Find all active actor token records for a given guardianPrincipalId.
160
- * Used for multi-device guardian fanout — returns all bound devices (macOS, iOS, etc.)
161
- * so notification targeting can reach every device for the same guardian identity.
162
- */
163
- export function findActiveByGuardianPrincipalId(
164
- guardianPrincipalId: string,
165
- ): ActorTokenRecord[] {
166
- const db = getDb();
167
- const rows = db
168
- .select()
169
- .from(actorTokenRecords)
170
- .where(
171
- and(
172
- eq(actorTokenRecords.guardianPrincipalId, guardianPrincipalId),
173
- eq(actorTokenRecords.status, "active"),
174
- ),
175
- )
176
- .all();
177
-
178
- return rows.map(rowToRecord);
179
- }
180
-
181
158
  /**
182
159
  * Revoke a single token by its hash.
183
160
  */
@@ -8,6 +8,7 @@
8
8
  * 2. No X-Actor-Token references in production code.
9
9
  * 3. No legacy gateway-origin proof in production code.
10
10
  * 4. Scope profile contract — every profile resolves to the expected scopes.
11
+ * 5. CURRENT_POLICY_EPOCH sync — the constant matches across all packages.
11
12
  */
12
13
 
13
14
  import { execSync } from "node:child_process";
@@ -330,3 +331,66 @@ describe("scope profile contract", () => {
330
331
  }
331
332
  });
332
333
  });
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // 5. CURRENT_POLICY_EPOCH sync across packages
337
+ // ---------------------------------------------------------------------------
338
+
339
+ describe("CURRENT_POLICY_EPOCH sync", () => {
340
+ /**
341
+ * The policy epoch constant is duplicated in assistant, gateway, and cli
342
+ * packages. This test reads the exported value from each source file and
343
+ * asserts they are all equal.
344
+ */
345
+
346
+ const EPOCH_FILES = [
347
+ {
348
+ label: "assistant",
349
+ path: resolve(PROJECT_ROOT, "assistant/src/runtime/auth/policy.ts"),
350
+ },
351
+ {
352
+ label: "gateway",
353
+ path: resolve(PROJECT_ROOT, "gateway/src/auth/policy.ts"),
354
+ },
355
+ {
356
+ label: "cli",
357
+ path: resolve(PROJECT_ROOT, "cli/src/lib/policy.ts"),
358
+ },
359
+ ];
360
+
361
+ function extractEpoch(filePath: string): number {
362
+ const src = readFileSync(filePath, "utf-8");
363
+ const match = src.match(
364
+ /export\s+const\s+CURRENT_POLICY_EPOCH\s*=\s*(\d+)/,
365
+ );
366
+ if (!match) {
367
+ throw new Error(`Could not find CURRENT_POLICY_EPOCH in ${filePath}`);
368
+ }
369
+ return parseInt(match[1], 10);
370
+ }
371
+
372
+ test("all non-skill packages export the same CURRENT_POLICY_EPOCH value", () => {
373
+ const values = EPOCH_FILES.map((f) => ({
374
+ label: f.label,
375
+ epoch: extractEpoch(f.path),
376
+ }));
377
+
378
+ const canonical = values[0];
379
+ const mismatches = values.filter((v) => v.epoch !== canonical.epoch);
380
+
381
+ if (mismatches.length > 0) {
382
+ const summary = values
383
+ .map((v) => ` - ${v.label}: ${v.epoch}`)
384
+ .join("\n");
385
+ const message = [
386
+ "CURRENT_POLICY_EPOCH is out of sync across packages:",
387
+ "",
388
+ summary,
389
+ "",
390
+ "All three locations must have the same value.",
391
+ "The canonical source is assistant/src/runtime/auth/policy.ts.",
392
+ ].join("\n");
393
+ expect(mismatches, message).toEqual([]);
394
+ }
395
+ });
396
+ });
@@ -86,10 +86,14 @@ export class HttpRouter {
86
86
  server: ReturnType<typeof Bun.serve>,
87
87
  authContext: AuthContext,
88
88
  ): Promise<Response | null> {
89
+ // Normalize trailing slashes so "/integrations/twilio/config/" matches
90
+ // a route defined as "integrations/twilio/config".
91
+ const normalized = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
92
+
89
93
  for (const compiled of this.compiledRoutes) {
90
94
  if (compiled.def.method !== req.method) continue;
91
95
 
92
- const match = endpoint.match(compiled.regex);
96
+ const match = normalized.match(compiled.regex);
93
97
  if (!match) continue;
94
98
 
95
99
  // Extract named params
@@ -42,6 +42,10 @@ import {
42
42
  recordConversationSeenSignal,
43
43
  type SignalType,
44
44
  } from "../memory/conversation-attention-store.js";
45
+ import {
46
+ getConversation,
47
+ getDisplayMetaForConversations,
48
+ } from "../memory/conversation-crud.js";
45
49
  import {
46
50
  countConversations,
47
51
  listConversations,
@@ -271,16 +275,16 @@ export class RuntimeHttpServer {
271
275
  }
272
276
 
273
277
  private get pairingContext(): PairingHandlerContext {
274
- const ipcBroadcast = this.pairingBroadcast;
278
+ const broadcast = this.pairingBroadcast;
275
279
  return {
276
280
  pairingStore: this.pairingStore,
277
281
  bearerToken: this.bearerToken,
278
282
  featureFlagToken: this.readFeatureFlagToken(),
279
- pairingBroadcast: ipcBroadcast
283
+ pairingBroadcast: broadcast
280
284
  ? (msg) => {
281
285
  // Broadcast to all clients via the event hub so HTTP/SSE clients
282
286
  // (e.g. macOS app) receive pairing approval requests.
283
- ipcBroadcast(msg);
287
+ broadcast(msg);
284
288
  void assistantEventHub.publish(
285
289
  buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg),
286
290
  );
@@ -795,6 +799,7 @@ export class RuntimeHttpServer {
795
799
  const conversations = listConversations(limit, false, offset);
796
800
  const totalCount = countConversations();
797
801
  const conversationIds = conversations.map((c) => c.id);
802
+ const displayMeta = getDisplayMetaForConversations(conversationIds);
798
803
  const bindings =
799
804
  externalConversationStore.getBindingsForConversations(
800
805
  conversationIds,
@@ -856,13 +861,22 @@ export class RuntimeHttpServer {
856
861
  ? { conversationOriginChannel: originChannel }
857
862
  : {}),
858
863
  ...(assistantAttention ? { assistantAttention } : {}),
864
+ ...(displayMeta.get(c.id)?.isPinned
865
+ ? {
866
+ isPinned: true,
867
+ displayOrder: displayMeta.get(c.id)!.displayOrder,
868
+ }
869
+ : displayMeta.get(c.id)?.displayOrder != null
870
+ ? {
871
+ displayOrder: displayMeta.get(c.id)!.displayOrder,
872
+ }
873
+ : {}),
859
874
  };
860
875
  }),
861
876
  hasMore: offset + conversations.length < totalCount,
862
877
  });
863
878
  },
864
879
  },
865
-
866
880
  ...conversationAttentionRouteDefinitions(),
867
881
 
868
882
  ...(this.sessionManagementDeps
@@ -933,6 +947,89 @@ export class RuntimeHttpServer {
933
947
  },
934
948
  },
935
949
 
950
+ // conversations/:id must be registered AFTER all literal conversations/<word>
951
+ // routes above (attention, seen, unread) so the parameterized :id does not
952
+ // shadow them.
953
+ {
954
+ endpoint: "conversations/:id",
955
+ method: "GET",
956
+ handler: ({ params }) => {
957
+ const conversation = getConversation(params.id);
958
+ if (!conversation) {
959
+ return httpError(
960
+ "NOT_FOUND",
961
+ `Conversation ${params.id} not found`,
962
+ 404,
963
+ );
964
+ }
965
+ const bindings =
966
+ externalConversationStore.getBindingsForConversations([
967
+ conversation.id,
968
+ ]);
969
+ const attentionStates = getAttentionStateByConversationIds([
970
+ conversation.id,
971
+ ]);
972
+ const binding = bindings.get(conversation.id);
973
+ const originChannel = parseChannelId(conversation.originChannel);
974
+ const attn = attentionStates.get(conversation.id);
975
+ const assistantAttention = attn
976
+ ? {
977
+ hasUnseenLatestAssistantMessage:
978
+ attn.latestAssistantMessageAt != null &&
979
+ (attn.lastSeenAssistantMessageAt == null ||
980
+ attn.lastSeenAssistantMessageAt <
981
+ attn.latestAssistantMessageAt),
982
+ ...(attn.latestAssistantMessageAt != null
983
+ ? {
984
+ latestAssistantMessageAt: attn.latestAssistantMessageAt,
985
+ }
986
+ : {}),
987
+ ...(attn.lastSeenAssistantMessageAt != null
988
+ ? {
989
+ lastSeenAssistantMessageAt:
990
+ attn.lastSeenAssistantMessageAt,
991
+ }
992
+ : {}),
993
+ ...(attn.lastSeenConfidence != null
994
+ ? { lastSeenConfidence: attn.lastSeenConfidence }
995
+ : {}),
996
+ ...(attn.lastSeenSignalType != null
997
+ ? { lastSeenSignalType: attn.lastSeenSignalType }
998
+ : {}),
999
+ }
1000
+ : undefined;
1001
+ return Response.json({
1002
+ session: {
1003
+ id: conversation.id,
1004
+ title: conversation.title ?? "Untitled",
1005
+ createdAt: conversation.createdAt,
1006
+ updatedAt: conversation.updatedAt,
1007
+ threadType:
1008
+ conversation.threadType === "private" ? "private" : "standard",
1009
+ source: conversation.source ?? "user",
1010
+ ...(conversation.scheduleJobId
1011
+ ? { scheduleJobId: conversation.scheduleJobId }
1012
+ : {}),
1013
+ ...(binding
1014
+ ? {
1015
+ channelBinding: {
1016
+ sourceChannel: binding.sourceChannel,
1017
+ externalChatId: binding.externalChatId,
1018
+ externalUserId: binding.externalUserId,
1019
+ displayName: binding.displayName,
1020
+ username: binding.username,
1021
+ },
1022
+ }
1023
+ : {}),
1024
+ ...(originChannel
1025
+ ? { conversationOriginChannel: originChannel }
1026
+ : {}),
1027
+ ...(assistantAttention ? { assistantAttention } : {}),
1028
+ },
1029
+ });
1030
+ },
1031
+ },
1032
+
936
1033
  ...btwRouteDefinitions({
937
1034
  sendMessageDeps: this.sendMessageDeps,
938
1035
  }),