vellum 0.2.13 → 0.2.14

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 (207) hide show
  1. package/README.md +32 -0
  2. package/bun.lock +2 -2
  3. package/docs/skills.md +4 -4
  4. package/package.json +2 -2
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
  6. package/src/__tests__/app-git-history.test.ts +176 -0
  7. package/src/__tests__/app-git-service.test.ts +169 -0
  8. package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
  9. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
  10. package/src/__tests__/browser-skill-endstate.test.ts +6 -6
  11. package/src/__tests__/call-bridge.test.ts +105 -13
  12. package/src/__tests__/call-domain.test.ts +163 -0
  13. package/src/__tests__/call-orchestrator.test.ts +113 -0
  14. package/src/__tests__/call-routes-http.test.ts +246 -6
  15. package/src/__tests__/channel-approval-routes.test.ts +438 -0
  16. package/src/__tests__/channel-approval.test.ts +266 -0
  17. package/src/__tests__/channel-approvals.test.ts +393 -0
  18. package/src/__tests__/channel-delivery-store.test.ts +447 -0
  19. package/src/__tests__/checker.test.ts +607 -1048
  20. package/src/__tests__/cli.test.ts +1 -56
  21. package/src/__tests__/config-schema.test.ts +137 -18
  22. package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
  23. package/src/__tests__/conflict-policy.test.ts +121 -0
  24. package/src/__tests__/conflict-store.test.ts +2 -0
  25. package/src/__tests__/contacts-tools.test.ts +3 -3
  26. package/src/__tests__/contradiction-checker.test.ts +99 -1
  27. package/src/__tests__/credential-security-invariants.test.ts +22 -6
  28. package/src/__tests__/credential-vault-unit.test.ts +780 -0
  29. package/src/__tests__/elevenlabs-client.test.ts +62 -0
  30. package/src/__tests__/ephemeral-permissions.test.ts +73 -23
  31. package/src/__tests__/filesystem-tools.test.ts +579 -0
  32. package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
  33. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
  34. package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
  35. package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
  36. package/src/__tests__/handlers-slack-config.test.ts +2 -1
  37. package/src/__tests__/handlers-telegram-config.test.ts +855 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +141 -1
  39. package/src/__tests__/hooks-runner.test.ts +6 -2
  40. package/src/__tests__/host-file-edit-tool.test.ts +124 -0
  41. package/src/__tests__/host-file-read-tool.test.ts +62 -0
  42. package/src/__tests__/host-file-write-tool.test.ts +59 -0
  43. package/src/__tests__/host-shell-tool.test.ts +251 -0
  44. package/src/__tests__/ingress-reconcile.test.ts +581 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +100 -41
  46. package/src/__tests__/ipc-validate.test.ts +50 -0
  47. package/src/__tests__/key-migration.test.ts +23 -0
  48. package/src/__tests__/memory-regressions.test.ts +99 -0
  49. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  50. package/src/__tests__/oauth-callback-registry.test.ts +11 -4
  51. package/src/__tests__/playbook-execution.test.ts +502 -0
  52. package/src/__tests__/playbook-tools.test.ts +4 -6
  53. package/src/__tests__/public-ingress-urls.test.ts +34 -0
  54. package/src/__tests__/qdrant-manager.test.ts +267 -0
  55. package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
  56. package/src/__tests__/recurrence-engine.test.ts +9 -0
  57. package/src/__tests__/recurrence-types.test.ts +8 -0
  58. package/src/__tests__/registry.test.ts +1 -1
  59. package/src/__tests__/runtime-runs.test.ts +1 -25
  60. package/src/__tests__/schedule-store.test.ts +16 -14
  61. package/src/__tests__/schedule-tools.test.ts +83 -0
  62. package/src/__tests__/scheduler-recurrence.test.ts +111 -10
  63. package/src/__tests__/secret-allowlist.test.ts +18 -17
  64. package/src/__tests__/secret-ingress-handler.test.ts +11 -0
  65. package/src/__tests__/secret-scanner.test.ts +43 -0
  66. package/src/__tests__/session-conflict-gate.test.ts +442 -6
  67. package/src/__tests__/session-init.benchmark.test.ts +3 -0
  68. package/src/__tests__/session-process-bridge.test.ts +242 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -1
  70. package/src/__tests__/shell-identity.test.ts +256 -0
  71. package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
  72. package/src/__tests__/subagent-tools.test.ts +637 -54
  73. package/src/__tests__/task-management-tools.test.ts +936 -0
  74. package/src/__tests__/task-runner.test.ts +2 -2
  75. package/src/__tests__/terminal-tools.test.ts +840 -0
  76. package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
  77. package/src/__tests__/tool-executor.test.ts +85 -151
  78. package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
  79. package/src/__tests__/trust-store.test.ts +27 -453
  80. package/src/__tests__/twilio-provider.test.ts +153 -3
  81. package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
  82. package/src/__tests__/twilio-routes-twiml.test.ts +4 -4
  83. package/src/__tests__/twilio-routes.test.ts +17 -262
  84. package/src/__tests__/twitter-auth-handler.test.ts +2 -1
  85. package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
  86. package/src/__tests__/twitter-cli-routing.test.ts +252 -0
  87. package/src/__tests__/twitter-oauth-client.test.ts +209 -0
  88. package/src/__tests__/workspace-policy.test.ts +213 -0
  89. package/src/calls/call-bridge.ts +92 -19
  90. package/src/calls/call-domain.ts +157 -5
  91. package/src/calls/call-orchestrator.ts +93 -7
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +8 -0
  94. package/src/calls/elevenlabs-config.ts +7 -5
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +32 -37
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +29 -7
  99. package/src/cli/twitter.ts +200 -21
  100. package/src/cli.ts +1 -20
  101. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
  102. package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
  103. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
  104. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  105. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
  106. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  107. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
  108. package/src/config/bundled-skills/phone-calls/SKILL.md +142 -34
  109. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
  110. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
  111. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
  112. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
  113. package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
  114. package/src/config/bundled-skills/twitter/SKILL.md +103 -17
  115. package/src/config/defaults.ts +10 -4
  116. package/src/config/schema.ts +80 -21
  117. package/src/config/types.ts +1 -0
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
  119. package/src/daemon/assistant-attachments.ts +4 -2
  120. package/src/daemon/handlers/apps.ts +69 -0
  121. package/src/daemon/handlers/config.ts +543 -24
  122. package/src/daemon/handlers/index.ts +1 -0
  123. package/src/daemon/handlers/sessions.ts +22 -6
  124. package/src/daemon/handlers/shared.ts +2 -1
  125. package/src/daemon/handlers/skills.ts +5 -20
  126. package/src/daemon/ipc-contract-inventory.json +28 -0
  127. package/src/daemon/ipc-contract.ts +168 -10
  128. package/src/daemon/ipc-validate.ts +17 -0
  129. package/src/daemon/lifecycle.ts +2 -0
  130. package/src/daemon/server.ts +78 -72
  131. package/src/daemon/session-attachments.ts +1 -1
  132. package/src/daemon/session-conflict-gate.ts +62 -6
  133. package/src/daemon/session-notifiers.ts +1 -1
  134. package/src/daemon/session-process.ts +62 -3
  135. package/src/daemon/session-tool-setup.ts +1 -2
  136. package/src/daemon/tls-certs.ts +189 -0
  137. package/src/daemon/video-thumbnail.ts +5 -3
  138. package/src/hooks/manager.ts +5 -9
  139. package/src/memory/app-git-service.ts +295 -0
  140. package/src/memory/app-store.ts +21 -0
  141. package/src/memory/conflict-intent.ts +47 -4
  142. package/src/memory/conflict-policy.ts +73 -0
  143. package/src/memory/conflict-store.ts +9 -1
  144. package/src/memory/contradiction-checker.ts +28 -0
  145. package/src/memory/conversation-key-store.ts +15 -0
  146. package/src/memory/db.ts +81 -0
  147. package/src/memory/embedding-local.ts +3 -13
  148. package/src/memory/external-conversation-store.ts +234 -0
  149. package/src/memory/job-handlers/conflict.ts +22 -2
  150. package/src/memory/jobs-worker.ts +67 -28
  151. package/src/memory/runs-store.ts +54 -7
  152. package/src/memory/schema.ts +20 -0
  153. package/src/messaging/provider.ts +9 -0
  154. package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
  155. package/src/messaging/providers/telegram-bot/client.ts +104 -0
  156. package/src/messaging/providers/telegram-bot/types.ts +15 -0
  157. package/src/messaging/registry.ts +1 -0
  158. package/src/permissions/checker.ts +48 -44
  159. package/src/permissions/prompter.ts +0 -4
  160. package/src/permissions/shell-identity.ts +227 -0
  161. package/src/permissions/trust-store.ts +76 -53
  162. package/src/permissions/types.ts +0 -19
  163. package/src/permissions/workspace-policy.ts +114 -0
  164. package/src/providers/retry.ts +12 -37
  165. package/src/runtime/assistant-event-hub.ts +41 -4
  166. package/src/runtime/channel-approval-parser.ts +60 -0
  167. package/src/runtime/channel-approval-types.ts +71 -0
  168. package/src/runtime/channel-approvals.ts +145 -0
  169. package/src/runtime/gateway-client.ts +16 -0
  170. package/src/runtime/http-server.ts +29 -9
  171. package/src/runtime/routes/call-routes.ts +52 -2
  172. package/src/runtime/routes/channel-routes.ts +296 -16
  173. package/src/runtime/routes/events-routes.ts +97 -28
  174. package/src/runtime/routes/run-routes.ts +2 -7
  175. package/src/runtime/run-orchestrator.ts +0 -3
  176. package/src/schedule/recurrence-engine.ts +26 -2
  177. package/src/schedule/recurrence-types.ts +1 -1
  178. package/src/schedule/schedule-store.ts +12 -3
  179. package/src/security/secret-scanner.ts +7 -0
  180. package/src/tasks/ephemeral-permissions.ts +0 -2
  181. package/src/tasks/task-scheduler.ts +2 -1
  182. package/src/tools/calls/call-start.ts +8 -0
  183. package/src/tools/execution-target.ts +21 -0
  184. package/src/tools/execution-timeout.ts +49 -0
  185. package/src/tools/executor.ts +6 -135
  186. package/src/tools/network/web-search.ts +9 -32
  187. package/src/tools/policy-context.ts +29 -0
  188. package/src/tools/schedule/update.ts +8 -1
  189. package/src/tools/terminal/parser.ts +16 -18
  190. package/src/tools/types.ts +4 -11
  191. package/src/twitter/oauth-client.ts +102 -0
  192. package/src/twitter/router.ts +101 -0
  193. package/src/util/debounce.ts +88 -0
  194. package/src/util/network-info.ts +47 -0
  195. package/src/util/platform.ts +29 -4
  196. package/src/util/promise-guard.ts +37 -0
  197. package/src/util/retry.ts +98 -0
  198. package/src/util/truncate.ts +1 -1
  199. package/src/workspace/git-service.ts +129 -112
  200. package/src/tools/contacts/contact-merge.ts +0 -55
  201. package/src/tools/contacts/contact-search.ts +0 -58
  202. package/src/tools/contacts/contact-upsert.ts +0 -64
  203. package/src/tools/playbooks/index.ts +0 -4
  204. package/src/tools/playbooks/playbook-create.ts +0 -96
  205. package/src/tools/playbooks/playbook-delete.ts +0 -52
  206. package/src/tools/playbooks/playbook-list.ts +0 -74
  207. package/src/tools/playbooks/playbook-update.ts +0 -111
