@vellumai/assistant 0.12.2-staging.7 → 0.12.2

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 (120) hide show
  1. package/docs/architecture/memory.md +2 -11
  2. package/docs/desktop-browser-cli.md +2 -4
  3. package/node_modules/@vellumai/environments/src/shell.test.ts +0 -21
  4. package/node_modules/@vellumai/environments/src/shell.ts +0 -24
  5. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +2 -8
  6. package/openapi.yaml +2 -6
  7. package/package.json +1 -1
  8. package/scripts/smoke-desktop-browser-cli.ts +0 -1
  9. package/src/__tests__/agent-loop.test.ts +0 -124
  10. package/src/__tests__/approval-interception-trust-gates.test.ts +0 -40
  11. package/src/__tests__/channel-approval.test.ts +14 -9
  12. package/src/__tests__/conversation-agent-loop.test.ts +0 -25
  13. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  14. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -138
  15. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  16. package/src/__tests__/subagent-tool-gate-mode.test.ts +0 -169
  17. package/src/__tests__/terminal-tools.test.ts +0 -8
  18. package/src/__tests__/unicode.test.ts +0 -36
  19. package/src/agent/loop.ts +0 -19
  20. package/src/api/index.ts +0 -6
  21. package/src/approvals/approval-primitive.ts +2 -5
  22. package/src/approvals/scoped-approval-grants.ts +2 -6
  23. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -43
  24. package/src/daemon/conversation-agent-loop.ts +0 -2
  25. package/src/daemon/conversation-tool-setup.ts +1 -57
  26. package/src/daemon/conversation.ts +0 -17
  27. package/src/daemon/daemon-control.ts +6 -2
  28. package/src/daemon/orphan-reaper.ts +3 -4
  29. package/src/daemon/tool-setup-types.ts +0 -6
  30. package/src/daemon/wake-conversation-ops.ts +15 -38
  31. package/src/desktop/desktop-automation-lease.test.ts +0 -143
  32. package/src/desktop/desktop-automation-lease.ts +3 -39
  33. package/src/messaging/provider-message-metadata.ts +3 -3
  34. package/src/notifications/__tests__/copy-composer.test.ts +0 -70
  35. package/src/notifications/copy-composer.ts +3 -11
  36. package/src/oauth/seed-providers.ts +30 -0
  37. package/src/persistence/conversation-plugin-facade.ts +0 -13
  38. package/src/persistence/schema/index.ts +0 -1
  39. package/src/persistence/steps.ts +0 -2
  40. package/src/plugin-api/conversation-turn.ts +7 -31
  41. package/src/plugin-api/index.ts +1 -9
  42. package/src/plugins/defaults/memory/AGENTS.md +2 -14
  43. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +0 -43
  44. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +0 -46
  45. package/src/plugins/defaults/memory/buffer-format.ts +0 -40
  46. package/src/plugins/defaults/memory/context-search/agent-runner.ts +2 -1
  47. package/src/plugins/defaults/memory/context-search/format.ts +1 -2
  48. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +1 -2
  49. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -2
  50. package/src/plugins/defaults/memory/graph/capability-seed.ts +2 -1
  51. package/src/plugins/defaults/memory/graph/tool-handlers.ts +42 -1
  52. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  53. package/src/plugins/defaults/memory/injectors.ts +3 -4
  54. package/src/plugins/defaults/memory/memory-retrospective-job.ts +181 -55
  55. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +99 -407
  56. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +0 -10
  57. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +7 -107
  58. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +86 -307
  59. package/src/plugins/defaults/memory/substrate/page-index.ts +1 -2
  60. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +49 -89
  61. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  62. package/src/plugins/defaults/memory/tools.ts +1 -1
  63. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  64. package/src/plugins/defaults/memory/v1/graph/extraction.ts +1 -2
  65. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  66. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +0 -5
  67. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +2 -5
  68. package/src/plugins/defaults/memory/v2/reranker.ts +1 -2
  69. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +1 -81
  70. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +0 -87
  71. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +0 -21
  72. package/src/plugins/defaults/memory/v3/card.ts +1 -2
  73. package/src/plugins/defaults/memory/v3/injector.ts +178 -212
  74. package/src/plugins/defaults/memory/v3/orchestrate.ts +22 -86
  75. package/src/plugins/defaults/memory/v3/pool-select.ts +7 -10
  76. package/src/plugins/defaults/memory/v3/sections.ts +1 -2
  77. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +1 -10
  78. package/src/plugins/defaults/tool-result-truncate/terminal.ts +46 -1
  79. package/src/runtime/AGENTS.md +1 -1
  80. package/src/runtime/__tests__/agent-wake.test.ts +1 -86
  81. package/src/runtime/agent-wake.ts +4 -20
  82. package/src/runtime/guardian-action-service.ts +17 -2
  83. package/src/runtime/guardian-reply-router.ts +8 -1
  84. package/src/runtime/routes/channel-route-shared.ts +9 -1
  85. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  86. package/src/runtime/routes/desktop-setup-routes.ts +3 -7
  87. package/src/runtime/routes/guardian-approval-interception.ts +0 -24
  88. package/src/runtime/routes/inbound-message-handler.ts +3 -2
  89. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  90. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -7
  91. package/src/schedule/run-script.ts +2 -2
  92. package/src/tools/host-terminal/host-shell.ts +6 -12
  93. package/src/tools/shared/filesystem/file-ops-service.ts +31 -1
  94. package/src/tools/shared/shell-output.test.ts +0 -10
  95. package/src/tools/shared/shell-output.ts +2 -14
  96. package/src/tools/skills/sandbox-runner.ts +2 -13
  97. package/src/tools/skills/scaffold-managed.ts +1 -2
  98. package/src/tools/terminal/__tests__/safe-env.test.ts +0 -29
  99. package/src/tools/terminal/safe-env.ts +1 -30
  100. package/src/tools/terminal/sanitized-bash.ts +2 -15
  101. package/src/tools/terminal/shell.test.ts +0 -29
  102. package/src/tools/terminal/shell.ts +7 -13
  103. package/src/util/host-process.test.ts +1 -17
  104. package/src/util/host-process.ts +0 -24
  105. package/src/util/unicode.ts +0 -29
  106. package/src/__tests__/db-conversation-tool-surface.test.ts +0 -144
  107. package/src/api/events/desktop-activity-changed.ts +0 -10
  108. package/src/persistence/conversation-tool-surface.ts +0 -86
  109. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +0 -78
  110. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +0 -29
  111. package/src/persistence/schema/conversation-tool-surfaces.ts +0 -22
  112. package/src/plugin-api/plugin-channel-turn-trust.test.ts +0 -133
  113. package/src/plugin-api/plugin-channel-turn-trust.ts +0 -71
  114. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +0 -320
  115. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +0 -17
  116. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +0 -161
  117. package/src/plugins/defaults/memory/buffer-file.ts +0 -354
  118. package/src/plugins/defaults/memory/memory-run-evidence.ts +0 -213
  119. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +0 -34
  120. package/src/tools/terminal/shell-launch.test.ts +0 -162
