@vellumai/assistant 0.9.0 → 0.9.1-staging.1

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 (222) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +4 -4
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
  12. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  13. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  14. package/openapi.yaml +135 -59
  15. package/package.json +2 -1
  16. package/scripts/sync-llm-catalog.ts +6 -15
  17. package/scripts/sync-web-search-catalog.ts +3 -11
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
  19. package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
  20. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  21. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  22. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  23. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  24. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  25. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  26. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  27. package/src/__tests__/btw-routes.test.ts +6 -8
  28. package/src/__tests__/checker.test.ts +0 -3
  29. package/src/__tests__/config-loader-backfill.test.ts +103 -6
  30. package/src/__tests__/config-watcher.test.ts +0 -18
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  32. package/src/__tests__/credential-broker.test.ts +449 -1
  33. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  34. package/src/__tests__/credential-prompt-route.test.ts +3 -4
  35. package/src/__tests__/credential-routes.test.ts +360 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -13
  37. package/src/__tests__/dynamic-page-surface.test.ts +101 -1
  38. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  39. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  40. package/src/__tests__/identity-routes.test.ts +0 -189
  41. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  42. package/src/__tests__/invite-redemption-service.test.ts +4 -4
  43. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  44. package/src/__tests__/llm-catalog-parity.test.ts +0 -22
  45. package/src/__tests__/llm-resolver.test.ts +49 -24
  46. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  47. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  48. package/src/__tests__/persona-resolver.test.ts +11 -14
  49. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  50. package/src/__tests__/registry.test.ts +2 -7
  51. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  52. package/src/__tests__/schedule-routes.test.ts +0 -30
  53. package/src/__tests__/schedule-tools.test.ts +2 -18
  54. package/src/__tests__/skill-execute-input.test.ts +46 -1
  55. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  56. package/src/__tests__/subagent-tools.test.ts +116 -0
  57. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  58. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  59. package/src/__tests__/token-manager.test.ts +519 -0
  60. package/src/__tests__/tool-executor.test.ts +0 -79
  61. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  62. package/src/__tests__/trusted-contact-verification.test.ts +6 -6
  63. package/src/__tests__/voice-invite-redemption.test.ts +2 -2
  64. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  65. package/src/__tests__/workspace-greetings.test.ts +152 -0
  66. package/src/agent/loop.ts +25 -5
  67. package/src/api/README.md +6 -6
  68. package/src/api/responses/conversation-message.ts +2 -4
  69. package/src/api/responses/home.ts +0 -4
  70. package/src/approvals/guardian-request-resolvers.ts +2 -2
  71. package/src/calls/relay-access-wait.ts +1 -1
  72. package/src/calls/voice-session-bridge.ts +2 -2
  73. package/src/cli/commands/plugins.ts +143 -2
  74. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  75. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
  76. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
  77. package/src/cli/lib/diff-plugin.ts +346 -0
  78. package/src/cli/lib/install-from-github.ts +105 -17
  79. package/src/cli/lib/merge-plugin-tree.ts +228 -0
  80. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  81. package/src/cli/lib/upgrade-plugin.ts +270 -10
  82. package/src/cli/program.ts +0 -2
  83. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  84. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  85. package/src/config/bundled-skills/workflows/SKILL.md +0 -1
  86. package/src/config/bundled-tool-registry.ts +2 -7
  87. package/src/config/call-site-defaults.ts +12 -2
  88. package/src/config/feature-flag-registry.json +1 -17
  89. package/src/config/inference-profile-validation.ts +26 -0
  90. package/src/config/loader.ts +4 -0
  91. package/src/config/profile-order.ts +28 -0
  92. package/src/config/schemas/elevenlabs.ts +0 -1
  93. package/src/config/schemas/platform.ts +0 -8
  94. package/src/config/seed-inference-profiles.ts +25 -20
  95. package/src/contacts/contact-store.ts +87 -96
  96. package/src/contacts/contacts-write.ts +5 -21
  97. package/src/context/compactor.ts +2 -2
  98. package/src/credential-execution/process-manager.ts +55 -14
  99. package/src/credential-execution/prompted-credential.ts +2 -3
  100. package/src/daemon/config-watcher.ts +0 -4
  101. package/src/daemon/conversation-agent-loop.ts +15 -4
  102. package/src/daemon/conversation-slash.ts +2 -23
  103. package/src/daemon/conversation-tool-setup.ts +11 -3
  104. package/src/daemon/conversation.ts +2 -0
  105. package/src/daemon/handlers/config-channels.ts +20 -16
  106. package/src/daemon/handlers/config-slack-channel.ts +2 -3
  107. package/src/daemon/lifecycle.ts +0 -7
  108. package/src/daemon/message-types/conversations.ts +3 -3
  109. package/src/daemon/message-types/sync.ts +0 -1
  110. package/src/daemon/orphan-reaper.test.ts +0 -19
  111. package/src/daemon/orphan-reaper.ts +2 -24
  112. package/src/daemon/server.ts +0 -10
  113. package/src/home/relationship-state.ts +2 -4
  114. package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
  115. package/src/memory/bookmark-crud.ts +1 -2
  116. package/src/memory/db-init.ts +7 -17
  117. package/src/memory/embedding-backend.ts +23 -0
  118. package/src/memory/embedding-billing-breaker.ts +96 -0
  119. package/src/memory/jobs-store.ts +25 -13
  120. package/src/memory/jobs-worker.ts +52 -0
  121. package/src/memory/memory-retrospective-constants.ts +4 -4
  122. package/src/memory/memory-retrospective-job.ts +19 -227
  123. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
  124. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
  125. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  126. package/src/memory/migrations/index.ts +1 -0
  127. package/src/memory/migrations/run-migrations.ts +41 -0
  128. package/src/memory/migrations/validate-migration-state.ts +1 -1
  129. package/src/memory/schema/contacts.ts +0 -4
  130. package/src/messaging/providers/slack/adapter.ts +1 -1
  131. package/src/notifications/adapters/shared.ts +29 -0
  132. package/src/notifications/adapters/slack.ts +5 -32
  133. package/src/notifications/adapters/telegram.ts +2 -20
  134. package/src/notifications/broadcaster.ts +10 -1
  135. package/src/notifications/home-feed-side-effect.ts +4 -3
  136. package/src/notifications/notification-utils.ts +17 -19
  137. package/src/notifications/types.ts +7 -0
  138. package/src/oauth/AGENTS.md +5 -24
  139. package/src/plugin-api/constants.ts +1 -1
  140. package/src/plugin-api/index.ts +6 -1
  141. package/src/plugin-api/model-profiles.ts +33 -0
  142. package/src/plugin-api/types.ts +50 -2
  143. package/src/plugins/defaults/index.ts +25 -0
  144. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
  145. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
  146. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  147. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  148. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  149. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  150. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
  151. package/src/prompts/persona-resolver.ts +2 -2
  152. package/src/runtime/AGENTS.md +0 -1
  153. package/src/runtime/actor-trust-resolver.ts +12 -44
  154. package/src/runtime/btw-sidechain.ts +3 -6
  155. package/src/runtime/channel-approval-types.ts +18 -45
  156. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  157. package/src/runtime/channel-verification-service.ts +4 -3
  158. package/src/runtime/invite-redemption-service.ts +3 -3
  159. package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
  160. package/src/runtime/routes/app-routes.ts +1 -1
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  162. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  163. package/src/runtime/routes/btw-routes.ts +1 -27
  164. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  165. package/src/runtime/routes/conversation-routes.ts +2 -2
  166. package/src/runtime/routes/credential-routes.ts +40 -16
  167. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  168. package/src/runtime/routes/identity-routes.ts +1 -296
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
  170. package/src/runtime/routes/plugins-routes.ts +171 -5
  171. package/src/runtime/routes/schedule-routes.ts +0 -22
  172. package/src/runtime/routes/workflow-routes.test.ts +4 -43
  173. package/src/runtime/routes/workflow-routes.ts +0 -28
  174. package/src/runtime/routes/workspace-greetings.ts +55 -0
  175. package/src/runtime/sync/resource-sync-events.ts +1 -11
  176. package/src/schedule/inference-profile.ts +2 -14
  177. package/src/subagent/manager.ts +6 -0
  178. package/src/subagent/types.ts +6 -0
  179. package/src/tools/AGENTS.md +3 -3
  180. package/src/tools/browser/browser-execution.ts +1 -1
  181. package/src/tools/network/web-search-error.ts +1 -1
  182. package/src/tools/permission-checker.ts +1 -1
  183. package/src/tools/schedule/create.ts +3 -9
  184. package/src/tools/schedule/update.ts +2 -10
  185. package/src/tools/side-effects.ts +2 -17
  186. package/src/tools/skills/execute.ts +34 -0
  187. package/src/tools/subagent/spawn.ts +34 -9
  188. package/src/tools/tool-approval-handler.ts +1 -3
  189. package/src/tools/tool-manifest.ts +0 -2
  190. package/src/tools/ui-surface/definitions.ts +39 -1
  191. package/src/tools/workflows/run-workflow.test.ts +8 -18
  192. package/src/util/platform.ts +2 -2
  193. package/src/workflows/capabilities.ts +2 -3
  194. package/src/workflows/run-manager.test.ts +0 -25
  195. package/src/workflows/run-manager.ts +2 -24
  196. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  197. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  198. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  199. package/src/__tests__/credential-vault.test.ts +0 -1706
  200. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  201. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  202. package/src/cli/commands/__tests__/task.test.ts +0 -914
  203. package/src/cli/commands/task.ts +0 -771
  204. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  205. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  206. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  207. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  208. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  209. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  210. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  211. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  212. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  213. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  214. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  215. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  216. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  217. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  218. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  219. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  220. package/src/memory/preloaded-apps.ts +0 -116
  221. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  222. package/src/tools/credentials/vault.ts +0 -712