package/README.md CHANGED
@@ -120,6 +120,38 @@ assistant/
120
120
  └── package.json
121
121
  ```
122
122
 
123
+ ## Channel Approval Flow
124
+
125
+ When the assistant needs tool-use confirmation during a channel session (e.g., Telegram), the approval flow intercepts the run and surfaces an interactive prompt to the user. This is gated behind the `CHANNEL_APPROVALS_ENABLED=true` environment variable.
126
+
127
+ ### How it works
128
+
129
+ 1. **Detection** — When a channel inbound message triggers an agent loop, the runtime polls the run status. If the run transitions to `needs_confirmation`, the runtime sends an approval prompt to the gateway with inline keyboard metadata.
130
+ 2. **Interception** — Subsequent inbound messages on the same conversation are intercepted before normal processing. The handler checks for a pending approval and attempts to extract a decision from either callback data (button clicks) or plain text.
131
+ 3. **Decision** — The user's decision is mapped to the permission system (`allow` or `deny`) and applied to the pending run. For `approve_always`, a trust rule is persisted so future invocations of the same tool are auto-approved.
132
+ 4. **Reminder** — If the user sends a non-decision message while an approval is pending, a reminder prompt is re-sent with the approval buttons.
133
+
134
+ ### Key modules
135
+
136
+ | File | Purpose |
137
+ |------|---------|
138
+ | `src/runtime/channel-approvals.ts` | Orchestration: `getChannelApprovalPrompt`, `buildApprovalUIMetadata`, `handleChannelDecision`, `buildReminderPrompt` |
139
+ | `src/runtime/channel-approval-parser.ts` | Plain-text decision parser — matches phrases like `yes`, `approve`, `always`, `no`, `reject`, `deny`, `cancel` (case-insensitive) |
140
+ | `src/runtime/channel-approval-types.ts` | Shared types: `ApprovalAction`, `ChannelApprovalPrompt`, `ApprovalUIMetadata`, `ApprovalDecisionResult` |
141
+ | `src/runtime/routes/channel-routes.ts` | Integration point: `handleApprovalInterception` and `processChannelMessageWithApprovals` in the channel inbound handler |
142
+ | `src/runtime/gateway-client.ts` | `deliverApprovalPrompt()` — sends the approval payload (text + UI metadata) to the gateway for rendering |
143
+ | `src/memory/runs-store.ts` | `getPendingConfirmationsByConversation` — queries runs in `needs_confirmation` state |
144
+
145
+ ### Enabling
146
+
147
+ Set the environment variable before starting the daemon:
148
+
149
+ ```bash
150
+ CHANNEL_APPROVALS_ENABLED=true
151
+ ```
152
+
153
+ When disabled (the default), channel messages follow the standard fire-and-forget processing path without approval interception.
154
+
123
155
  ## Database
124
156
 
125
157
  SQLite via Drizzle ORM, stored at `~/.vellum/workspace/data/db/assistant.db`. Key tables include conversations, messages, tool invocations, attachments, memory segments (with FTS5), memory items, entities, reminders, and recurrence schedules (cron + RRULE).
package/bun.lock CHANGED
@@ -11,7 +11,7 @@
11
11
  "@huggingface/transformers": "^3.8.1",
12
12
  "@qdrant/js-client-rest": "^1.16.2",
13
13
  "@sentry/node": "^10.38.0",
14
- "@vellumai/cli": "0.1.13",
14
+ "@vellumai/cli": "0.1.14",
15
15
  "@vellumai/vellum-gateway": "0.1.10",
16
16
  "agentmail": "^0.1.0",
17
17
  "archiver": "^7.0.1",
@@ -542,7 +542,7 @@
542
542
 
543
543
  "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg=="],
544
544
 
545
- "@vellumai/cli": ["@vellumai/cli@0.1.13", "", { "dependencies": { "ink": "^6.7.0", "react": "^19.2.4", "react-devtools-core": "^6.1.2" }, "bin": { "vellum-cli": "src/index.ts" } }, "sha512-zoES1ddavpHZljC/uPkelJvIsen77aTtFwfLYAA4zdtCCf3zktS2+TaDVQxB1Eh+dbRi3Tgc+XCXbq7S86kFiQ=="],
545
+ "@vellumai/cli": ["@vellumai/cli@0.1.14", "", { "dependencies": { "ink": "^6.7.0", "react": "^19.2.4", "react-devtools-core": "^6.1.2" }, "bin": { "vellum-cli": "src/index.ts" } }, "sha512-SjPCBWhZIOsQaYdGswMlVpIqfFPvLEIwyBRQSMXcNayTxkMuj8nS6SyLiJau+8aD86EjrvAT1mp+Csd83wABvw=="],
546
546
 
547
547
  "@vellumai/vellum-gateway": ["@vellumai/vellum-gateway@0.1.10", "", { "dependencies": { "file-type": "^21.3.0", "pino": "^9.6.0", "pino-pretty": "^13.1.3", "zod": "^4.3.6" } }, "sha512-a41fGexW8RpWL4RTfZ3EM+XJMvz7t26D1axu2xAtZioXW3ZWMLGuogHnIJsgglzESl49E6VmmUsUGeD+dseV2w=="],
548
548
 
package/docs/skills.md CHANGED
@@ -6,7 +6,7 @@ This document describes the security model for the Vellum Assistant skill system
6
6
 
7
7
  Skills extend the assistant's capabilities by providing instructions (via `SKILL.md`) and optional custom tools (via `TOOLS.json`). Skills can be **bundled** (shipped with the application), **managed** (user-installed via `scaffold_managed_skill`), **workspace** (project-local), or **extra** (additional directories configured by the user).
8
8
 
9
- Because skills can introduce arbitrary tool behavior, they are subject to stricter permission defaults than core tools. The permission system uses **principal-aware trust rules** and **version-bound approvals** to ensure that skill-originated actions are explicitly authorized by the user.
9
+ Because skills can introduce arbitrary tool behavior, they are subject to stricter permission defaults than core tools.
10
10
 
11
11
  ## Permission Defaults for Skill Tools
12
12
 
@@ -37,7 +37,7 @@ The allowlist options presented during a permission prompt include both version-
37
37
 
38
38
  ## Version-Bound Approvals
39
39
 
40
- Trust rules can include a `principalVersion` field that binds the rule to a specific content hash of the skill's source files. This is the primary mechanism for ensuring that approving a skill does not grant blanket permission to future (potentially modified) versions of that skill.
40
+ Trust rules for `skill_load` can use version-specific patterns (e.g., `skill_load:my-skill@v1:abc123...`) to pin approval to a specific content hash of the skill's source files.
41
41
 
42
42
  ### How version hashing works
43
43
 
@@ -50,7 +50,7 @@ The `computeSkillVersionHash(directoryPath)` function computes a deterministic S
50
50
 
51
51
  ### Version invalidation
52
52
 
53
- When a skill's source files change (any file added, removed, or modified), the hash changes. Trust rules with the old `principalVersion` no longer match, and the user is re-prompted. This protects against:
53
+ When a skill's source files change (any file added, removed, or modified), the hash changes. Version-specific trust rules with the old hash no longer match, and the user is re-prompted. This protects against:
54
54
 
55
55
  - **Supply-chain attacks**: A malicious update to a managed or workspace skill cannot silently inherit previous approvals.
56
56
  - **Accidental drift**: Editing a skill's tool scripts invalidates stale approvals, ensuring the user reviews the new behavior.
@@ -155,4 +155,4 @@ Trust rules are stored in `~/.vellum/protected/trust.json`. You can inspect this
155
155
 
156
156
  ### "A skill tool keeps prompting even though I approved it."
157
157
 
158
- Check whether the rule in `trust.json` has a `principalVersion` field. If so, the rule is version-specific and will stop matching if the skill's code has changed since approval. Also check whether the rule has the correct `executionTarget` — a rule scoped to `sandbox` will not match a tool running on `host`.
158
+ Check whether the rule has the correct `executionTarget` — a rule scoped to `sandbox` will not match a tool running on `host`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vellum",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -29,7 +29,7 @@
29
29
  "@huggingface/transformers": "^3.8.1",
30
30
  "@qdrant/js-client-rest": "^1.16.2",
31
31
  "@sentry/node": "^10.38.0",
32
- "@vellumai/cli": "0.1.13",
32
+ "@vellumai/cli": "0.1.14",
33
33
  "@vellumai/vellum-gateway": "0.1.10",
34
34
  "agentmail": "^0.1.0",
35
35
  "archiver": "^7.0.1",
@@ -318,7 +318,9 @@ exports[`IPC message snapshots ClientMessage types suggestion_request serializes
318
318
 
