@vellumai/assistant 0.5.9 → 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 (278) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +48 -48
  3. package/Dockerfile +2 -0
  4. package/README.md +1 -1
  5. package/docs/architecture/integrations.md +6 -13
  6. package/docs/architecture/memory.md +7 -12
  7. package/docs/architecture/security.md +5 -5
  8. package/docs/credential-execution-service.md +9 -9
  9. package/docs/skills.md +1 -1
  10. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  11. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  12. package/openapi.yaml +7130 -0
  13. package/package.json +2 -1
  14. package/scripts/generate-openapi.ts +562 -0
  15. package/src/__tests__/acp-session.test.ts +239 -44
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  17. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  19. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  20. package/src/__tests__/btw-routes.test.ts +8 -0
  21. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  22. package/src/__tests__/channel-approvals.test.ts +7 -7
  23. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  24. package/src/__tests__/config-schema.test.ts +10 -2
  25. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  26. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  27. package/src/__tests__/conversation-title-service.test.ts +2 -15
  28. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  29. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  30. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  31. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  32. package/src/__tests__/credentials-cli.test.ts +3 -3
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  34. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  35. package/src/__tests__/heartbeat-service.test.ts +35 -0
  36. package/src/__tests__/host-shell-tool.test.ts +1 -1
  37. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  38. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  39. package/src/__tests__/log-export-workspace.test.ts +1 -1
  40. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  42. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  43. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  44. package/src/__tests__/memory-regressions.test.ts +53 -42
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  46. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  47. package/src/__tests__/oauth-cli.test.ts +98 -551
  48. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  49. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  50. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  51. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  52. package/src/__tests__/secret-ingress.test.ts +283 -0
  53. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  54. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  55. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  56. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  57. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  58. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  59. package/src/__tests__/skill-memory.test.ts +2 -4
  60. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  61. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  62. package/src/__tests__/skills.test.ts +16 -2
  63. package/src/__tests__/slack-channel-config.test.ts +1 -1
  64. package/src/__tests__/slack-skill.test.ts +5 -69
  65. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  66. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
  67. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
  68. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  69. package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
  70. package/src/acp/client-handler.ts +113 -31
  71. package/src/acp/session-manager.ts +29 -27
  72. package/src/approvals/guardian-request-resolvers.ts +1 -1
  73. package/src/cli/AGENTS.md +73 -0
  74. package/src/cli/commands/autonomy.ts +3 -5
  75. package/src/cli/commands/credential-execution.ts +1 -2
  76. package/src/cli/commands/credentials.ts +4 -4
  77. package/src/cli/commands/memory.ts +2 -3
  78. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  79. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  80. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  81. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  82. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  83. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  84. package/src/cli/commands/oauth/apps.ts +29 -11
  85. package/src/cli/commands/oauth/connect.ts +373 -0
  86. package/src/cli/commands/oauth/connections.ts +14 -493
  87. package/src/cli/commands/oauth/disconnect.ts +333 -0
  88. package/src/cli/commands/oauth/index.ts +62 -10
  89. package/src/cli/commands/oauth/mode.ts +263 -0
  90. package/src/cli/commands/oauth/ping.ts +222 -0
  91. package/src/cli/commands/oauth/providers.ts +30 -3
  92. package/src/cli/commands/oauth/request.ts +576 -0
  93. package/src/cli/commands/oauth/shared.ts +132 -0
  94. package/src/cli/commands/oauth/status.ts +202 -0
  95. package/src/cli/commands/oauth/token.ts +159 -0
  96. package/src/cli/commands/platform.ts +20 -14
  97. package/src/cli.ts +82 -17
  98. package/src/config/assistant-feature-flags.ts +74 -11
  99. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  100. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  101. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  102. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  103. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  104. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  105. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  106. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  107. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  108. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  109. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  111. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  112. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  113. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  114. package/src/config/bundled-tool-registry.ts +2 -19
  115. package/src/config/env.ts +5 -1
  116. package/src/config/feature-flag-registry.json +57 -41
  117. package/src/config/loader.ts +4 -0
  118. package/src/config/schemas/platform.ts +0 -8
  119. package/src/config/schemas/security.ts +9 -1
  120. package/src/config/schemas/services.ts +1 -1
  121. package/src/config/skill-state.ts +1 -3
  122. package/src/config/skills.ts +2 -4
  123. package/src/credential-execution/feature-gates.ts +9 -16
  124. package/src/credential-execution/process-manager.ts +12 -0
  125. package/src/daemon/config-watcher.ts +4 -0
  126. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  127. package/src/daemon/conversation-agent-loop.ts +49 -2
  128. package/src/daemon/conversation-memory.ts +0 -1
  129. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  130. package/src/daemon/handlers/conversations.ts +41 -33
  131. package/src/daemon/lifecycle.ts +28 -5
  132. package/src/daemon/message-types/acp.ts +0 -15
  133. package/src/daemon/message-types/memory.ts +0 -1
  134. package/src/daemon/message-types/messages.ts +9 -1
  135. package/src/daemon/message-types/schedules.ts +9 -0
  136. package/src/daemon/server.ts +19 -7
  137. package/src/email/feature-gate.ts +3 -3
  138. package/src/heartbeat/heartbeat-service.ts +48 -0
  139. package/src/inbound/platform-callback-registration.ts +61 -7
  140. package/src/mcp/mcp-oauth-provider.ts +3 -3
  141. package/src/memory/app-store.ts +3 -3
  142. package/src/memory/conversation-crud.ts +124 -0
  143. package/src/memory/conversation-title-service.ts +7 -17
  144. package/src/memory/db-init.ts +8 -0
  145. package/src/memory/embedding-local.ts +47 -2
  146. package/src/memory/indexer.ts +13 -10
  147. package/src/memory/items-extractor.ts +12 -4
  148. package/src/memory/job-utils.ts +5 -0
  149. package/src/memory/jobs-store.ts +10 -2
  150. package/src/memory/journal-memory.ts +6 -2
  151. package/src/memory/llm-request-log-store.ts +88 -21
  152. package/src/memory/memory-recall-log-store.ts +128 -0
  153. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  154. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  155. package/src/memory/migrations/index.ts +2 -0
  156. package/src/memory/migrations/validate-migration-state.ts +14 -1
  157. package/src/memory/retriever.test.ts +4 -5
  158. package/src/memory/schema/infrastructure.ts +31 -0
  159. package/src/memory/schema/oauth.ts +3 -0
  160. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  161. package/src/oauth/connect-orchestrator.ts +54 -0
  162. package/src/oauth/manual-token-connection.ts +5 -5
  163. package/src/oauth/oauth-store.ts +26 -5
  164. package/src/oauth/seed-providers.ts +10 -1
  165. package/src/permissions/checker.ts +2 -2
  166. package/src/permissions/trust-client.ts +2 -2
  167. package/src/platform/client.ts +2 -2
  168. package/src/prompts/journal-context.ts +6 -1
  169. package/src/providers/anthropic/client.ts +143 -1
  170. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  171. package/src/runtime/auth/route-policy.ts +0 -1
  172. package/src/runtime/btw-sidechain.ts +7 -1
  173. package/src/runtime/channel-approvals.ts +2 -2
  174. package/src/runtime/channel-readiness-service.ts +30 -7
  175. package/src/runtime/http-router.ts +31 -0
  176. package/src/runtime/http-server.ts +21 -4
  177. package/src/runtime/http-types.ts +2 -0
  178. package/src/runtime/pending-interactions.ts +21 -3
  179. package/src/runtime/routes/acp-routes.ts +46 -28
  180. package/src/runtime/routes/app-management-routes.ts +123 -0
  181. package/src/runtime/routes/app-routes.ts +31 -0
  182. package/src/runtime/routes/approval-routes.ts +108 -3
  183. package/src/runtime/routes/attachment-routes.ts +45 -0
  184. package/src/runtime/routes/avatar-routes.ts +16 -0
  185. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  186. package/src/runtime/routes/btw-routes.ts +20 -0
  187. package/src/runtime/routes/call-routes.ts +81 -0
  188. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  189. package/src/runtime/routes/channel-routes.ts +18 -0
  190. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  191. package/src/runtime/routes/contact-routes.ts +77 -0
  192. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  193. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  194. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  195. package/src/runtime/routes/conversation-routes.ts +115 -38
  196. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  197. package/src/runtime/routes/debug-routes.ts +23 -0
  198. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  199. package/src/runtime/routes/documents-routes.ts +42 -0
  200. package/src/runtime/routes/events-routes.ts +10 -0
  201. package/src/runtime/routes/global-search-routes.ts +35 -0
  202. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  203. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  204. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  205. package/src/runtime/routes/host-bash-routes.ts +16 -1
  206. package/src/runtime/routes/host-cu-routes.ts +23 -1
  207. package/src/runtime/routes/host-file-routes.ts +18 -1
  208. package/src/runtime/routes/identity-routes.ts +35 -0
  209. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  210. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  211. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  212. package/src/runtime/routes/integrations/twilio.ts +32 -22
  213. package/src/runtime/routes/invite-routes.ts +83 -0
  214. package/src/runtime/routes/log-export-routes.ts +14 -0
  215. package/src/runtime/routes/memory-item-routes.ts +99 -1
  216. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  217. package/src/runtime/routes/migration-routes.ts +40 -0
  218. package/src/runtime/routes/notification-routes.ts +20 -0
  219. package/src/runtime/routes/oauth-apps.ts +11 -3
  220. package/src/runtime/routes/pairing-routes.ts +15 -0
  221. package/src/runtime/routes/recording-routes.ts +72 -0
  222. package/src/runtime/routes/schedule-routes.ts +77 -5
  223. package/src/runtime/routes/secret-routes.ts +63 -1
  224. package/src/runtime/routes/settings-routes.ts +91 -1
  225. package/src/runtime/routes/skills-routes.ts +98 -16
  226. package/src/runtime/routes/subagents-routes.ts +38 -3
  227. package/src/runtime/routes/surface-action-routes.ts +66 -24
  228. package/src/runtime/routes/surface-content-routes.ts +20 -0
  229. package/src/runtime/routes/telemetry-routes.ts +12 -0
  230. package/src/runtime/routes/trace-event-routes.ts +25 -0
  231. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  232. package/src/runtime/routes/tts-routes.ts +15 -4
  233. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  234. package/src/runtime/routes/usage-routes.ts +59 -0
  235. package/src/runtime/routes/watch-routes.ts +28 -0
  236. package/src/runtime/routes/work-items-routes.ts +59 -0
  237. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  238. package/src/runtime/routes/workspace-routes.ts +102 -0
  239. package/src/schedule/scheduler.ts +7 -1
  240. package/src/security/AGENTS.md +7 -0
  241. package/src/security/credential-backend.ts +1 -1
  242. package/src/security/encrypted-store.ts +3 -3
  243. package/src/security/oauth2.ts +55 -0
  244. package/src/security/secret-ingress.ts +174 -0
  245. package/src/security/secret-patterns.ts +133 -0
  246. package/src/security/secret-scanner.ts +28 -117
  247. package/src/signals/confirm.ts +12 -8
  248. package/src/signals/user-message.ts +18 -3
  249. package/src/skills/skill-memory.ts +1 -2
  250. package/src/tasks/task-runner.ts +7 -1
  251. package/src/tools/credentials/broker.ts +1 -1
  252. package/src/tools/credentials/metadata-store.ts +1 -1
  253. package/src/tools/credentials/vault.ts +2 -3
  254. package/src/tools/memory/definitions.ts +1 -1
  255. package/src/tools/memory/handlers.test.ts +2 -4
  256. package/src/tools/skills/load.ts +1 -1
  257. package/src/tools/terminal/safe-env.ts +7 -0
  258. package/src/tools/tool-manifest.ts +1 -1
  259. package/src/util/log-redact.ts +9 -34
  260. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
  261. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
  262. package/src/workspace/migrations/AGENTS.md +11 -0
  263. package/src/workspace/migrations/runner.ts +16 -6
  264. package/src/workspace/migrations/types.ts +7 -0
  265. package/docs/architecture/keychain-broker.md +0 -69
  266. package/src/__tests__/keychain-broker-client.test.ts +0 -800
  267. package/src/cli/commands/oauth/platform.ts +0 -525
  268. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  269. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  270. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  271. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  272. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  273. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  274. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  275. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  276. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  277. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
  278. package/src/security/keychain-broker-client.ts +0 -446
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { statSync } from "node:fs";
6
6
 
