@vellumai/assistant 0.5.10 → 0.5.11

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 (263) hide show
  1. package/AGENTS.md +8 -0
  2. package/ARCHITECTURE.md +43 -43
  3. package/Dockerfile +2 -0
  4. package/docs/architecture/integrations.md +3 -10
  5. package/docs/architecture/memory.md +7 -12
  6. package/docs/credential-execution-service.md +9 -9
  7. package/docs/skills.md +1 -1
  8. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  9. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  10. package/openapi.yaml +7130 -0
  11. package/package.json +2 -1
  12. package/scripts/generate-openapi.ts +562 -0
  13. package/src/__tests__/acp-session.test.ts +239 -44
  14. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  17. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  18. package/src/__tests__/btw-routes.test.ts +8 -0
  19. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  20. package/src/__tests__/channel-approvals.test.ts +7 -7
  21. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  22. package/src/__tests__/config-schema.test.ts +10 -2
  23. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  24. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  25. package/src/__tests__/conversation-title-service.test.ts +2 -15
  26. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  27. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  28. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  29. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  30. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  31. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  32. package/src/__tests__/heartbeat-service.test.ts +35 -0
  33. package/src/__tests__/host-shell-tool.test.ts +1 -1
  34. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  35. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  36. package/src/__tests__/log-export-workspace.test.ts +1 -1
  37. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  38. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  39. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  40. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  41. package/src/__tests__/memory-regressions.test.ts +53 -42
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  43. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  44. package/src/__tests__/oauth-cli.test.ts +98 -551
  45. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  46. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  47. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  48. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  49. package/src/__tests__/secret-ingress.test.ts +283 -0
  50. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  51. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  52. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  53. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  54. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  55. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  56. package/src/__tests__/skill-memory.test.ts +2 -4
  57. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  58. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  59. package/src/__tests__/skills.test.ts +16 -2
  60. package/src/__tests__/slack-channel-config.test.ts +1 -1
  61. package/src/__tests__/slack-skill.test.ts +5 -69
  62. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  63. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  64. package/src/acp/client-handler.ts +113 -31
  65. package/src/acp/session-manager.ts +29 -27
  66. package/src/approvals/guardian-request-resolvers.ts +1 -1
  67. package/src/cli/AGENTS.md +73 -0
  68. package/src/cli/commands/autonomy.ts +3 -5
  69. package/src/cli/commands/credential-execution.ts +1 -2
  70. package/src/cli/commands/memory.ts +2 -3
  71. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  72. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  73. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  74. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  75. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  76. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  77. package/src/cli/commands/oauth/apps.ts +26 -8
  78. package/src/cli/commands/oauth/connect.ts +373 -0
  79. package/src/cli/commands/oauth/connections.ts +14 -493
  80. package/src/cli/commands/oauth/disconnect.ts +333 -0
  81. package/src/cli/commands/oauth/index.ts +62 -10
  82. package/src/cli/commands/oauth/mode.ts +263 -0
  83. package/src/cli/commands/oauth/ping.ts +222 -0
  84. package/src/cli/commands/oauth/providers.ts +30 -3
  85. package/src/cli/commands/oauth/request.ts +576 -0
  86. package/src/cli/commands/oauth/shared.ts +132 -0
  87. package/src/cli/commands/oauth/status.ts +202 -0
  88. package/src/cli/commands/oauth/token.ts +159 -0
  89. package/src/cli/commands/platform.ts +20 -14
  90. package/src/cli.ts +82 -17
  91. package/src/config/assistant-feature-flags.ts +74 -11
  92. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  93. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  94. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  95. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  96. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  97. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  98. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  99. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  100. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  101. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  102. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  103. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  104. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  105. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  106. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  107. package/src/config/bundled-tool-registry.ts +2 -19
  108. package/src/config/env.ts +5 -1
  109. package/src/config/feature-flag-registry.json +57 -41
  110. package/src/config/loader.ts +4 -0
  111. package/src/config/schemas/platform.ts +0 -8
  112. package/src/config/schemas/security.ts +9 -1
  113. package/src/config/schemas/services.ts +1 -1
  114. package/src/config/skill-state.ts +1 -3
  115. package/src/config/skills.ts +2 -4
  116. package/src/credential-execution/feature-gates.ts +9 -16
  117. package/src/credential-execution/process-manager.ts +12 -0
  118. package/src/daemon/config-watcher.ts +4 -0
  119. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  120. package/src/daemon/conversation-agent-loop.ts +49 -2
  121. package/src/daemon/conversation-memory.ts +0 -1
  122. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  123. package/src/daemon/handlers/conversations.ts +41 -33
  124. package/src/daemon/lifecycle.ts +26 -2
  125. package/src/daemon/message-types/acp.ts +0 -15
  126. package/src/daemon/message-types/memory.ts +0 -1
  127. package/src/daemon/message-types/messages.ts +9 -1
  128. package/src/daemon/message-types/schedules.ts +9 -0
  129. package/src/daemon/server.ts +19 -7
  130. package/src/email/feature-gate.ts +3 -3
  131. package/src/heartbeat/heartbeat-service.ts +48 -0
  132. package/src/inbound/platform-callback-registration.ts +61 -7
  133. package/src/mcp/mcp-oauth-provider.ts +3 -3
  134. package/src/memory/app-store.ts +3 -3
  135. package/src/memory/conversation-crud.ts +124 -0
  136. package/src/memory/conversation-title-service.ts +7 -17
  137. package/src/memory/db-init.ts +8 -0
  138. package/src/memory/embedding-local.ts +47 -2
  139. package/src/memory/indexer.ts +13 -10
  140. package/src/memory/items-extractor.ts +12 -4
  141. package/src/memory/job-utils.ts +5 -0
  142. package/src/memory/jobs-store.ts +10 -2
  143. package/src/memory/journal-memory.ts +6 -2
  144. package/src/memory/llm-request-log-store.ts +88 -21
  145. package/src/memory/memory-recall-log-store.ts +128 -0
  146. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  147. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  148. package/src/memory/migrations/index.ts +2 -0
  149. package/src/memory/retriever.test.ts +4 -5
  150. package/src/memory/schema/infrastructure.ts +31 -0
  151. package/src/memory/schema/oauth.ts +3 -0
  152. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  153. package/src/oauth/connect-orchestrator.ts +54 -0
  154. package/src/oauth/manual-token-connection.ts +5 -5
  155. package/src/oauth/oauth-store.ts +26 -5
  156. package/src/oauth/seed-providers.ts +10 -1
  157. package/src/permissions/checker.ts +2 -2
  158. package/src/permissions/trust-client.ts +2 -2
  159. package/src/platform/client.ts +2 -2
  160. package/src/prompts/journal-context.ts +6 -1
  161. package/src/providers/anthropic/client.ts +143 -1
  162. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  163. package/src/runtime/auth/route-policy.ts +0 -1
  164. package/src/runtime/btw-sidechain.ts +7 -1
  165. package/src/runtime/channel-approvals.ts +2 -2
  166. package/src/runtime/channel-readiness-service.ts +30 -7
  167. package/src/runtime/http-router.ts +31 -0
  168. package/src/runtime/http-server.ts +21 -4
  169. package/src/runtime/http-types.ts +2 -0
  170. package/src/runtime/pending-interactions.ts +21 -3
  171. package/src/runtime/routes/acp-routes.ts +46 -28
  172. package/src/runtime/routes/app-management-routes.ts +123 -0
  173. package/src/runtime/routes/app-routes.ts +31 -0
  174. package/src/runtime/routes/approval-routes.ts +108 -3
  175. package/src/runtime/routes/attachment-routes.ts +45 -0
  176. package/src/runtime/routes/avatar-routes.ts +16 -0
  177. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  178. package/src/runtime/routes/btw-routes.ts +20 -0
  179. package/src/runtime/routes/call-routes.ts +81 -0
  180. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  181. package/src/runtime/routes/channel-routes.ts +18 -0
  182. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  183. package/src/runtime/routes/contact-routes.ts +77 -0
  184. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  185. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  186. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  187. package/src/runtime/routes/conversation-routes.ts +115 -38
  188. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  189. package/src/runtime/routes/debug-routes.ts +23 -0
  190. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  191. package/src/runtime/routes/documents-routes.ts +42 -0
  192. package/src/runtime/routes/events-routes.ts +10 -0
  193. package/src/runtime/routes/global-search-routes.ts +35 -0
  194. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  195. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  196. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  197. package/src/runtime/routes/host-bash-routes.ts +16 -1
  198. package/src/runtime/routes/host-cu-routes.ts +23 -1
  199. package/src/runtime/routes/host-file-routes.ts +18 -1
  200. package/src/runtime/routes/identity-routes.ts +35 -0
  201. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  202. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  203. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  204. package/src/runtime/routes/integrations/twilio.ts +32 -22
  205. package/src/runtime/routes/invite-routes.ts +83 -0
  206. package/src/runtime/routes/log-export-routes.ts +14 -0
  207. package/src/runtime/routes/memory-item-routes.ts +99 -1
  208. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  209. package/src/runtime/routes/migration-routes.ts +40 -0
  210. package/src/runtime/routes/notification-routes.ts +20 -0
  211. package/src/runtime/routes/oauth-apps.ts +11 -3
  212. package/src/runtime/routes/pairing-routes.ts +15 -0
  213. package/src/runtime/routes/recording-routes.ts +72 -0
  214. package/src/runtime/routes/schedule-routes.ts +77 -5
  215. package/src/runtime/routes/secret-routes.ts +63 -1
  216. package/src/runtime/routes/settings-routes.ts +90 -0
  217. package/src/runtime/routes/skills-routes.ts +98 -16
  218. package/src/runtime/routes/subagents-routes.ts +38 -3
  219. package/src/runtime/routes/surface-action-routes.ts +66 -24
  220. package/src/runtime/routes/surface-content-routes.ts +20 -0
  221. package/src/runtime/routes/telemetry-routes.ts +12 -0
  222. package/src/runtime/routes/trace-event-routes.ts +25 -0
  223. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  224. package/src/runtime/routes/tts-routes.ts +15 -4
  225. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  226. package/src/runtime/routes/usage-routes.ts +59 -0
  227. package/src/runtime/routes/watch-routes.ts +28 -0
  228. package/src/runtime/routes/work-items-routes.ts +59 -0
  229. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  230. package/src/runtime/routes/workspace-routes.ts +102 -0
  231. package/src/schedule/scheduler.ts +7 -1
  232. package/src/security/AGENTS.md +7 -0
  233. package/src/security/credential-backend.ts +1 -1
  234. package/src/security/encrypted-store.ts +3 -3
  235. package/src/security/oauth2.ts +55 -0
  236. package/src/security/secret-ingress.ts +174 -0
  237. package/src/security/secret-patterns.ts +133 -0
  238. package/src/security/secret-scanner.ts +28 -117
  239. package/src/signals/confirm.ts +12 -8
  240. package/src/signals/user-message.ts +18 -3
  241. package/src/skills/skill-memory.ts +1 -2
  242. package/src/tasks/task-runner.ts +7 -1
  243. package/src/tools/credentials/broker.ts +1 -1
  244. package/src/tools/credentials/metadata-store.ts +1 -1
  245. package/src/tools/credentials/vault.ts +2 -3
  246. package/src/tools/memory/definitions.ts +1 -1
  247. package/src/tools/memory/handlers.test.ts +2 -4
  248. package/src/tools/skills/load.ts +1 -1
  249. package/src/tools/terminal/safe-env.ts +7 -0
  250. package/src/tools/tool-manifest.ts +1 -1
  251. package/src/util/log-redact.ts +9 -34
  252. package/docs/architecture/keychain-broker.md +0 -68
  253. package/src/cli/commands/oauth/platform.ts +0 -525
  254. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  255. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  256. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  257. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  258. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  259. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  260. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  261. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  262. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  263. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