319
319
  exports[`IPC message snapshots ClientMessage types add_trust_rule serializes to expected JSON 1`] = `
320
320
  {
321
+ "allowHighRisk": true,
321
322
  "decision": "allow",
323
+ "executionTarget": "host",
322
324
  "pattern": "git *",
323
325
  "scope": "/projects/my-app",
324
326
  "toolName": "bash",
@@ -509,6 +511,40 @@ exports[`IPC message snapshots ClientMessage types app_preview_request serialize
509
511
  }
510
512
  `;
511
513
 
514
+ exports[`IPC message snapshots ClientMessage types app_history_request serializes to expected JSON 1`] = `
515
+ {
516
+ "appId": "app-001",
517
+ "limit": 25,
518
+ "type": "app_history_request",
519
+ }
520
+ `;
521
+
522
+ exports[`IPC message snapshots ClientMessage types app_diff_request serializes to expected JSON 1`] = `
523
+ {
524
+ "appId": "app-001",
525
+ "fromCommit": "abc123def456",
526
+ "toCommit": "789abc123def",
527
+ "type": "app_diff_request",
528
+ }
529
+ `;
530
+
531
+ exports[`IPC message snapshots ClientMessage types app_file_at_version_request serializes to expected JSON 1`] = `
532
+ {
533
+ "appId": "app-001",
534
+ "commitHash": "abc123def456",
535
+ "path": "index.html",
536
+ "type": "app_file_at_version_request",
537
+ }
538
+ `;
539
+
540
+ exports[`IPC message snapshots ClientMessage types app_restore_request serializes to expected JSON 1`] = `
541
+ {
542
+ "appId": "app-001",
543
+ "commitHash": "abc123def456",
544
+ "type": "app_restore_request",
545
+ }
546
+ `;
547
+
512
548
  exports[`IPC message snapshots ClientMessage types share_app_cloud serializes to expected JSON 1`] = `