7
+ import { z } from "zod";
8
+
7
9
  import { getConfig } from "../../config/loader.js";
8
10
  import { countConversations } from "../../memory/conversation-queries.js";
9
11
  import { rawAll } from "../../memory/db.js";
@@ -93,7 +95,28 @@ export function debugRouteDefinitions(): RouteDefinition[] {
93
95
  {
94
96
  endpoint: "debug",
95
97
  method: "GET",
98
+ summary: "Debug introspection",
99
+ description:
100
+ "Return runtime diagnostics: uptime, provider info, memory stats, job counts, and schedule counts.",
101
+ tags: ["debug"],
96
102
  handler: () => handleDebug(),
103
+ responseBody: z.object({
104
+ session: z.object({}).passthrough().describe("Uptime and start time"),
105
+ provider: z
106
+ .object({})
107
+ .passthrough()
108
+ .describe("Inference provider configuration"),
109
+ memory: z
110
+ .object({})
111
+ .passthrough()
112
+ .describe("Conversation and memory item counts"),
113
+ jobs: z.object({}).passthrough().describe("Background job counts"),
114
+ schedules: z
115
+ .object({})
116
+ .passthrough()
117
+ .describe("Schedule counts (total, enabled)"),
118
+ timestamp: z.string().describe("Current server timestamp (ISO 8601)"),
119
+ }),
97
120
  },
98
121
  ];
