@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
@@ -1,68 +1,19 @@
1
1
  /**
2
- * Per-conversation wire projections of the UI surface tool definitions.
2
+ * Conversation-stable wire projections of the UI surface tool definitions.
3
3
  *
4
- * Slack renders exactly one surface — the task_progress card (everything else
5
- * is rejected at execution; see `isSlackTaskProgressUiException` in
6
- * `daemon/conversation-surfaces.ts`) — so Slack conversations get a `ui_show`
7
- * variant documenting only that shape instead of the full multi-surface docs.
8
- * Activation-rail conversations additionally get the `activation_moment`
9
- * telemetry param, which no other conversation pays schema tokens for (the
10
- * emit path is gated on `isActivationSession` regardless).
4
+ * `ui_show`, `ui_update`, and `ui_dismiss` keep one definition across every
5
+ * channel and across live vs clientless turns. Channel renderers enforce the
6
+ * surfaces they can display at execution. Clientless calls persist surface
7
+ * content for the next capable client that opens the conversation.
11
8
  *
12
- * Projection contract: shared definition objects are never mutated (variants
13
- * are module-level constants swapped in via spread-copy), and the projected
14
- * set is a pure function of per-conversation-stable state, so a
15
- * conversation's tools+system prompt-cache prefix stays stable across turns.
9
+ * `activation_moment` is the remaining schema projection. It applies only to
10
+ * activation-rail conversations, whose marker is written before the first
11
+ * tool resolution, so the extra property is stable for that conversation's
12
+ * lifetime. It must not vary by channel or turn presence.
16
13
  */
17
14
 
18
15
  import type { ToolDefinition } from "../../providers/types.js";
19
16
  import { ACTIVATION_MOMENT_PARAMS } from "../../telemetry/activation-funnel.js";
20
- import { TASK_PROGRESS_TEMPLATE_SHAPE } from "./surface-shape-docs.js";
21
-
22
- const SLACK_UI_SHOW_DESCRIPTION = `Show a live task-progress card for the current work — the only surface this channel renders. data: { template: "task_progress", templateData: ${TASK_PROGRESS_TEMPLATE_SHAPE} }. Show it early on multi-step turns and advance step statuses via ui_update as work progresses.`;
23
-
24
- const SLACK_UI_SHOW_INPUT_SCHEMA = {
25
- type: "object",
26
- properties: {
27
- surface_type: {
28
- type: "string",
29
- enum: ["card"],
30
- description: "Always card on this channel",
31
- },
32
- title: {
33
- type: "string",
34
- description: "Optional surface title",
35
- },
36
- data: {
37
- type: "object",
38
- description:
39
- 'Card data: { template: "task_progress", templateData: { title, status, steps } }',
40
- },
41
- },
42
- required: ["surface_type", "data"],
43
- };
44
-
45
- /**
46
- * Swap UI surface tool definitions for channel-appropriate variants. Returns
47
- * the input array unchanged (same identity) for channels without a variant.
48
- */
49
- export function projectUiToolsForChannel(
50
- definitions: ToolDefinition[],
51
- channel: string | undefined,
52
- ): ToolDefinition[] {
53
- if (channel !== "slack") {
54
- return definitions;
55
- }
56
- return definitions.map((def) =>
57
- def.name === "ui_show"
58
- ? {
59
- ...def,
60
- description: SLACK_UI_SHOW_DESCRIPTION,
61
- input_schema: SLACK_UI_SHOW_INPUT_SCHEMA,
62
- }
63
- : def,
64
- );
65
- }
66
17
 