513
549
  {
514
550
  "appId": "app-001",
@@ -551,6 +587,13 @@ exports[`IPC message snapshots ClientMessage types twitter_integration_config se
551
587
  }
552
588
  `;
553
589
 
590
+ exports[`IPC message snapshots ClientMessage types telegram_config serializes to expected JSON 1`] = `
591
+ {
592
+ "action": "get",
593
+ "type": "telegram_config",
594
+ }
595
+ `;
596
+
554
597
  exports[`IPC message snapshots ClientMessage types twitter_auth_start serializes to expected JSON 1`] = `
555
598
  {
556
599
  "type": "twitter_auth_start",
@@ -830,6 +873,19 @@ exports[`IPC message snapshots ClientMessage types identity_get serializes to ex
830
873
  }
831
874
  `;
832
875
 
876
+ exports[`IPC message snapshots ClientMessage types tool_permission_simulate serializes to expected JSON 1`] = `
877
+ {
878
+ "forcePromptSideEffects": false,
879
+ "input": {
880
+ "command": "rm -rf /tmp/test",
881
+ },
882
+ "isInteractive": true,
883
+ "toolName": "bash",
884
+ "type": "tool_permission_simulate",
885
+ "workingDir": "/projects/my-app",
886
+ }
887
+ `;
888
+
833
889
  exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
834
890
  {
835
891
  "success": true,
@@ -946,9 +1002,6 @@ exports[`IPC message snapshots ServerMessage types confirmation_request serializ
946
1002
  "input": {
947
1003
  "command": "rm -rf /tmp/test",
948
1004
  },
949
- "principalId": "my-skill",
950
- "principalKind": "skill",
951
- "principalVersion": "sha256:abcdef1234567890",
952
1005
  "requestId": "req-002",
953
1006
  "riskLevel": "high",
954
1007
  "sandboxed": false,
@@ -1841,6 +1894,17 @@ exports[`IPC message snapshots ServerMessage types twitter_integration_config_re
1841
1894
  }
1842
1895
  `;
1843
1896
 
1897
+ exports[`IPC message snapshots ServerMessage types telegram_config_response serializes to expected JSON 1`] = `
1898
+ {
1899
+ "botUsername": "my_test_bot",
1900
+ "connected": true,
1901
+ "hasBotToken": true,
1902
+ "hasWebhookSecret": true,
1903
+ "success": true,
1904
+ "type": "telegram_config_response",
1905
+ }
1906
+ `;
1907
+
1844
1908
  exports[`IPC message snapshots ServerMessage types twitter_auth_result serializes to expected JSON 1`] = `
1845
1909
  {
1846
1910
  "accountInfo": "@vellum_test",
@@ -1882,6 +1946,49 @@ exports[`IPC message snapshots ServerMessage types app_preview_response serializ
1882
1946
  }
1883
1947
  `;
1884
1948
 
1949
+ exports[`IPC message snapshots ServerMessage types app_history_response serializes to expected JSON 1`] = `
1950
+ {
1951
+ "appId": "app-001",
1952
+ "type": "app_history_response",
1953
+ "versions": [
1954
+ {
1955
+ "commitHash": "abc123def456",
1956
+ "message": "Initial app commit",
1957
+ "timestamp": 1700000000,
1958
+ },
1959
+ {
1960
+ "commitHash": "789abc123def",
1961
+ "message": "Update landing page",
1962
+ "timestamp": 1700001000,
1963
+ },
1964
+ ],
1965
+ }
1966
+ `;
1967
+
1968
+ exports[`IPC message snapshots ServerMessage types app_diff_response serializes to expected JSON 1`] = `
1969
+ {
1970
+ "appId": "app-001",
1971
+ "diff": "diff --git a/index.html b/index.html",
1972
+ "type": "app_diff_response",
1973
+ }
1974
+ `;
1975
+
1976
+ exports[`IPC message snapshots ServerMessage types app_file_at_version_response serializes to expected JSON 1`] = `
1977
+ {
1978
+ "appId": "app-001",
1979
+ "content": "<html><body>Hello</body></html>",
1980
+ "path": "index.html",
1981
+ "type": "app_file_at_version_response",
1982
+ }
1983
+ `;
1984
+
1985
+ exports[`IPC message snapshots ServerMessage types app_restore_response serializes to expected JSON 1`] = `
1986
+ {
1987
+ "success": true,
1988
+ "type": "app_restore_response",
1989
+ }
1990
+ `;
1991
+
1885
1992
  exports[`IPC message snapshots ServerMessage types ui_surface_undo_result serializes to expected JSON 1`] = `
1886
1993
  {
1887
1994
  "remainingUndos": 3,
@@ -2314,3 +2421,106 @@ exports[`IPC message snapshots ServerMessage types identity_get_response seriali
2314
2421
  "type": "identity_get_response",
2315
2422
  }
2316
2423
  `;
2424
+
2425
+ exports[`IPC message snapshots ServerMessage types tool_permission_simulate_response serializes to expected JSON 1`] = `
2426
+ {
2427
+ "decision": "prompt",
2428
+ "executionTarget": "host",
2429
+ "promptPayload": {
2430
+ "allowlistOptions": [
2431
+ {
2432
+ "description": "Allow rm commands",
2433
+ "label": "Allow rm commands",
2434
+ "pattern": "bash:rm *",
2435
+ },
2436
+ ],
2437
+ "persistentDecisionsAllowed": true,
2438
+ "scopeOptions": [
2439
+ {
2440
+ "label": "In /projects/my-app",
2441
+ "scope": "/projects/my-app",
2442
+ },
2443
+ ],
2444
+ },
2445
+ "reason": "No matching trust rule; tool requires approval",
2446
+ "riskLevel": "high",
2447
+ "success": true,
2448
+ "type": "tool_permission_simulate_response",
2449
+ }
2450
+ `;
2451
+
2452
+ exports[`IPC message snapshots ClientMessage types app_history_request serializes to expected JSON 1`] = `
2453
+ {
2454
+ "appId": "app-001",
2455
+ "type": "app_history_request",
2456
+ }
2457
+ `;
2458
+
2459
+ exports[`IPC message snapshots ClientMessage types app_diff_request serializes to expected JSON 1`] = `
2460
+ {
2461
+ "appId": "app-001",
2462
+ "fromCommit": "abc123",
2463
+ "type": "app_diff_request",
2464
+ }
2465
+ `;
2466
+
2467
+ exports[`IPC message snapshots ClientMessage types app_file_at_version_request serializes to expected JSON 1`] = `
2468
+ {
2469
+ "appId": "app-001",
2470
+ "commitHash": "abc123",
2471
+ "path": "index.html",
2472
+ "type": "app_file_at_version_request",
2473
+ }
2474
+ `;
2475
+
2476
+ exports[`IPC message snapshots ClientMessage types app_restore_request serializes to expected JSON 1`] = `
2477
+ {
2478
+ "appId": "app-001",
2479
+ "commitHash": "abc123",
2480
+ "type": "app_restore_request",
2481
+ }
2482
+ `;
2483
+
2484
+ exports[`IPC message snapshots ServerMessage types app_history_response serializes to expected JSON 1`] = `
2485
+ {
2486
+ "appId": "app-001",
2487
+ "type": "app_history_response",
2488
+ "versions": [
2489
+ {
2490
+ "commitHash": "abc123",
2491
+ "message": "Initial commit",
2492
+ "timestamp": 1700000000,
2493
+ },
2494
+ ],
2495
+ }
2496
+ `;
2497
+
2498
+ exports[`IPC message snapshots ServerMessage types app_diff_response serializes to expected JSON 1`] = `
2499
+ {
2500
+ "appId": "app-001",
2501
+ "diff":
2502
+ "--- a/index.html
2503
+ +++ b/index.html
2504
+ @@ -1 +1 @@
2505
+ -old
2506
+ +new"
2507
+ ,
2508
+ "type": "app_diff_response",
2509
+ }
2510
+ `;
2511
+
2512
+ exports[`IPC message snapshots ServerMessage types app_file_at_version_response serializes to expected JSON 1`] = `
2513
+ {
2514
+ "appId": "app-001",
2515
+ "content": "<html></html>",
2516
+ "path": "index.html",
2517
+ "type": "app_file_at_version_response",
2518
+ }
2519
+ `;
2520
+
2521
+ exports[`IPC message snapshots ServerMessage types app_restore_response serializes to expected JSON 1`] = `
2522
+ {
2523
+ "success": true,
2524
+ "type": "app_restore_response",
2525
+ }
2526
+ `;
@@ -0,0 +1,176 @@
1
+ import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
2
+ import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { _resetGitServiceRegistry } from '../workspace/git-service.js';
6
+ import { _resetAppGitState } from '../memory/app-git-service.js';
7
+
8
+ // Mock getDataDir to use a temp directory
9
+ let testDataDir: string;
10
+
11
+ mock.module('../util/platform.js', () => ({
12
+ getDataDir: () => testDataDir,
13
+ getProjectDir: () => testDataDir,
14
+ }));
15
+
16
+ // Re-import after mocking so modules use our temp dir
17
+ const { createApp, updateApp, deleteApp: _deleteApp, writeAppFile: _writeAppFile, editAppFile: _editAppFile, getAppsDir } = await import('../memory/app-store.js');
18
+ const { getAppHistory, getAppDiff, getAppFileAtVersion, restoreAppVersion, commitAppChange: _commitAppChange } = await import('../memory/app-git-service.js');
19
+
20
+ describe('App Git History', () => {
21
+ beforeEach(() => {
22
+ testDataDir = join(tmpdir(), `vellum-app-git-history-${Date.now()}-${Math.random().toString(36).slice(2)}`);
23
+ mkdirSync(join(testDataDir, 'apps'), { recursive: true });
24
+ _resetGitServiceRegistry();
25
+ _resetAppGitState();
26
+ });
27
+
28
+ afterEach(() => {
29
+ if (existsSync(testDataDir)) {
30
+ rmSync(testDataDir, { recursive: true, force: true });
31
+ }
32
+ });
33
+
34
+ /** Wait for fire-and-forget commits to complete. */
35
+ async function waitForCommits(): Promise<void> {
36
+ await new Promise(resolve => setTimeout(resolve, 500));
37
+ }
38
+
39
+ test('getAppHistory returns commits for a specific app', async () => {
40
+ const app = createApp({
41
+ name: 'History App',
42
+ schemaJson: '{}',
43
+ htmlDefinition: '<h1>v1</h1>',
44
+ });
45
+ await waitForCommits();
46
+
47
+ updateApp(app.id, { htmlDefinition: '<h1>v2</h1>' });
48
+ await waitForCommits();
49
+
50
+ const history = await getAppHistory(app.id);
51
+ expect(history.length).toBeGreaterThanOrEqual(2);
52
+ expect(history[0].message).toContain('Update app');
53
+ // The create commit may be absorbed into the "Initial commit" on a fresh repo
54
+ expect(history[history.length - 1].message).toMatch(/Create app|Initial commit/);
55
+ expect(history[0].commitHash).toMatch(/^[0-9a-f]+$/);
56
+ expect(history[0].timestamp).toBeGreaterThan(0);
57
+ });
58
+
59
+ test('getAppHistory does not return commits for other apps', async () => {
60
+ const app1 = createApp({
61
+ name: 'App One',
62
+ schemaJson: '{}',
63
+ htmlDefinition: '<p>one</p>',
64
+ });
65
+ await waitForCommits();
66
+
67
+ const app2 = createApp({
68
+ name: 'App Two',
69
+ schemaJson: '{}',
70
+ htmlDefinition: '<p>two</p>',
71
+ });
72
+ await waitForCommits();
73
+
74
+ const history1 = await getAppHistory(app1.id);
75
+ const history2 = await getAppHistory(app2.id);
76
+
77
+ // App1's history should only contain its own commits
78
+ expect(history1.every(v => v.message.includes('App One') || v.message.includes('Initial commit'))).toBe(true);
79
+ // App2's history should only contain its own commits
80
+ expect(history2.every(v => v.message.includes('App Two') || v.message.includes('Initial commit'))).toBe(true);
81
+ });
82
+
83
+ test('getAppHistory respects limit', async () => {
84
+ const app = createApp({
85
+ name: 'Limited App',
86
+ schemaJson: '{}',
87
+ htmlDefinition: '<p>v1</p>',
88
+ });
89
+ await waitForCommits();
90
+
91
+ updateApp(app.id, { htmlDefinition: '<p>v2</p>' });
92
+ await waitForCommits();
93
+
94
+ updateApp(app.id, { htmlDefinition: '<p>v3</p>' });
95
+ await waitForCommits();
96
+
97
+ const limited = await getAppHistory(app.id, 2);
98
+ expect(limited.length).toBe(2);
99
+ });
100
+
101
+ test('getAppDiff shows changes between versions', async () => {
102
+ const app = createApp({
103
+ name: 'Diff App',
104
+ schemaJson: '{}',
105
+ htmlDefinition: '<p>original</p>',
106
+ });
107
+ await waitForCommits();
108
+
109
+ const history1 = await getAppHistory(app.id);
110
+ const createHash = history1[0].commitHash;
111
+
112
+ updateApp(app.id, { htmlDefinition: '<p>modified</p>' });
113
+ await waitForCommits();
114
+
115
+ const history2 = await getAppHistory(app.id);
116
+ const updateHash = history2[0].commitHash;
117
+
118
+ const diff = await getAppDiff(app.id, createHash, updateHash);
119
+ expect(diff).toContain('original');
120
+ expect(diff).toContain('modified');
121
+ });
122
+
123
+ test('getAppFileAtVersion returns file content at a specific commit', async () => {
124
+ const app = createApp({
125
+ name: 'File Version App',
126
+ schemaJson: '{}',
127
+ htmlDefinition: '<p>version one</p>',
128
+ });
129
+ await waitForCommits();
130
+
131
+ const history1 = await getAppHistory(app.id);
132
+ const v1Hash = history1[0].commitHash;
133
+
134
+ updateApp(app.id, { htmlDefinition: '<p>version two</p>' });
135
+ await waitForCommits();
136
+
137
+ // Get the file at v1 — should show old content
138
+ const v1Content = await getAppFileAtVersion(app.id, 'index.html', v1Hash);
139
+ expect(v1Content).toContain('version one');
140
+ expect(v1Content).not.toContain('version two');
141
+
142
+ // Current file should show new content
143
+ const currentContent = readFileSync(join(getAppsDir(), app.id, 'index.html'), 'utf-8');
144
+ expect(currentContent).toContain('version two');
145
+ });
146
+
147
+ test('restoreAppVersion restores files and creates a new commit', async () => {
148
+ const app = createApp({
149
+ name: 'Restore App',
150
+ schemaJson: '{}',
151
+ htmlDefinition: '<p>original content</p>',
152
+ });
153
+ await waitForCommits();
154
+
155
+ const history1 = await getAppHistory(app.id);
156
+ const originalHash = history1[0].commitHash;
157
+
158
+ updateApp(app.id, { htmlDefinition: '<p>new content</p>' });
159
+ await waitForCommits();
160
+
161
+ // Verify current content is "new content"
162
+ let current = readFileSync(join(getAppsDir(), app.id, 'index.html'), 'utf-8');
163
+ expect(current).toContain('new content');
164
+
165
+ // Restore to original
166
+ await restoreAppVersion(app.id, originalHash);
167
+
168
+ // Verify content is restored
169
+ current = readFileSync(join(getAppsDir(), app.id, 'index.html'), 'utf-8');
170
+ expect(current).toContain('original content');
171
+
172
+ // Verify a restore commit was created
173
+ const history2 = await getAppHistory(app.id);
174
+ expect(history2[0].message).toContain('Restore app');
175
+ });
176
+ });