@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321

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 (209) hide show
  1. package/Dockerfile +7 -0
  2. package/README.md +12 -12
  3. package/docker/mousepad/90-vellum.gschema.override +4 -0
  4. package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
  5. package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
  6. package/openapi.yaml +97 -4
  7. package/package.json +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +4 -2
  9. package/src/__tests__/call-routes-http.test.ts +4 -2
  10. package/src/__tests__/channel-readiness-discord.test.ts +8 -0
  11. package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +1 -0
  13. package/src/__tests__/conversation-pairing.test.ts +10 -0
  14. package/src/__tests__/conversation-resurface-done.test.ts +289 -0
  15. package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
  16. package/src/__tests__/gemini-provider.test.ts +31 -0
  17. package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
  18. package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
  19. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  20. package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
  21. package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
  22. package/src/__tests__/platform-callback-registration.test.ts +46 -1
  23. package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
  24. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  25. package/src/__tests__/pricing.test.ts +33 -0
  26. package/src/__tests__/provider-error-scenarios.test.ts +46 -0
  27. package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
  28. package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
  29. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  30. package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
  31. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
  32. package/src/__tests__/send-endpoint-busy.test.ts +4 -2
  33. package/src/__tests__/skill-load-inline-command.test.ts +35 -0
  34. package/src/__tests__/skill-load-tool.test.ts +42 -11
  35. package/src/__tests__/skill-tool-factory.test.ts +50 -2
  36. package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
  37. package/src/__tests__/validate-input.test.ts +73 -0
  38. package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
  39. package/src/api/ask-question-tool.ts +50 -0
  40. package/src/api/events/tool-result.ts +11 -0
  41. package/src/api/index.ts +7 -0
  42. package/src/api/requests/dictation.ts +2 -2
  43. package/src/api/responses/home.ts +27 -0
  44. package/src/avatar/notification-avatar.test.ts +42 -34
  45. package/src/avatar/notification-avatar.ts +1 -0
  46. package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
  47. package/src/calls/voice-escalation-judge.ts +6 -5
  48. package/src/calls/voice-screen-annotation.ts +44 -0
  49. package/src/calls/voice-session-bridge.ts +22 -0
  50. package/src/cli/commands/__tests__/plugins.test.ts +29 -1
  51. package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
  52. package/src/cli/commands/platform/index.help.ts +2 -1
  53. package/src/cli/commands/platform/index.ts +8 -2
  54. package/src/cli/commands/plugins.ts +16 -0
  55. package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
  56. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
  57. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
  58. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
  59. package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
  60. package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
  61. package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
  62. package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
  63. package/src/cli/lib/bundled-marketplace.json +4 -4
  64. package/src/cli/lib/inspect-plugin.ts +5 -0
  65. package/src/cli/lib/plugin-catalog-cache.ts +41 -4
  66. package/src/cli/lib/plugin-catalog-local.ts +9 -0
  67. package/src/cli/lib/plugin-catalog-platform.ts +48 -9
  68. package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
  69. package/src/cli/lib/plugin-details.ts +10 -7
  70. package/src/cli/lib/search-plugins.ts +2 -0
  71. package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
  72. package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
  73. package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
  74. package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
  75. package/src/config/call-site-defaults.ts +17 -11
  76. package/src/config/default-profile-catalog.ts +118 -19
  77. package/src/config/default-profile-names.ts +69 -0
  78. package/src/config/feature-flag-registry.json +25 -9
  79. package/src/config/inference-profile-validation.ts +7 -6
  80. package/src/config/llm-resolver.ts +16 -0
  81. package/src/config/profile-text-generation.test.ts +9 -0
  82. package/src/config/schemas/call-site-catalog.ts +8 -1
  83. package/src/config/schemas/llm.ts +70 -9
  84. package/src/config/sidebar-done-gate.ts +27 -0
  85. package/src/config/sync-gated-profiles.ts +96 -44
  86. package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
  87. package/src/daemon/auto-profile-router.ts +220 -0
  88. package/src/daemon/conversation-agent-loop.ts +77 -4
  89. package/src/daemon/conversation-slash.ts +2 -2
  90. package/src/daemon/conversation-turn-finalize.ts +12 -0
  91. package/src/daemon/dictation-text-processing.ts +4 -4
  92. package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
  93. package/src/daemon/handlers/config-channels.ts +3 -1
  94. package/src/daemon/handlers/conversation-history.ts +11 -1
  95. package/src/daemon/handlers/conversations.ts +118 -10
  96. package/src/daemon/pkb-reminder-builder.test.ts +10 -3
  97. package/src/daemon/pkb-reminder-builder.ts +1 -1
  98. package/src/desktop/desktop-browser-endpoint.ts +1 -0
  99. package/src/home/feed-types.ts +2 -0
  100. package/src/inbound/platform-callback-registration.ts +19 -6
  101. package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
  102. package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
  103. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
  104. package/src/live-voice/__tests__/protocol.test.ts +22 -0
  105. package/src/live-voice/continuation-judge.ts +1 -1
  106. package/src/live-voice/live-voice-session.ts +7 -6
  107. package/src/live-voice/protocol.ts +16 -5
  108. package/src/messaging/providers/gmail/client.ts +4 -0
  109. package/src/notifications/README.md +11 -3
  110. package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
  111. package/src/notifications/conversation-pairing.ts +23 -8
  112. package/src/notifications/home-feed-mirror.ts +39 -0
  113. package/src/notifications/home-feed-side-effect.ts +46 -19
  114. package/src/oauth/AGENTS.md +7 -1
  115. package/src/permissions/question-prompter.ts +14 -4
  116. package/src/persistence/conversation-crud.ts +114 -3
  117. package/src/persistence/conversation-queries.ts +48 -3
  118. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
  119. package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
  120. package/src/persistence/embeddings/embedding-backend.ts +23 -41
  121. package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
  122. package/src/persistence/embeddings/embedding-gemini.ts +3 -1
  123. package/src/persistence/embeddings/embedding-local.ts +72 -17
  124. package/src/persistence/embeddings/embedding-types.ts +0 -19
  125. package/src/persistence/jobs-store.ts +229 -0
  126. package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
  127. package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
  128. package/src/persistence/steps.ts +12 -0
  129. package/src/platform/sync-avatar.test.ts +29 -0
  130. package/src/plugin-api/model-profiles.ts +5 -3
  131. package/src/plugins/defaults/memory/AGENTS.md +5 -3
  132. package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
  133. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
  134. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
  135. package/src/plugins/defaults/memory/graph/tools.ts +9 -5
  136. package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
  137. package/src/plugins/defaults/memory/job-handlers.ts +8 -2
  138. package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
  139. package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
  140. package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
  141. package/src/plugins/defaults/memory/tools.test.ts +17 -0
  142. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
  143. package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
  144. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  145. package/src/providers/anthropic/client.ts +2 -2
  146. package/src/providers/gemini/client.ts +17 -1
  147. package/src/providers/inference/auth.ts +25 -0
  148. package/src/providers/jev/ask.test.ts +66 -0
  149. package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
  150. package/src/providers/malformed-tool-call.ts +55 -0
  151. package/src/providers/model-catalog.ts +257 -4
  152. package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
  153. package/src/providers/openai/chat-completions-provider.ts +30 -0
  154. package/src/providers/openai/codex-models.ts +2 -0
  155. package/src/providers/platform-proxy/constants.ts +5 -0
  156. package/src/providers/retry.ts +68 -36
  157. package/src/providers/search-provider-catalog.ts +10 -0
  158. package/src/providers/vellum-model-routing.test.ts +1 -0
  159. package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
  160. package/src/runtime/__tests__/question-resolution.test.ts +32 -0
  161. package/src/runtime/agent-wake.ts +9 -2
  162. package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
  163. package/src/runtime/auth/middleware.ts +15 -11
  164. package/src/runtime/middleware/auth.ts +7 -1
  165. package/src/runtime/pending-interactions.ts +8 -8
  166. package/src/runtime/question-resolution.ts +54 -8
  167. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
  168. package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
  169. package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
  170. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
  171. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
  172. package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
  173. package/src/runtime/routes/conversation-list-routes.ts +16 -14
  174. package/src/runtime/routes/conversation-query-routes.ts +4 -0
  175. package/src/runtime/routes/conversation-routes.ts +4 -2
  176. package/src/runtime/routes/diagnostics-routes.ts +66 -229
  177. package/src/runtime/routes/global-search-routes.ts +22 -7
  178. package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
  179. package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
  180. package/src/runtime/routes/platform-routes.ts +6 -1
  181. package/src/runtime/routes/plugins-routes.ts +5 -2
  182. package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
  183. package/src/runtime/sync/resource-sync-events.ts +11 -0
  184. package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
  185. package/src/runtime/sync/worker-daemon-notify.ts +44 -0
  186. package/src/runtime/verification-outbound-actions.ts +2 -2
  187. package/src/skills/validate-input.ts +75 -11
  188. package/src/telegram/__tests__/webhook-health.test.ts +8 -0
  189. package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
  190. package/src/tools/ask-question/ask-question-tool.ts +12 -1
  191. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
  192. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
  193. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
  194. package/src/tools/network/__tests__/web-search.test.ts +96 -0
  195. package/src/tools/network/web-fetch.ts +30 -9
  196. package/src/tools/network/web-search.ts +235 -4
  197. package/src/tools/skills/execute.ts +1 -1
  198. package/src/tools/skills/format-input-schema.ts +15 -0
  199. package/src/tools/skills/load.ts +15 -43
  200. package/src/tools/skills/scaffold-managed.ts +86 -71
  201. package/src/tools/skills/skill-tool-factory.ts +2 -1
  202. package/src/tools/workflows/manage-workflows.ts +4 -2
  203. package/src/util/worker-pipe.test.ts +146 -0
  204. package/src/util/worker-pipe.ts +49 -0
  205. package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
  206. package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
  207. package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
  208. package/src/workspace/migrations/registry.ts +4 -0
  209. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