67
18
  const ACTIVATION_MOMENT_PROPERTY = {
68
19
  type: "string",
@@ -73,7 +24,7 @@ const ACTIVATION_MOMENT_PROPERTY = {
73
24
 
74
25
  /**
75
26
  * Add the optional `activation_moment` param to ui_show's schema. Applied
76
- * only for activation-rail conversations (`isActivationSession`) — the rail
27
+ * only for activation-rail conversations (`isActivationSession`). The rail
77
28
  * bootstrap prompt instructs the tagging, and the daemon's emit path reads
78
29
  * the tag from tool input independently of this schema.
79
30
  */
@@ -21,7 +21,18 @@ export type AbortReasonKind =
21
21
  /** A signal-file cancel was written by an out-of-process caller (CLI, hook). */
22
22
  | "signal_cancel"
23
23
  /** Voice session bridge aborted the conversation (turn supersession, call end). */
24
- | "voice_session_aborted";
24
+ | "voice_session_aborted"
25
+ /**
26
+ * One spoken progress update ran out of its generation budget.
27
+ *
28
+ * Distinct from {@link "voice_session_aborted"} because nothing was aborted
29
+ * but the update itself: the call is still up, the turn is still running,
30
+ * and the only consequence is that this beat goes unspoken. Sharing the
31
+ * session-abort kind made a narrator that had gone fail-open read in the
32
+ * logs as a session dying over and over, which is the opposite of what the
33
+ * line means.
34
+ */
35
+ | "voice_progress_narration_timeout";
25
36
 
26
37
  const ABORT_REASON_TAG = "__vellumAbortReason" as const;
27
38
 
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Resolve the manifest that identifies an installed plugin.
3
+ *
4
+ * `package.json` is the primary manifest for Vellum plugins. A root
5
+ * `plugin.json` is selected only when `package.json` is absent, so a legacy
6
+ * plugin can carry another ecosystem's `plugin.json` without being
7
+ * reclassified.
8
+ */
9
+
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ import { z } from "zod";
14
+
15
+ export const LEGACY_PLUGIN_MANIFEST = "package.json";
16
+ export const STANDARD_PLUGIN_MANIFEST = "plugin.json";
17
+ export const AGENT_PLUGINS_MANIFEST_SCHEMA_URL =
18
+ "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
19
+
20
+ const PluginNameSchema = z
21
+ .string()
22
+ .min(1)
23
+ .max(64)
24
+ .regex(/^[a-z0-9](?!.*(?:--|\.\.))[a-z0-9.-]*[a-z0-9]$|^[a-z0-9]$/);
25
+
26
+ const LegacyPluginManifestSchema = z
27
+ .object({
28
+ name: z.string().min(1),
29
+ version: z.string().optional(),
30
+ description: z.string().min(1).optional(),
31
+ homepage: z.string().optional(),
32
+ license: z.unknown().optional(),
33
+ peerDependencies: z.record(z.string(), z.string()).optional(),
34
+ credentialKeyPatterns: z.unknown().optional(),
35
+ displayName: z.string().min(1).optional(),
36
+ icon: z.string().min(1).optional(),
37
+ })
38
+ .passthrough();
39
+
40
+ const StandardPluginManifestSchema = z
41
+ .object({
42
+ $schema: z.literal(AGENT_PLUGINS_MANIFEST_SCHEMA_URL),
43
+ name: PluginNameSchema,
44
+ version: z.string().optional(),
45
+ description: z.string().optional(),
46
+ author: z
47
+ .object({
48
+ name: z.string().optional(),
49
+ email: z.string().optional(),
50
+ url: z.string().optional(),
51
+ })
52
+ .strict()
53
+ .optional(),
54
+ homepage: z.string().optional(),
55
+ repository: z.string().optional(),
56
+ license: z.string().optional(),
57
+ keywords: z.array(z.string()).optional(),
58
+ // The core specification says a non-object extensions value is reported
59
+ // and ignored rather than rejecting the plugin. This host does not consume
60
+ // a standard extension namespace here, so keep it opaque.
61
+ extensions: z.unknown().optional(),
62
+ })
63
+ // Unknown core fields are reported-and-ignored by the specification. The
64
+ // loader does not assign them semantics, so passthrough preserves that
65
+ // non-fatal behavior without copying them into runtime metadata.
66
+ .passthrough();
67
+
68
+ export type PluginManifestSource =
69
+ | typeof LEGACY_PLUGIN_MANIFEST
70
+ | typeof STANDARD_PLUGIN_MANIFEST;
71
+
72
+ export interface ResolvedPluginManifest {
73
+ readonly source: PluginManifestSource;
74
+ readonly name: string;
75
+ readonly version?: string;
76
+ readonly description?: string;
77
+ readonly homepage?: string;
78
+ readonly license?: unknown;
79
+ /** Parsed selected manifest, retained for Vellum-specific legacy fields. */
80
+ readonly raw: Record<string, unknown>;
81
+ }
82
+
83
+ export type PluginManifestInstallAction = "keep" | "synthesize-legacy";
84
+
85
+ export class PluginManifestError extends Error {
86
+ constructor(
87
+ readonly pluginDir: string,
88
+ readonly source: PluginManifestSource | null,
89
+ message: string,
90
+ readonly cause?: unknown,
91
+ ) {
92
+ super(message);
93
+ this.name = "PluginManifestError";
94
+ }
95
+ }
96
+
97
+ /** True when the directory carries either supported root manifest. */
98
+ export function hasPluginManifest(pluginDir: string): boolean {
99
+ return (
100
+ existsSync(join(pluginDir, LEGACY_PLUGIN_MANIFEST)) ||
101
+ existsSync(join(pluginDir, STANDARD_PLUGIN_MANIFEST))
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Decide whether an installed artifact needs the compatibility `package.json`.
107
+ *
108
+ * A `package.json` file is always preserved. A root `plugin.json`
109
+ * claims the Agent Plugins format only when it names the supported schema. A
110
+ * claimed standard manifest is validated before install; other `plugin.json`
111
+ * files remain foreign metadata beside the synthesized compatibility manifest.
112
+ */
113
+ export function getPluginManifestInstallAction(
114
+ pluginDir: string,
115
+ ): PluginManifestInstallAction {
116
+ if (existsSync(join(pluginDir, LEGACY_PLUGIN_MANIFEST))) {
117
+ return "keep";
118
+ }
119
+
120
+ const standardPath = join(pluginDir, STANDARD_PLUGIN_MANIFEST);
121
+ if (!existsSync(standardPath)) {
122
+ return "synthesize-legacy";
123
+ }
124
+
125
+ let json: unknown;
126
+ try {
127
+ json = JSON.parse(readFileSync(standardPath, "utf8"));
128
+ } catch {
129
+ return "synthesize-legacy";
130
+ }
131
+
132
+ if (
133
+ typeof json !== "object" ||
134
+ json === null ||
135
+ Array.isArray(json) ||
136
+ (json as Record<string, unknown>).$schema !==
137
+ AGENT_PLUGINS_MANIFEST_SCHEMA_URL
138
+ ) {
139
+ return "synthesize-legacy";
140
+ }
141
+
142
+ readPluginManifest(pluginDir);
143
+ return "keep";
144
+ }
145
+
146
+ /**
147
+ * Read and validate the selected root manifest.
148
+ *
149
+ * Selection is deterministic: `package.json` wins when present, including
150
+ * when it is malformed. The loader never falls through
151
+ * from a selected malformed legacy manifest to `plugin.json`.
152
+ */
153
+ export function readPluginManifest(pluginDir: string): ResolvedPluginManifest {
154
+ const source = existsSync(join(pluginDir, LEGACY_PLUGIN_MANIFEST))
155
+ ? LEGACY_PLUGIN_MANIFEST
156
+ : existsSync(join(pluginDir, STANDARD_PLUGIN_MANIFEST))
157
+ ? STANDARD_PLUGIN_MANIFEST
158
+ : null;
159
+ if (source === null) {
160
+ throw new PluginManifestError(
161
+ pluginDir,
162
+ null,
163
+ `plugin at ${pluginDir} is missing package.json and plugin.json`,
164
+ );
165
+ }
166
+
167
+ const path = join(pluginDir, source);
168
+ let json: unknown;
169
+ try {
170
+ json = JSON.parse(readFileSync(path, "utf8"));
171
+ } catch (cause) {
172
+ const reason = cause instanceof Error ? cause.message : String(cause);
173
+ throw new PluginManifestError(
174
+ pluginDir,
175
+ source,
176
+ `${source} at ${pluginDir} could not be read or parsed: ${reason}`,
177
+ cause,
178
+ );
179
+ }
180
+
181
+ const parsed =
182
+ source === LEGACY_PLUGIN_MANIFEST
183
+ ? LegacyPluginManifestSchema.safeParse(json)
184
+ : StandardPluginManifestSchema.safeParse(json);
185
+ if (!parsed.success) {
186
+ throw new PluginManifestError(
187
+ pluginDir,
188
+ source,
189
+ `${source} at ${pluginDir} failed schema validation: ${parsed.error.message}`,
190
+ parsed.error,
191
+ );
192
+ }
193
+
194
+ return {
195
+ source,
196
+ name: parsed.data.name,
197
+ version: parsed.data.version,
198
+ description: parsed.data.description,
199
+ homepage: parsed.data.homepage,
200
+ license: parsed.data.license,
201
+ raw: parsed.data,
202
+ };
203
+ }
@@ -61,14 +61,12 @@ const WATCH_RETRO_WAKE_SOURCE = "watch-retro";
61
61
  * under the `watch_retro` template, which the client draws as a paged card: the
62
62
  * record on the first page, one question per page after it.
63
63
  *
64
- * **The daemon appends the card; the model never reaches `ui_show`.** This wake
65
- * is `clientless`, and `conversation-tool-setup` gates the whole `ui_surface`
66
- * family on a client being present, so `ui_show` is absent from this turn's
67
- * tool set rather than merely denied. A retrospective told to call it can only
68
- * report that it cannot. `watch_retro_report` is an ordinary tool and passes
69
- * that gate, and the surface is appended here, the way the memory
70
- * retrospective's `skill_card` is appended by the daemon rather than requested
71
- * by the model.
64
+ * **The daemon appends the card; the model calls `watch_retro_report`.** This
65
+ * wake is `clientless`. `ui_show` stays on the wire so background surfaces can
66
+ * persist, but this flow still reports through `watch_retro_report` so the
67
+ * daemon can append the card after the turn ends. The memory retrospective's
68
+ * `skill_card` is appended the same way: the daemon writes the surface, the
69
+ * model does not request it.
72
70
  *
73
71
  * **The append waits for the turn to end.** A `ui_surface` row written mid-turn
74
72
  * can land between a persisted `tool_use` and its `tool_result`, an ordering
@@ -1,18 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
4
- import type { AssistantConfig } from "../config/schema.js";
5
- import { isAssistantDesktopEnabled } from "./desktop-feature.js";
6
-
7
- describe("isAssistantDesktopEnabled", () => {
8
- const config = {} as AssistantConfig;
9
-
10
- test("needs both the flag and a containerized runtime", () => {
11
- setOverridesForTesting({ "assistant-desktop": true });
12
- expect(isAssistantDesktopEnabled(config, true)).toBe(true);
13
- expect(isAssistantDesktopEnabled(config, false)).toBe(false);
14
-
15
- setOverridesForTesting({ "assistant-desktop": false });
16
- expect(isAssistantDesktopEnabled(config, true)).toBe(false);
17
- });
18
- });
@@ -1,19 +0,0 @@
1
- import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
2
- import { getIsContainerized } from "../config/env-registry.js";
3
- import type { AssistantConfig } from "../config/schema.js";
4
-
5
- const ASSISTANT_DESKTOP_FLAG = "assistant-desktop" as const;
6
-
7
- /**
8
- * Whether this daemon serves `/v1/desktop/stream`: the `assistant-desktop` flag
9
- * plus a containerized runtime where desktop components can be installed.
10
- */
11
- export function isAssistantDesktopEnabled(
12
- config: AssistantConfig,
13
- containerized: boolean = getIsContainerized(),
14
- ): boolean {
15
- return (
16
- containerized &&
17
- isAssistantFeatureFlagEnabled(ASSISTANT_DESKTOP_FLAG, config)
18
- );
19
- }