@vellumai/assistant 0.12.2-staging.1 → 0.12.2-staging.3

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 (194) hide show
  1. package/docs/desktop-browser-cli.md +33 -0
  2. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  3. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  4. package/openapi.yaml +50 -4
  5. package/package.json +1 -1
  6. package/scripts/smoke-desktop-browser-cli.ts +279 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +5 -5
  8. package/src/__tests__/config-schema.test.ts +1 -1
  9. package/src/__tests__/config-sounds-sync.test.ts +23 -0
  10. package/src/__tests__/conversation-error.test.ts +4 -1
  11. package/src/__tests__/conversation-runtime-assembly.test.ts +33 -3
  12. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  13. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  14. package/src/__tests__/credential-routes.test.ts +38 -0
  15. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  16. package/src/__tests__/cu-unified-flow.test.ts +6 -2
  17. package/src/__tests__/external-plugin-loader.test.ts +52 -0
  18. package/src/__tests__/headless-browser-mode.test.ts +48 -0
  19. package/src/__tests__/host-cu-proxy.test.ts +185 -30
  20. package/src/__tests__/list-all-apps.test.ts +40 -8
  21. package/src/__tests__/mcp-auth-routes.test.ts +99 -0
  22. package/src/__tests__/mcp-client-auth.test.ts +54 -0
  23. package/src/__tests__/mcp-health-check.test.ts +32 -32
  24. package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
  25. package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
  26. package/src/__tests__/oauth-commands-routes.test.ts +55 -0
  27. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  28. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  29. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  30. package/src/__tests__/skills.test.ts +10 -0
  31. package/src/__tests__/subagent-tool-gate-mode.test.ts +51 -0
  32. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  33. package/src/__tests__/user-plugin-loader.test.ts +21 -0
  34. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  35. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  36. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  37. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  38. package/src/acp/acp-claude-oauth.ts +328 -14
  39. package/src/acp/acp-credentials.ts +19 -0
  40. package/src/acp/claude-token-refresh.ts +150 -0
  41. package/src/acp/prepare-agent-env.ts +21 -6
  42. package/src/apps/app-store.ts +6 -5
  43. package/src/browser/operations.ts +2 -1
  44. package/src/browser/types.ts +7 -0
  45. package/src/browser/virtual-desktop-target.ts +34 -0
  46. package/src/calls/__tests__/progress-narration.test.ts +23 -0
  47. package/src/calls/__tests__/voice-control-protocol.test.ts +69 -0
  48. package/src/calls/__tests__/voice-session-bridge.test.ts +81 -0
  49. package/src/calls/progress-narration.ts +15 -2
  50. package/src/calls/voice-control-protocol.ts +132 -0
  51. package/src/calls/voice-session-bridge.ts +56 -26
  52. package/src/calls/voice-triage-escalate.ts +3 -3
  53. package/src/cli/commands/__tests__/browser.test.ts +84 -3
  54. package/src/cli/commands/browser.help.ts +43 -0
  55. package/src/cli/commands/browser.ts +52 -8
  56. package/src/cli/commands/oauth/status.ts +5 -0
  57. package/src/cli/commands/plugins.ts +16 -5
  58. package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
  59. package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
  60. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
  61. package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
  62. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
  63. package/src/cli/lib/bundled-marketplace.json +13 -0
  64. package/src/cli/lib/install-from-github.ts +17 -18
  65. package/src/cli/lib/install-from-platform.ts +14 -9
  66. package/src/cli/lib/list-installed-plugins.ts +21 -35
  67. package/src/cli/lib/plugin-details.ts +21 -8
  68. package/src/cli/lib/uninstall-plugin.ts +59 -1
  69. package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
  70. package/src/config/bundled-skills/computer-use/SKILL.md +43 -4
  71. package/src/config/bundled-skills/computer-use/TOOLS.json +3 -3
  72. package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
  73. package/src/config/feature-flag-registry.json +11 -3
  74. package/src/config/loader.ts +1 -0
  75. package/src/config/schemas/__tests__/voice.test.ts +2 -2
  76. package/src/config/schemas/mcp.ts +9 -7
  77. package/src/config/schemas/monitoring.ts +9 -0
  78. package/src/config/schemas/services.ts +10 -0
  79. package/src/config/schemas/voice.ts +2 -2
  80. package/src/config/skills.ts +13 -21
  81. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
  82. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
  83. package/src/daemon/conversation-client-surface.ts +26 -0
  84. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  85. package/src/daemon/conversation-surfaces.ts +6 -1
  86. package/src/daemon/conversation-tool-setup.ts +37 -43
  87. package/src/daemon/host-cu-proxy.ts +94 -31
  88. package/src/daemon/host-proxy-preactivation.ts +2 -0
  89. package/src/daemon/mcp-reload-service.ts +7 -0
  90. package/src/daemon/message-types/sync.ts +1 -0
  91. package/src/daemon/providers-setup.ts +4 -0
  92. package/src/desktop/__tests__/fake-desktop.ts +1 -0
  93. package/src/desktop/desktop-automation-lease.test.ts +208 -0
  94. package/src/desktop/desktop-automation-lease.ts +270 -0
  95. package/src/desktop/desktop-browser-client.test.ts +413 -0
  96. package/src/desktop/desktop-browser-client.ts +430 -0
  97. package/src/desktop/desktop-browser-cursor.ts +40 -0
  98. package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
  99. package/src/desktop/desktop-browser-endpoint.ts +145 -0
  100. package/src/desktop/desktop-browser-operations.ts +130 -0
  101. package/src/desktop/desktop-chrome-session.test.ts +48 -0
  102. package/src/desktop/desktop-chrome-session.ts +58 -8
  103. package/src/desktop/desktop-dependencies.test.ts +29 -0
  104. package/src/desktop/desktop-dependencies.ts +26 -5
  105. package/src/desktop/desktop-display.ts +9 -0
  106. package/src/desktop/desktop-panel-config.ts +14 -1
  107. package/src/desktop/desktop-session-manager.test.ts +86 -3
  108. package/src/desktop/desktop-session-manager.ts +144 -24
  109. package/src/desktop/desktop-stream-bridge.ts +1 -1
  110. package/src/desktop/virtual-desktop-feature.ts +18 -0
  111. package/src/desktop/virtual-desktop-platform.test.ts +61 -0
  112. package/src/i18n/__tests__/i18n.test.ts +9 -5
  113. package/src/i18n/messages.ts +13 -4
  114. package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
  115. package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
  116. package/src/ipc/assistant-server.ts +15 -1
  117. package/src/ipc/cli-client.ts +13 -5
  118. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +143 -0
  119. package/src/live-voice/__tests__/live-voice-events.test.ts +9 -7
  120. package/src/live-voice/__tests__/live-voice-progress.test.ts +79 -0
  121. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
  122. package/src/live-voice/__tests__/protocol.test.ts +47 -0
  123. package/src/live-voice/__tests__/session-controls.test.ts +113 -0
  124. package/src/live-voice/live-voice-metrics.ts +9 -0
  125. package/src/live-voice/live-voice-session.ts +88 -7
  126. package/src/live-voice/protocol.ts +69 -0
  127. package/src/live-voice/session-controls.ts +139 -0
  128. package/src/mcp/__tests__/credential-target.test.ts +59 -0
  129. package/src/mcp/__tests__/effective-config.test.ts +11 -0
  130. package/src/mcp/__tests__/manager-state.test.ts +128 -0
  131. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
  132. package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
  133. package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
  134. package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
  135. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
  136. package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
  137. package/src/mcp/client.ts +64 -16
  138. package/src/mcp/credential-target.ts +105 -0
  139. package/src/mcp/effective-config.ts +42 -15
  140. package/src/mcp/manager.ts +61 -8
  141. package/src/mcp/mcp-auth-orchestrator.ts +17 -4
  142. package/src/mcp/mcp-auth-state.ts +38 -6
  143. package/src/mcp/mcp-oauth-provider.ts +101 -33
  144. package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
  145. package/src/monitoring/mount-watch.ts +348 -0
  146. package/src/monitoring/worker.ts +9 -0
  147. package/src/oauth/__tests__/seed-providers-managed.test.ts +95 -0
  148. package/src/oauth/connection-resolver.test.ts +27 -0
  149. package/src/oauth/connection-resolver.ts +25 -1
  150. package/src/oauth/provider-serializer.ts +9 -0
  151. package/src/oauth/seed-providers.ts +121 -2
  152. package/src/onboarding/checkin-event.ts +2 -1
  153. package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
  154. package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
  155. package/src/plugins/external-plugin-loader.ts +49 -86
  156. package/src/plugins/installed-plugin-dirs.ts +6 -4
  157. package/src/plugins/mcp-servers.ts +9 -15
  158. package/src/plugins/mtime-cache.ts +2 -1
  159. package/src/plugins/user-loader.ts +2 -1
  160. package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
  161. package/src/providers/inference/endpoint-probe.ts +27 -12
  162. package/src/providers/openai/responses-provider.ts +9 -2
  163. package/src/providers/opencode/client.test.ts +167 -0
  164. package/src/providers/opencode/client.ts +73 -4
  165. package/src/runtime/AGENTS.md +4 -0
  166. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
  167. package/src/runtime/http-server.ts +6 -6
  168. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  169. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +74 -4
  170. package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
  171. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  172. package/src/runtime/routes/app-management-routes.ts +17 -2
  173. package/src/runtime/routes/browser-context.ts +39 -0
  174. package/src/runtime/routes/browser-routes.ts +16 -47
  175. package/src/runtime/routes/browser-tabs-routes.ts +72 -29
  176. package/src/runtime/routes/credential-routes.ts +1 -4
  177. package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
  178. package/src/runtime/routes/desktop-setup-routes.ts +7 -5
  179. package/src/runtime/routes/mcp-auth-routes.ts +106 -94
  180. package/src/runtime/routes/oauth-commands-routes.ts +14 -0
  181. package/src/runtime/routes/oauth-providers.ts +7 -0
  182. package/src/runtime/routes/plugins-routes.ts +14 -1
  183. package/src/runtime/sync/resource-sync-events.ts +4 -0
  184. package/src/tools/browser/browser-execution.ts +44 -13
  185. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
  186. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
  187. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
  188. package/src/tools/computer-use/definitions.ts +3 -4
  189. package/src/tools/ui-surface/channel-variants.ts +10 -59
  190. package/src/util/abort-reasons.ts +12 -1
  191. package/src/util/plugin-manifest.ts +203 -0
  192. package/src/watch/watch-retro.ts +6 -8
  193. package/src/desktop/desktop-feature.test.ts +0 -18
  194. package/src/desktop/desktop-feature.ts +0 -19