@@ -187,7 +187,6 @@ export async function prepareMemoryContext(
187
187
  }
188
188
  : undefined,
189
189
  semanticHits: recall.semanticHits,
190
- recencyHits: 0,
191
190
  tier1Count: recall.tier1Count ?? 0,
192
191
  tier2Count: recall.tier2Count ?? 0,
193
192
  hybridSearchLatencyMs: recall.hybridSearchMs ?? 0,
@@ -19,6 +19,7 @@ import {
19
19
  } from "../../security/secure-keys.js";
20
20
  import {
21
21
  deleteCredentialMetadata,
22
+ getCredentialMetadata,
22
23
  upsertCredentialMetadata,
23
24
  } from "../../tools/credentials/metadata-store.js";
24
25
  import { log as _log } from "./shared.js";
@@ -38,6 +39,39 @@ export interface SlackChannelConfigResult {
38
39
  warning?: string;
39
40
  }
40
41
 
42
+ // -- Helpers --
43
+
44
+ const SLACK_INJECTION_TEMPLATES = [
45
+ {
46
+ hostPattern: "slack.com" as const,
47
+ injectionType: "header" as const,
48
+ headerName: "Authorization",
49
+ valuePrefix: "Bearer ",
50
+ },
51
+ ];
52
+
53
+ /** Ensure the bot token credential has injection templates for the proxy. */
54
+ function ensureBotTokenInjectionTemplates(): void {
55
+ upsertCredentialMetadata("slack_channel", "bot_token", {
56
+ allowedDomains: ["slack.com"],
57
+ injectionTemplates: SLACK_INJECTION_TEMPLATES,
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Backfill injection templates on the Slack bot token credential.
63
+ * Called on daemon startup so existing credentials get proxy support.
64
+ */
65
+ export function backfillSlackInjectionTemplates(): void {
66
+ const meta = getCredentialMetadata("slack_channel", "bot_token");
67
+ if (
68
+ meta &&
69
+ (!meta.injectionTemplates || meta.injectionTemplates.length === 0)
70
+ ) {
71
+ ensureBotTokenInjectionTemplates();
72
+ }
73
+ }
74
+
41
75
  // -- Business logic --
42
76
 
43
77
  export async function getSlackChannelConfig(): Promise<SlackChannelConfigResult> {
@@ -56,6 +90,13 @@ export async function getSlackChannelConfig(): Promise<SlackChannelConfigResult>
56
90
  const conn = getConnectionByProvider("slack_channel");
57
91
  const connected =
58
92
  !!(conn && conn.status === "active") && hasBotToken && hasAppToken;
93
+
94
+ // Backfill injection templates for existing credentials that were stored
95
+ // before proxy support was added. Safe to call repeatedly (upsert merges).
96
+ if (hasBotToken) {
97
+ ensureBotTokenInjectionTemplates();
98
+ }
99
+
59
100
  return {
60
101
  success: true,
61
102
  hasBotToken,
@@ -168,7 +209,7 @@ export async function setSlackChannelConfig(
168
209
  };
169
210
  }
170
211
 
171
- upsertCredentialMetadata("slack_channel", "bot_token", {});
212
+ ensureBotTokenInjectionTemplates();
172
213
 
173
214
  const raw = loadRawConfig();
174
215
  setNestedValue(raw, "slack.teamId", metadata.teamId ?? "");
@@ -255,6 +296,7 @@ export async function setSlackChannelConfig(
255
296
  // Sync oauth_connection record so getConnectionByProvider("slack_channel")
256
297
  // reflects the current credential state.
257
298
  if (hasBotToken && hasAppToken) {
299
+ ensureBotTokenInjectionTemplates();
258
300
  const accountInfo = metadata.teamName
259
301
  ? `${metadata.teamName}${metadata.botUsername ? ` (@${metadata.botUsername})` : ""}`
260
302
  : undefined;
@@ -59,41 +59,49 @@ export function makeEventSender(params: {
59
59
 
60
60
  return (event: ServerMessage) => {
61
61
  if (event.type === "confirmation_request") {
62
- pendingInteractions.register(event.requestId, {
63
- conversation,
64
- conversationId,
65
- kind: "confirmation",
66
- confirmationDetails: {
67
- toolName: event.toolName,
68
- input: event.input,
69
- riskLevel: event.riskLevel,
70
- executionTarget: event.executionTarget,
71
- allowlistOptions: event.allowlistOptions,
72
- scopeOptions: event.scopeOptions,
73
- persistentDecisionsAllowed: event.persistentDecisionsAllowed,
74
- temporaryOptionsAvailable: event.temporaryOptionsAvailable,
75
- },
76
- });
77
-
78
- try {
79
- const trustContext = conversation.trustContext;
80
- createCanonicalGuardianRequest({
81
- id: event.requestId,
82
- kind: "tool_approval",
83
- sourceType: "desktop",
84
- sourceChannel,
62
+ // ACP permission requests are handled by client-handler.ts — skip
63
+ // the normal registration and guardian request creation for them.
64
+ // The ACP handler registers its own entry with directResolve after
65
+ // this callback returns.
66
+ const isAcpPermission = "acpToolKind" in event && !!event.acpToolKind;
67
+
68
+ if (!isAcpPermission) {
69
+ pendingInteractions.register(event.requestId, {
70
+ conversation,
85
71
  conversationId,
86
- guardianPrincipalId: trustContext?.guardianPrincipalId ?? undefined,
87
- toolName: event.toolName,
88
- status: "pending",
89
- requestCode: generateCanonicalRequestCode(),
90
- expiresAt: Date.now() + 5 * 60 * 1000,
72
+ kind: "confirmation",
73
+ confirmationDetails: {
74
+ toolName: event.toolName,
75
+ input: event.input,
76
+ riskLevel: event.riskLevel,
77
+ executionTarget: event.executionTarget,
78
+ allowlistOptions: event.allowlistOptions,
79
+ scopeOptions: event.scopeOptions,
80
+ persistentDecisionsAllowed: event.persistentDecisionsAllowed,
81
+ temporaryOptionsAvailable: event.temporaryOptionsAvailable,
82
+ },
91
83
  });
92
- } catch (err) {
93
- log.debug(
94
- { err, requestId: event.requestId, conversationId },
95
- "Failed to create canonical request from local confirmation event",
96
- );
84
+
85
+ try {
86
+ const trustContext = conversation.trustContext;
87
+ createCanonicalGuardianRequest({
88
+ id: event.requestId,
89
+ kind: "tool_approval",
90
+ sourceType: "desktop",
91
+ sourceChannel,
92
+ conversationId,
93
+ guardianPrincipalId: trustContext?.guardianPrincipalId ?? undefined,
94
+ toolName: event.toolName,
95
+ status: "pending",
96
+ requestCode: generateCanonicalRequestCode(),
97
+ expiresAt: Date.now() + 5 * 60 * 1000,
98
+ });
99
+ } catch (err) {
100
+ log.debug(
101
+ { err, requestId: event.requestId, conversationId },
102
+ "Failed to create canonical request from local confirmation event",
103
+ );
104
+ }
97
105
  }
98
106
  } else if (event.type === "secret_request") {
99
107
  pendingInteractions.register(event.requestId, {
@@ -109,6 +109,7 @@ import {
109
109
  createGuardianActionCopyGenerator,
110
110
  createGuardianFollowUpConversationGenerator,
111
111
  } from "./guardian-action-generators.js";
112
+ import { backfillSlackInjectionTemplates } from "./handlers/config-slack-channel.js";
112
113
  import {
113
114
  cancelGeneration,
114
115
  clearAllConversations,
@@ -282,7 +283,7 @@ export async function runDaemon(): Promise<void> {
282
283
  log.info("Daemon startup: workspace migrations complete");
283
284
 
284
285
  // Backfill oauth_connection rows for manual-token providers (Telegram,
285
- // Slack channel) that already have keychain credentials from before the
286
+ // Slack channel) that already have stored credentials from before the
286
287
  // oauth_connection migration. Safe to call on every startup.
287
288
  //
288
289
  // Must run AFTER workspace migrations.
@@ -297,6 +298,17 @@ export async function runDaemon(): Promise<void> {
297
298
  );
298
299
  }
299
300
 
301
+ // Backfill injection templates on Slack bot token credentials so the
302
+ // credential proxy can inject Authorization headers. Safe on every startup.
303
+ try {
304
+ backfillSlackInjectionTemplates();
305
+ } catch (err) {
306
+ log.warn(
307
+ { err },
308
+ "Slack injection template backfill failed — continuing startup",
309
+ );
310
+ }
311
+
300
312
  // Now that workspace migrations have run (including 003-seed-device-id
301
313
  // which may copy the legacy installationId into device.json), it is safe
302
314
  // to read the device ID and set the Sentry tag.
@@ -839,6 +851,7 @@ export async function runDaemon(): Promise<void> {
839
851
  getHandlerContext: () => server.getHandlerContext(),
840
852
  }),
841
853
  getCesClient: () => server.getCesClient(),
854
+ getHeartbeatService: () => server.getHeartbeatService(),
842
855
  });
843
856
 
844
857
  // Inject voice bridge deps BEFORE attempting to start the HTTP server.
@@ -1092,8 +1105,19 @@ export async function runDaemon(): Promise<void> {
1092
1105
  const heartbeatConfig = config.heartbeat;
1093
1106
  const heartbeat = new HeartbeatService({
1094
1107
  processMessage: (conversationId, content) =>
1095
- server.processMessage(conversationId, content),
1108
+ server.processMessage(conversationId, content, undefined, {
1109
+ trustContext: {
1110
+ sourceChannel: "vellum",
1111
+ trustClass: "guardian",
1112
+ },
1113
+ }),
1096
1114
  alerter: (alert) => server.broadcast(alert),
1115
+ onConversationCreated: (info) =>
1116
+ server.broadcast({
1117
+ type: "heartbeat_conversation_created",
1118
+ conversationId: info.conversationId,
1119
+ title: info.title,
1120
+ }),
1097
1121
  });
1098
1122
  heartbeat.start();
1099
1123
  server.setHeartbeatService(heartbeat);
@@ -25,20 +25,6 @@ export interface AcpSessionUpdate {
25
25
  toolStatus?: string;
26
26
  }
27
27
 
28
- export interface AcpPermissionRequest {
29
- type: "acp_permission_request";
30
- acpSessionId: string;
31
- requestId: string;
32
- toolTitle: string;
33
- toolKind: string;
34
- rawInput?: unknown;
35
- options: Array<{
36
- optionId: string;
37
- name: string;
38
- kind: "allow_once" | "allow_always" | "reject_once" | "reject_always";
39
- }>;
40
- }
41
-
42
28
  export interface AcpSessionCompleted {
43
29
  type: "acp_session_completed";
44
30
  acpSessionId: string;
@@ -61,6 +47,5 @@ export interface AcpSessionError {
61
47
  export type _AcpServerMessages =
62
48
  | AcpSessionSpawned
63
49
  | AcpSessionUpdate
64
- | AcpPermissionRequest
65
50
  | AcpSessionCompleted
66
51
  | AcpSessionError;
@@ -21,7 +21,6 @@ export interface MemoryRecalled {
21
21
  model: string;
22
22
  degradation?: MemoryRecalledDegradation;
23
23
  semanticHits: number;
24
- recencyHits: number;
25
24
  tier1Count: number;
26
25
  tier2Count: number;
27
26
  hybridSearchLatencyMs: number;
@@ -44,7 +44,7 @@ export interface SecretResponse {
44
44
  type: "secret_response";
45
45
  requestId: string;
46
46
  value?: string; // undefined = user cancelled
47
- /** How the secret should be delivered: 'store' persists to keychain (default), 'transient_send' for one-time use without persisting. */
47
+ /** How the secret should be delivered: 'store' persists to credential store (default), 'transient_send' for one-time use without persisting. */
48
48
  delivery?: "store" | "transient_send";
49
49
  }
50
50
 
@@ -156,6 +156,14 @@ export interface ConfirmationRequest {
156
156
  temporaryOptionsAvailable?: Array<"allow_10m" | "allow_conversation">;
157
157
  /** The tool_use block ID for client-side correlation with specific tool calls. */
158
158
  toolUseId?: string;
159
+ /** ACP tool kind from the agent (e.g. "read", "edit", "execute"). Present only for ACP permission requests. */
160
+ acpToolKind?: string;
161
+ /** ACP permission options from the agent. Present only for ACP permission requests. Clients should use these to render the correct buttons. */
162
+ acpOptions?: Array<{
163
+ optionId: string;
164
+ name: string;
165
+ kind: "allow_once" | "allow_always" | "reject_once" | "reject_always";
166
+ }>;
159
167
  }
160
168
 
161
169
  export interface SecretRequest {
@@ -84,6 +84,13 @@ export interface HeartbeatAlert {
84
84
  body: string;
85
85
  }
86
86
 
87
+ /** Server push — broadcast when a heartbeat creates a conversation. */
88
+ export interface HeartbeatConversationCreated {
89
+ type: "heartbeat_conversation_created";
90
+ conversationId: string;
91
+ title: string;
92
+ }
93
+
87
94
  export interface HeartbeatConfigResponse {
88
95
  type: "heartbeat_config_response";
89
96
  enabled: boolean;
@@ -91,6 +98,7 @@ export interface HeartbeatConfigResponse {
91
98
  activeHoursStart: number | null;
92
99
  activeHoursEnd: number | null;
93
100
  nextRunAt: number | null;
101
+ lastRunAt: number | null;
94
102
  success: boolean;
95
103
  error?: string;
96
104
  }
@@ -141,6 +149,7 @@ export type _SchedulesClientMessages =
141
149
  export type _SchedulesServerMessages =
142
150
  | SchedulesListResponse
143
151
  | HeartbeatAlert
152
+ | HeartbeatConversationCreated
144
153
  | HeartbeatConfigResponse
145
154
  | HeartbeatRunsListResponse
146
155
  | HeartbeatRunNowResponse
@@ -40,16 +40,14 @@ import { updateMetaFile } from "../memory/conversation-disk-view.js";
40
40
  import { getOrCreateConversation } from "../memory/conversation-key-store.js";
41
41
  import { buildSystemPrompt } from "../prompts/system-prompt.js";
42
42
  import { RateLimitProvider } from "../providers/ratelimit.js";
43
- import {
44
- getProvider,
45
- initializeProviders,
46
- } from "../providers/registry.js";
43
+ import { getProvider, initializeProviders } from "../providers/registry.js";
47
44
  import { buildAssistantEvent } from "../runtime/assistant-event.js";
48
45
  import { assistantEventHub } from "../runtime/assistant-event-hub.js";
49
46
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
50
47
  import { getSigningKeyFingerprint } from "../runtime/auth/token-service.js";
51
48
  import { bridgeConfirmationRequestToGuardian } from "../runtime/confirmation-request-guardian-bridge.js";
52
49
  import * as pendingInteractions from "../runtime/pending-interactions.js";
50
+ import { checkIngressForSecrets } from "../security/secret-ingress.js";
53
51
  import { registerCancelCallback } from "../signals/cancel.js";
54
52
  import { registerConversationUndoCallback } from "../signals/conversation-undo.js";
55
53
  import { appendEventToStream } from "../signals/event-stream.js";
@@ -304,6 +302,10 @@ export class DaemonServer {
304
302
  this._heartbeatService = service;
305
303
  }
306
304
 
305
+ getHeartbeatService(): HeartbeatService | undefined {
306
+ return this._heartbeatService;
307
+ }
308
+
307
309
  private deriveMemoryPolicy(conversationId: string): ConversationMemoryPolicy {
308
310
  const conversationType = getConversationType(conversationId);
309
311
  if (conversationType === "private") {
@@ -508,6 +510,18 @@ export class DaemonServer {
508
510
  );
509
511
 
510
512
  registerUserMessageCallback(async (params) => {
513
+ // Block messages containing known-format secrets before persistence
514
+ if (!params.bypassSecretCheck) {
515
+ const ingressResult = checkIngressForSecrets(params.content);
516
+ if (ingressResult.blocked) {
517
+ return {
518
+ accepted: false,
519
+ error: "secret_blocked" as const,
520
+ message: ingressResult.userNotice,
521
+ };
522
+ }
523
+ }
524
+
511
525
  const { conversationId } = getOrCreateConversation(
512
526
  params.conversationKey,
513
527
  );
@@ -702,9 +716,7 @@ export class DaemonServer {
702
716
 
703
717
  const createPromise = (async () => {
704
718
  const config = getConfig();
705
- let provider = getProvider(
706
- config.services.inference.provider,
707
- );
719
+ let provider = getProvider(config.services.inference.provider);
708
720
  const { rateLimit } = config;
709
721
  if (rateLimit.maxRequestsPerMinute > 0) {
710
722
  provider = new RateLimitProvider(
@@ -5,15 +5,15 @@
5
5
  * Delegates to the unified feature-flag resolver so that config overrides
6
6
  * and registry defaults are respected uniformly.
7
7
  *
8
- * The flag key follows the canonical `feature_flags.<id>.enabled` format
9
- * and is declared in `meta/feature-flags/feature-flag-registry.json`.
8
+ * The flag key uses simple kebab-case format and is declared in
9
+ * `meta/feature-flags/feature-flag-registry.json`.
10
10
  */
11
11
 
12
12
  import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
13
13
  import type { AssistantConfig } from "../config/schema.js";
14
14
 
15
15
  /** Gate for the entire email integration. */
16
- export const EMAIL_FLAG_KEY = "feature_flags.email-channel.enabled" as const;
16
+ export const EMAIL_FLAG_KEY = "email-channel" as const;
17
17
 
18
18
  /**
19
19
  * Whether the email integration is enabled.
@@ -17,6 +17,10 @@ export interface HeartbeatDeps {
17
17
  content: string,
18
18
  ) => Promise<{ messageId: string }>;
19
19
  alerter: (alert: HeartbeatAlert) => void;
20
+ onConversationCreated?: (info: {
21
+ conversationId: string;
22
+ title: string;
23
+ }) => void;
20
24
  /** Override for current hour (0-23), for testing. */
21
25
  getCurrentHour?: () => number;
22
26
  }
@@ -25,20 +29,34 @@ export class HeartbeatService {
25
29
  private readonly deps: HeartbeatDeps;
26
30
  private timer: ReturnType<typeof setInterval> | null = null;
27
31
  private activeRun: Promise<void> | null = null;
32
+ private _lastRunAt: number | null = null;
33
+ private _nextRunAt: number | null = null;
28
34
 
29
35
  constructor(deps: HeartbeatDeps) {
30
36
  this.deps = deps;
31
37
  }
32
38
 
39
+ /** Epoch-ms timestamp of the last completed heartbeat run. */
40
+ get lastRunAt(): number | null {
41
+ return this._lastRunAt;
42
+ }
43
+
44
+ /** Epoch-ms timestamp of the next scheduled heartbeat run. */
45
+ get nextRunAt(): number | null {
46
+ return this._nextRunAt;
47
+ }
48
+
33
49
  start(): void {
34
50
  const config = getConfig().heartbeat;
35
51
  if (!config.enabled) {
36
52
  log.info("Heartbeat disabled by config");
53
+ this._nextRunAt = null;
37
54
  return;
38
55
  }
39
56
  if (this.timer) return;
40
57
 
41
58
  log.info({ intervalMs: config.intervalMs }, "Heartbeat service started");
59
+ this.scheduleNextRun(config.intervalMs);
42
60
  this.timer = setInterval(() => {
43
61
  this.runOnce().catch((err) => {
44
62
  log.error({ err }, "Heartbeat runOnce failed");
@@ -52,14 +70,33 @@ export class HeartbeatService {
52
70
  clearInterval(this.timer);
53
71
  this.timer = null;
54
72
  }
73
+ this._nextRunAt = null;
55
74
  this.start();
56
75
  }
57
76
 
77
+ /**
78
+ * Reset the heartbeat timer so the next run is a full interval from now.
79
+ * Called when the guardian sends a message — no need for a heartbeat shortly
80
+ * after an active conversation.
81
+ */
82
+ resetTimer(): void {
83
+ if (!this.timer) return;
84
+ const config = getConfig().heartbeat;
85
+ clearInterval(this.timer);
86
+ this.scheduleNextRun(config.intervalMs);
87
+ this.timer = setInterval(() => {
88
+ this.runOnce().catch((err) => {
89
+ log.error({ err }, "Heartbeat runOnce failed");
90
+ });
91
+ }, config.intervalMs);
92
+ }
93
+
58
94
  async stop(): Promise<void> {
59
95
  if (this.timer) {
60
96
  clearInterval(this.timer);
61
97
  this.timer = null;
62
98
  }
99
+ this._nextRunAt = null;
63
100
  if (this.activeRun) {
64
101
  let timerId: ReturnType<typeof setTimeout>;
65
102
  const timeout = new Promise<void>((resolve) => {
@@ -116,10 +153,16 @@ export class HeartbeatService {
116
153
  await run;
117
154
  } finally {
118
155
  this.activeRun = null;
156
+ this._lastRunAt = Date.now();
157
+ this.scheduleNextRun(getConfig().heartbeat.intervalMs);
119
158
  }
120
159
  return true;
121
160
  }
122
161
 
162
+ private scheduleNextRun(intervalMs: number): void {
163
+ this._nextRunAt = Date.now() + intervalMs;
164
+ }
165
+
123
166
  private async executeRun(): Promise<void> {
124
167
  log.info("Running heartbeat");
125
168
 
@@ -134,6 +177,11 @@ export class HeartbeatService {
134
177
  systemHint: "Heartbeat",
135
178
  });
136
179
 
180
+ this.deps.onConversationCreated?.({
181
+ conversationId: conversation.id,
182
+ title: "Heartbeat",
183
+ });
184
+
137
185
  await this.deps.processMessage(conversation.id, prompt);
138
186
  log.info({ conversationId: conversation.id }, "Heartbeat completed");
139
187
  } catch (err) {
@@ -24,10 +24,22 @@ import {
24
24
  getPlatformInternalApiKey,
25
25
  } from "../config/env.js";
26
26
  import { getIsContainerized } from "../config/env-registry.js";
27
+ import { credentialKey } from "../security/credential-key.js";
28
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
27
29
  import { getLogger } from "../util/logger.js";
28
30
 
29
31
  const log = getLogger("platform-callback-registration");
30
32
 
33
+ export interface PlatformCallbackRegistrationContext {
34
+ containerized: boolean;
35
+ platformBaseUrl: string;
36
+ assistantId: string;
37
+ hasInternalApiKey: boolean;
38
+ hasAssistantApiKey: boolean;
39
+ authHeader: string | null;
40
+ enabled: boolean;
41
+ }
42
+
31
43
  /**
32
44
  * Whether the daemon should register callback routes with the platform.
33
45
  * True when IS_CONTAINERIZED, VELLUM_PLATFORM_URL, and PLATFORM_ASSISTANT_ID
@@ -43,6 +55,45 @@ export function shouldUsePlatformCallbacks(): boolean {
43
55
  );
44
56
  }
45
57
 
58
+ export async function resolvePlatformCallbackRegistrationContext(): Promise<PlatformCallbackRegistrationContext> {
59
+ const containerized = getIsContainerized();
60
+ const [storedBaseUrlRaw, storedAssistantIdRaw, storedAssistantApiKeyRaw] =
61
+ await Promise.all([
62
+ getSecureKeyAsync(credentialKey("vellum", "platform_base_url")),
63
+ getSecureKeyAsync(credentialKey("vellum", "platform_assistant_id")),
64
+ getSecureKeyAsync(credentialKey("vellum", "assistant_api_key")),
65
+ ]);
66
+
67
+ const storedBaseUrl = storedBaseUrlRaw?.trim();
68
+ const platformBaseUrl = (storedBaseUrl || getPlatformBaseUrl()).replace(
69
+ /\/+$/,
70
+ "",
71
+ );
72
+ const assistantId =
73
+ getPlatformAssistantId().trim() || storedAssistantIdRaw?.trim() || "";
74
+ const internalApiKey = getPlatformInternalApiKey().trim();
75
+ const assistantApiKey = storedAssistantApiKeyRaw?.trim() || "";
76
+ const authHeader = internalApiKey
77
+ ? `Bearer ${internalApiKey}`
78
+ : assistantApiKey
79
+ ? `Api-Key ${assistantApiKey}`
80
+ : null;
81
+
82
+ return {
83
+ containerized,
84
+ platformBaseUrl,
85
+ assistantId,
86
+ hasInternalApiKey: internalApiKey.length > 0,
87
+ hasAssistantApiKey: assistantApiKey.length > 0,
88
+ authHeader,
89
+ enabled:
90
+ containerized &&
91
+ platformBaseUrl.length > 0 &&
92
+ assistantId.length > 0 &&
93
+ authHeader !== null,
94
+ };
95
+ }
96
+
46
97
  interface RegisterCallbackRouteResponse {
47
98
  callback_url: string;
48
99
  callback_path: string;
@@ -65,20 +116,23 @@ export async function registerCallbackRoute(
65
116
  callbackPath: string,
66
117
  type: string,
67
118
  ): Promise<string> {
68
- const platformBaseUrl = getPlatformBaseUrl().replace(/\/+$/, "");
69
- const assistantId = getPlatformAssistantId();
70
- const apiKey = getPlatformInternalApiKey();
119
+ const context = await resolvePlatformCallbackRegistrationContext();
120
+ if (!context.enabled || !context.authHeader) {
121
+ throw new Error(
122
+ "Platform callbacks not available — missing containerized platform registration context",
123
+ );
124
+ }
125
+
126
+ const platformBaseUrl = context.platformBaseUrl;
127
+ const assistantId = context.assistantId;
71
128
 
72
129
  const url = `${platformBaseUrl}/v1/internal/gateway/callback-routes/register/`;
73
130
 
74
131
  const headers: Record<string, string> = {
75
132
  "Content-Type": "application/json",
133
+ Authorization: context.authHeader,
76
134
  };
77
135
 
78
- if (apiKey) {
79
- headers["Authorization"] = `Bearer ${apiKey}`;
80
- }
81
-
82
136
  const body = JSON.stringify({
83
137
  assistant_id: assistantId,
84
138
  callback_path: callbackPath,
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * OAuthClientProvider implementation for MCP servers.
3
3
  *
4
- * Uses secure-keys (OS keychain / encrypted file store) for persistent
5
- * credential storage and a loopback HTTP server for the browser callback.
4
+ * Uses secure-keys (credential store) for persistent credential storage
5
+ * and a loopback HTTP server for the browser callback.
6
6
  */
7
7
 
8
8
  import { createServer, type Server } from "node:http";
@@ -30,7 +30,7 @@ const log = getLogger("mcp-oauth");
30
30
  const CALLBACK_PATH = "/oauth/callback";
31
31
  const CALLBACK_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
32
32
 
33
- // Keychain key helpers
33
+ // Credential store key helpers
34
34
  function tokensKey(serverId: string): string {
35
35
  return `mcp:${serverId}:tokens`;
36
36
  }
@@ -75,10 +75,10 @@ export function inlineDistAssets(appDir: string, html: string): string {
75
75
  // Inline main.js
76
76
  const jsPath = join(distDir, "main.js");
77
77
  if (existsSync(jsPath)) {
78
- const js = readFileSync(jsPath, "utf-8");
78
+ const js = readFileSync(jsPath, "utf-8").replace(/<\/script>/g, "<\\/script>");
79
79
  html = html.replace(
80
80
  /<script\s+type="module"\s+src="main\.js"\s*><\/script>/,
81
- `<script type="module">${js}</script>`,
81
+ () => `<script type="module">${js}</script>`,
82
82
  );
83
83
  }
84
84
 
@@ -88,7 +88,7 @@ export function inlineDistAssets(appDir: string, html: string): string {
88
88
  const css = readFileSync(cssPath, "utf-8");
89
89
  html = html.replace(
90
90
  /<link\s+rel="stylesheet"\s+href="main\.css"\s*>/,
91
- `<style>${css}</style>`,
91
+ () => `<style>${css}</style>`,
92
92
  );
93
93
  }
94
94