@@ -22,7 +22,7 @@ import {
22
22
  import { syncMessageToDisk } from "../persistence/conversation-disk-view.js";
23
23
  import { backfillMessageIdOnLogs } from "../persistence/llm-request-log-store.js";
24
24
  import { resolveMediaSourceData } from "../providers/media-resolve.js";
25
- import type { Message, ToolDefinition } from "../providers/types.js";
25
+ import type { Message } from "../providers/types.js";
26
26
  import { broadcastMessage } from "../runtime/assistant-event-hub.js";
27
27
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
28
28
  import type { CompletedBackgroundTool } from "../tools/background-tool-registry.js";
@@ -350,60 +350,38 @@ export async function persistWakeTriggerMessage(
350
350
  }
351
351
  }
352
352
 
353
- /**
354
- * How a wake's tool allowlist is applied beyond the allowlist itself. Every
355
- * field is set on the conversation together with the allowlist and restored
356
- * with it.
357
- */
358
- export interface WakeToolScopeOptions {
359
- /** How the allowlist is enforced; see {@link SubagentToolGateMode}. */
360
- gateMode?: SubagentToolGateMode;
361
- /**
362
- * Freezes the client-context inputs for tool-definition resolution
363
- * (execution-gate-mode cache-parity wakes); see {@link WakeToolContextPin}.
364
- * Its `requestOrigin`, when set, is stamped onto the conversation's per-turn
365
- * origin so the permission checker's origin-scoped auto-grants fire.
366
- */
367
- toolContextPin?: WakeToolContextPin;
368
- /**
369
- * Skills whose bundled tools join the turn's active set
370
- * (`allowedToolNames`) so they are callable without a prior `skill_load`.
371
- */
372
- preactivateSkillIds?: readonly string[];
373
- /**
374
- * Sent verbatim as the wake's wire tool array in place of the resolved one
375
- * (the source conversation's recorded surface, replayed for provider
376
- * prompt-cache parity). Changes nothing about what may execute.
377
- */
378
- wireToolDefinitions?: readonly ToolDefinition[];
379
- }
380
-
381
353
  /**
382
354
  * Temporarily restrict the tools visible/executable during a wake by
383
355
  * reusing the conversation's subagent allowlist slot. Returns a restore
384
356
  * callback that reinstates the previous allowlist so the wake can release
385
357
  * the scope before any queued user turn is drained.
358
+ *
359
+ * `gateMode` controls how the allowlist is enforced — see
360
+ * {@link SubagentToolGateMode}. `toolContextPin`, when provided
361
+ * (execution-gate-mode cache-parity wakes), freezes the client-context
362
+ * inputs for tool-definition resolution — see {@link WakeToolContextPin}.
363
+ * Its `requestOrigin`, when set, is stamped onto the conversation's per-turn
364
+ * origin so the permission checker's origin-scoped auto-grants fire for the
365
+ * wake. `preactivateSkillIds`, when non-empty, activates those skills' bundled
366
+ * tools in the turn's active set (`allowedToolNames`) for the wake so they are
367
+ * callable without a prior `skill_load`. All are set and restored alongside the
368
+ * allowlist.
386
369
  */