@@ -25,16 +25,55 @@ desktop client and is never handled locally by the assistant.
25
25
 
26
26
  ## Observations
27
27
 
28
- Every computer-use step returns the accessibility tree. A screenshot comes with
29
- a desktop's first look, with window-scoped observations, or when you call
30
- `computer_use_observe` with `include_screenshot: true`. Ask for one whenever the
31
- tree is not enough to act on: a canvas, a game, a custom-drawn view, few or
28
+ Every computer-use step returns the accessibility tree. Every action also
29
+ returns a screenshot taken after it ran (one at the end of a
30
+ `computer_use_sequence`), so check it to see what your action did. An
31
+ observation comes with a screenshot on a desktop's first look, when it is
32
+ window-scoped, or when you pass `include_screenshot: true`. Ask for one whenever
33
+ the tree is not enough to act on: a canvas, a game, a custom-drawn view, few or
32
34
  unlabeled controls, or a layout question.
33
35
 
34
36
  The tree is walked to a limited depth to keep steps fast, and says when it was
35
37
  cut off. If the element you need is not in it, call `computer_use_observe` with
36
38
  `full_tree: true`.
37
39
 
40
+ ## Scripting apps (macOS)
41
+
42
+ Reach for `computer_use_run_applescript` first when an app can be driven by
43
+ script. It does not take the cursor.
44
+
45
+ **Try the app's own scripting dictionary before anything else.** Ask it for the
46
+ state you want, not for the clicks that would produce that state. Finder, Mail,
47
+ Music, Notes, Safari, Terminal and many third-party apps have one, and where
48
+ there is a dictionary the whole task is usually a sentence with no window to
49
+ open, no tree to read and nothing to click:
50
+
51
+ ```applescript
52
+ tell application "Finder"
53
+ set target of front window to desktop
54
+ set current view of front window to list view
55
+ set sort column of list view options of front window to name column
56
+ end tell
57
+ ```
58
+
59
+ **System Events menu clicking is the fallback**, for apps with no dictionary
60
+ entry for what you need: `click menu item "Split Clip" of menu "Modify" of menu
61
+ bar 1` inside `tell application "System Events" to tell process "iMovie"`. It is
62
+ UI automation in a script's clothes: it still depends on the menu sitting where
63
+ you expect and on the app being frontmost. A menu item that needs a selection or
64
+ a playhead position does nothing when that context is missing, so set it up
65
+ first, and read `enabled of menu item` when unsure.
66
+
67
+ Click and type for everything a script cannot reach. `host_bash` is for shell
68
+ commands, not for driving apps.
69
+
70
+ ## Typing is not sending
71
+
72
+ Pressing enter in a chat, email or form usually sends or submits it, and that
73
+ cannot be taken back. Send only when the user asked you to send, post or
74
+ submit. When they asked you to type, write or draft something, type it and stop
75
+ before pressing enter; tell them it is ready to send.
76
+
38
77
  ## Batching known steps (macOS)