@@ -25,6 +25,10 @@
25
25
 
26
26
  import { z } from "zod";
27
27
 
28
+ import {
29
+ diffPlugin,
30
+ PluginDiffUnavailableError,
31
+ } from "../../cli/lib/diff-plugin.js";
28
32
  import {
29
33
  inspectPlugin,
30
34
  PluginInspectNotFoundError,
@@ -58,7 +62,10 @@ import {
58
62
  uninstallPlugin,
59
63
  } from "../../cli/lib/uninstall-plugin.js";
60
64
  import {
65
+ PluginMergeBaselineError,
61
66
  PluginNotUpgradableError,
67
+ type PluginUpgradeStrategy,
68
+ PluginUpgradeStrategyUnsupportedError,
62
69
  upgradePlugin,
63
70
  } from "../../cli/lib/upgrade-plugin.js";
64
71
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
@@ -410,6 +417,12 @@ const pluginUpgradeRequestSchema = z.object({
410
417
  .describe(
411
418
  "Report what would change without modifying the install. Defaults to false.",
412
419
  ),
420
+ strategy: z
421
+ .enum(["ours", "theirs", "overwrite", "assistant"])
422
+ .optional()
423
+ .describe(
424
+ "How to reconcile local edits with the pin. `overwrite` (default) discards local edits and re-installs the pin; `ours`/`theirs` three-way merge, resolving conflicting hunks toward the local edit or the pin respectively; `assistant` is not yet supported.",
425
+ ),
413
426
  });
414
427
 
415
428
  const pluginUpgradeResponseSchema = z.object({
@@ -452,6 +465,9 @@ const pluginUpgradeResponseSchema = z.object({
452
465
  "Files materialized by the upgrade; null for a no-op or dry run.",
453
466
  ),
454
467
  dryRun: z.boolean().describe("Whether this was a dry run (no changes made)."),
468
+ strategy: z
469
+ .enum(["ours", "theirs", "overwrite", "assistant"])
470
+ .describe("Conflict-resolution strategy the upgrade applied."),
455
471
  provenanceWasUnknown: z
456
472
  .boolean()
457
473
  .describe(
@@ -459,6 +475,62 @@ const pluginUpgradeResponseSchema = z.object({
459
475
  ),
460
476
  });
461
477
 
478
+ const pluginFileDiffSchema = z
479
+ .object({
480
+ path: z.string().describe("POSIX-relative path within the plugin root."),
481
+ status: z
482
+ .enum(["modified", "added", "removed"])
483
+ .describe(
484
+ "Whether the file was edited, newly added, or deleted since install.",
485
+ ),
486
+ diff: z
487
+ .string()
488
+ .describe(
489
+ "Unified diff (`--- a/… / +++ b/…`) of the file. A short `Binary files differ` marker for binary files, or a `Baseline unavailable` marker when `reconstructed` is false.",
490
+ ),
491
+ binary: z
492
+ .boolean()
493
+ .describe(
494
+ "True when either side was detected as binary (NUL byte present).",
495
+ ),
496
+ reconstructed: z
497
+ .boolean()
498
+ .describe(
499
+ "True when the install-time baseline for this file was faithfully recovered (re-materialized bytes hash-match the install fingerprint). False when it could not be reconstructed (e.g. the curated adapter overlay changed since install), in which case `diff` is a marker, not a patch. Always true for added files.",
500
+ ),
501
+ })
502
+ .describe("Unified diff of a single drifted file.");
503
+
504
+ const pluginDiffResponseSchema = z.object({
505
+ name: z
506
+ .string()
507
+ .describe("Install name. Matches `assistant plugins install <name>`."),
508
+ target: z
509
+ .string()
510
+ .describe("Absolute path to the installed plugin directory on the host."),
511
+ commit: z
512
+ .string()
513
+ .describe(
514
+ "Commit the baseline was re-materialized from (the recorded install SHA).",
515
+ ),
516
+ committedAt: z
517
+ .string()
518
+ .nullable()
519
+ .describe(
520
+ "ISO-8601 committer timestamp (UTC) of `commit`; null when not recorded.",
521
+ ),
522
+ clean: z
523
+ .boolean()
524
+ .describe(
525
+ "True when the on-disk tree exactly matches the re-materialized baseline.",
526
+ ),
527
+ files: z
528
+ .array(pluginFileDiffSchema)
529
+ .describe(
530
+ "One entry per drifted file, sorted by path. Empty when `clean`.",
531
+ ),
532
+ });
533
+
462
534
  // ---------------------------------------------------------------------------
463
535
  // Helpers
464
536
  // ---------------------------------------------------------------------------
@@ -740,6 +812,45 @@ async function handleInspectPlugin({ pathParams = {} }: RouteHandlerArgs) {
740
812
  }
741
813
  }
742
814
 
815
+ // ---------------------------------------------------------------------------
816
+ // Handler — diff
817
+ // ---------------------------------------------------------------------------
818
+
819
+ async function handleDiffPlugin({ pathParams = {} }: RouteHandlerArgs) {
820
+ const rawName = pathParams.name ?? "";
821
+
822
+ try {
823
+ return await diffPlugin(
824
+ { name: rawName },
825
+ { fetch: globalThis.fetch.bind(globalThis) },
826
+ );
827
+ } catch (err) {
828
+ if (err instanceof InvalidPluginNameError) {
829
+ throw new BadRequestError(err.message);
830
+ }
831
+ if (
832
+ err instanceof PluginNotInstalledError ||
833
+ err instanceof PluginNotFoundError
834
+ ) {
835
+ throw new NotFoundError(err.message);
836
+ }
837
+ // The install exists but recorded no commit to re-materialize a baseline
838
+ // from — a permanent state the caller cannot resolve by retrying. 409
839
+ // marks the request as well-formed but not actionable in the current state.
840
+ if (err instanceof PluginDiffUnavailableError) {
841
+ throw new ConflictError(err.message);
842
+ }
843
+ // A rate-limited or temporarily-down source (the plugin repo) is a
844
+ // retryable outage, not a conflict — 503.
845
+ if (err instanceof PluginSourceUnavailableError) {
846
+ throw new ServiceUnavailableError(err.message);
847
+ }
848
+ throw new InternalError(
849
+ err instanceof Error ? err.message : "plugin diff failed",
850
+ );
851
+ }
852
+ }
853
+
743
854
  // ---------------------------------------------------------------------------
744
855
  // Handler — upgrade
745
856
  // ---------------------------------------------------------------------------
@@ -750,6 +861,10 @@ async function handleUpgradePlugin({
750
861
  }: RouteHandlerArgs) {
751
862
  const rawName = pathParams.name ?? "";
752
863
  const dryRun = typeof body.dryRun === "boolean" ? body.dryRun : undefined;
864
+ const strategy =
865
+ typeof body.strategy === "string"
866
+ ? (body.strategy as PluginUpgradeStrategy)
867
+ : undefined;
753
868
 
754
869
  // Like install, the upgrade target ref is the curated marketplace pin
755
870
  // (resolved inside `upgradePlugin` via `inspectPlugin`), never a
@@ -757,7 +872,7 @@ async function handleUpgradePlugin({
757
872
  // upgrade at an unreviewed revision.
758
873
  try {
759
874
  const result = await upgradePlugin(
760
- { name: rawName, dryRun },
875
+ { name: rawName, dryRun, strategy },
761
876
  { fetch: globalThis.fetch.bind(globalThis) },
762
877
  );
763
878
  return {
@@ -770,10 +885,14 @@ async function handleUpgradePlugin({
770
885
  target: result.target,
771
886
  fileCount: result.fileCount,
772
887
  dryRun: result.dryRun,
888
+ strategy: result.strategy,
773
889
  provenanceWasUnknown: result.provenanceWasUnknown,
774
890
  };
775
891
  } catch (err) {
776
- if (err instanceof InvalidPluginNameError) {
892
+ if (
893
+ err instanceof InvalidPluginNameError ||
894
+ err instanceof PluginUpgradeStrategyUnsupportedError
895
+ ) {
777
896
  throw new BadRequestError(err.message);
778
897
  }
779
898
  if (err instanceof PluginNotInstalledError) {
@@ -782,6 +901,12 @@ async function handleUpgradePlugin({
782
901
  if (err instanceof PluginNotFoundError) {
783
902
  throw new NotFoundError(err.message);
784
903
  }
904
+ // A merge strategy was requested but the install-time baseline can't be
905
+ // reconstructed — a well-formed request that isn't actionable in the
906
+ // current state (the caller can retry with `overwrite` or reinstall).
907
+ if (err instanceof PluginMergeBaselineError) {
908
+ throw new ConflictError(err.message);
909
+ }
785
910
  // The install exists but has no marketplace entry to advance to — a
786
911
  // permanent state the caller cannot resolve by retrying. 409 marks the
787
912
  // request as well-formed but not actionable in the current state.
@@ -992,6 +1117,47 @@ export const ROUTES: RouteDefinition[] = [
992
1117
  },
993
1118
  handler: handleInspectPlugin,
994
1119
  },
1120
+ {
1121
+ operationId: "plugins_diff",
1122
+ endpoint: "plugins/:name/diff",
1123
+ method: "POST",
1124
+ policy: {
1125
+ requiredScopes: ["settings.read"],
1126
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
1127
+ },
1128
+ summary: "Diff a plugin against its install commit",
1129
+ description:
1130
+ "Show a unified diff of local edits to an installed plugin against the exact commit it was installed at (recorded in its `install-meta.json`). Computing the diff re-materializes the baseline through the install pipeline — a network clone plus a temp-dir write — so this is a POST: it is not a safe, cacheable GET even though it leaves persistent state untouched (requires only `settings.read`). Drift is classified against the install-time fingerprint so an adapter overlay that moved since install never reads as a local change. Returns the baseline `commit`, a `clean` flag, and a `files` array of `{ path, status, diff, binary, reconstructed }` for each modified/added/removed file. The baseline is the install commit, not the marketplace pin — comparing against the current pin is `POST /v1/plugins/:name/upgrade` with `dryRun`. A name with no installed copy returns 404; an install that recorded no commit or fingerprint returns 409; an unreachable source returns 503. Mirrors the CLI's `assistant plugins diff <name>`.",
1131
+ tags: ["plugins"],
1132
+ pathParams: [
1133
+ {
1134
+ name: "name",
1135
+ type: "string",
1136
+ description:
1137
+ "Install name. Must match the kebab-case name accepted by `assistant plugins install`.",
1138
+ },
1139
+ ],
1140
+ responseBody: pluginDiffResponseSchema,
1141
+ additionalResponses: {
1142
+ "400": {
1143
+ description:
1144
+ "The plugin name failed sanitization (e.g. contained slashes, dots, or uppercase letters).",
1145
+ },
1146
+ "404": {
1147
+ description:
1148
+ "No copy of the plugin is installed, or its recorded commit resolves to nothing.",
1149
+ },
1150
+ "409": {
1151
+ description:
1152
+ "The install recorded no commit or no fingerprint to re-materialize and verify a baseline from.",
1153
+ },
1154
+ "503": {
1155
+ description:
1156
+ "The plugin source (GitHub) was temporarily unavailable; the diff is retryable.",
1157
+ },
1158
+ },
1159
+ handler: handleDiffPlugin,
1160
+ },
995
1161
  {
996
1162
  operationId: "plugins_upgrade",
997
1163
  endpoint: "plugins/:name/upgrade",
@@ -1002,7 +1168,7 @@ export const ROUTES: RouteDefinition[] = [
1002
1168
  },
1003
1169
  summary: "Upgrade a plugin to the marketplace pin",
1004
1170
  description:
1005
- 'Move an installed plugin to the marketplace\'s current pinned commit, re-materializing it under `<workspaceDir>/plugins/<name>/`. Always resolves against the curated marketplace pin (no caller-supplied ref), mirroring `plugins install`\'s curation boundary. A no-op (`outcome: "already-up-to-date"`) when the installed commit already equals the pin; pass `dryRun` to preview the move (`outcome: "would-upgrade"`) without touching the install. Installs lacking provenance are re-pinned to the current SHA. The assistant must be restarted to load the upgraded code. This does not gate on local edits — callers should consult `GET /v1/plugins/:name/inspect` (`local.localChanges`) first and confirm before overwriting. Mirrors the CLI\'s `assistant plugins upgrade <name>`.',
1171
+ 'Move an installed plugin to the marketplace\'s current pinned commit, re-materializing it under `<workspaceDir>/plugins/<name>/`. Always resolves against the curated marketplace pin (no caller-supplied ref), mirroring `plugins install`\'s curation boundary. A no-op (`outcome: "already-up-to-date"`) when the installed commit already equals the pin; pass `dryRun` to preview the move (`outcome: "would-upgrade"`) without touching the install. Installs lacking provenance are re-pinned to the current SHA. The assistant must be restarted to load the upgraded code. `strategy` controls how local edits are reconciled: `overwrite` (default) discards them and re-installs the pin wholesale; `ours`/`theirs` perform a three-way merge against the re-materialized install commit, carrying non-conflicting edits from both sides forward and resolving conflicting hunks toward the local edit or the pin respectively; `assistant` is not yet supported (400). A merge strategy whose install-time baseline cannot be reconstructed returns 409. Mirrors the CLI\'s `assistant plugins upgrade <name> [--strategy <s>]`.',
1006
1172
  tags: ["plugins"],
1007
1173
  pathParams: [
1008
1174
  {
@@ -1017,7 +1183,7 @@ export const ROUTES: RouteDefinition[] = [
1017
1183
  additionalResponses: {
1018
1184
  "400": {
1019
1185
  description:
1020
- "The plugin name failed sanitization (e.g. contained slashes, dots, or uppercase letters).",
1186
+ "The plugin name failed sanitization (e.g. contained slashes, dots, or uppercase letters), or the `assistant` strategy was requested (not yet supported).",
1021
1187
  },
1022
1188
  "404": {
1023
1189
  description:
@@ -1025,7 +1191,7 @@ export const ROUTES: RouteDefinition[] = [
1025
1191
  },
1026
1192
  "409": {
1027
1193
  description:
1028
- "The install exists but has no marketplace entry to advance to.",
1194
+ "The install exists but has no marketplace entry to advance to, or a merge strategy was requested whose install-time baseline cannot be reconstructed.",
1029
1195
  },
1030
1196
  "503": {
1031
1197
  description:
@@ -7,8 +7,6 @@
7
7
 
8
8
  import { z } from "zod";
9
9
 
10
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
11
- import { getConfig } from "../../config/loader.js";
12
10
  import { getOrCreateConversation } from "../../daemon/conversation-store.js";
13
11
  import { INTERNAL_GUARDIAN_TRUST_CONTEXT } from "../../daemon/trust-context.js";
14
12
  import { bootstrapConversation } from "../../memory/conversation-bootstrap.js";
@@ -299,7 +297,6 @@ function handleCreateSchedule(body: Record<string, unknown>) {
299
297
  }
300
298
 
301
299
  if (mode === "workflow") {
302
- assertWorkflowsEnabled();
303
300
  const workflowName =
304
301
  typeof body.workflowName === "string" ? body.workflowName.trim() : "";
305
302
  if (!workflowName) {
@@ -351,17 +348,6 @@ function handleCreateSchedule(body: Record<string, unknown>) {
351
348
  return handleListSchedules({});
352
349
  }
353
350
 
354
- /**
355
- * Reject workflow-mode schedule operations when the `workflows` feature flag
356
- * is off. Scheduled workflow runs would fail at trigger time (the run manager
357
- * hard-fails with WorkflowsDisabledError), so we fail fast at the route.
358
- */
359
- function assertWorkflowsEnabled(): void {
360
- if (!isAssistantFeatureFlagEnabled("workflows", getConfig())) {
361
- throw new BadRequestError("Workflows are not enabled.");
362
- }
363
- }
364
-
365
351
  function handleToggleSchedule(id: string, body: Record<string, unknown>) {
366
352
  const enabled = body.enabled;
367
353
  if (typeof enabled !== "boolean") {
@@ -427,11 +413,6 @@ function handleUpdateSchedule(id: string, body: Record<string, unknown>) {
427
413
  );
428
414
  }
429
415
 
430
- // Switching a schedule into workflow mode requires the workflows flag.
431
- if (body.mode === "workflow") {
432
- assertWorkflowsEnabled();
433
- }
434
-
435
416
  // Mirror the create-side validation: a schedule whose RESULTING mode is
436
417
  // `workflow` must carry a non-empty `workflowName`. Without this, a PATCH can
437
418
  // leave a workflow-mode schedule nameless — the scheduler then hits the
@@ -946,9 +927,6 @@ async function handleRunScheduleNow(id: string) {
946
927
  // `schedule.message` (usually empty — workflow-mode create no longer requires
947
928
  // a message), running a no-op normal turn instead of the workflow.
948
929
  if (schedule.mode === "workflow") {
949
- // Fail fast at the route when the flag is off (the run manager would also
950
- // hard-fail with WorkflowsDisabledError); matches create/update handling.
951
- assertWorkflowsEnabled();
952
930
  if (!schedule.workflowName) {
953
931
  throw new BadRequestError("Workflow schedule has no workflowName");
954
932
  }
@@ -1,6 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
 
3
- import type { AssistantConfig } from "../../config/schema.js";
4
3
  import type { WorkflowRun } from "../../workflows/journal-store.js";
5
4
  import type { SavedWorkflowEntry } from "../../workflows/library.js";
6
5
  import {
@@ -60,7 +59,6 @@ interface FakeManager {
60
59
  }
61
60
 
62
61
  function setup(opts: {
63
- flagEnabled?: boolean;
64
62
  runs?: WorkflowRun[];
65
63
  saved?: SavedWorkflowEntry[];
66
64
  /** Custom resume impl; defaults to a success that records the id. */
@@ -98,8 +96,6 @@ function setup(opts: {
98
96
  __setWorkflowRoutesDeps({
99
97
  getManager: () => manager,
100
98
  listWorkflows: () => opts.saved ?? [],
101
- getConfig: () => ({}) as AssistantConfig,
102
- isFlagEnabled: () => opts.flagEnabled ?? true,
103
99
  getAutoApproveThreshold: async () => opts.threshold ?? "none",
104
100
  });
105
101
  return { aborted, resumed, listCalls };
@@ -113,10 +109,9 @@ afterEach(() => {
113
109
  // Happy paths
114
110
  // ---------------------------------------------------------------------------
115
111
 
116
- describe("workflow routes (flag on)", () => {
112
+ describe("workflow routes (happy paths)", () => {
117
113
  beforeEach(() => {
118
114
  setup({
119
- flagEnabled: true,
120
115
  runs: [
121
116
  makeRun({ id: "run-1" }),
122
117
  makeRun({ id: "run-2", status: "completed" }),
@@ -145,7 +140,6 @@ describe("workflow routes (flag on)", () => {
145
140
 
146
141
  test("listWorkflowRuns honors limit + status query params", async () => {
147
142
  const { listCalls } = setup({
148
- flagEnabled: true,
149
143
  runs: [makeRun({ id: "run-1", status: "completed" })],
150
144
  });
151
145
  await route("listWorkflowRuns").handler({
@@ -163,7 +157,6 @@ describe("workflow routes (flag on)", () => {
163
157
 
164
158
  test("abortWorkflowRun aborts a known run", async () => {
165
159
  const { aborted } = setup({
166
- flagEnabled: true,
167
160
  runs: [makeRun({ id: "run-1" })],
168
161
  });
169
162
  const result = (await route("abortWorkflowRun").handler({
@@ -175,7 +168,6 @@ describe("workflow routes (flag on)", () => {
175
168
 
176
169
  test("resumeWorkflowRun resumes an interrupted run", async () => {
177
170
  const { resumed } = setup({
178
- flagEnabled: true,
179
171
  runs: [makeRun({ id: "run-1", status: "interrupted" })],
180
172
  });
181
173
  const result = (await route("resumeWorkflowRun").handler({
@@ -192,7 +184,6 @@ describe("workflow routes (flag on)", () => {
192
184
  // full-access posture it must refuse rather than silently bypass consent —
193
185
  // resume() is never called.
194
186
  const { resumed } = setup({
195
- flagEnabled: true,
196
187
  threshold: "medium",
197
188
  runs: [
198
189
  makeRun({
@@ -213,7 +204,6 @@ describe("workflow routes (flag on)", () => {
213
204
  // high-risk tools, so no prompt is needed — the side-effecting resume
214
205
  // proceeds directly.
215
206
  const { resumed } = setup({
216
- flagEnabled: true,
217
207
  threshold: "high",
218
208
  runs: [
219
209
  makeRun({
@@ -236,7 +226,6 @@ describe("workflow routes (flag on)", () => {
236
226
  // the object shape too — a strict parse would treat it as read-only and let
237
227
  // the side-effecting resume through without approval.
238
228
  const { resumed } = setup({
239
- flagEnabled: true,
240
229
  threshold: "medium",
241
230
  runs: [
242
231
  makeRun({
@@ -259,7 +248,6 @@ describe("workflow routes (flag on)", () => {
259
248
  // An explicit empty manifest grants no side effects, so the route resumes
260
249
  // it directly — the gate keys on the stored manifest, not run existence.
261
250
  const { resumed } = setup({
262
- flagEnabled: true,
263
251
  threshold: "medium",
264
252
  runs: [
265
253
  makeRun({
@@ -275,7 +263,6 @@ describe("workflow routes (flag on)", () => {
275
263
 
276
264
  test("resumeWorkflowRun maps a non-interrupted run to a 409 ConflictError", async () => {
277
265
  setup({
278
- flagEnabled: true,
279
266
  runs: [makeRun({ id: "run-1", status: "completed" })],
280
267
  resume: (id) => {
281
268
  throw new WorkflowResumeNotPossibleError(
@@ -292,7 +279,6 @@ describe("workflow routes (flag on)", () => {
292
279
 
293
280
  test("resumeWorkflowRun maps a cap error to a 429 TooManyRequestsError", async () => {
294
281
  setup({
295
- flagEnabled: true,
296
282
  runs: [makeRun({ id: "run-1", status: "interrupted" })],
297
283
  resume: () => {
298
284
  throw new WorkflowRunCapError(3);
@@ -323,14 +309,14 @@ describe("workflow routes (flag on)", () => {
323
309
 
324
310
  describe("workflow routes (unknown run)", () => {
325
311
  test("getWorkflowRun throws NotFoundError for an unknown id", () => {
326
- setup({ flagEnabled: true, runs: [] });
312
+ setup({ runs: [] });
327
313
  expect(() =>
328
314
  route("getWorkflowRun").handler({ pathParams: { id: "nope" } }),
329
315
  ).toThrow(NotFoundError);
330
316
  });
331
317
 
332
318
  test("abortWorkflowRun throws NotFoundError for an unknown id", () => {
333
- const { aborted } = setup({ flagEnabled: true, runs: [] });
319
+ const { aborted } = setup({ runs: [] });
334
320
  expect(() =>
335
321
  route("abortWorkflowRun").handler({ pathParams: { id: "nope" } }),
336
322
  ).toThrow(NotFoundError);
@@ -338,35 +324,10 @@ describe("workflow routes (unknown run)", () => {
338
324
  });
339
325
 
340
326
  test("resumeWorkflowRun throws NotFoundError for an unknown id", async () => {
341
- const { resumed } = setup({ flagEnabled: true, runs: [] });
327
+ const { resumed } = setup({ runs: [] });
342
328
  await expect(
343
329
  route("resumeWorkflowRun").handler({ pathParams: { id: "nope" } }),
344
330
  ).rejects.toThrow(NotFoundError);
345
331
  expect(resumed).toEqual([]);
346
332
  });
347
333
  });
348
-
349
- // ---------------------------------------------------------------------------
350
- // Flag off → every route 404s
351
- // ---------------------------------------------------------------------------
352
-
353
- describe("workflow routes (flag off)", () => {
354
- beforeEach(() => {
355
- setup({ flagEnabled: false, runs: [makeRun({ id: "run-1" })], saved: [] });
356
- });
357
-
358
- test.each([
359
- ["listWorkflowRuns", { queryParams: {} }],
360
- ["getWorkflowRun", { pathParams: { id: "run-1" } }],
361
- ["abortWorkflowRun", { pathParams: { id: "run-1" } }],
362
- ["listSavedWorkflows", {}],
363
- ] as const)("%s throws NotFoundError when the flag is off", (op, args) => {
364
- expect(() => route(op).handler(args)).toThrow(NotFoundError);
365
- });
366
-
367
- test("resumeWorkflowRun rejects with NotFoundError when the flag is off", async () => {
368
- await expect(
369
- route("resumeWorkflowRun").handler({ pathParams: { id: "run-1" } }),
370
- ).rejects.toThrow(NotFoundError);
371
- });
372
- });
@@ -5,17 +5,10 @@
5
5
  * shared ROUTES array. They are read/abort surfaces over the workflow run
6
6
  * manager and the saved-workflow library — a human (via the `vellum workflows`
7
7
  * CLI or the app) can inspect runs and abort an in-flight one.
8
- *
9
- * Every handler is gated on the `workflows` feature flag: when it is off, the
10
- * routes behave as if they do not exist (404), so disabling the flag fully
11
- * hides the surface.
12
8
  */
13
9
 
14
10
  import { z } from "zod";
15
11
 
16
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
17
- import { getConfig } from "../../config/loader.js";
18
- import type { AssistantConfig } from "../../config/schema.js";
19
12
  import { getAutoApproveThreshold } from "../../permissions/gateway-threshold-reader.js";
20
13
  import { isFullAccessThreshold } from "../../permissions/threshold.js";
21
14
  import { manifestGrantsSideEffects } from "../../workflows/capabilities.js";
@@ -46,8 +39,6 @@ export interface WorkflowRoutesDeps {
46
39
  "list" | "status" | "abort" | "resume"
47
40
  >;
48
41
  listWorkflows: typeof listWorkflows;
49
- getConfig: () => AssistantConfig;
50
- isFlagEnabled: (config: AssistantConfig) => boolean;
51
42
  getAutoApproveThreshold: typeof getAutoApproveThreshold;
52
43
  }
53
44
 
@@ -55,9 +46,6 @@ function defaultDeps(): WorkflowRoutesDeps {
55
46
  return {
56
47
  getManager: getWorkflowRunManager,
57
48
  listWorkflows,
58
- getConfig,
59
- isFlagEnabled: (config) =>
60
- isAssistantFeatureFlagEnabled("workflows", config),
61
49
  getAutoApproveThreshold,
62
50
  };
63
51
  }
@@ -109,17 +97,6 @@ const savedWorkflowSchema = z.object({
109
97
  // Handlers (transport-agnostic)
110
98
  // ---------------------------------------------------------------------------
111
99
 
112
- /**
113
- * Throw a 404 when the `workflows` flag is off, hiding the whole surface. The
114
- * NotFoundError (rather than a 403) keeps a disabled-flag indistinguishable
115
- * from a route that does not exist.
116
- */
117
- function assertFlagEnabled(): void {
118
- if (!deps.isFlagEnabled(deps.getConfig())) {
119
- throw new NotFoundError("Workflows are not enabled.");
120
- }
121
- }
122
-
123
100
  /**
124
101
  * The single compact-run wire contract, inferred from {@link workflowRunSchema}.
125
102
  * The HTTP routes return this shape; other producers (e.g. the CLI's client-side
@@ -162,7 +139,6 @@ function parseStatus(
162
139
  }
163
140
 
164
141
  function handleListRuns(queryParams: Record<string, string>) {
165
- assertFlagEnabled();
166
142
  const limit = parseLimit(queryParams.limit);
167
143
  const status = parseStatus(queryParams.status);
168
144
  const runs = deps.getManager().list({
@@ -173,7 +149,6 @@ function handleListRuns(queryParams: Record<string, string>) {
173
149
  }
174
150
 
175
151
  function handleGetRun(id: string) {
176
- assertFlagEnabled();
177
152
  const run = deps.getManager().status(id);
178
153
  if (!run) {
179
154
  throw new NotFoundError(`Workflow run ${id} not found`);
@@ -182,7 +157,6 @@ function handleGetRun(id: string) {
182
157
  }
183
158
 
184
159
  function handleAbortRun(id: string) {
185
- assertFlagEnabled();
186
160
  // status() is the source of truth for existence; abort() itself is a no-op
187
161
  // for unknown/finished runs, so 404 on an unknown id is surfaced here.
188
162
  const manager = deps.getManager();
@@ -194,7 +168,6 @@ function handleAbortRun(id: string) {
194
168
  }
195
169
 
196
170
  async function handleResumeRun(id: string) {
197
- assertFlagEnabled();
198
171
  const manager = deps.getManager();
199
172
  // status() is the source of truth for existence, so 404 an unknown id here
200
173
  // (matching abort) rather than letting resume's "not_found" reach the client
@@ -245,7 +218,6 @@ async function handleResumeRun(id: string) {
245
218
  }
246
219
 
247
220
  function handleListSavedWorkflows() {
248
- assertFlagEnabled();
249
221
  return { workflows: deps.listWorkflows() };
250
222
  }
251
223
 
@@ -0,0 +1,55 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+
3
+ import { getWorkspacePromptPath } from "../../util/platform.js";
4
+
5
+ function readWorkspaceFile(name: string): string {
6
+ try {
7
+ const path = getWorkspacePromptPath(name);
8
+ if (!existsSync(path)) return "";
9
+ return readFileSync(path, "utf-8");
10
+ } catch {
11
+ return "";
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Parse the `## Greetings` section from SOUL.md. Returns bullet items as an
17
+ * array of strings, or `null` if the section is missing or empty.
18
+ */
19
+ export function parseGreetingsSection(content: string): string[] | null {
20
+ let inSection = false;
21
+ let sectionLevel: number | null = null;
22
+ const greetings: string[] = [];
23
+
24
+ for (const line of content.split("\n")) {
25
+ const trimmed = line.trim();
26
+ const heading = /^(#{1,6})\s+(.+?)\s*#*$/.exec(trimmed);
27
+ if (heading) {
28
+ const level = heading[1]!.length;
29
+ const title = heading[2]!.trim();
30
+ if (inSection) {
31
+ if (sectionLevel !== null && level <= sectionLevel) break;
32
+ continue;
33
+ }
34
+ if (level === 2 && /^greetings$/i.test(title)) {
35
+ inSection = true;
36
+ sectionLevel = level;
37
+ }
38
+ continue;
39
+ }
40
+ if (!inSection) continue;
41
+ const bullet = /^(?:[-*+]\s+|\d+[.)]\s+)(.+)$/.exec(trimmed);
42
+ const greeting = bullet?.[1]?.trim();
43
+ if (greeting) {
44
+ greetings.push(greeting);
45
+ }
46
+ }
47
+
48
+ return greetings.length > 0 ? greetings : null;
49
+ }
50
+
51
+ export function readWorkspaceGreetings(): string[] | null {
52
+ const soulContent = readWorkspaceFile("SOUL.md");
53
+ if (!soulContent) return null;
54
+ return parseGreetingsSection(soulContent);
55
+ }
@@ -29,17 +29,7 @@ export function publishIdentityChanged(
29
29
  emoji: fields.emoji,
30
30
  home: fields.home,
31
31
  });
32
- void publishSyncInvalidation(
33
- [SYNC_TAGS.assistantIdentity, SYNC_TAGS.assistantIdentityIntro],
34
- originClientId,
35
- );
36
- }
37
-
38
- export function publishIdentityIntroChanged(originClientId?: string): void {
39
- void publishSyncInvalidation(
40
- [SYNC_TAGS.assistantIdentityIntro],
41
- originClientId,
42
- );
32
+ void publishSyncInvalidation([SYNC_TAGS.assistantIdentity], originClientId);
43
33
  }
44
34
 
45
35
  export function publishConfigChanged(originClientId?: string): void {
@@ -1,4 +1,4 @@
1
- import { getConfigReadOnly } from "../config/loader.js";
1
+ import { validateInferenceProfileKey } from "../config/inference-profile-validation.js";
2
2
 
3
3
  /**
4
4
  * Validate a schedule's inference-profile key against the configured
@@ -12,17 +12,5 @@ import { getConfigReadOnly } from "../config/loader.js";
12
12
  export function validateScheduleInferenceProfile(
13
13
  profile: string,
14
14
  ): string | null {
15
- if (!profile.trim()) {
16
- return "inferenceProfile must be a non-empty string";
17
- }
18
- const profiles = getConfigReadOnly().llm?.profiles ?? {};
19
- if (!Object.prototype.hasOwnProperty.call(profiles, profile)) {
20
- const available = Object.keys(profiles).sort();
21
- const hint =
22
- available.length > 0
23
- ? ` Available profiles: ${available.join(", ")}.`
24
- : " No profiles defined in llm.profiles.";
25
- return `Inference profile "${profile}" is not defined in llm.profiles.${hint}`;
26
- }
27
- return null;
15
+ return validateInferenceProfileKey(profile);
28
16
  }