@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.
- package/docs/desktop-browser-cli.md +33 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +50 -4
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +5 -5
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +33 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/credential-routes.test.ts +38 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +6 -2
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +185 -30
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/oauth-commands-routes.test.ts +55 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +51 -0
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +69 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +81 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +132 -0
- package/src/calls/voice-session-bridge.ts +56 -26
- package/src/calls/voice-triage-escalate.ts +3 -3
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/oauth/status.ts +5 -0
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +43 -4
- package/src/config/bundled-skills/computer-use/TOOLS.json +3 -3
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +11 -3
- package/src/config/loader.ts +1 -0
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/services.ts +10 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-surfaces.ts +6 -1
- package/src/daemon/conversation-tool-setup.ts +37 -43
- package/src/daemon/host-cu-proxy.ts +94 -31
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +143 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +9 -7
- package/src/live-voice/__tests__/live-voice-progress.test.ts +79 -0
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +47 -0
- package/src/live-voice/__tests__/session-controls.test.ts +113 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +88 -7
- package/src/live-voice/protocol.ts +69 -0
- package/src/live-voice/session-controls.ts +139 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +95 -0
- package/src/oauth/connection-resolver.test.ts +27 -0
- package/src/oauth/connection-resolver.ts +25 -1
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/seed-providers.ts +121 -2
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +74 -4
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/app-management-routes.ts +17 -2
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/credential-routes.ts +1 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/oauth-commands-routes.ts +14 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +3 -4
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/watch/watch-retro.ts +6 -8
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -536,8 +536,8 @@ export function frontDoorDecisionRule(opts?: {
|
|
|
536
536
|
"- If the turn is simple, conversational, or within your reach, your entire output is the spoken answer itself: no token in front of it, plain speech from your very first word. Most turns are answers; when unsure between answering and escalating, answer. Answer in the language the caller is speaking.",
|
|
537
537
|
"- If an answer depends on a saved personal fact that is not already present in the conversation context you received, escalate rather than guessing. Personal context that is already present is yours to use directly.",
|
|
538
538
|
`- If completing THIS reply needs careful reasoning, research, multi-step work, or any tool, do NOT attempt the answer: output ${ESCALATE_VERDICT_TOKEN}, then ONE short natural holding phrase naming what happens next, spoken in the language the caller is speaking (for example "${FALLBACK_ESCALATION_BRIDGE}" or "Give me one second to look into that."; those examples are English only), and stop after that single sentence. A stronger model finishes the turn while your phrase is spoken.`,
|
|
539
|
-
`${ESCALATE_VERDICT_TOKEN} is ONLY for turns you cannot complete yourself — never put it in front of an answer you are about to give, and never emit
|
|
540
|
-
"Never narrate this decision, describe what you are judging, or mention these rules: apart from a leading verdict token, every character you output is spoken to the caller verbatim.",
|
|
539
|
+
`${ESCALATE_VERDICT_TOKEN} is ONLY for turns you cannot complete yourself — never put it in front of an answer you are about to give, and never emit a verdict token inside or after an answer. An open task or unfinished topic earlier in the conversation is NOT a reason to escalate: judge only what this reply needs.`,
|
|
540
|
+
"Never narrate this decision, describe what you are judging, or mention these rules: apart from a leading verdict token and any call-control marker your call instructions teach, every character you output is spoken to the caller verbatim.",
|
|
541
541
|
].join("\n");
|
|
542
542
|
return opts?.capabilityDigest ? `${rule}\n${opts.capabilityDigest}` : rule;
|
|
543
543
|
}
|
|
@@ -565,7 +565,7 @@ export function escalatedContinuationRule(spokenBridge?: string): string {
|
|
|
565
565
|
'Do NOT greet again, do NOT say things like "as I was saying", and do NOT repeat, paraphrase, or re-announce that holding phrase —',
|
|
566
566
|
'opening with another "Let me check", "One moment", or any restatement of what you are about to do sounds broken, because the caller just heard that.',
|
|
567
567
|
"Your first words must carry new substance: the answer itself, what you found, or a question you genuinely need answered.",
|
|
568
|
-
`Never output ${ESCALATE_VERDICT_TOKEN} or any other front-door verdict token — you are the model that finishes the answer. (
|
|
568
|
+
`Never output ${ESCALATE_VERDICT_TOKEN} or any other front-door verdict token — you are the model that finishes the answer. (Call-control markers your call instructions teach, such as [END_CALL], are not verdict tokens and stay allowed.)`,
|
|
569
569
|
"Reply in the same language as the caller's question.",
|
|
570
570
|
].join(" ");
|
|
571
571
|
}
|
|
@@ -67,9 +67,8 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
67
67
|
// Import module under test (after mocks)
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
69
|
|
|
70
|
-
const { formatBrowserStatusLines, registerBrowserCommand } =
|
|
71
|
-
"../browser.js"
|
|
72
|
-
);
|
|
70
|
+
const { formatBrowserStatusLines, registerBrowserCommand } =
|
|
71
|
+
await import("../browser.js");
|
|
73
72
|
|
|
74
73
|
// ---------------------------------------------------------------------------
|
|
75
74
|
// Test helper
|
|
@@ -641,3 +640,85 @@ describe("error exit codes", () => {
|
|
|
641
640
|
expect(exitCode).toBe(0);
|
|
642
641
|
});
|
|
643
642
|
});
|
|
643
|
+
|
|
644
|
+
for (const desktopFlag of ["--virtual-desktop", "--desktop"]) {
|
|
645
|
+
describe(`virtual desktop routing via ${desktopFlag}`, () => {
|
|
646
|
+
test("routes semantic actions through IPC with conversation and element identity", async () => {
|
|
647
|
+
process.env.__CONVERSATION_ID = "conv-desktop";
|
|
648
|
+
const result = await runCommand([
|
|
649
|
+
"browser",
|
|
650
|
+
desktopFlag,
|
|
651
|
+
"--json",
|
|
652
|
+
"click",
|
|
653
|
+
"--element-id",
|
|
654
|
+
"e7",
|
|
655
|
+
]);
|
|
656
|
+
expect(result.exitCode).toBe(0);
|
|
657
|
+
expect(lastIpcCall?.method).toBe("browser_execute");
|
|
658
|
+
expect(lastBody()).toMatchObject({
|
|
659
|
+
desktop: true,
|
|
660
|
+
conversationId: "conv-desktop",
|
|
661
|
+
operation: "click",
|
|
662
|
+
input: { element_id: "e7" },
|
|
663
|
+
});
|
|
664
|
+
expect(
|
|
665
|
+
(lastBody().input as Record<string, unknown>).desktop,
|
|
666
|
+
).toBeUndefined();
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test.each([
|
|
670
|
+
{ args: ["list"] },
|
|
671
|
+
{ args: ["select", "--tab-id", "7"] },
|
|
672
|
+
{ args: ["new"] },
|
|
673
|
+
{ args: ["close", "--tab-id", "7"] },
|
|
674
|
+
])("routes desktop tabs $args through IPC", async ({ args }) => {
|
|
675
|
+
mockIpcResult = { ok: true, result: { ok: true, tabs: [] } };
|
|
676
|
+
const result = await runCommand([
|
|
677
|
+
"browser",
|
|
678
|
+
desktopFlag,
|
|
679
|
+
"--json",
|
|
680
|
+
"tabs",
|
|
681
|
+
...args,
|
|
682
|
+
]);
|
|
683
|
+
expect(result.exitCode).toBe(0);
|
|
684
|
+
expect(lastIpcCall?.method).toBe("browser_tabs");
|
|
685
|
+
expect(lastBody()).toMatchObject({ desktop: true, command: args[0] });
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test.each([
|
|
689
|
+
{ flags: ["--browser-mode", "extension"] },
|
|
690
|
+
{ flags: ["--target-client-id", "client-123"] },
|
|
691
|
+
])("rejects conflicting desktop targets before IPC", async ({ flags }) => {
|
|
692
|
+
const result = await runCommand([
|
|
693
|
+
"browser",
|
|
694
|
+
desktopFlag,
|
|
695
|
+
...flags,
|
|
696
|
+
"tabs",
|
|
697
|
+
"list",
|
|
698
|
+
]);
|
|
699
|
+
expect(result.exitCode).not.toBe(0);
|
|
700
|
+
expect(lastIpcCall).toBeNull();
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
test.each([
|
|
706
|
+
{ args: ["list"] },
|
|
707
|
+
{ args: ["select", "--tab-id", "1"] },
|
|
708
|
+
{ args: ["new", "--url", "https://example.com"] },
|
|
709
|
+
{ args: ["close", "--tab-id", "1"] },
|
|
710
|
+
])(
|
|
711
|
+
"tab command $args forwards an explicit backend override",
|
|
712
|
+
async ({ args }) => {
|
|
713
|
+
mockIpcResult = { ok: true, result: { ok: true, tabs: [] } };
|
|
714
|
+
await runCommand([
|
|
715
|
+
"browser",
|
|
716
|
+
"--browser-mode",
|
|
717
|
+
"extension",
|
|
718
|
+
"tabs",
|
|
719
|
+
...args,
|
|
720
|
+
]);
|
|
721
|
+
expect(lastIpcCall?.method).toBe("browser_tabs");
|
|
722
|
+
expect(lastBody().browserMode).toBe("extension");
|
|
723
|
+
},
|
|
724
|
+
);
|
|
@@ -96,6 +96,15 @@ export const browserHelp: CliCommandHelp = {
|
|
|
96
96
|
name: "browser",
|
|
97
97
|
description: "Control the browser via the running assistant.",
|
|
98
98
|
options: [
|
|
99
|
+
{
|
|
100
|
+
flags: "--desktop",
|
|
101
|
+
description: "Compatibility alias for --virtual-desktop.",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
flags: "--virtual-desktop",
|
|
105
|
+
description:
|
|
106
|
+
"Use Chrome in the platform-hosted virtual desktop with visible cursor feedback.",
|
|
107
|
+
},
|
|
99
108
|
{
|
|
100
109
|
flags: "--session <id>",
|
|
101
110
|
description: "Session ID to preserve browser state across invocations.",
|
|
@@ -118,6 +127,38 @@ export const browserHelp: CliCommandHelp = {
|
|
|
118
127
|
],
|
|
119
128
|
helpText: `
|
|
120
129
|
Browser operations are executed through the running assistant.
|
|
130
|
+
Virtual desktop is available only for platform-hosted assistants.
|
|
131
|
+
In the web client, automatic selection prefers virtual desktop Chrome when
|
|
132
|
+
its feature is enabled and the conversation has an
|
|
133
|
+
identified guardian. The macOS and Windows apps keep their existing browser
|
|
134
|
+
selection and fallback behavior; use --virtual-desktop to select it there.
|
|
135
|
+
Existing sessions and explicit backend/client choices take priority.
|
|
136
|
+
--browser-mode local (alias playwright) means the assistant's Playwright browser,
|
|
137
|
+
not the user's Chrome. --desktop remains an alias for --virtual-desktop.
|
|
138
|
+
Use --virtual-desktop for Chrome in the Desktop panel.
|
|
139
|
+
Run navigate --url <url> directly to open a page.
|
|
140
|
+
The assistant-desktop feature flag and completed installation are required.
|
|
141
|
+
The first browser command installs missing components, starts Chrome, and completes
|
|
142
|
+
the requested action in one call. Allow up to 600 seconds for this command when
|
|
143
|
+
using bash (timeout_seconds: 600). Opening the Desktop panel also starts
|
|
144
|
+
the same installation and shows progress. No separate setup command is needed.
|
|
145
|
+
Report other availability errors without switching to a personal computer.
|
|
146
|
+
The assistant manages Chrome startup. Do not launch a separate desktop stack,
|
|
147
|
+
install desktop packages, or drive webpages with shell-level xdotool.
|
|
148
|
+
With --virtual-desktop, screenshot captures the page directly from Chrome over CDP
|
|
149
|
+
as a color JPEG. Use --output to save it and file_read to view it. Do not use
|
|
150
|
+
shell-level xwd or custom screenshot conversion scripts.
|
|
151
|
+
Use snapshot for current element IDs and tabs list for current tab IDs.
|
|
152
|
+
Refresh the snapshot after navigation, tab changes or stale-element errors.
|
|
153
|
+
Inspect the result before retrying a failed action; it may have happened.
|
|
154
|
+
A purple page pointer shows CDP mouse movement in the stream. Browser toolbar
|
|
155
|
+
controls, native dialogs and other apps are outside this CLI's page controls.
|
|
156
|
+
Do not combine --virtual-desktop with personal browser targets, other browser modes
|
|
157
|
+
or --use-active-tab. Download waiting is unavailable on this target.
|
|
158
|
+
Users can interact directly with the expanded desktop. If they ask you to pause,
|
|
159
|
+
stop and wait. Take a fresh snapshot before resuming. Closing the viewer does not
|
|
160
|
+
end automation. Run assistant browser --virtual-desktop detach when finished or
|
|
161
|
+
blocked, including before asking a question. Chrome stays running.
|
|
121
162
|
Each subcommand maps to a browser operation and communicates
|
|
122
163
|
with the assistant process.
|
|
123
164
|
|
|
@@ -131,6 +172,8 @@ local. Useful for debugging or when deterministic backend selection
|
|
|
131
172
|
is required.
|
|
132
173
|
|
|
133
174
|
Examples:
|
|
175
|
+
$ assistant browser --virtual-desktop status
|
|
176
|
+
$ assistant browser --virtual-desktop screenshot --output /tmp/desktop-page.jpg
|
|
134
177
|
$ assistant browser navigate --url https://example.com
|
|
135
178
|
$ assistant browser snapshot
|
|
136
179
|
$ assistant browser click --selector "#login"
|
|
@@ -28,6 +28,8 @@ import { registerCommand } from "../lib/register-command.js";
|
|
|
28
28
|
import { log } from "../logger.js";
|
|
29
29
|
import { browserHelp, toKebab } from "./browser.help.js";
|
|
30
30
|
|
|
31
|
+
const BROWSER_REQUEST_TIMEOUT_MS = 10 * 60_000;
|
|
32
|
+
|
|
31
33
|
// ── Naming helpers ───────────────────────────────────────────────────
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -116,6 +118,7 @@ function attachOperationAction(
|
|
|
116
118
|
|
|
117
119
|
subcmd.action(async (opts: Record<string, unknown>) => {
|
|
118
120
|
const parentOpts = browser.opts() as {
|
|
121
|
+
desktop?: boolean;
|
|
119
122
|
session?: string;
|
|
120
123
|
json?: boolean;
|
|
121
124
|
browserMode?: string;
|
|
@@ -130,6 +133,8 @@ function attachOperationAction(
|
|
|
130
133
|
// targetClientId) and screenshot ergonomics (output).
|
|
131
134
|
const input: Record<string, unknown> = {};
|
|
132
135
|
const excludeKeys = new Set([
|
|
136
|
+
"desktop",
|
|
137
|
+
"virtualDesktop",
|
|
133
138
|
"session",
|
|
134
139
|
"json",
|
|
135
140
|
"output",
|
|
@@ -163,9 +168,7 @@ function attachOperationAction(
|
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
//
|
|
167
|
-
// challenges, downloads up to 120s, etc.), so use a generous
|
|
168
|
-
// IPC timeout that exceeds any server-side operation timeout.
|
|
171
|
+
// First use can include virtual desktop installation and Chrome startup.
|
|
169
172
|
const ipcResult = await cliIpcCall<BrowserExecuteResult>(
|
|
170
173
|
"browser_execute",
|
|
171
174
|
{
|
|
@@ -173,10 +176,11 @@ function attachOperationAction(
|
|
|
173
176
|
operation: meta.operation,
|
|
174
177
|
input,
|
|
175
178
|
sessionId,
|
|
179
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
176
180
|
...(conversationId ? { conversationId } : {}),
|
|
177
181
|
},
|
|
178
182
|
},
|
|
179
|
-
{ timeoutMs:
|
|
183
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
180
184
|
);
|
|
181
185
|
|
|
182
186
|
if (!ipcResult.ok) {
|
|
@@ -328,6 +332,22 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
328
332
|
description: browserHelp.description,
|
|
329
333
|
build: (browser) => {
|
|
330
334
|
applyCommandHelp(browser, browserHelp);
|
|
335
|
+
browser.hook("preAction", () => {
|
|
336
|
+
if (browser.opts().virtualDesktop) {
|
|
337
|
+
browser.setOptionValue("desktop", true);
|
|
338
|
+
}
|
|
339
|
+
const options = browser.opts();
|
|
340
|
+
if (
|
|
341
|
+
options.desktop &&
|
|
342
|
+
(options.targetClientId ||
|
|
343
|
+
(options.browserMode &&
|
|
344
|
+
!["auto", "cdp-inspect"].includes(options.browserMode)))
|
|
345
|
+
) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
"--virtual-desktop cannot be combined with a personal browser target or another browser mode",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
331
351
|
|
|
332
352
|
// Attach one action per browser operation
|
|
333
353
|
for (const meta of BROWSER_OPERATION_META) {
|
|
@@ -339,6 +359,8 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
339
359
|
|
|
340
360
|
subcommand(tabs, "list").action(async (opts: { pretty?: boolean }) => {
|
|
341
361
|
const parentOpts = browser.opts() as {
|
|
362
|
+
desktop?: boolean;
|
|
363
|
+
browserMode?: string;
|
|
342
364
|
session?: string;
|
|
343
365
|
json?: boolean;
|
|
344
366
|
targetClientId?: string;
|
|
@@ -364,11 +386,15 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
364
386
|
body: {
|
|
365
387
|
command: "list",
|
|
366
388
|
sessionId,
|
|
389
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
390
|
+
...(parentOpts.browserMode
|
|
391
|
+
? { browserMode: parentOpts.browserMode }
|
|
392
|
+
: {}),
|
|
367
393
|
...(conversationId ? { conversationId } : {}),
|
|
368
394
|
...(targetClientId ? { targetClientId } : {}),
|
|
369
395
|
},
|
|
370
396
|
},
|
|
371
|
-
{ timeoutMs:
|
|
397
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
372
398
|
);
|
|
373
399
|
|
|
374
400
|
if (!ipcResult.ok) {
|
|
@@ -409,6 +435,8 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
409
435
|
|
|
410
436
|
subcommand(tabs, "select").action(async (opts: { tabId: string }) => {
|
|
411
437
|
const parentOpts = browser.opts() as {
|
|
438
|
+
desktop?: boolean;
|
|
439
|
+
browserMode?: string;
|
|
412
440
|
session?: string;
|
|
413
441
|
json?: boolean;
|
|
414
442
|
targetClientId?: string;
|
|
@@ -425,12 +453,16 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
425
453
|
body: {
|
|
426
454
|
command: "select",
|
|
427
455
|
sessionId,
|
|
456
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
457
|
+
...(parentOpts.browserMode
|
|
458
|
+
? { browserMode: parentOpts.browserMode }
|
|
459
|
+
: {}),
|
|
428
460
|
tabId,
|
|
429
461
|
...(conversationId ? { conversationId } : {}),
|
|
430
462
|
...(targetClientId ? { targetClientId } : {}),
|
|
431
463
|
},
|
|
432
464
|
},
|
|
433
|
-
{ timeoutMs:
|
|
465
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
434
466
|
);
|
|
435
467
|
|
|
436
468
|
if (!ipcResult.ok) {
|
|
@@ -456,6 +488,8 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
456
488
|
|
|
457
489
|
subcommand(tabs, "new").action(async (opts: { url?: string }) => {
|
|
458
490
|
const parentOpts = browser.opts() as {
|
|
491
|
+
desktop?: boolean;
|
|
492
|
+
browserMode?: string;
|
|
459
493
|
session?: string;
|
|
460
494
|
json?: boolean;
|
|
461
495
|
targetClientId?: string;
|
|
@@ -475,12 +509,16 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
475
509
|
body: {
|
|
476
510
|
command: "new",
|
|
477
511
|
sessionId,
|
|
512
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
513
|
+
...(parentOpts.browserMode
|
|
514
|
+
? { browserMode: parentOpts.browserMode }
|
|
515
|
+
: {}),
|
|
478
516
|
...(opts.url ? { url: opts.url } : {}),
|
|
479
517
|
...(conversationId ? { conversationId } : {}),
|
|
480
518
|
...(targetClientId ? { targetClientId } : {}),
|
|
481
519
|
},
|
|
482
520
|
},
|
|
483
|
-
{ timeoutMs:
|
|
521
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
484
522
|
);
|
|
485
523
|
|
|
486
524
|
if (!ipcResult.ok) {
|
|
@@ -512,6 +550,8 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
512
550
|
|
|
513
551
|
subcommand(tabs, "close").action(async (opts: { tabId: string }) => {
|
|
514
552
|
const parentOpts = browser.opts() as {
|
|
553
|
+
desktop?: boolean;
|
|
554
|
+
browserMode?: string;
|
|
515
555
|
session?: string;
|
|
516
556
|
json?: boolean;
|
|
517
557
|
targetClientId?: string;
|
|
@@ -532,12 +572,16 @@ export function registerBrowserCommand(program: Command): void {
|
|
|
532
572
|
body: {
|
|
533
573
|
command: "close",
|
|
534
574
|
sessionId,
|
|
575
|
+
...(parentOpts.desktop ? { desktop: true } : {}),
|
|
576
|
+
...(parentOpts.browserMode
|
|
577
|
+
? { browserMode: parentOpts.browserMode }
|
|
578
|
+
: {}),
|
|
535
579
|
tabId,
|
|
536
580
|
...(conversationId ? { conversationId } : {}),
|
|
537
581
|
...(targetClientId ? { targetClientId } : {}),
|
|
538
582
|
},
|
|
539
583
|
},
|
|
540
|
-
{ timeoutMs:
|
|
584
|
+
{ timeoutMs: BROWSER_REQUEST_TIMEOUT_MS, cancelOnDisconnect: true },
|
|
541
585
|
);
|
|
542
586
|
|
|
543
587
|
if (!ipcResult.ok) {
|
|
@@ -40,6 +40,8 @@ interface ConnectionSummary {
|
|
|
40
40
|
status: string;
|
|
41
41
|
expiresAt?: string | null;
|
|
42
42
|
hasRefreshToken?: boolean;
|
|
43
|
+
/** Values the provider scopes the connection by (QuickBooks' realm id). */
|
|
44
|
+
providerParams?: Record<string, string>;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
interface OAuthStatusResponse {
|
|
@@ -92,6 +94,9 @@ function formatConnection(c: ConnectionSummary, mode: string): string {
|
|
|
92
94
|
} else {
|
|
93
95
|
lines.push(` Granted scopes: (none)`);
|
|
94
96
|
}
|
|
97
|
+
for (const [key, value] of Object.entries(c.providerParams ?? {})) {
|
|
98
|
+
lines.push(` ${key}: ${value}`);
|
|
99
|
+
}
|
|
95
100
|
if (mode === "byo") {
|
|
96
101
|
if (c.expiresAt) {
|
|
97
102
|
lines.push(` Expires: ${c.expiresAt}`);
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
PluginAlreadyInStateException,
|
|
54
54
|
PluginDirectoryNotFoundError,
|
|
55
55
|
} from "../lib/toggle-plugin.js";
|
|
56
|
+
import type { PluginUninstallWarningKey } from "../lib/uninstall-plugin.js";
|
|
56
57
|
import type { PluginUpgradeResult } from "../lib/upgrade-plugin.js";
|
|
57
58
|
import { getCliLogger } from "../logger.js";
|
|
58
59
|
import { PLUGINS_SEARCH_INSTALL_HINT, pluginsHelp } from "./plugins.help.js";
|
|
@@ -666,11 +667,16 @@ export function registerPluginsCommand(program: Command): void {
|
|
|
666
667
|
// when the daemon is unreachable (a transport error carries no
|
|
667
668
|
// `statusCode`); an operator can still uninstall while it's stopped,
|
|
668
669
|
// and `shutdown` then runs in this process, the only one available.
|
|
669
|
-
const daemon = await cliIpcCall<{
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
670
|
+
const daemon = await cliIpcCall<{
|
|
671
|
+
name: string;
|
|
672
|
+
target: string;
|
|
673
|
+
warnings?: PluginUninstallWarningKey[];
|
|
674
|
+
}>("plugins_uninstall", { pathParams: { name } });
|
|
675
|
+
let result: {
|
|
676
|
+
name: string;
|
|
677
|
+
target: string;
|
|
678
|
+
warnings?: PluginUninstallWarningKey[];
|
|
679
|
+
};
|
|
674
680
|
if (daemon.ok && daemon.result) {
|
|
675
681
|
result = daemon.result;
|
|
676
682
|
} else if (daemon.statusCode === undefined) {
|
|
@@ -693,6 +699,11 @@ export function registerPluginsCommand(program: Command): void {
|
|
|
693
699
|
console.log(
|
|
694
700
|
`Uninstalled plugin "${result.name}" from ${result.target}`,
|
|
695
701
|
);
|
|
702
|
+
for (const warning of result.warnings ?? []) {
|
|
703
|
+
console.warn(
|
|
704
|
+
`Warning: ${libs.uninstall.resolvePluginUninstallWarning(warning)}`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
696
707
|
} catch (err) {
|
|
697
708
|
if (err instanceof libs.installGitHub.InvalidPluginNameError) {
|
|
698
709
|
console.error(err.message);
|
|
@@ -22,6 +22,8 @@ import { tmpdir } from "node:os";
|
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
23
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
24
24
|
|
|
25
|
+
import { readPluginMcpServers } from "../../../plugins/mcp-servers.js";
|
|
26
|
+
import { PluginManifestError } from "../../../util/plugin-manifest.js";
|
|
25
27
|
import type { FetchLike } from "../fetch-like.js";
|
|
26
28
|
import {
|
|
27
29
|
type GitRunner,
|
|
@@ -1225,6 +1227,115 @@ describe("installPlugin — direct (untrusted) install", () => {
|
|
|
1225
1227
|
expect(meta?.commit).toBe("a".repeat(40));
|
|
1226
1228
|
});
|
|
1227
1229
|
|
|
1230
|
+
test("installs a standard-only plugin without synthesizing package.json", async () => {
|
|
1231
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1232
|
+
const runGit = fakeGitRunner({
|
|
1233
|
+
tree: {
|
|
1234
|
+
"plugin.json": JSON.stringify({
|
|
1235
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
1236
|
+
name: "standard-plugin",
|
|
1237
|
+
version: "1.4.0",
|
|
1238
|
+
}),
|
|
1239
|
+
"mcp.json": JSON.stringify({
|
|
1240
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
|
|
1241
|
+
mcpServers: {
|
|
1242
|
+
"standard-plugin": {
|
|
1243
|
+
type: "streamable-http",
|
|
1244
|
+
url: "https://mcp.example.com",
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1247
|
+
}),
|
|
1248
|
+
},
|
|
1249
|
+
commit: "9".repeat(40),
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
const result = await installPlugin(
|
|
1253
|
+
{
|
|
1254
|
+
name: "standard-plugin",
|
|
1255
|
+
directSource: {
|
|
1256
|
+
owner: "owner",
|
|
1257
|
+
repo: "standard-plugin",
|
|
1258
|
+
rootPath: "",
|
|
1259
|
+
ref: "HEAD",
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1263
|
+
);
|
|
1264
|
+
|
|
1265
|
+
expect(existsSync(join(result.target, "plugin.json"))).toBe(true);
|
|
1266
|
+
expect(existsSync(join(result.target, "package.json"))).toBe(false);
|
|
1267
|
+
expect(readInstallMeta(result.target)?.version).toBe("1.4.0");
|
|
1268
|
+
const mcp = readPluginMcpServers({ workspacePluginsDir: pluginsDir });
|
|
1269
|
+
expect(mcp.issues).toEqual([]);
|
|
1270
|
+
expect(mcp.servers.map((server) => server.id)).toEqual(["standard-plugin"]);
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test("synthesizes package.json beside a foreign plugin.json", async () => {
|
|
1274
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1275
|
+
const runGit = fakeGitRunner({
|
|
1276
|
+
tree: {
|
|
1277
|
+
"plugin.json": JSON.stringify({ name: "foreign-plugin" }),
|
|
1278
|
+
"mcp.json": JSON.stringify({
|
|
1279
|
+
mcpServers: {
|
|
1280
|
+
"foreign-plugin": {
|
|
1281
|
+
type: "streamable-http",
|
|
1282
|
+
url: "https://mcp.example.com",
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
}),
|
|
1286
|
+
},
|
|
1287
|
+
commit: "8".repeat(40),
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const result = await installPlugin(
|
|
1291
|
+
{
|
|
1292
|
+
name: "foreign-plugin",
|
|
1293
|
+
directSource: {
|
|
1294
|
+
owner: "owner",
|
|
1295
|
+
repo: "foreign-plugin",
|
|
1296
|
+
rootPath: "",
|
|
1297
|
+
ref: "HEAD",
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1300
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1301
|
+
);
|
|
1302
|
+
|
|
1303
|
+
expect(existsSync(join(result.target, "plugin.json"))).toBe(true);
|
|
1304
|
+
expect(existsSync(join(result.target, "package.json"))).toBe(true);
|
|
1305
|
+
const mcp = readPluginMcpServers({ workspacePluginsDir: pluginsDir });
|
|
1306
|
+
expect(mcp.issues).toEqual([]);
|
|
1307
|
+
expect(mcp.servers.map((server) => server.id)).toEqual(["foreign-plugin"]);
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
test("rejects a malformed claimed standard manifest before install", async () => {
|
|
1311
|
+
const fetch = makeContentsFetch({ tree: {} });
|
|
1312
|
+
const runGit = fakeGitRunner({
|
|
1313
|
+
tree: {
|
|
1314
|
+
"plugin.json": JSON.stringify({
|
|
1315
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
1316
|
+
name: "Invalid Standard Name",
|
|
1317
|
+
}),
|
|
1318
|
+
},
|
|
1319
|
+
commit: "7".repeat(40),
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
await expect(
|
|
1323
|
+
installPlugin(
|
|
1324
|
+
{
|
|
1325
|
+
name: "invalid-standard",
|
|
1326
|
+
directSource: {
|
|
1327
|
+
owner: "owner",
|
|
1328
|
+
repo: "invalid-standard",
|
|
1329
|
+
rootPath: "",
|
|
1330
|
+
ref: "HEAD",
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
1334
|
+
),
|
|
1335
|
+
).rejects.toBeInstanceOf(PluginManifestError);
|
|
1336
|
+
expect(existsSync(join(pluginsDir, "invalid-standard"))).toBe(false);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1228
1339
|
test("never overlays a curated adapter stub, even if one matches the name", async () => {
|
|
1229
1340
|
// GIVEN a fetch that WOULD serve an adapter stub for the name, and a
|
|
1230
1341
|
// postinstall runner that fails the test if it is ever invoked
|
|
@@ -14,6 +14,8 @@ import { join } from "node:path";
|
|
|
14
14
|
import { gzipSync } from "node:zlib";
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
16
16
|
|
|
17
|
+
import { readPluginMcpServers } from "../../../plugins/mcp-servers.js";
|
|
18
|
+
import { PluginManifestError } from "../../../util/plugin-manifest.js";
|
|
17
19
|
import type { FetchLike } from "../fetch-like.js";
|
|
18
20
|
import {
|
|
19
21
|
PluginInstallDeclinedError,
|
|
@@ -145,7 +147,13 @@ const noSleep = async () => {};
|
|
|
145
147
|
describe("installPluginFromPlatform — success", () => {
|
|
146
148
|
test("downloads, verifies, and extracts files at the plugin root", async () => {
|
|
147
149
|
const entries: TarEntry[] = [
|
|
148
|
-
{
|
|
150
|
+
{
|
|
151
|
+
name: "plugin.json",
|
|
152
|
+
content: JSON.stringify({
|
|
153
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
154
|
+
name: "reading-pal",
|
|
155
|
+
}),
|
|
156
|
+
},
|
|
149
157
|
{ name: "README.md", content: "# reading pal" },
|
|
150
158
|
{ name: "skills/read/SKILL.md", content: "skill body" },
|
|
151
159
|
];
|
|
@@ -177,7 +185,7 @@ describe("installPluginFromPlatform — success", () => {
|
|
|
177
185
|
);
|
|
178
186
|
expect(existsSync(join(target, "README.md"))).toBe(true);
|
|
179
187
|
expect(existsSync(join(target, "skills", "read", "SKILL.md"))).toBe(true);
|
|
180
|
-
expect(existsSync(join(target, "package.json"))).toBe(
|
|
188
|
+
expect(existsSync(join(target, "package.json"))).toBe(false);
|
|
181
189
|
|
|
182
190
|
// Provenance records the pinned commit and the verified ETag.
|
|
183
191
|
const meta = readInstallMeta(target);
|
|
@@ -227,6 +235,73 @@ describe("installPluginFromPlatform — success", () => {
|
|
|
227
235
|
expect(pkg.peerDependencies["@vellumai/plugin-api"]).toBeDefined();
|
|
228
236
|
});
|
|
229
237
|
|
|
238
|
+
test("synthesizes package.json beside a foreign plugin.json", async () => {
|
|
239
|
+
const fetchFn = makeInstallFetch({
|
|
240
|
+
entries: [
|
|
241
|
+
{
|
|
242
|
+
name: "plugin.json",
|
|
243
|
+
content: JSON.stringify({ name: "foreign-plugin" }),
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "mcp.json",
|
|
247
|
+
content: JSON.stringify({
|
|
248
|
+
mcpServers: {
|
|
249
|
+
"foreign-plugin": {
|
|
250
|
+
type: "streamable-http",
|
|
251
|
+
url: "https://mcp.example.com",
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
ref: "d".repeat(40),
|
|
258
|
+
repo: "example-org/foreign-plugin",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const result = await installPluginFromPlatform(
|
|
262
|
+
{ name: "foreign-plugin" },
|
|
263
|
+
{
|
|
264
|
+
fetch: fetchFn,
|
|
265
|
+
platformBaseUrl: PLATFORM,
|
|
266
|
+
workspacePluginsDir: pluginsDir,
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(existsSync(join(result.target, "plugin.json"))).toBe(true);
|
|
271
|
+
expect(existsSync(join(result.target, "package.json"))).toBe(true);
|
|
272
|
+
const mcp = readPluginMcpServers({ workspacePluginsDir: pluginsDir });
|
|
273
|
+
expect(mcp.issues).toEqual([]);
|
|
274
|
+
expect(mcp.servers.map((server) => server.id)).toEqual(["foreign-plugin"]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("rejects a malformed claimed standard manifest before install", async () => {
|
|
278
|
+
const fetchFn = makeInstallFetch({
|
|
279
|
+
entries: [
|
|
280
|
+
{
|
|
281
|
+
name: "plugin.json",
|
|
282
|
+
content: JSON.stringify({
|
|
283
|
+
$schema:
|
|
284
|
+
"https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
285
|
+
}),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
ref: "e".repeat(40),
|
|
289
|
+
repo: "example-org/invalid-standard",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await expect(
|
|
293
|
+
installPluginFromPlatform(
|
|
294
|
+
{ name: "invalid-standard" },
|
|
295
|
+
{
|
|
296
|
+
fetch: fetchFn,
|
|
297
|
+
platformBaseUrl: PLATFORM,
|
|
298
|
+
workspacePluginsDir: pluginsDir,
|
|
299
|
+
},
|
|
300
|
+
),
|
|
301
|
+
).rejects.toBeInstanceOf(PluginManifestError);
|
|
302
|
+
expect(existsSync(join(pluginsDir, "invalid-standard"))).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
|
|
230
305
|
test("leaves an upstream package.json in place", async () => {
|
|
231
306
|
const fetchFn = makeInstallFetch({
|
|
232
307
|
entries: [
|