387
370
  export function scopeWakeAllowedTools(
388
371
  conversation: Conversation,
389
372
  tools: ReadonlySet<string>,
390
- {
391
- gateMode = "wire",
392
- toolContextPin,
393
- preactivateSkillIds,
394
- wireToolDefinitions,
395
- }: WakeToolScopeOptions = {},
373
+ gateMode: SubagentToolGateMode = "wire",
374
+ toolContextPin?: WakeToolContextPin,
375
+ preactivateSkillIds?: readonly string[],
396
376
  ): () => void {
397
377
  const previous = conversation.subagentAllowedTools;
398
378
  const previousGateMode = conversation.subagentToolGateMode;
399
379
  const previousToolContextPin = conversation.toolContextPin;
400
380
  const previousRequestOrigin = conversation.currentTurnRequestOrigin;
401
381
  const previousPreactivated = conversation.preactivatedSkillIds;
402
- const previousWireToolReplay = conversation.wireToolReplay;
403
382
  conversation.setSubagentAllowedTools(new Set(tools));
404
383
  conversation.subagentToolGateMode = gateMode;
405
384
  conversation.toolContextPin = toolContextPin;
406
- conversation.wireToolReplay = wireToolDefinitions;
407
385
  if (toolContextPin?.requestOrigin !== undefined) {
408
386
  conversation.currentTurnRequestOrigin = toolContextPin.requestOrigin;
409
387
  }
@@ -416,6 +394,5 @@ export function scopeWakeAllowedTools(
416
394
  conversation.toolContextPin = previousToolContextPin;
417
395
  conversation.currentTurnRequestOrigin = previousRequestOrigin;
418
396
  conversation.setPreactivatedSkillIds(previousPreactivated);
419
- conversation.wireToolReplay = previousWireToolReplay;
420
397
  };
421
398
  }