39
78
 
40
79
  When you already know the next few actions and none depends on seeing the
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "include_screenshot": {
19
19
  "type": "boolean",
20
- "description": "Force a screenshot with this observation. The accessibility tree is returned every step; ask for pixels whenever the tree is not enough to act on, such as a canvas, a game, a custom-drawn view, a window with few or unlabeled controls, or a question about layout."
20
+ "description": "Force a screenshot with this observation. The accessibility tree is returned every step and actions always return a screenshot of their result; ask for pixels on an observation whenever the tree is not enough to act on, such as a canvas, a game, a custom-drawn view, a window with few or unlabeled controls, or a question about layout."
21
21
  },
22
22
  "full_tree": {
23
23
  "type": "boolean",
@@ -269,7 +269,7 @@
269
269
  },
270
270
  {
271
271
  "name": "computer_use_run_applescript",
272
- "description": "Run an AppleScript command. Prefer this over click/type when possible - it doesn't move the cursor or interrupt foreground activity. Never use 'do shell script' inside AppleScript (blocked for security).",
272
+ "description": "Run an AppleScript on the Mac. Try the target app's own scripting dictionary first, asking it for the state you want rather than for the clicks that would produce it; fall back to System Events menu clicking for apps with no dictionary entry for what you need. Prefer this over click and type when the app supports it; it does not move the cursor. The result is the script's return value, and the accessibility tree may not change even when the script worked. Never use 'do shell script' inside AppleScript (blocked for security).",
273
273
  "category": "computer-use",
274
274
  "risk": "medium",
275
275
  "input_schema": {
@@ -281,7 +281,7 @@
281
281
  },
282
282
  "reasoning": {
283
283
  "type": "string",
284
- "description": "Explanation of what this script does and why AppleScript is better than UI interaction for this step"
284
+ "description": "What this script does"
285
285
  },
286
286
  "target_client_id": {
287
287
  "type": "string",
@@ -8,11 +8,11 @@ metadata:
8
8
  display-name: "Screen Annotation"
9
9
  category: "system"
10
10
  activation-hints:
11
- - "User asks where something is, how to do it, or to be walked through it, in an app they share on a call"
11
+ - "User asks where a control is, or to be walked through doing something themselves, in an app they share"
12
12
  - "User wants to be shown, not have it done for them"
13
13
  - "The answer is a place on the user's screen"
14
14
  avoid-when:
15
- - "User wants the assistant to do the thing rather than be shown it (use computer-use)"
15
+ - "User asks the assistant to do it (click, type, edit), even on a call: use computer-use"
16
16
  - "Nothing is being shared, so there is no surface to point at"
17
17
  ---
18
18
 
@@ -175,7 +175,7 @@
175
175
  "scope": "client",
176
176
  "key": "teleport",
177
177
  "label": "Teleport",
178
- "description": "Enable teleport UI in General settings for moving assistants between hosting environments",
178
+ "description": "Enable the local-to-platform teleport UI in General settings. Platform-to-local teleport is GA and not gated by this flag",
179
179
  "defaultEnabled": false
180
180
  },
181
181
  {
@@ -474,6 +474,14 @@
474
474
  "description": "Gates the seeded Figma OAuth provider. When off, the provider is hidden from GET /v1/oauth/providers, its get-by-id route, and the connect/update routes, so it cannot be listed or connected from the CLI, gateway, or web integrations list. The row is still seeded into oauth_providers on startup, and existing connections keep injecting credentials; only its visibility changes.",
475
475
  "defaultEnabled": false
476
476
  },
477
+ {
478
+ "id": "quickbooks-oauth",
479
+ "scope": "assistant",
480
+ "key": "quickbooks-oauth",
481
+ "label": "QuickBooks Integration",
482
+ "description": "Gates the seeded QuickBooks OAuth provider. When off, the provider is hidden from GET /v1/oauth/providers, its get-by-id route, and the connect/update routes, so it cannot be listed or connected from the CLI, gateway, or web integrations list. The row is still seeded into oauth_providers on startup, and existing connections keep injecting credentials; only its visibility changes.",
483
+ "defaultEnabled": false
484
+ },
477
485
  {
478
486
  "id": "shopify-oauth",
479
487
  "scope": "assistant",
@@ -532,8 +540,8 @@
532
540
  "id": "assistant-desktop",
533
541
  "scope": "assistant",
534
542
  "key": "assistant-desktop",
535
- "label": "Assistant Desktop",
536
- "description": "Streams an interactive desktop from a containerized assistant pod to the web client over /v1/desktop/stream. Off: no desktop affordance is rendered. On: the chat header offers a desktop panel that views and controls the pod's display.",
543
+ "label": "Virtual desktop",
544
+ "description": "Streams an interactive virtual desktop from a platform-hosted, containerized assistant over /v1/desktop/stream. Requires platform hosting; self-hosted assistants are excluded. Off: the virtual desktop is hidden. On: the chat header offers its viewer and controls.",
537
545
  "defaultEnabled": false
538
546
  }
539
547
  ]