99
122
  }
@@ -2,6 +2,8 @@
2
2
  * HTTP route handlers for dictation processing.
3
3
  */
4
4
 
5
+ import { z } from "zod";
6
+
5
7
  import {
6
8
  type ProfileResolution,
7
9
  resolveProfile,
@@ -435,6 +437,34 @@ export function diagnosticsRouteDefinitions(): RouteDefinition[] {
435
437
  endpoint: "dictation",
436
438
  method: "POST",
437
439
  policyKey: "dictation",
440
+ summary: "Process dictation",
441
+ description:
442
+ "Classify voice input as dictation or action, clean up text, and apply user style preferences.",
443
+ tags: ["diagnostics"],
444
+ requestBody: z.object({
445
+ transcription: z.string().describe("Raw speech transcription"),
446
+ context: z
447
+ .object({})
448
+ .passthrough()
449
+ .describe(
450
+ "Dictation context (app name, window title, bundle ID, cursor state, selected text)",
451
+ ),
452
+ profileId: z
453
+ .string()
454
+ .describe("Optional dictation profile ID")
455
+ .optional(),
456
+ }),
457
+ responseBody: z.object({
458
+ text: z.string().describe("Processed text output"),
459
+ mode: z
460
+ .string()
461
+ .describe("Detected mode: dictation, command, or action"),
462
+ actionPlan: z
463
+ .string()
464
+ .describe("Action plan (only when mode is action)"),
465
+ resolvedProfileId: z.string().describe("Resolved dictation profile ID"),
466
+ profileSource: z.string().describe("How the profile was resolved"),
467
+ }),
438
468
  handler: async ({ req }) => {
439
469
  const body = (await req.json()) as DictationBody;
440
470
  if (!body.transcription) {
@@ -4,6 +4,8 @@
4
4
  * Exposes document CRUD over HTTP, sharing business logic with the
5
5
  * handlers in `daemon/handlers/documents.ts`.
6
6
  */
7
+ import { z } from "zod";
8
+
7
9
  import { rawAll, rawGet, rawRun } from "../../memory/db.js";
8
10
  import { getLogger } from "../../util/logger.js";
9
11
  import { httpError } from "../http-errors.js";
@@ -161,6 +163,19 @@ export function documentRouteDefinitions(): RouteDefinition[] {
161
163
  endpoint: "documents",
162
164
  method: "GET",
163
165
  policyKey: "documents",
166
+ summary: "List documents",
167
+ description: "Return all documents, optionally filtered by conversation.",
168
+ tags: ["documents"],
169
+ queryParams: [
170
+ {
171
+ name: "conversationId",
172
+ schema: { type: "string" },
173
+ description: "Filter by conversation ID",
174
+ },
175
+ ],
176
+ responseBody: z.object({
177
+ documents: z.array(z.unknown()).describe("Document summary objects"),
178
+ }),
164
179
  handler: ({ url }) => {
165
180
  const conversationId =
166
181
  url.searchParams.get("conversationId") ?? undefined;
@@ -172,6 +187,19 @@ export function documentRouteDefinitions(): RouteDefinition[] {
172
187
  endpoint: "documents/:id",
173
188
  method: "GET",
174
189
  policyKey: "documents",
190
+ summary: "Get a document",
191
+ description: "Return a single document by surface ID.",
192
+ tags: ["documents"],
193
+ responseBody: z.object({
194
+ success: z.boolean(),
195
+ surfaceId: z.string(),
196
+ conversationId: z.string(),
197
+ title: z.string(),
198
+ content: z.string(),
199
+ wordCount: z.number(),
200
+ createdAt: z.number(),
201
+ updatedAt: z.number(),
202
+ }),
175
203
  handler: ({ params }) => {
176
204
  const result = loadDocument(params.id);
177
205
  if (!result.success) {
@@ -184,6 +212,20 @@ export function documentRouteDefinitions(): RouteDefinition[] {
184
212
  endpoint: "documents",
185
213
  method: "POST",
186
214
  policyKey: "documents",
215
+ summary: "Save a document",
216
+ description: "Create or upsert a document (by surfaceId).",
217
+ tags: ["documents"],
218
+ requestBody: z.object({
219
+ surfaceId: z.string().describe("Surface ID (unique key)"),
220
+ conversationId: z.string().describe("Owning conversation"),
221
+ title: z.string().describe("Document title"),
222
+ content: z.string().describe("Document content"),
223
+ wordCount: z.number().describe("Word count"),
224
+ }),
225
+ responseBody: z.object({
226
+ success: z.boolean(),
227
+ surfaceId: z.string(),
228
+ }),
187
229
  handler: async ({ req }) => {
188
230
  const body = (await req.json()) as {
189
231
  surfaceId?: string;
@@ -215,6 +215,16 @@ export function eventsRouteDefinitions(): RouteDefinition[] {
215
215
  {
216
216
  endpoint: "events",
217
217
  method: "GET",
218
+ summary: "Subscribe to assistant events",
219
+ description: "Stream assistant events as Server-Sent Events (SSE).",
220
+ tags: ["events"],
221
+ queryParams: [
222
+ {
223
+ name: "conversationKey",
224
+ schema: { type: "string" },
225
+ description: "Scope to a single conversation",
226
+ },
227
+ ],
218
228
  handler: ({ req, url, authContext }) =>
219
229
  handleSubscribeAssistantEvents(req, url, { authContext }),
220
230
  },
@@ -8,6 +8,8 @@
8
8
  * and merges results with lexical matches.
9
9
  */
10
10
 
11
+ import { z } from "zod";
12
+
11
13
  import { getConfig } from "../../config/loader.js";
12
14
  import { searchContacts } from "../../contacts/contact-store.js";
13
15
  import { searchConversations } from "../../memory/conversation-queries.js";
@@ -273,6 +275,39 @@ export function globalSearchRouteDefinitions(): RouteDefinition[] {
273
275
  {
274
276
  endpoint: "search/global",
275
277
  method: "GET",
278
+ summary: "Global search",
279
+ description:
280
+ "Federated search across conversations, memories, schedules, and contacts.",
281
+ tags: ["search"],
282
+ queryParams: [
283
+ {
284
+ name: "q",
285
+ schema: { type: "string" },
286
+ description: "Search query (required)",
287
+ },
288
+ {
289
+ name: "limit",
290
+ schema: { type: "integer" },
291
+ description: "Max results per category (1–100, default 20)",
292
+ },
293
+ {
294
+ name: "categories",
295
+ schema: { type: "string" },
296
+ description: "Comma-separated categories to search",
297
+ },
298
+ {
299
+ name: "deep",
300
+ schema: { type: "string" },
301
+ description: "Enable semantic search for memories (true/false)",
302
+ },
303
+ ],
304
+ responseBody: z.object({
305
+ query: z.string(),
306
+ results: z
307
+ .object({})
308
+ .passthrough()
309
+ .describe("Results grouped by category"),
310
+ }),
276
311
  handler: async ({ url }) => handleGlobalSearch(url),
277
312
  },
278
313
  ];
@@ -11,6 +11,10 @@
11
11
  * Guardian decisions additionally verify the actor is the bound guardian
12
12
  * via the AuthContext's actorPrincipalId.
13
13
  */
14
+ import { z } from "zod";
15
+
16
+ import { isHttpAuthDisabled } from "../../config/env.js";
17
+ import { findGuardianForChannel } from "../../contacts/contact-store.js";
14
18
  import {
15
19
  type CanonicalGuardianRequest,
16
20
  listPendingRequestsByConversationScope,
@@ -91,14 +95,25 @@ export async function handleGuardianActionDecision(
91
95
  return httpError("BAD_REQUEST", "action is required", 400);
92
96
  }
93
97
 
98
+ // Resolve the actor's guardian principal ID. For JWT-verified actors this
99
+ // comes from the token claims. For dev bypass (HTTP auth disabled) the
100
+ // synthetic "dev-bypass" principal won't match the real guardian binding,
101
+ // so fall back to the local guardian binding to avoid identity_mismatch.
102
+ let guardianPrincipalId: string | undefined =
103
+ authContext.actorPrincipalId ?? undefined;
104
+ if (isHttpAuthDisabled() && authContext.actorPrincipalId === "dev-bypass") {
105
+ const binding = findGuardianForChannel("vellum");
106
+ guardianPrincipalId = binding?.contact.principalId ?? undefined;
107
+ }
108
+
94
109
  const result = await processGuardianDecision({
95
110
  requestId,
96
111
  action,
97
112
  conversationId,
98
113
  channel: "vellum",
99
114
  actorContext: {
100
- actorPrincipalId: authContext.actorPrincipalId ?? undefined,
101
- guardianPrincipalId: authContext.actorPrincipalId ?? undefined,
115
+ actorPrincipalId: guardianPrincipalId,
116
+ guardianPrincipalId,
102
117
  },
103
118
  });
104
119
 
@@ -248,12 +263,42 @@ export function guardianActionRouteDefinitions(): RouteDefinition[] {
248
263
  {
249
264
  endpoint: "guardian-actions/pending",
250
265
  method: "GET",
266
+ summary: "List pending guardian actions",
267
+ description:
268
+ "Return pending guardian decision prompts for a conversation.",
269
+ tags: ["guardian"],
270
+ queryParams: [
271
+ {
272
+ name: "conversationId",
273
+ schema: { type: "string" },
274
+ description: "Conversation ID (required)",
275
+ },
276
+ ],
277
+ responseBody: z.object({
278
+ conversationId: z.string(),
279
+ prompts: z
280
+ .array(z.unknown())
281
+ .describe("Guardian decision prompt objects"),
282
+ }),
251
283
  handler: ({ url, authContext }) =>
252
284
  handleGuardianActionsPending(url, authContext),
253
285
  },
254
286
  {
255
287
  endpoint: "guardian-actions/decision",
256
288
  method: "POST",
289
+ summary: "Submit guardian decision",
290
+ description: "Submit a guardian action decision (approve/reject).",
291
+ tags: ["guardian"],
292
+ requestBody: z.object({
293
+ requestId: z.string().describe("Guardian request ID"),
294
+ action: z.string().describe("Decision action"),
295
+ conversationId: z.string().describe("Conversation ID").optional(),
296
+ }),
297
+ responseBody: z.object({
298
+ applied: z.boolean(),
299
+ requestId: z.string(),
300
+ reason: z.string(),
301
+ }),
257
302
  handler: async ({ req, authContext }) =>
258
303
  handleGuardianActionDecision(req, authContext),
259
304
  },
@@ -2,6 +2,7 @@
2
2
  * Approval prompt delivery: rich UI (buttons) with plain-text fallback.
3
3
  */
4
4
  import type { ChannelId } from "../../channels/types.js";
5
+ import { redactSecrets } from "../../security/secret-scanner.js";
5
6
  import { getLogger } from "../../util/logger.js";
6
7
  import type { ApprovalMessageContext } from "../approval-message-composer.js";
7
8
  import { composeApprovalMessageGenerative } from "../approval-message-composer.js";
@@ -20,6 +21,66 @@ import { requiredDecisionKeywords } from "./channel-route-shared.js";
20
21
 
21
22
  const log = getLogger("runtime-http");
22
23
 
24
+ // ---------------------------------------------------------------------------
25
+ // Tool input summary for rich-UI approval prompts
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Max characters for the tool input preview line. */
29
+ const INPUT_PREVIEW_MAX_LENGTH = 200;
30
+
31
+ /**
32
+ * Extract a concise, human-readable preview of the tool input so that
33
+ * sequential approval prompts for the same tool are distinguishable.
34
+ *
35
+ * Returns `null` when no meaningful preview can be produced.
36
+ */
37
+ /** Escape backticks in user-controlled input so they don't break inline code spans. */
38
+ function escapeBackticks(value: string): string {
39
+ return value.replace(/`/g, "'");
40
+ }
41
+
42
+ /** Redact potential secrets from tool input before previewing. */
43
+ function sanitizePreviewValue(value: string): string {
44
+ return escapeBackticks(redactSecrets(value));
45
+ }
46
+
47
+ function formatToolInputPreview(
48
+ toolName: string,
49
+ toolInput: Record<string, unknown>,
50
+ ): string | null {
51
+ // Pick the most relevant field based on tool type
52
+ const command = toolInput.command ?? toolInput.cmd;
53
+ if (typeof command === "string" && command.length > 0) {
54
+ return truncatePreview(`\`${sanitizePreviewValue(command)}\``);
55
+ }
56
+
57
+ const path = toolInput.path ?? toolInput.file_path ?? toolInput.filePath;
58
+ if (typeof path === "string" && path.length > 0) {
59
+ const verb =
60
+ toolName.includes("write") || toolName.includes("edit")
61
+ ? "writing to"
62
+ : toolName.includes("read")
63
+ ? "reading"
64
+ : "on";
65
+ return truncatePreview(`${verb} \`${sanitizePreviewValue(path)}\``);
66
+ }
67
+
68
+ const url = toolInput.url;
69
+ if (typeof url === "string" && url.length > 0) {
70
+ return truncatePreview(`fetching \`${sanitizePreviewValue(url)}\``);
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ function truncatePreview(text: string): string {
77
+ if (text.length <= INPUT_PREVIEW_MAX_LENGTH) return text;
78
+ const truncated = text.slice(0, INPUT_PREVIEW_MAX_LENGTH - 1) + "…";
79
+ // Preserve backtick pairing so markdown renders correctly.
80
+ const openBackticks = (truncated.match(/`/g) || []).length;
81
+ return openBackticks % 2 !== 0 ? truncated + "`" : truncated;
82
+ }
83
+
23
84
  export interface DeliverGeneratedApprovalPromptParams {
24
85
  replyCallbackUrl: string;
25
86
  chatId: string;
@@ -61,15 +122,29 @@ export async function deliverGeneratedApprovalPrompt(
61
122
  approvalCopyGenerator,
62
123
  );
63
124
 
125
+ // Append a tool input preview so sequential approvals are distinguishable
126
+ let enrichedText = richText;
127
+ if (uiMetadata.permissionDetails) {
128
+ const preview = formatToolInputPreview(
129
+ uiMetadata.permissionDetails.toolName,
130
+ uiMetadata.permissionDetails.toolInput,
131
+ );
132
+ if (preview) {
133
+ enrichedText = `${enrichedText}\n\n${preview}`;
134
+ }
135
+ }
136
+
64
137
  // Append a legend explaining what each button does
65
138
  const legend = buildActionLegend(uiMetadata.actions);
66
- const richTextWithLegend = legend ? `${richText}\n\n${legend}` : richText;
139
+ if (legend) {
140
+ enrichedText = `${enrichedText}\n\n${legend}`;
141
+ }
67
142
 
68
143
  try {
69
144
  await deliverApprovalPrompt(
70
145
  replyCallbackUrl,
71
146
  chatId,
72
- richTextWithLegend,
147
+ enrichedText,
73
148
  uiMetadata,
74
149
  assistantId,
75
150
  bearerToken,
@@ -0,0 +1,278 @@
1
+ /**
2
+ * HTTP route handlers for heartbeat management.
3
+ */
4
+
5
+ import { mkdirSync, writeFileSync } from "node:fs";
6
+ import { dirname } from "node:path";
7
+
8
+ import { desc, eq } from "drizzle-orm";
9
+ import { z } from "zod";
10
+
11
+ import { getConfig, saveConfig } from "../../config/loader.js";
12
+ import type { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
13
+ import { getDb } from "../../memory/db.js";
14
+ import { conversations } from "../../memory/schema/conversations.js";
15
+ import { readTextFileSync } from "../../util/fs.js";
16
+ import { getLogger } from "../../util/logger.js";
17
+ import { getWorkspacePromptPath } from "../../util/platform.js";
18
+ import { httpError } from "../http-errors.js";
19
+ import type { RouteDefinition } from "../http-router.js";
20
+
21
+ const log = getLogger("heartbeat-routes");
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Handlers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function handleGetConfig(heartbeatService?: HeartbeatService): Response {
28
+ const config = getConfig().heartbeat;
29
+ return Response.json({
30
+ enabled: config.enabled,
31
+ intervalMs: config.intervalMs,
32
+ activeHoursStart: config.activeHoursStart ?? null,
33
+ activeHoursEnd: config.activeHoursEnd ?? null,
34
+ nextRunAt: heartbeatService?.nextRunAt ?? null,
35
+ lastRunAt: heartbeatService?.lastRunAt ?? null,
36
+ success: true,
37
+ });
38
+ }
39
+
40
+ function handleUpdateConfig(
41
+ body: Record<string, unknown>,
42
+ heartbeatService?: HeartbeatService,
43
+ ): Response {
44
+ const config = getConfig();
45
+ const heartbeat = { ...config.heartbeat };
46
+
47
+ if (typeof body.enabled === "boolean") heartbeat.enabled = body.enabled;
48
+ if (typeof body.intervalMs === "number")
49
+ heartbeat.intervalMs = body.intervalMs;
50
+ if ("activeHoursStart" in body) {
51
+ heartbeat.activeHoursStart =
52
+ typeof body.activeHoursStart === "number"
53
+ ? body.activeHoursStart
54
+ : undefined;
55
+ }
56
+ if ("activeHoursEnd" in body) {
57
+ heartbeat.activeHoursEnd =
58
+ typeof body.activeHoursEnd === "number" ? body.activeHoursEnd : undefined;
59
+ }
60
+
61
+ try {
62
+ saveConfig({ ...config, heartbeat });
63
+ log.info({ heartbeat }, "Heartbeat config updated via HTTP");
64
+ } catch (err) {
65
+ log.error({ err }, "Failed to save heartbeat config");
66
+ return httpError("INTERNAL_ERROR", "Failed to save config", 500);
67
+ }
68
+
69
+ return Response.json({
70
+ enabled: heartbeat.enabled,
71
+ intervalMs: heartbeat.intervalMs,
72
+ activeHoursStart: heartbeat.activeHoursStart ?? null,
73
+ activeHoursEnd: heartbeat.activeHoursEnd ?? null,
74
+ nextRunAt: heartbeatService?.nextRunAt ?? null,
75
+ lastRunAt: heartbeatService?.lastRunAt ?? null,
76
+ success: true,
77
+ });
78
+ }
79
+
80
+ function handleListRuns(limit: number): Response {
81
+ const db = getDb();
82
+ const rows = db
83
+ .select({
84
+ id: conversations.id,
85
+ title: conversations.title,
86
+ createdAt: conversations.createdAt,
87
+ })
88
+ .from(conversations)
89
+ .where(eq(conversations.source, "heartbeat"))
90
+ .orderBy(desc(conversations.createdAt))
91
+ .limit(limit)
92
+ .all();
93
+
94
+ return Response.json({
95
+ runs: rows.map((r) => ({
96
+ id: r.id,
97
+ title: r.title ?? "Heartbeat",
98
+ createdAt: r.createdAt,
99
+ result: "ok",
100
+ })),
101
+ });
102
+ }
103
+
104
+ async function handleRunNow(
105
+ heartbeatService?: HeartbeatService,
106
+ ): Promise<Response> {
107
+ if (!heartbeatService) {
108
+ return httpError(
109
+ "SERVICE_UNAVAILABLE",
110
+ "Heartbeat service not available",
111
+ 503,
112
+ );
113
+ }
114
+
115
+ try {
116
+ const ran = await heartbeatService.runOnce({ force: true });
117
+ return Response.json({ success: true, ran });
118
+ } catch (err) {
119
+ const message = err instanceof Error ? err.message : String(err);
120
+ log.error({ err }, "Heartbeat run-now failed");
121
+ return Response.json({ success: false, error: message });
122
+ }
123
+ }
124
+
125
+ function handleGetChecklist(): Response {
126
+ const path = getWorkspacePromptPath("HEARTBEAT.md");
127
+ const content = readTextFileSync(path);
128
+ return Response.json({
129
+ content: content ?? "",
130
+ isDefault: content == null,
131
+ });
132
+ }
133
+
134
+ function handleWriteChecklist(content: string): Response {
135
+ const path = getWorkspacePromptPath("HEARTBEAT.md");
136
+ try {
137
+ mkdirSync(dirname(path), { recursive: true });
138
+ writeFileSync(path, content, "utf-8");
139
+ log.info("Heartbeat checklist updated via HTTP");
140
+ return Response.json({ success: true });
141
+ } catch (err) {
142
+ log.error({ err }, "Failed to write heartbeat checklist");
143
+ return httpError("INTERNAL_ERROR", "Failed to write checklist", 500);
144
+ }
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Route definitions
149
+ // ---------------------------------------------------------------------------
150
+
151
+ export function heartbeatRouteDefinitions(deps: {
152
+ getHeartbeatService?: () => HeartbeatService | undefined;
153
+ }): RouteDefinition[] {
154
+ return [
155
+ {
156
+ endpoint: "heartbeat/config",
157
+ method: "GET",
158
+ policyKey: "heartbeat",
159
+ summary: "Get heartbeat config",
160
+ description: "Return the current heartbeat schedule configuration.",
161
+ tags: ["heartbeat"],
162
+ responseBody: z.object({
163
+ enabled: z.boolean(),
164
+ intervalMs: z.number(),
165
+ activeHoursStart: z.number(),
166
+ activeHoursEnd: z.number(),
167
+ nextRunAt: z.string(),
168
+ success: z.boolean(),
169
+ }),
170
+ handler: () => handleGetConfig(deps.getHeartbeatService?.()),
171
+ },
172
+ {
173
+ endpoint: "heartbeat/config",
174
+ method: "PUT",
175
+ policyKey: "heartbeat",
176
+ summary: "Update heartbeat config",
177
+ description: "Update the heartbeat schedule configuration.",
178
+ tags: ["heartbeat"],
179
+ requestBody: z.object({
180
+ enabled: z.boolean().describe("Enable or disable heartbeat"),
181
+ intervalMs: z.number().describe("Heartbeat interval in ms"),
182
+ activeHoursStart: z.number().describe("Active hours start (0–23)"),
183
+ activeHoursEnd: z.number().describe("Active hours end (0–23)"),
184
+ }),
185
+ responseBody: z.object({
186
+ enabled: z.boolean(),
187
+ intervalMs: z.number(),
188
+ activeHoursStart: z.number(),
189
+ activeHoursEnd: z.number(),
190
+ nextRunAt: z.string(),
191
+ success: z.boolean(),
192
+ }),
193
+ handler: async ({ req }) => {
194
+ const body: unknown = await req.json();
195
+ if (typeof body !== "object" || !body || Array.isArray(body)) {
196
+ return httpError(
197
+ "BAD_REQUEST",
198
+ "Request body must be a JSON object",
199
+ 400,
200
+ );
201
+ }
202
+ return handleUpdateConfig(
203
+ body as Record<string, unknown>,
204
+ deps.getHeartbeatService?.(),
205
+ );
206
+ },
207
+ },
208
+ {
209
+ endpoint: "heartbeat/runs",
210
+ method: "GET",
211
+ policyKey: "heartbeat",
212
+ summary: "List heartbeat runs",
213
+ description: "Return recent heartbeat conversation runs.",
214
+ tags: ["heartbeat"],
215
+ queryParams: [
216
+ {
217
+ name: "limit",
218
+ schema: { type: "integer" },
219
+ description: "Max runs to return (default 20)",
220
+ },
221
+ ],
222
+ responseBody: z.object({
223
+ runs: z.array(z.unknown()).describe("Heartbeat run records"),
224
+ }),
225
+ handler: ({ url }) => {
226
+ const limit = Number(url.searchParams.get("limit") ?? 20);
227
+ return handleListRuns(limit);
228
+ },
229
+ },
230
+ {
231
+ endpoint: "heartbeat/run-now",
232
+ method: "POST",
233
+ policyKey: "heartbeat",
234
+ summary: "Run heartbeat now",
235
+ description: "Trigger an immediate heartbeat run.",
236
+ tags: ["heartbeat"],
237
+ responseBody: z.object({
238
+ success: z.boolean(),
239
+ ran: z.boolean().describe("Whether the heartbeat actually ran"),
240
+ }),
241
+ handler: () => handleRunNow(deps.getHeartbeatService?.()),
242
+ },
243
+ {
244
+ endpoint: "heartbeat/checklist",
245
+ method: "GET",
246
+ policyKey: "heartbeat",
247
+ summary: "Get heartbeat checklist",
248
+ description: "Return the HEARTBEAT.md checklist content.",
249
+ tags: ["heartbeat"],
250
+ responseBody: z.object({
251
+ content: z.string().describe("Checklist markdown content"),
252
+ isDefault: z.boolean().describe("True when no custom checklist exists"),
253
+ }),
254
+ handler: () => handleGetChecklist(),
255
+ },
256
+ {
257
+ endpoint: "heartbeat/checklist",
258
+ method: "PUT",
259
+ policyKey: "heartbeat",
260
+ summary: "Write heartbeat checklist",
261
+ description: "Overwrite the HEARTBEAT.md checklist content.",
262
+ tags: ["heartbeat"],
263
+ requestBody: z.object({
264
+ content: z.string().describe("Checklist markdown content"),
265
+ }),
266
+ responseBody: z.object({
267
+ success: z.boolean(),
268
+ }),
269
+ handler: async ({ req }) => {
270
+ const body = (await req.json()) as { content?: string };
271
+ if (typeof body.content !== "string") {
272
+ return httpError("BAD_REQUEST", "content is required", 400);
273
+ }
274
+ return handleWriteChecklist(body.content);
275
+ },
276
+ },
277
+ ];
278
+ }