@@ -191,6 +191,7 @@ const MANAGED_FALLBACK_PROVIDERS: string[] = [
191
191
  "openai",
192
192
  "fireworks",
193
193
  "together",
194
+ "typesafe",
194
195
  ];
195
196
 
196
197
  function enableManagedProxy() {
@@ -314,28 +315,57 @@ describe("managed proxy integration — credential precedence", () => {
314
315
  },
315
316
  );
316
317
 
317
- test("managed bootstrap registers anthropic, openai, gemini, fireworks, and together", async () => {
318
+ test("managed bootstrap registers every managed-routable catalog provider", async () => {
318
319
  enableManagedProxy();
319
320
  mockProviderKeys = {};
320
321
  await initializeProviders(makeProvidersConfig("anthropic", "test-model"));
321
- expect(listProviders()).toEqual(
322
- expect.arrayContaining([
323
- "anthropic",
324
- "openai",
325
- "gemini",
326
- "fireworks",
327
- "together",
328
- ]),
322
+ expect([...listProviders()].sort()).toEqual(
323
+ [...MANAGED_FALLBACK_PROVIDERS].sort(),
329
324
  );
330
- expect(listProviders()).toHaveLength(5);
331
- expect(getProviderRoutingSource("anthropic")).toBe("managed-proxy");
332
- expect(getProviderRoutingSource("openai")).toBe("managed-proxy");
333
- expect(getProviderRoutingSource("gemini")).toBe("managed-proxy");
334
- expect(getProviderRoutingSource("fireworks")).toBe("managed-proxy");
335
- expect(getProviderRoutingSource("together")).toBe("managed-proxy");
325
+ for (const provider of MANAGED_FALLBACK_PROVIDERS) {
326
+ expect(getProviderRoutingSource(provider)).toBe("managed-proxy");
327
+ }
336
328
  expect(getProviderRoutingSource("openrouter")).toBeUndefined();
337
329
  });
338
330
 
331
+ test("managed typesafe sends System One through the typesafe proxy path with the assistant key", async () => {
332
+ enableManagedProxy();
333
+ mockProviderKeys = {};
334
+ await initializeProviders(makeProvidersConfig("anthropic", "test-model"));
335
+
336
+ const provider = getProvider("typesafe");
337
+ const originalFetch = globalThis.fetch;
338
+ let capturedUrl = "";
339
+ let capturedHeaders: Record<string, string> = {};
340
+ globalThis.fetch = (async (
341
+ input: string | URL | Request,
342
+ init?: RequestInit,
343
+ ) => {
344
+ capturedUrl = String(input);
345
+ capturedHeaders = (init?.headers ?? {}) as Record<string, string>;
346
+ return new Response(
347
+ JSON.stringify({
348
+ model: "jev-latest",
349
+ answers: {},
350
+ usage: { input_tokens: 1, output_tokens: 0 },
351
+ }),
352
+ { status: 200, headers: { "Content-Type": "application/json" } },
353
+ );
354
+ }) as typeof fetch;
355
+ try {
356
+ await provider.sendMessage([userMsg("hello")], {
357
+ config: { model: "jev-latest" },
358
+ });
359
+ } finally {
360
+ globalThis.fetch = originalFetch;
361
+ }
362
+
363
+ expect(capturedUrl).toBe(
364
+ `${PLATFORM_BASE}/v1/runtime-proxy/typesafe/v1/systemone`,
365
+ );
366
+ expect(capturedHeaders.Authorization).toBe(`Bearer ${MANAGED_API_KEY}`);
367
+ });
368
+
339
369
  test("managed anthropic uses anthropic proxy path", async () => {
340
370
  enableManagedProxy();
341
371
  mockProviderKeys = {};
@@ -12,7 +12,9 @@ mock.module("../util/retry.js", () => ({
12
12
  import {
13
13
  recordFallbackServed,
14
14
  resetFallbackBreaker,
15
+ shouldSkipPrimary,
15
16
  } from "../providers/fallback-breaker.js";
17
+ import { malformedToolCallError } from "../providers/malformed-tool-call.js";
16
18
  import type {
17
19
  Message,
18
20
  Provider,
@@ -666,6 +668,26 @@ describe("RetryProvider fallback-route escalation", () => {
666
668
  expect(backup.calls()).toBe(1);
667
669
  });
668
670
 
671
+ test("malformed tool call exhausts retries → backup serves, breaker stays closed", async () => {
672
+ const primary = failingProvider("openai", () =>
673
+ malformedToolCallError("openai", "MALFORMED_FUNCTION_CALL"),
674
+ );
675
+ const backup = backupProvider();
676
+ const route = makeRoute(backup.provider);
677
+ const wrapped = new RetryProvider(primary.provider, {
678
+ resolveFallbackRoute: route.resolveFallbackRoute,
679
+ });
680
+
681
+ const result = await wrapped.sendMessage(MESSAGES, {
682
+ config: { callSite: "mainAgent" },
683
+ });
684
+
685
+ expect(primary.calls()).toBe(1 + DEFAULT_MAX_RETRIES);
686
+ expect(backup.calls()).toBe(1);
687
+ expect(result.model).toBe("backup-model");
688
+ expect(shouldSkipPrimary({ upstream: "openai" })).toBe(false);
689
+ });
690
+
669
691
  test("resolveFallbackRoute unset → behavior unchanged (original error rethrown)", async () => {
670
692
  const finalError = new ProviderError("Service Unavailable", "openai", 503);
671
693
  const primary = failingProvider("openai", () => finalError);
@@ -55,8 +55,10 @@ afterAll(() => {
55
55
  resetDbForTesting();
56
56
  });
57
57
 
58
- const TEST_TOKEN = "test-bearer-token-attach";
59
- const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };
58
+ // This suite mocks isHttpAuthDisabled() to true, so a request carrying no
59
+ // bearer authenticates through the dev bypass. A bearer that is present is
60
+ // verified against the signing key, which these tests do not set up.
61
+ const AUTH_HEADERS: Record<string, string> = {};
60
62
 
61
63
  describe("Runtime attachment metadata", () => {
62
64
  let server: RuntimeHttpServer;
@@ -32,25 +32,24 @@ mock.module("../daemon/skill-memory-refresh.js", () => ({
32
32
  refreshSkillCapabilityMemories: mockRefreshSkillCapabilityMemories,
33
33
  }));
34
34
 
35
- // Background skill updates route through the notification pipeline; record
36
- // the signals rather than standing up delivery.
37
- let emittedSignals: Array<{
38
- sourceEventName: string;
39
- sourceContextId: string;
40
- dedupeKey?: string;
41
- contextPayload?: Record<string, unknown>;
42
- }> = [];
43
- mock.module("../notifications/emit-signal.js", () => ({
44
- emitNotificationSignal: async (params: {
45
- sourceEventName: string;
46
- sourceContextId: string;
47
- dedupeKey?: string;
48
- contextPayload?: Record<string, unknown>;
49
- }) => {
50
- emittedSignals.push(params);
51
- return { signalId: "test-signal" };
52
- },
53
- }));
35
+ // Receipt recorder: the executor's one side effect for a background
36
+ // overwrite, captured at the job module's boundary so the tests read what
37
+ // was recorded rather than standing up the memory job queue.
38
+ import type * as ReceiptJob from "../plugins/defaults/memory/skill-update-receipt-job.js";
39
+
40
+ let recordedUpdates: Parameters<typeof ReceiptJob.recordSkillUpdate>[0][] = [];
41
+ let recordUpdateFails = false;
42
+ mock.module(
43
+ "../plugins/defaults/memory/skill-update-receipt-job.js",
44
+ (): Partial<typeof ReceiptJob> => ({
45
+ recordSkillUpdate: (input) => {
46
+ recordedUpdates.push(input);
47
+ if (recordUpdateFails) {
48
+ throw new Error("memory database unavailable");
49
+ }
50
+ },
51
+ }),
52
+ );
54
53
 
55
54
  // Skill-card enqueue recorder. Snapshot + override (rather than a full module
56
55
  // replacement) because other modules in this import graph (e.g. the managed
@@ -152,7 +151,7 @@ beforeEach(() => {
152
151
  skillCardJobUpserts = [];
153
152
  skillCardUpsertThrows = false;
154
153
  watchdogEvents.length = 0;
155
- emittedSignals = [];
154
+ recordedUpdates = [];
156
155
  });
157
156
 
158
157
  afterEach(() => {
@@ -1771,7 +1770,8 @@ describe("background skill update notification", () => {
1771
1770
  });
1772
1771
  watchdogEvents.length = 0;
1773
1772
  skillCardJobUpserts = [];
1774
- emittedSignals = [];
1773
+ recordedUpdates = [];
1774
+ recordUpdateFails = false;
1775
1775
  }
1776
1776
 
1777
1777
  const lineage = () => ({
@@ -1781,9 +1781,73 @@ describe("background skill update notification", () => {
1781
1781
  : null,
1782
1782
  });
1783
1783
 
1784
- test("a background overwrite of an existing skill notifies, keyed to the source conversation", async () => {
1784
+ test("a background overwrite of an existing skill records a receipt entry keyed to the tool call", async () => {
1785
1785
  await seedAssistantSkill("weekly-export", "Old body.");
1786
1786
 
1787
+ const result = await executeScaffoldManagedSkill(
1788
+ {
1789
+ skill_id: "weekly-export",
1790
+ name: "Weekly Report Export",
1791
+ description: "export the weekly usage report",
1792
+ body_markdown: "1. Refined steps.",
1793
+ activation_hints: HINTS,
1794
+ overwrite: true,
1795
+ change_summary: "Refined the steps.",
1796
+ },
1797
+ makeRetrospectiveContext({
1798
+ conversationId: "retro-run-conv",
1799
+ toolUseId: "toolu_1",
1800
+ }),
1801
+ lineage(),
1802
+ );
1803
+
1804
+ expect(result.isError).toBe(false);
1805
+ // Nothing is announced here: the entry joins the pending receipt job,
1806
+ // which announces the burst once it has settled.
1807
+ expect(recordedUpdates).toEqual([
1808
+ {
1809
+ // The producing tool call, so a re-executed call records nothing new.
1810
+ entryId: "retro-run-conv:toolu_1",
1811
+ skillId: "weekly-export",
1812
+ name: "Weekly Report Export",
1813
+ changeSummary: "Refined the steps.",
1814
+ runConversationId: "retro-run-conv",
1815
+ // The receipt's link for this entry resolves through this id, so it
1816
+ // must be the conversation the work came from, not the hidden fork.
1817
+ sourceConversationId: "source-conv",
1818
+ },
1819
+ ]);
1820
+ });
1821
+
1822
+ test("an entry without a tool call id gets a fresh key, so a second rewrite of the same skill is still recorded", async () => {
1823
+ await seedAssistantSkill("weekly-export", "Old body.");
1824
+
1825
+ for (const body of ["1. Refined steps.", "1. Refined again."]) {
1826
+ await executeScaffoldManagedSkill(
1827
+ {
1828
+ skill_id: "weekly-export",
1829
+ name: "Weekly Report Export",
1830
+ description: "export the weekly usage report",
1831
+ body_markdown: body,
1832
+ activation_hints: HINTS,
1833
+ overwrite: true,
1834
+ change_summary: `Refined: ${body}`,
1835
+ },
1836
+ makeRetrospectiveContext({ conversationId: "retro-run-conv" }),
1837
+ );
1838
+ }
1839
+
1840
+ expect(recordedUpdates).toHaveLength(2);
1841
+ const [first, second] = recordedUpdates;
1842
+ expect(first?.entryId.startsWith("retro-run-conv:")).toBe(true);
1843
+ expect(second?.entryId.startsWith("retro-run-conv:")).toBe(true);
1844
+ expect(first?.entryId).not.toBe(second?.entryId);
1845
+ });
1846
+
1847
+ test("a receipt that cannot be recorded is logged and counted, and the skill write stands", async () => {
1848
+ await seedAssistantSkill("weekly-export", "Old body.");
1849
+ recordUpdateFails = true;
1850
+
1787
1851
  const result = await executeScaffoldManagedSkill(
1788
1852
  {
1789
1853
  skill_id: "weekly-export",
@@ -1799,29 +1863,20 @@ describe("background skill update notification", () => {
1799
1863
  );
1800
1864
 
1801
1865
  expect(result.isError).toBe(false);
1802
- expect(emittedSignals).toHaveLength(1);
1803
- const signal = emittedSignals[0]!;
1804
- expect(signal.sourceEventName).toBe("activity.complete");
1805
- // The feed item's "Go to Convo" target resolves through this id, so it
1806
- // must be the conversation the work came from, not the hidden fork.
1807
- expect(signal.sourceContextId).toBe("source-conv");
1808
- expect(signal.contextPayload?.skillId).toBe("weekly-export");
1809
- expect(signal.contextPayload?.summary).toBe("Refined the steps.");
1810
- // The home feed falls back to `title`/`body` when no channel copy was
1811
- // rendered, which is the intended quiet shape for this signal. Without
1812
- // them a suppressed delivery would leave the feed item unwritten.
1813
- // Named so the row is scannable in a feed several entries deep.
1814
- expect(signal.contextPayload?.title).toBe(
1815
- "Skill updated: Weekly Report Export",
1816
- );
1817
- expect(signal.contextPayload?.body).toBe("Refined the steps.");
1818
- // Deduped per skill per day so repeated refinements cannot flood the feed.
1819
- expect(signal.dedupeKey).toBe(
1820
- `skill-updated:weekly-export:${new Date().toISOString().slice(0, 10)}`,
1821
- );
1822
- });
1823
-
1824
- test("the notice body is the pass's change_summary, so the feed says what changed", async () => {
1866
+ expect(
1867
+ readFileSync(
1868
+ join(TEST_DIR, "skills", "weekly-export", "SKILL.md"),
1869
+ "utf-8",
1870
+ ),
1871
+ ).toContain("Refined steps.");
1872
+ expect(watchdogEvents).toContainEqual(
1873
+ expect.objectContaining({
1874
+ checkName: "skill_update_receipt_record_failed",
1875
+ }),
1876
+ );
1877
+ });
1878
+
1879
+ test("the entry body is the pass's change_summary, so the receipt says what changed", async () => {
1825
1880
  await seedAssistantSkill("weekly-export", "Old body.");
1826
1881
 
1827
1882
  const result = await executeScaffoldManagedSkill(
@@ -1840,14 +1895,9 @@ describe("background skill update notification", () => {
1840
1895
  );
1841
1896
 
1842
1897
  expect(result.isError).toBe(false);
1843
- expect(emittedSignals).toHaveLength(1);
1844
- const payload = emittedSignals[0]!.contextPayload;
1845
- // The title still names the skill; the body is the change itself.
1846
- expect(payload?.title).toBe("Skill updated: Weekly Report Export");
1847
- expect(payload?.body).toBe(
1898
+ expect(recordedUpdates[0]?.changeSummary).toBe(
1848
1899
  "Added the retry after an expired session and the export endpoint that held steady.",
1849
1900
  );
1850
- expect(payload?.summary).toBe(payload?.body);
1851
1901
  });
1852
1902
 
1853
1903
  test("a background overwrite without a change_summary is refused before the write", async () => {
@@ -1881,7 +1931,7 @@ describe("background skill update notification", () => {
1881
1931
  'Error: change_summary is required when updating an existing skill: pass one or two short sentences (under 200 characters) for the person who reads the "Skill updated" notice, naming what you changed and what in the trace prompted it (for example "Added the retry after an expired session and the export endpoint that held steady.").',
1882
1932
  );
1883
1933
  expect(readFileSync(skillFile, "utf-8")).toBe(before);
1884
- expect(emittedSignals).toHaveLength(0);
1934
+ expect(recordedUpdates).toHaveLength(0);
1885
1935
  }
1886
1936
  });
1887
1937
 
@@ -1905,7 +1955,7 @@ describe("background skill update notification", () => {
1905
1955
  expect(result.isError).toBe(true);
1906
1956
  expect(result.content).toContain("Set overwrite=true to replace it.");
1907
1957
  expect(result.content).not.toContain("change_summary");
1908
- expect(emittedSignals).toHaveLength(0);
1958
+ expect(recordedUpdates).toHaveLength(0);
1909
1959
  });
1910
1960
 
1911
1961
  test("the ownership backstop speaks before the change_summary requirement", async () => {
@@ -1937,7 +1987,7 @@ describe("background skill update notification", () => {
1937
1987
  expect(result.content).toContain("not verifiably assistant-authored");
1938
1988
  });
1939
1989
 
1940
- test("a user-directed overwrite needs no change_summary: it never notifies", async () => {
1990
+ test("a user-directed overwrite needs no change_summary: it records no entry", async () => {
1941
1991
  await executeScaffoldManagedSkill(
1942
1992
  {
1943
1993
  skill_id: "user-skill",
@@ -1964,7 +2014,7 @@ describe("background skill update notification", () => {
1964
2014
  expect(result.isError).toBe(false);
1965
2015
  });
1966
2016
 
1967
- test("change_summary is sanitized like any other notification body", async () => {
2017
+ test("change_summary is sanitized like any notification body", async () => {
1968
2018
  await seedAssistantSkill("weekly-export", "Old body.");
1969
2019
 
1970
2020
  await executeScaffoldManagedSkill(
@@ -1981,7 +2031,7 @@ describe("background skill update notification", () => {
1981
2031
  lineage(),
1982
2032
  );
1983
2033
 
1984
- const body = String(emittedSignals[0]!.contextPayload?.body);
2034
+ const body = String(recordedUpdates[0]?.changeSummary);
1985
2035
  // Control characters go, horizontal whitespace collapses, blank-line runs
1986
2036
  // collapse to one paragraph break, and the whole thing is clamped to the
1987
2037
  // shared notification preview budget.
@@ -2012,7 +2062,7 @@ describe("background skill update notification", () => {
2012
2062
 
2013
2063
  expect(result.isError).toBe(true);
2014
2064
  expect(result.content).toBe("Error: change_summary must be a string");
2015
- expect(emittedSignals).toHaveLength(0);
2065
+ expect(recordedUpdates).toHaveLength(0);
2016
2066
  });
2017
2067
 
2018
2068
  test("the registered tool's schema declares change_summary as an optional string", () => {
@@ -2044,7 +2094,7 @@ describe("background skill update notification", () => {
2044
2094
  expect(result.ok).toBe(true);
2045
2095
  });
2046
2096
 
2047
- test("it falls back to the run conversation when fork lineage does not resolve", async () => {
2097
+ test("an entry whose fork lineage does not resolve carries no source, never the run", async () => {
2048
2098
  await seedAssistantSkill("weekly-export", "Old body.");
2049
2099
 
2050
2100
  await executeScaffoldManagedSkill(
@@ -2061,10 +2111,10 @@ describe("background skill update notification", () => {
2061
2111
  { getConversation: () => null },
2062
2112
  );
2063
2113
 
2064
- // Still a real conversation id, so the deep link resolves rather than
2065
- // falling through to an unrelated target.
2066
- expect(emittedSignals).toHaveLength(1);
2067
- expect(emittedSignals[0]!.sourceContextId).toBe("retro-run-conv");
2114
+ // The run is an ephemeral fork; naming it as the source would have the
2115
+ // receipt job drop the entry once the fork is garbage collected.
2116
+ expect(recordedUpdates).toHaveLength(1);
2117
+ expect(recordedUpdates[0]).not.toHaveProperty("sourceConversationId");
2068
2118
  });
2069
2119
 
2070
2120
  test("a background CREATE gets the skill card instead, with no duplicate signal", async () => {
@@ -2082,10 +2132,10 @@ describe("background skill update notification", () => {
2082
2132
 
2083
2133
  expect(result.isError).toBe(false);
2084
2134
  expect(skillCardJobUpserts).toHaveLength(1);
2085
- expect(emittedSignals).toHaveLength(0);
2135
+ expect(recordedUpdates).toHaveLength(0);
2086
2136
  });
2087
2137
 
2088
- test("a user-directed overwrite does not notify: it is not unattended work", async () => {
2138
+ test("a user-directed overwrite records no receipt entry: it is not unattended work", async () => {
2089
2139
  await executeScaffoldManagedSkill(
2090
2140
  {
2091
2141
  skill_id: "user-skill",
@@ -2096,7 +2146,7 @@ describe("background skill update notification", () => {
2096
2146
  },
2097
2147
  makeContext(),
2098
2148
  );
2099
- emittedSignals = [];
2149
+ recordedUpdates = [];
2100
2150
 
2101
2151
  await executeScaffoldManagedSkill(
2102
2152
  {
@@ -2113,6 +2163,6 @@ describe("background skill update notification", () => {
2113
2163
  expect(
2114
2164
  readFileSync(join(TEST_DIR, "skills", "user-skill", "SKILL.md"), "utf-8"),
2115
2165
  ).toContain("V2.");
2116
- expect(emittedSignals).toHaveLength(0);
2166
+ expect(recordedUpdates).toHaveLength(0);
2117
2167
  });
2118
2168
  });
@@ -25,6 +25,7 @@ const MANAGED_PROVIDERS = [
25
25
  "gemini",
26
26
  "fireworks",
27
27
  "together",
28
+ "typesafe",
28
29
  ] as const;
29
30
 
30
31
  let platformBaseUrlOverride: string | undefined;
@@ -302,8 +302,10 @@ function makePendingApprovalConversation(
302
302
  // Tests
303
303
  // ---------------------------------------------------------------------------
304
304
 
305
- const TEST_TOKEN = "test-bearer-token-send";
306
- const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };
305
+ // This suite mocks isHttpAuthDisabled() to true, so a request carrying no
306
+ // bearer authenticates through the dev bypass. A bearer that is present is
307
+ // verified against the signing key, which these tests do not set up.
308
+ const AUTH_HEADERS: Record<string, string> = {};
307
309
 
308
310
  describe("POST /v1/messages — queue-if-busy and hub publishing", () => {
309
311
  let server: RuntimeHttpServer;
@@ -109,6 +109,41 @@ async function executeSkillLoad(
109
109
  // ── Tests ─────────────────────────────────────────────────────────────────
110
110
 
111
111
  describe("skill_load inline command expansion", () => {
112
+ test("read-only preloading returns instructions without running inline commands", async () => {
113
+ writeSkill(
114
+ "readonly-example",
115
+ "readonly-example",
116
+ "Example skill",
117
+ "Context: !`echo example`",
118
+ );
119
+ const result = await skillLoadTool.execute(
120
+ { skill: "readonly-example" },
121
+ {
122
+ conversationId: "conversation-1",
123
+ workingDir: TEST_DIR,
124
+ trustClass: "guardian",
125
+ },
126
+ { readOnly: true },
127
+ );
128
+ expect(result.isError).toBe(false);
129
+ expect(result.content).toContain("[inline command not executed]");
130
+ expect(runInlineCommandCalls).toHaveLength(0);
131
+ expect(mockAutoInstall).not.toHaveBeenCalled();
132
+ });
133
+
134
+ test("read-only preloading does not install missing skills", async () => {
135
+ const result = await skillLoadTool.execute(
136
+ { skill: "missing-readonly-skill" },
137
+ {
138
+ conversationId: "conversation-1",
139
+ workingDir: TEST_DIR,
140
+ trustClass: "guardian",
141
+ },
142
+ { readOnly: true },
143
+ );
144
+ expect(result.isError).toBe(true);
145
+ expect(mockAutoInstall).not.toHaveBeenCalled();
146
+ });
112
147
  beforeEach(() => {
113
148
  mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
114
149
  runInlineCommandCalls.length = 0;
@@ -1,4 +1,4 @@
1
- import { mkdirSync, symlinkSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { beforeEach, describe, expect, mock, test } from "bun:test";
4
4
 
@@ -781,16 +781,48 @@ describe("skill_load tool", () => {
781
781
  expect(result.content).toContain("Deploy the application to production");
782
782
  expect(result.content).toContain("Rollback to previous version");
783
783
 
784
- // Should list parameters with types and required/optional markers
785
- expect(result.content).toContain(
786
- "- environment (string, required): Target environment",
784
+ const schemas = [
785
+ ...result.content.matchAll(/```json\n([\s\S]*?)\n```/g),
786
+ ].map((match) => JSON.parse(match[1]));
787
+ expect(schemas[0].required).toEqual(["environment"]);
788
+ expect(schemas[0].properties.environment).toEqual({
789
+ type: "string",
790
+ description: "Target environment",
791
+ });
792
+ expect(schemas[0].properties.force.type).toBe("boolean");
793
+ expect(schemas[1].required).toEqual(["version"]);
794
+ });
795
+
796
+ test("discloses the complete annotation mark schema before execution", async () => {
797
+ const manifest = JSON.parse(
798
+ readFileSync(
799
+ join(
800
+ import.meta.dir,
801
+ "../config/bundled-skills/screen-annotation/TOOLS.json",
802
+ ),
803
+ "utf-8",
804
+ ),
787
805
  );
788
- expect(result.content).toContain(
789
- "- force (boolean, optional): Force deploy even if checks fail",
806
+ writeSkill(
807
+ "annotation-schema",
808
+ "Annotation Schema",
809
+ "Point at a control",
810
+ "Name the control.",
790
811
  );
791
- expect(result.content).toContain(
792
- "- version (string, required): Version to rollback to",
812
+ writeToolsJson("annotation-schema", manifest.tools);
813
+
814
+ const result = await executeSkillLoad(
815
+ { skill: "annotation-schema" },
816
+ "macos",
793
817
  );
818
+ expect(result.isError).toBe(false);
819
+ const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
820
+ expect(schemaMatch).not.toBeNull();
821
+ const schema = JSON.parse(schemaMatch![1]);
822
+ expect(schema).toEqual(manifest.tools[0].input_schema);
823
+ expect(schema.properties.marks.items.oneOf).toHaveLength(2);
824
+ expect(schema.properties.marks.items.properties.target.minLength).toBe(1);
825
+ expect(schema.properties.marks.items.properties.caption.maxLength).toBe(80);
794
826
  });
795
827
 
796
828
  test("skill without TOOLS.json does not include tool schemas section", async () => {
@@ -862,9 +894,8 @@ describe("skill_load tool", () => {
862
894
  // The child skill's tool schemas should appear (#### level under ### Tools from …)
863
895
  expect(result.content).toContain("#### child_action");
864
896
  expect(result.content).toContain("A child tool action");
865
- expect(result.content).toContain(
866
- "- target (string, required): Action target",
867
- );
897
+ const schemaMatch = result.content.match(/```json\n([\s\S]*?)\n```/);
898
+ expect(JSON.parse(schemaMatch![1]).required).toEqual(["target"]);
868
899
  expect(result.content).toContain(
869
900
  "Use `skill_execute` to call these tools.",
870
901
  );
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
6
 
7
- import type { SkillToolEntry } from "../config/skills.js";
7
+ import { getBundledSkillsDir, type SkillToolEntry } from "../config/skills.js";
8
8
  import { RiskLevel } from "../permissions/types.js";
9
9
  import { computeSkillVersionHash } from "../skills/version-hash.js";
10
10
  import { resolveSubagentRole } from "../subagent/role-resolution.js";
@@ -261,6 +261,54 @@ describe("createSkillTool — unknown parameter validation", () => {
261
261
  expect(result.content).toContain('Unknown parameter "bar"');
262
262
  });
263
263
 
264
+ test.each([
265
+ { description: "Filters" },
266
+ { marks: [{ description: "Filters" }] },
267
+ { marks: [{}] },
268
+ { marks: [{ target: 42 }] },
269
+ { marks: [{ x: 0.1, y: 0.2 }] },
270
+ { marks: [{ target: "Filters", x: 0.1, y: 0.2, width: 0.3, height: 0.4 }] },
271
+ ])(
272
+ "an annotation validation error provides enough schema for one corrected retry: %j",
273
+ async (invalid) => {
274
+ const skillDir = join(getBundledSkillsDir(), "screen-annotation");
275
+ const manifest = JSON.parse(
276
+ readFileSync(join(skillDir, "TOOLS.json"), "utf-8"),
277
+ );
278
+ const entry = manifest.tools.find(
279
+ (tool: SkillToolEntry) => tool.name === "screen_point_at",
280
+ );
281
+ const tool = createSkillTool(
282
+ entry,
283
+ skillDir,
284
+ computeSkillVersionHash(skillDir),
285
+ BUNDLED,
286
+ );
287
+ const calls: unknown[] = [];
288
+ const context = makeContext({
289
+ proxyToolResolver: async (name, input) => {
290
+ calls.push({ name, input });
291
+ return { content: "Drew 1 mark", isError: false };
292
+ },
293
+ });
294
+
295
+ const rejected = await tool.execute(invalid, context);
296
+ expect(rejected.isError).toBe(true);
297
+ expect(calls).toHaveLength(0);
298
+ expect(rejected.content).toContain("skill_load");
299
+ const schemaMatch = rejected.content.match(/```json\n([\s\S]*?)\n```/);
300
+ expect(schemaMatch).not.toBeNull();
301
+ expect(JSON.parse(schemaMatch![1])).toEqual(entry.input_schema);
302
+
303
+ const input = {
304
+ marks: [{ target: "Filters", caption: "Click Filters" }],
305
+ };
306
+ const corrected = await tool.execute(input, context);
307
+ expect(corrected.isError).toBe(false);
308
+ expect(calls).toEqual([{ name: "computer_use_point_at", input }]);
309
+ },
310
+ );
311
+
264
312
  test("allows input with only known parameters", async () => {
265
313
  const hash = computeSkillVersionHash(tempDir);
266
314
  const tool = createSkillTool(
@@ -471,7 +519,7 @@ describe("createSkillTool — required/type/enum validation", () => {
471
519
 
472
520
  expect(result.isError).toBe(true);
473
521
  expect(result.content).toContain(
474
- "auto_open must be a boolean — pass true or false as a JSON boolean, not a string",
522
+ "auto_open must be a boolean: pass true or false as a JSON boolean, not a string",
475
523
  );
476
524
  });
477
525