@@ -183,6 +183,7 @@ export function getDeploymentContextDefaults(): Record<string, unknown> {
183
183
  "shopify-oauth": managed,
184
184
  "stripe-link-oauth": managed,
185
185
  "figma-oauth": managed,
186
+ "quickbooks-oauth": managed,
186
187
  "eventbrite-oauth": managed,
187
188
  "calendly-oauth": managed,
188
189
  },
@@ -9,7 +9,7 @@ const PROGRESS_DEFAULTS = {
9
9
  maxSilenceMs: 35_000,
10
10
  longOpMs: 15_000,
11
11
  minGapMs: 6_000,
12
- generationTimeoutMs: 1_500,
12
+ generationTimeoutMs: 5_000,
13
13
  };
14
14
 
15
15
  const FRONT_MODEL_DEFAULTS = {
@@ -75,7 +75,7 @@ describe("VoiceFrontModelConfigSchema", () => {
75
75
  expect(parsed.progress.maxSilenceMs).toBe(35_000);
76
76
  expect(parsed.progress.longOpMs).toBe(15_000);
77
77
  expect(parsed.progress.minGapMs).toBe(6_000);
78
- expect(parsed.progress.generationTimeoutMs).toBe(1_500);
78
+ expect(parsed.progress.generationTimeoutMs).toBe(5_000);
79
79
  });
80
80
 
81
81
  test("a stale maxPerTurn key is stripped, not rejected", () => {
@@ -55,9 +55,7 @@ const McpSseTransportSchema = z
55
55
  .optional()
56
56
  .describe("Custom HTTP headers sent with SSE requests"),
57
57
  })
58
- .describe(
59
- "SSE transport: connects to an MCP server over Server-Sent Events",
60
- );
58
+ .describe("SSE transport: connects to an MCP server over Server-Sent Events");
61
59
 
62
60
  const McpStreamableHttpTransportSchema = z
63
61
  .object({
@@ -114,10 +112,14 @@ export type McpConfig = z.infer<typeof McpConfigSchema>;
114
112
  */
115
113
  export type McpServerSource = "workspace" | "plugin";
116
114
 
117
- /** A server config with its origin resolved. */
118
- export interface ResolvedMcpServerConfig extends McpServerConfig {
119
- readonly source: McpServerSource;
120
- }
115
+ /** A server config with its runtime-only origin and credential identity. */
116
+ export type ResolvedMcpServerConfig =
117
+ | (McpServerConfig & { readonly source: "workspace" })
118
+ | (McpServerConfig & {
119
+ readonly source: "plugin";
120
+ readonly pluginName: string;
121
+ readonly serverKey: string;
122
+ });
121
123
 
122
124
  /** The MCP config the daemon runs: both origins, every server attributed. */
123
125
  export interface ResolvedMcpConfig {
@@ -75,6 +75,15 @@ export const MonitoringConfigSchema = z
75
75
  .describe(
76
76
  "Minimum interval between file-descriptor warnings while usage stays over the threshold, in milliseconds. Crossing the threshold always warns immediately; this only throttles the repeats.",
77
77
  ),
78
+ mountWatchIntervalMs: z
79
+ .number({ error: "monitoring.mountWatchIntervalMs must be a number" })
80
+ .int("monitoring.mountWatchIntervalMs must be an integer")
81
+ .min(1_000, "monitoring.mountWatchIntervalMs must be at least 1000ms")
82
+ .max(60_000, "monitoring.mountWatchIntervalMs must be <= 60000ms")
83
+ .default(10_000)
84
+ .describe(
85
+ "How often the monitor diffs the assistant container's mount table (/proc/self/mountinfo plus statfs of /workspace and /data), in milliseconds. Logs only when the watched mounts change. Default 10 seconds so a virtiofs bind drop is timestamped without a line per poll.",
86
+ ),
78
87
  pluginSourceScanIntervalMs: z
79
88
  .number({
80
89
  error: "monitoring.pluginSourceScanIntervalMs must be a number",
@@ -157,6 +157,13 @@ export const FigmaOAuthServiceSchema = BaseServiceSchema.extend({
157
157
  });
158
158
  export type FigmaOAuthService = z.infer<typeof FigmaOAuthServiceSchema>;
159
159
 
160
+ export const QuickBooksOAuthServiceSchema = BaseServiceSchema.extend({
161
+ mode: ServiceModeSchema.default("your-own"),
162
+ });
163
+ export type QuickBooksOAuthService = z.infer<
164
+ typeof QuickBooksOAuthServiceSchema
165
+ >;
166
+
160
167
  export const EventbriteOAuthServiceSchema = BaseServiceSchema.extend({
161
168
  mode: ServiceModeSchema.default("your-own"),
162
169
  });
@@ -228,6 +235,9 @@ export const ServicesSchema = z.object({
228
235
  "figma-oauth": FigmaOAuthServiceSchema.default(
229
236
  FigmaOAuthServiceSchema.parse({}),
230
237
  ),
238
+ "quickbooks-oauth": QuickBooksOAuthServiceSchema.default(
239
+ QuickBooksOAuthServiceSchema.parse({}),
240
+ ),
231
241
  "eventbrite-oauth": EventbriteOAuthServiceSchema.default(
232
242
  EventbriteOAuthServiceSchema.parse({}),
233
243
  ),
@@ -74,9 +74,9 @@ export const VoiceProgressConfigSchema = z
74
74
  .positive(
75
75
  "voice.frontModel.progress.generationTimeoutMs must be a positive integer",
76
76
  )
77
- .default(1_500)
77
+ .default(5_000)
78
78
  .describe(
79
- "Budget (ms) for LLM-generated progress text. Not latency-critical: it speaks into dead air",
79
+ "Budget (ms) for LLM-generated progress text. Not latency-critical: it speaks into dead air, so the budget is set to cover the narrator's real roundtrip rather than to be tight. A budget that only sometimes covers that roundtrip thins narration out rather than speeding it up, and it thins out exactly where the updates matter most, since the slow beats are the ones with the longest silence behind them (see endpointDecisionTimeoutMs for the same failure mode). Kept under progress.minGapMs so an update that spends the whole budget still lands before the next one is due",
80
80
  ),
81
81
  })
82
82
  // The heartbeat is checked when the idle tick finds the turn silent, so a
@@ -39,6 +39,10 @@ import {
39
39
  getWorkspacePluginsDir,
40
40
  getWorkspaceSkillsDir,
41
41
  } from "../util/platform.js";
42
+ import {
43
+ hasPluginManifest,
44
+ readPluginManifest,
45
+ } from "../util/plugin-manifest.js";
42
46
  import { stripCommentLines } from "../util/strip-comment-lines.js";
43
47
  import { isAssistantFeatureFlagEnabled } from "./assistant-feature-flags.js";
44
48
  import { getConfig } from "./loader.js";
@@ -734,8 +738,7 @@ function discoverSkillDirectories(skillsDir: string): string[] {
734
738
  }
735
739
 
736
740
  /**
737
- * Whether `pluginDir` carries a plugin manifest the runtime can load: a
738
- * parseable `package.json` with a non-empty string `name`. This mirrors the
741
+ * Whether `pluginDir` carries a plugin manifest the runtime can load. This mirrors the
739
742
  * external plugin loader (`buildPluginFromDir`), which builds a plugin from
740
743
  * any such directory and derives the plugin's identity from `package.json`
741
744
  * `name` — it imposes no match between that `name` and the directory name.
@@ -745,31 +748,20 @@ function discoverSkillDirectories(skillsDir: string): string[] {
745
748
  * requiring the two to match would silently drop the resident skills of every
746
749
  * such plugin even though the runtime loads its hooks and tools fine.
747
750
  *
748
- * The caller is responsible for the missing-`package.json` case (it emits a
751
+ * The caller is responsible for the missing-manifest case (it emits a
749
752
  * diagnostic warning); this function only judges a manifest that is present.
750
753
  */
751
754
  function hasLoadablePluginManifest(pluginDir: string): boolean {
752
- const manifestPath = join(pluginDir, "package.json");
753
- if (!existsSync(manifestPath)) {
754
- return false;
755
- }
756
- let parsed: unknown;
757
755
  try {
758
- parsed = JSON.parse(readFileSync(manifestPath, "utf-8"));
756
+ readPluginManifest(pluginDir);
757
+ return true;
759
758
  } catch (err) {
760
759
  log.warn(
761
- { err, manifestPath },
762
- "Skipping plugin dir with unparseable package.json for resident skills",
760
+ { err, pluginDir },
761
+ "Skipping plugin dir with an invalid manifest for resident skills",
763
762
  );
764
763
  return false;
765
764
  }
766
- return (
767
- typeof parsed === "object" &&
768
- parsed !== null &&
769
- "name" in parsed &&
770
- typeof (parsed as { name: unknown }).name === "string" &&
771
- (parsed as { name: string }).name.length > 0
772
- );
773
765
  }
774
766
 
775
767
  /**
@@ -885,15 +877,15 @@ function discoverInstalledPluginResidentSkills(): SkillSummary[] {
885
877
  }
886
878
  const pluginDir = join(pluginsDir, entry.name);
887
879
 
888
- // A directory under `plugins/` with no `package.json` is not a plugin the
880
+ // A directory under `plugins/` with no supported manifest is not a plugin the
889
881
  // runtime can load, so its skills are never surfaced. This is an easy
890
882
  // footgun — a plugin dropped in without its manifest looks installed but
891
883
  // silently contributes nothing — so warn loudly with the path rather than
892
884
  // skipping in silence, to make the misconfiguration diagnosable.
893
- if (!existsSync(join(pluginDir, "package.json"))) {
885
+ if (!hasPluginManifest(pluginDir)) {
894
886
  log.warn(
895
887
  { pluginDir },
896
- "Plugin directory is missing package.json — skipping; its skills will not be available. Add a package.json with a `name`.",
888
+ "Plugin directory is missing package.json and plugin.json; its skills will not be available.",
897
889
  );
898
890
  continue;
899
891
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
8
8
 
9
+ import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
9
10
  import * as configLoader from "../../config/loader.js";
10
11
  import type { AssistantConfig } from "../../config/schema.js";
11
12
  import * as disabledState from "../../plugins/disabled-state.js";
@@ -245,3 +246,49 @@ describe("createResolveToolsCallback — config.tools.exclude", () => {
245
246
  expect(ctx.allowedToolNames?.has("bash")).toBe(false);
246
247
  });
247
248
  });
249
+
250
+ test("managed browser guidance follows each turn without changing native or shared tool definitions", () => {
251
+ const platform = process.env.IS_PLATFORM;
252
+ const containerized = process.env.IS_CONTAINERIZED;
253
+ process.env.IS_PLATFORM = "true";
254
+ process.env.IS_CONTAINERIZED = "true";
255
+ setOverridesForTesting({ "assistant-desktop": true });
256
+ const bash = def("bash");
257
+ const ctx = makeCtx({
258
+ conversationId: "conv-browser-guidance",
259
+ transportInterface: "web",
260
+ currentTurnClientOs: "web",
261
+ trustContext: { trustClass: "guardian" } as Conversation["trustContext"],
262
+ getTurnActorPrincipalId: () => "user-123",
263
+ });
264
+ const resolve = createResolveToolsCallback([bash], ctx)!;
265
+ try {
266
+ expect(
267
+ resolve([]).find((tool) => tool.name === "bash")?.description,
268
+ ).toContain("assistant browser navigate");
269
+ expect(bash.description).toBe("bash");
270
+ for (const clientOs of ["macos", "windows", "linux"] as const) {
271
+ ctx.currentTurnClientOs = clientOs;
272
+ expect(
273
+ resolve([]).find((tool) => tool.name === "bash")?.description,
274
+ ).toBe("bash");
275
+ }
276
+ ctx.currentTurnClientOs = "web";
277
+ setOverridesForTesting({ "assistant-desktop": false });
278
+ expect(resolve([]).find((tool) => tool.name === "bash")?.description).toBe(
279
+ "bash",
280
+ );
281
+ } finally {
282
+ setOverridesForTesting({});
283
+ if (platform === undefined) {
284
+ delete process.env.IS_PLATFORM;
285
+ } else {
286
+ process.env.IS_PLATFORM = platform;
287
+ }
288
+ if (containerized === undefined) {
289
+ delete process.env.IS_CONTAINERIZED;
290
+ } else {
291
+ process.env.IS_CONTAINERIZED = containerized;
292
+ }
293
+ }
294
+ });
@@ -41,6 +41,7 @@ mock.module("../../mcp/mcp-header-store.js", () => ({
41
41
  }));
42
42
 
43
43
  mock.module("../../config/loader.js", () => ({
44
+ getConfig: () => ({}),
44
45
  invalidateConfigCache: () => {},
45
46
  }));
46
47
 
@@ -0,0 +1,26 @@
1
+ import { type ClientOs, parseClientOs } from "../channels/types.js";
2
+ import type { Conversation } from "./conversation.js";
3
+
4
+ /**
5
+ * Host OS of the client driving this turn. The Electron renderer reports
6
+ * `interface: "web"` and carries the real OS in `clientOs`, so this prefers
7
+ * the frozen per-turn value and only falls back to a desktop transport.
8
+ */
9
+ export function resolveTurnClientOs(ctx: Conversation): {
10
+ clientOs: ClientOs | undefined;
11
+ transportInterface: Conversation["transportInterface"];
12
+ } {
13
+ const pin = ctx.toolContextPin;
14
+ const transportInterface = pin
15
+ ? pin.transportInterface
16
+ : ctx.transportInterface;
17
+ const clientOs = pin
18
+ ? pin.clientOs
19
+ : (parseClientOs(ctx.currentTurnClientOs ?? ctx.clientOs) ??
20
+ (transportInterface === "macos" ||
21
+ transportInterface === "windows" ||
22
+ transportInterface === "linux"
23
+ ? transportInterface
24
+ : undefined));
25
+ return { clientOs, transportInterface };
26
+ }
@@ -819,10 +819,15 @@ export function applySightFrameRetention(
819
819
  * `null` on the happy path (desktop, full capabilities, no special context)
820
820
  * where no block is injected. Split from {@link injectChannelCapabilityContext}
821
821
  * so callers can capture the exact injected text for metadata persistence.
822
+ *
823
+ * Live vs clientless guidance belongs here, not in tool schemas. Tool
824
+ * definitions stay identical so the tools cache prefix can reuse; this block
825
+ * sits later in the system prompt and can describe the current turn.
822
826
  */
823
827
  export function buildChannelCapabilityBlock(
824
828
  caps: ChannelCapabilities,
825
829
  clientOs: string | undefined = caps.clientOS,
830
+ isNonInteractive = false,
826
831
  ): string | null {
827
832
  // Happy path: desktop with full capabilities and no special context — skip injection.
828
833
  if (
@@ -849,7 +854,7 @@ export function buildChannelCapabilityBlock(
849
854
  if (clientOs === "macos") {
850
855
  lines.push("");
851
856
  lines.push(
852
- "On macOS, prefer osascript/CLI via `host_bash` over computer use tools, which take over the user's cursor. Use foreground computer use only when no scripting alternative exists or the user explicitly asks.",
857
+ "On macOS, drive apps with the computer-use skill; `host_bash` is for shell commands.",
853
858
  );
854
859
  }
855
860
 
@@ -874,18 +879,24 @@ export function buildChannelCapabilityBlock(
874
879
  "- Do NOT reference the dashboard UI, settings panels, or visual preference pickers.",
875
880
  );
876
881
  if (!caps.supportsDynamicUi) {
877
- if (caps.channel === "slack") {
882
+ if (isNonInteractive) {
878
883
  lines.push(
879
- '- Do NOT use app_create. Only use ui_show/ui_update for card surfaces with template: "task_progress"; present all other information as text.',
884
+ "- ui_show, ui_update, and ui_dismiss persist conversation content for the next capable client that opens this conversation. Do not claim a surface is visible now or wait for an action.",
880
885
  );
881
886
  } else {
887
+ if (caps.channel === "slack") {
888
+ lines.push(
889
+ '- Do NOT use app_create. Only use ui_show/ui_update for card surfaces with template: "task_progress"; present all other information as text.',
890
+ );
891
+ } else {
892
+ lines.push(
893
+ "- Do NOT use ui_show, ui_update, or app_create. This channel cannot render them.",
894
+ );
895
+ }
882
896
  lines.push(
883
- "- Do NOT use ui_show, ui_update, or app_create — this channel cannot render them.",
897
+ "- Present information as well-formatted text instead of dynamic UI.",
884
898
  );
885
899
  }
886
- lines.push(
887
- "- Present information as well-formatted text instead of dynamic UI.",
888
- );
889
900
  }
890
901
  if (caps.channel === "whatsapp") {
891
902
  lines.push(
@@ -2853,6 +2864,7 @@ export async function applyRuntimeInjections(
2853
2864
  const channelCapabilityBlock = buildChannelCapabilityBlock(
2854
2865
  channelCapabilities,
2855
2866
  clientOs,
2867
+ options.isNonInteractive === true,
2856
2868
  );
2857
2869
  if (channelCapabilityBlock !== null) {
2858
2870
  channelCapabilitiesCaptured = channelCapabilityBlock;
@@ -3180,7 +3180,7 @@ export async function surfaceProxyResolver(
3180
3180
  : typeof input.answer === "string"
3181
3181
  ? input.answer
3182
3182
  : "Task complete";
3183
- hostCuProxy.reset();
3183
+ hostCuProxy.endTask(ctx.conversationId);
3184
3184
  return { content: summary, isError: false };
3185
3185
  }
3186
3186
 
@@ -3346,6 +3346,11 @@ export async function surfaceProxyResolver(
3346
3346
 
3347
3347
  if (toolName === "ui_show" || toolName === "ui_update") {
3348
3348
  const caps = ctx.channelCapabilities;
3349
+ // Live non-dynamic channels reject unsupported surfaces. Clientless turns
3350
+ // skip this gate so surfaces persist for a later capable client.
3351
+ // `canShowInteractiveUi` fails closed on clientless turns because
3352
+ // standalone surfaces have nobody to answer them, so it is not the
3353
+ // predicate here.
3349
3354
  if (
3350
3355
  caps &&
3351
3356
  !ctx.hasNoClient &&
@@ -7,10 +7,9 @@
7
7
  */
8
8
 
9
9
  import type { AssistantEvent } from "../api/index.js";
10
+ import { shouldUseVirtualDesktopBrowser } from "../browser/virtual-desktop-target.js";
10
11
  import {
11
- type ClientOs,
12
12
  type HostProxyCapability,
13
- parseClientOs,
14
13
  supportsHostProxy,
15
14
  } from "../channels/types.js";
16
15
  import { getIsPlatform } from "../config/env-registry.js";
@@ -62,10 +61,7 @@ import {
62
61
  type ToolContext,
63
62
  type ToolExecutionResult,
64
63
  } from "../tools/types.js";
65
- import {
66
- injectActivationMomentParam,
67
- projectUiToolsForChannel,
68
- } from "../tools/ui-surface/channel-variants.js";
64
+ import { injectActivationMomentParam } from "../tools/ui-surface/channel-variants.js";
69
65
  import { loadWorkspaceTools } from "../tools/workspace-tools/loader.js";
70
66
  import {
71
67
  resolveUsageAttribution,
@@ -77,6 +73,7 @@ import {
77
73
  conversationSupportsGuardianQuestionCards,
78
74
  } from "./channel-ui-capability.js";
79
75
  import type { Conversation } from "./conversation.js";
76
+ import { resolveTurnClientOs } from "./conversation-client-surface.js";
80
77
  import { projectSkillTools } from "./conversation-skill-tools.js";
81
78
  import {
82
79
  restoreSurfaceStateEntry,
@@ -745,30 +742,6 @@ export const ALLOWLIST_ONLY_TOOL_NAMES = new Set<string>([
745
742
  "delete_memory_page",
746
743
  ]);
747
744
 
748
- /**
749
- * Host OS of the client driving this turn. The Electron renderer reports
750
- * `interface: "web"` and carries the real OS in `clientOs`, so this prefers
751
- * the frozen per-turn value and only falls back to a desktop transport.
752
- */
753
- function resolveTurnClientOs(ctx: Conversation): {
754
- clientOs: ClientOs | undefined;
755
- transportInterface: Conversation["transportInterface"];
756
- } {
757
- const pin = ctx.toolContextPin;
758
- const transportInterface = pin
759
- ? pin.transportInterface
760
- : ctx.transportInterface;
761
- const clientOs = pin
762
- ? pin.clientOs
763
- : (parseClientOs(ctx.currentTurnClientOs ?? ctx.clientOs) ??
764
- (transportInterface === "macos" ||
765
- transportInterface === "windows" ||
766
- transportInterface === "linux"
767
- ? transportInterface
768
- : undefined));
769
- return { clientOs, transportInterface };
770
- }
771
-
772
745
  /**
773
746
  * Windows parity gate: skill tools may declare `supported_client_os`; drop
774
747
  * them when the turn's client OS (or pinned OS for wakes) is not listed.
@@ -1147,19 +1120,40 @@ export function createResolveToolsCallback(
1147
1120
  : currentWorkspaceDefs
1148
1121
  ).filter((d) => !readOnlyHidesFromWire(d.name));
1149
1122
  const excluded = new Set(getConfig().tools.exclude);
1150
- // Swap UI surface tools for channel-appropriate variants (e.g. Slack's
1151
- // task_progress-only ui_show). Mirrors the pin handling in
1152
- // `isToolActiveForContext`: execution-gate-mode wakes pin channel
1153
- // capabilities to undefined, which resolves to the unprojected defs.
1154
- const channelForUiTools = ctx.toolContextPin
1155
- ? undefined
1156
- : ctx.channelCapabilities?.channel;
1157
- let allBaseDefs = projectUiToolsForChannel(
1158
- [...scopedCoreDefs, ...scopedWorkspaceDefs, ...scopedMcpDefs].filter(
1159
- (d) => !excluded.has(d.name),
1160
- ),
1161
- channelForUiTools,
1162
- );
1123
+ // UI definitions stay identical across channel and background turns.
1124
+ // Channel renderers enforce their supported surface subset at execution,
1125
+ // while background calls persist the full surface content for the next
1126
+ // capable client that opens the conversation. Skill tools stay off this
1127
+ // list (`skill_execute` dispatch) and are a separate disclosure path.
1128
+ let allBaseDefs = [
1129
+ ...scopedCoreDefs,
1130
+ ...scopedWorkspaceDefs,
1131
+ ...scopedMcpDefs,
1132
+ ].filter((d) => !excluded.has(d.name));
1133
+ if (
1134
+ ctx.transportInterface === "web" &&
1135
+ shouldUseVirtualDesktopBrowser(
1136
+ undefined,
1137
+ {},
1138
+ {
1139
+ workingDir: ctx.workingDir,
1140
+ conversationId: ctx.conversationId,
1141
+ trustClass: ctx.trustContext?.trustClass ?? "unknown",
1142
+ transportInterface: ctx.transportInterface,
1143
+ clientOs: resolveTurnClientOs(ctx).clientOs,
1144
+ sourceActorPrincipalId: ctx.getTurnActorPrincipalId?.(),
1145
+ },
1146
+ )
1147
+ ) {
1148
+ allBaseDefs = allBaseDefs.map((definition) =>
1149
+ definition.name === "bash"
1150
+ ? {
1151
+ ...definition,
1152
+ description: `${definition.description} For browser tasks, use assistant browser navigate --url <url> directly. It installs the virtual desktop if needed, starts Chrome, and completes the action in one call. Use timeout_seconds: ${getConfig().timeouts.shellMaxTimeoutSec} for first use; setup progress is visible in the Virtual desktop panel. Use assistant browser --help for other browser actions. Use this managed path even if saved notes describe manual setup. Do not install packages or launch Chrome, X servers, or screenshot scripts yourself.`,
1153
+ }
1154
+ : definition,
1155
+ );
1156
+ }
1163
1157
  // Activation-rail conversations carry the optional `activation_moment`
1164
1158
  // telemetry param on ui_show. The marker is written before the first
1165
1159
  // tool resolution (see `applyBootstrapTemplate` in system-prompt.ts), so