@@ -35,9 +35,7 @@ function fixture() {
35
35
  const started = mock(async () => {});
36
36
  const released = mock(() => {});
37
37
  const releaseBrowser = mock(async () => {});
38
- const notify = mock(async () => {});
39
38
  const lease = new DesktopAutomationLease({
40
- notify,
41
39
  enabled: () => enabled,
42
40
  ready: () => ready,
43
41
  ensureReady,
@@ -52,7 +50,6 @@ function fixture() {
52
50
  cleanups.push(lease);
53
51
  return {
54
52
  lease,
55
- notify,
56
53
  ensureReady,
57
54
  install,
58
55
  failSetup: () => setup.reject(new Error("download failed")),
@@ -131,7 +128,6 @@ test("cancellation stops running and queued browser commands without blocking a
131
128
  const callback = mock(operation);
132
129
  const queued = f.lease.runBrowser(context, callback);
133
130
  abort.abort();
134
- expect(f.lease.isActive).toBe(false);
135
131
  expect(await running).toBeInstanceOf(Error);
136
132
  expect((await queued).isError).toBe(true);
137
133
  expect(callback).not.toHaveBeenCalled();
@@ -210,142 +206,3 @@ test("disabled, unidentified, cancelled and released browser calls cannot instal
210
206
  await expect(f.lease.runBrowser(context, operation)).rejects.toThrow();
211
207
  expect(f.ensureReady).not.toHaveBeenCalled();
212
208
  });
213
-
214
- test("activity follows the browser lease and clears before failed cleanup", async () => {
215
- const f = fixture();
216
- expect(f.lease.isActive).toBe(false);
217
- await f.lease.runBrowser(context, operation);
218
- expect(f.lease.isActive).toBe(true);
219
- expect(f.notify).toHaveBeenCalledTimes(1);
220
- await f.lease.runBrowser(context, operation);
221
- expect(f.notify).toHaveBeenCalledTimes(1);
222
- f.releaseBrowser.mockRejectedValueOnce(new Error("cleanup failed"));
223
- await expect(f.lease.runBrowser(context, operation, true)).rejects.toThrow(
224
- "cleanup failed",
225
- );
226
- expect(f.lease.isActive).toBe(false);
227
- expect(f.notify).toHaveBeenCalledTimes(2);
228
- await f.lease.runBrowser(context, operation, true);
229
- expect(f.notify).toHaveBeenCalledTimes(2);
230
- });
231
-
232
- test("turn completion releases only its own desktop before cleanup finishes", async () => {
233
- const f = fixture();
234
- await f.lease.runBrowser(context, operation);
235
- f.lease.releaseForConversation("conv-456");
236
- expect(f.lease.isActive).toBe(true);
237
- expect(f.notify).toHaveBeenCalledTimes(1);
238
-
239
- const cleanup = Promise.withResolvers<void>();
240
- f.releaseBrowser.mockImplementationOnce(() => cleanup.promise);
241
- f.lease.releaseForConversation(context.conversationId);
242
- expect(f.lease.isActive).toBe(false);
243
- expect(f.notify).toHaveBeenCalledTimes(2);
244
- await Bun.sleep(0);
245
- expect(f.released).not.toHaveBeenCalled();
246
-
247
- const callback = mock(operation);
248
- const next = f.lease.runBrowser(context, callback);
249
- await Bun.sleep(0);
250
- expect(callback).not.toHaveBeenCalled();
251
- cleanup.resolve();
252
- expect((await next).isError).toBe(false);
253
- expect(f.released).toHaveBeenCalledTimes(1);
254
- expect(f.started).toHaveBeenCalledTimes(2);
255
- expect(f.lease.isActive).toBe(true);
256
- });
257
-
258
- test("turn completion cancels running and queued desktop work", async () => {
259
- const f = fixture();
260
- const started = Promise.withResolvers<void>();
261
- const running = f.lease
262
- .runBrowser(context, async (signal) => {
263
- started.resolve();
264
- await new Promise<void>((_resolve, reject) => {
265
- signal.addEventListener("abort", () => reject(new Error("aborted")), {
266
- once: true,
267
- });
268
- });
269
- return operation();
270
- })
271
- .catch((error: unknown) => error);
272
- await started.promise;
273
- const callback = mock(operation);
274
- const queued = f.lease.runBrowser(context, callback);
275
- f.lease.releaseForConversation(context.conversationId);
276
- expect(f.lease.isActive).toBe(false);
277
- expect(await running).toBeInstanceOf(Error);
278
- expect((await queued).isError).toBe(true);
279
- expect(callback).not.toHaveBeenCalled();
280
- await f.lease.runBrowser(context, callback);
281
- expect(callback).toHaveBeenCalledTimes(1);
282
- });
283
-
284
- test("turn handoff retries failed cleanup without another turn or restart", async () => {
285
- const f = fixture();
286
- await f.lease.runBrowser(context, operation);
287
- f.releaseBrowser.mockRejectedValueOnce(new Error("Chrome cleanup timed out"));
288
- f.lease.releaseForConversation(context.conversationId);
289
- expect(f.lease.isActive).toBe(false);
290
- await Bun.sleep(0);
291
- expect(f.released).not.toHaveBeenCalled();
292
- await Bun.sleep(1_100);
293
- expect(f.released).toHaveBeenCalledTimes(1);
294
- const nextContext = { ...context, conversationId: "conv-456" };
295
- expect((await f.lease.runBrowser(nextContext, operation)).isError).toBe(
296
- false,
297
- );
298
- await f.lease.runBrowser(nextContext, operation, true);
299
- });
300
-
301
- test("a delayed cleanup retry cannot release a replacement owner", async () => {
302
- const f = fixture();
303
- await f.lease.runBrowser(context, operation);
304
- f.releaseBrowser.mockRejectedValueOnce(new Error("Chrome cleanup timed out"));
305
- f.lease.releaseForConversation(context.conversationId);
306
- await f.lease.runBrowser(context, operation, true);
307
- const nextContext = { ...context, conversationId: "conv-456" };
308
- await f.lease.runBrowser(nextContext, operation);
309
- await Bun.sleep(1_100);
310
- expect(f.lease.isActive).toBe(true);
311
- expect(f.released).toHaveBeenCalledTimes(1);
312
- await f.lease.runBrowser(nextContext, operation, true);
313
- });
314
-
315
- test("cleanup retries preserve commands queued behind an explicit handoff", async () => {
316
- const f = fixture();
317
- await f.lease.runBrowser(context, operation);
318
- f.releaseBrowser.mockRejectedValueOnce(new Error("Chrome cleanup timed out"));
319
- f.lease.releaseForConversation(context.conversationId);
320
- await Bun.sleep(0);
321
- const cleanup = Promise.withResolvers<void>();
322
- f.releaseBrowser.mockImplementationOnce(() => cleanup.promise);
323
- const detached = f.lease.runBrowser(context, operation, true);
324
- await Bun.sleep(0);
325
- const callback = mock(operation);
326
- const next = f.lease.runBrowser(context, callback);
327
- await Bun.sleep(1_100);
328
- cleanup.resolve();
329
- await detached;
330
- expect((await next).isError).toBe(false);
331
- expect(callback).toHaveBeenCalledTimes(1);
332
- expect(f.lease.isActive).toBe(true);
333
- });
334
-
335
- test("turn completion cancels commands queued behind an in-progress detach", async () => {
336
- const f = fixture();
337
- await f.lease.runBrowser(context, operation);
338
- const cleanup = Promise.withResolvers<void>();
339
- f.releaseBrowser.mockImplementationOnce(() => cleanup.promise);
340
- const detached = f.lease.runBrowser(context, operation, true);
341
- await Bun.sleep(0);
342
- const callback = mock(operation);
343
- const queued = f.lease.runBrowser(context, callback);
344
- f.lease.releaseForConversation(context.conversationId);
345
- cleanup.resolve();
346
- await detached;
347
- expect((await queued).isError).toBe(true);
348
- expect(callback).not.toHaveBeenCalled();
349
- await f.lease.runBrowser(context, callback);
350
- expect(callback).toHaveBeenCalledTimes(1);
351
- });
@@ -1,5 +1,4 @@
1
1
  import { getConfig } from "../config/loader.js";
2
- import { broadcastMessage } from "../runtime/assistant-event-hub.js";
3
2
  import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
4
3
  import { getLogger } from "../util/logger.js";
5
4
  import { desktopDependencyInstaller } from "./desktop-dependencies.js";
@@ -36,29 +35,14 @@ export class DesktopAutomationLease {
36
35
  ready: () => boolean;
37
36
  ensureReady: (signal: AbortSignal) => Promise<void>;
38
37
  manager: () => DesktopSessionManager;
39
- notify: () => Promise<unknown>;
40
38
  } = {
41
39
  enabled: () => isVirtualDesktopEnabled(getConfig()),
42
40
  ready: () => desktopDependencyInstaller.getStatus().state === "ready",
43
41
  ensureReady: (signal) => desktopDependencyInstaller.ensureReady(signal),
44
42
  manager: getDesktopSessionManager,
45
- notify: async () =>
46
- broadcastMessage({ type: "desktop_activity_changed" }),
47
43
  },
48
44
  ) {}
49
45
 
50
- get isActive(): boolean {
51
- return this.owner !== null && !this.owner.abort.signal.aborted;
52
- }
53
-
54
- private notify(): void {
55
- void this.deps
56
- .notify()
57
- .catch((err) =>
58
- log.warn({ err }, "Desktop browser activity notification failed"),
59
- );
60
- }
61
-
62
46
  private exclusive<T>(run: () => Promise<T>): Promise<T> {
63
47
  const next = this.tail.then(run, run);
64
48
  this.tail = next.catch(() => {});
@@ -100,25 +84,9 @@ export class DesktopAutomationLease {
100
84
  }
101
85
  this.generation += 1;
102
86
  owner.abort.abort();
103
- this.releaseCancelledOwner(owner);
104
- }
105
-
106
- private releaseCancelledOwner(owner: Owner): void {
107
- void this.exclusive(async () => {
108
- if (this.owner === owner) {
109
- await this.release();
110
- }
111
- }).catch((err) => {
112
- log.warn({ err }, "Desktop browser session cleanup failed");
113
- const retry = setTimeout(() => this.releaseCancelledOwner(owner), 1_000);
114
- retry.unref?.();
115
- });
116
- }
117
-
118
- releaseForConversation(conversationId: string): void {
119
- if (this.owner?.conversationId === conversationId) {
120
- this.cancel(this.owner);
121
- }
87
+ void this.exclusive(() => this.release()).catch((err) =>
88
+ log.warn({ err }, "Desktop browser session cleanup failed"),
89
+ );
122
90
  }
123
91
 
124
92
  private bindCancellation(owner: Owner, signal?: AbortSignal): void {
@@ -154,10 +122,6 @@ export class DesktopAutomationLease {
154
122
  throw new Error("The desktop is busy or shutting down");
155
123
  }
156
124
  this.owner = owner;
157
- owner.abort.signal.addEventListener("abort", () => this.notify(), {
158
- once: true,
159
- });
160
- this.notify();
161
125
  this.bindCancellation(owner, context.signal);
162
126
  const cancel = () => this.cancel(owner);
163
127
  this.watchdog = setInterval(() => {
@@ -30,9 +30,9 @@ import { CHANNEL_IDS } from "../channels/types.js";
30
30
  * The schema passes through what it does not name, so a provider carries its
31
31
  * own fields (Slack's file markers and timezone labels) on this same object
32
32
  * and validates them with its own schema. This is how `SourceMetadataSchema`
33
- * carries `botMentioned` and the email fields on the wire: one object per
34
- * row, no per-provider sub-envelope, and no second copy of anything named
35
- * here.
33
+ * already carries `slackBotMentioned` and the email fields on the wire: one
34
+ * object per row, no per-provider sub-envelope, and no second copy of
35
+ * anything named here.
36
36
  */
37
37
 
38
38
  const providerReactionMetadataSchema = z.object({
@@ -503,76 +503,6 @@ describe("composeFallbackCopy plugin schedule templates", () => {
503
503
  });
504
504
  });
505
505
 
506
- // ── activity.complete rendering ───────────────────────────────────────
507
-
508
- describe("activity.complete copy", () => {
509
- function completeSignal(
510
- contextPayload: Record<string, unknown>,
511
- ): NotificationSignal {
512
- return makeSignal({
513
- sourceEventName: "activity.complete",
514
- contextPayload,
515
- });
516
- }
517
-
518
- test("a producer-authored title survives the fallback path", () => {
519
- // The home feed reads the payload title first; the popup must say the
520
- // same thing when the classifier was unreachable.
521
- const copy = composeFallbackCopy(
522
- completeSignal({
523
- title: "Skill updated: Weekly Report Export",
524
- summary: "Added the retry after an expired session.",
525
- }),
526
- CHANNELS,
527
- );
528
- expect(copy.vellum?.title).toBe("Skill updated: Weekly Report Export");
529
- expect(copy.vellum?.body).toBe("Added the retry after an expired session.");
530
- });
531
-
532
- test("the pass-through's requestedTitle is honored when no title is set", () => {
533
- const copy = composeFallbackCopy(
534
- completeSignal({
535
- requestedTitle: "Nightly export finished",
536
- summary: "Wrote 12 files to the reports folder.",
537
- }),
538
- CHANNELS,
539
- );
540
- expect(copy.vellum?.title).toBe("Nightly export finished");
541
- });
542
-
543
- test("without an authored title the headline is the summary's first sentence", () => {
544
- const copy = composeFallbackCopy(
545
- completeSignal({
546
- summary:
547
- "Added the retry after an expired session. Dropped the login step.",
548
- }),
549
- CHANNELS,
550
- );
551
- expect(copy.vellum?.title).toBe(
552
- "Added the retry after an expired session.",
553
- );
554
- });
555
-
556
- test("an authored title the normalizer rejects falls back to the derived one", () => {
557
- const copy = composeFallbackCopy(
558
- completeSignal({
559
- title: "Skill\nupdated",
560
- summary: "Added the retry after an expired session.",
561
- }),
562
- CHANNELS,
563
- );
564
- expect(copy.vellum?.title).toBe(
565
- "Added the retry after an expired session.",
566
- );
567
- });
568
-
569
- test("neither a title nor a summary still says something happened", () => {
570
- const copy = composeFallbackCopy(completeSignal({}), CHANNELS);
571
- expect(copy.vellum?.title).toBe("Activity complete");
572
- expect(copy.vellum?.body).toBe("An activity has completed");
573
- });
574
- });
575
-
576
506
  // ── deriveTitle ───────────────────────────────────────────────────────
577
507
 
578
508
  describe("deriveTitle", () => {
@@ -346,22 +346,14 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
346
346
 
347
347
  // Titled by what was done rather than by the kind of event: the summary's
348
348
  // first sentence is the outcome ("Finished the fuel-system diagnostic app"),
349
- // which is what a reader scanning the bell wants to see. A producer that
350
- // names its subject in the payload keeps that name here, read in the same
351
- // order the home feed reads it, so the headline is the same whether or not
352
- // the classifier ran. A summary with neither still gets a title that says
353
- // something happened.
349
+ // which is what a reader scanning the bell wants to see. A summary without
350
+ // one still gets a title that says something happened.
354
351
  "activity.complete": (payload) => {
355
352
  const summary = nonEmpty(
356
353
  typeof payload.summary === "string" ? payload.summary : undefined,
357
354
  );
358
- const authored = normalizeTitle(
359
- readPayloadString(payload, "title") ??
360
- readPayloadString(payload, "requestedTitle") ??
361
- "",
362
- );
363
355
  return {
364
- title: authored || (summary ? deriveTitle(summary) : "Activity complete"),
356
+ title: summary ? deriveTitle(summary) : "Activity complete",
365
357
  body: summary ?? "An activity has completed",
366
358
  };
367
359
  },
@@ -1398,11 +1398,20 @@ export const PROVIDER_SEED_DATA: Record<
1398
1398
  "read_customers",
1399
1399
  "write_customers",
1400
1400
  "read_inventory",
1401
+ "write_inventory",
1401
1402
  "read_locations",
1402
1403
  "read_fulfillments",
1404
+ "write_fulfillments",
1403
1405
  "read_discounts",
1404
1406
  "write_discounts",
1405
1407
  "read_price_rules",
1408
+ "write_price_rules",
1409
+ "read_content",
1410
+ "write_content",
1411
+ "read_reports",
1412
+ // Location, shipping, and marketing writes stay opt-in from the scope
1413
+ // picker: they change store setup rather than day-to-day operations,
1414
+ // and keeping them out of the defaults keeps the install consent short.
1406
1415
  // write_themes covers listing, duplicating, and publishing themes.
1407
1416
  // Writing theme files (settings, JSON templates, Liquid) also requires
1408
1417
  // Shopify's theme-code exemption on the app itself, which is granted
@@ -1449,6 +1458,7 @@ export const PROVIDER_SEED_DATA: Record<
1449
1458
  description: "Adjust inventory levels and items",
1450
1459
  },
1451
1460
  { scope: "read_locations", description: "Read store locations" },
1461
+ { scope: "write_locations", description: "Add and edit store locations" },
1452
1462
  { scope: "read_fulfillments", description: "Read fulfillment services" },
1453
1463
  {
1454
1464
  scope: "write_fulfillments",
@@ -1468,6 +1478,14 @@ export const PROVIDER_SEED_DATA: Record<
1468
1478
  },
1469
1479
  { scope: "read_files", description: "Read files uploaded to the store" },
1470
1480
  { scope: "write_files", description: "Upload and update files" },
1481
+ {
1482
+ scope: "read_content",
1483
+ description: "Read blog posts, pages, and comments",
1484
+ },
1485
+ {
1486
+ scope: "write_content",
1487
+ description: "Create and update blog posts, pages, and comments",
1488
+ },
1471
1489
  { scope: "read_gift_cards", description: "Read gift cards" },
1472
1490
  {
1473
1491
  scope: "write_gift_cards",
@@ -1481,11 +1499,23 @@ export const PROVIDER_SEED_DATA: Record<
1481
1499
  scope: "read_shipping",
1482
1500
  description: "Read shipping and delivery carrier services",
1483
1501
  },
1502
+ {
1503
+ scope: "write_shipping",
1504
+ description: "Create and update delivery carrier services",
1505
+ },
1506
+ {
1507
+ scope: "read_reports",
1508
+ description: "Run ShopifyQL sales and analytics queries",
1509
+ },
1484
1510
  { scope: "read_analytics", description: "Read analytics and reports" },
1485
1511
  {
1486
1512
  scope: "read_marketing_events",
1487
1513
  description: "Read marketing events and activities",
1488
1514
  },
1515
+ {
1516
+ scope: "write_marketing_events",
1517
+ description: "Create and update marketing events and activities",
1518
+ },
1489
1519
  { scope: "read_order_edits", description: "Read order edits" },
1490
1520
  {
1491
1521
  scope: "write_order_edits",
@@ -1,4 +1,3 @@
1
- import type { ToolDefinition } from "../providers/types.js";
2
1
  import type {
3
2
  AddMessageOptions,
4
3
  ConversationRow,
@@ -53,18 +52,6 @@ export async function getMessages(
53
52
  return fn(conversationId);
54
53
  }
55
54
 
56
- /**
57
- * The tool definitions the conversation's most recent live turn sent to the
58
- * provider, or `null` when none has been recorded.
59
- */
60
- export async function getConversationToolSurface(
61
- conversationId: string,
62
- ): Promise<ToolDefinition[] | null> {
63
- const { getConversationToolSurface: fn } =
64
- await import("./conversation-tool-surface.js");
65
- return fn(conversationId);
66
- }
67
-
68
55
  /** Whether the conversation currently has a turn in flight. */
69
56
  export async function isConversationProcessing(id: string): Promise<boolean> {
70
57
  const { isConversationProcessing: fn } =
@@ -5,7 +5,6 @@ export * from "./calls.js";
5
5
  export * from "./contacts.js";
6
6
  export * from "./conversation-groups.js";
7
7
  export * from "./conversation-starters.js";
8
- export * from "./conversation-tool-surfaces.js";
9
8
  export * from "./conversations.js";
10
9
  export * from "./documents.js";
11
10
  export * from "./guardian.js";
@@ -485,7 +485,6 @@ import { migrateChannelInboundMessageIdIndex } from "./migrations/374-channel-in
485
485
  import { migrateCreateChannelOutboundPosts } from "./migrations/375-create-channel-outbound-posts.js";
486
486
  import { migrateNotificationDeliveriesCanonicalMessageId } from "./migrations/376-notification-deliveries-canonical-message-id.js";
487
487
  import { migrateAddSubagentBudgetStopReason } from "./migrations/377-add-subagent-budget-stop-reason.js";
488
- import { migrateCreateConversationToolSurfaces } from "./migrations/378-create-conversation-tool-surfaces.js";
489
488
  import type { MigrationStep } from "./migrations/run-migrations.js";
490
489
 
491
490
  export const migrationSteps: MigrationStep[] = [
@@ -1612,5 +1611,4 @@ export const migrationSteps: MigrationStep[] = [
1612
1611
  // this as a no-op.
1613
1612
  dependsOn: ["migrateCreateSubagentsTable"],
1614
1613
  },
1615
- migrateCreateConversationToolSurfaces,
1616
1614
  ];
@@ -284,20 +284,13 @@ export async function runConversationTurn(
284
284
  const { resolveChannelCapabilities } =
285
285
  await import("../daemon/conversation-runtime-assembly.js");
286
286
 
287
- // Channel-addressed turns are inbound: the gateway classifies the sender
288
- // and the turn runs only if that actor clears the channel admission floor.
289
- // Internal plugin jobs (no channel address) use guardian trust because they
290
- // are assistant-owned work, not an external sender.
287
+ // Plugin-driven turns run as the guardian: plugins are installed by the
288
+ // guardian, so their conversations inherit guardian trust. This lets the
289
+ // existing non-interactive auto-approve machinery handle tool permissions
290
+ // (the conversation is already non-interactive via `isInteractive: false`
291
+ // below) without requiring a client to approve prompts.
291
292
  const { INTERNAL_GUARDIAN_TRUST_CONTEXT } =
292
293
  await import("../daemon/trust-context.js");
293
- const { resolvePluginChannelTurnTrust } =
294
- await import("./plugin-channel-turn-trust.js");
295
- const { prepareChannelInboundContent } =
296
- await import("../runtime/routes/inbound-stages/inbound-content-prep.js");
297
-
298
- const trustContext = options.channel
299
- ? await resolvePluginChannelTurnTrust(options.channel)
300
- : INTERNAL_GUARDIAN_TRUST_CONTEXT;
301
294
 
302
295
  // A channel address resolves through the same binding an inbound message
303
296
  // uses, so a turn addressed by chat lands in that chat's conversation
@@ -318,7 +311,7 @@ export async function runConversationTurn(
318
311
  ? !channelConversation.created
319
312
  : getConversation(conversationId) != null;
320
313
  const conversation = await getOrCreateConversation(conversationId, {
321
- trustContext,
314
+ trustContext: INTERNAL_GUARDIAN_TRUST_CONTEXT,
322
315
  ...(options.conversationType
323
316
  ? { conversationType: options.conversationType }
324
317
  : {}),
@@ -343,23 +336,10 @@ export async function runConversationTurn(
343
336
 
344
337
  // Convert ContentBlock[] input to the text + attachments shape the
345
338
  // conversation's processMessage path expects.
346
- const { text: rawText, attachments } = extractContentAndAttachments(
339
+ const { text, attachments } = extractContentAndAttachments(
347
340
  options.content,
348
341
  resolveMediaSourceData,
349
342
  );
350
- const inboundContent = options.channel
351
- ? prepareChannelInboundContent({
352
- trimmedContent: rawText,
353
- trustClass: trustContext.trustClass,
354
- sourceChannel: options.channel.sourceChannel,
355
- requesterIdentifier:
356
- options.channel.username ??
357
- options.channel.externalUserId ??
358
- undefined,
359
- })
360
- : { content: rawText, displayContent: undefined };
361
- const text = inboundContent.content;
362
- const displayContent = inboundContent.displayContent;
363
343
 
364
344
  // The channel this turn speaks on. Runtime assembly reads the per-turn
365
345
  // context first and falls back to the conversation's `originChannel`, then
@@ -429,8 +409,6 @@ export async function runConversationTurn(
429
409
  requestId,
430
410
  isInteractive: false,
431
411
  metadata,
432
- trustContext,
433
- ...(displayContent ? { displayContent } : {}),
434
412
  });
435
413
  if (enqueueResult.rejected) {
436
414
  throw new Error(
@@ -454,8 +432,6 @@ export async function runConversationTurn(
454
432
  onEvent,
455
433
  isInteractive: false,
456
434
  metadata,
457
- trustContext,
458
- ...(displayContent ? { displayContent } : {}),
459
435
  ...(options.callSite ? { callSite: options.callSite } : {}),
460
436
  });
461
437