@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4
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/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -279,7 +279,7 @@ describe("searchPlugins", () => {
|
|
|
279
279
|
source: {
|
|
280
280
|
source: "github",
|
|
281
281
|
repo: "JuliusBrussee/caveman",
|
|
282
|
-
ref: "
|
|
282
|
+
ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
283
283
|
},
|
|
284
284
|
description: "Ultra-compressed communication mode.",
|
|
285
285
|
},
|
|
@@ -311,12 +311,12 @@ describe("searchPlugins", () => {
|
|
|
311
311
|
expect(result.matches).toEqual([
|
|
312
312
|
{
|
|
313
313
|
name: "caveman",
|
|
314
|
-
path: "github:JuliusBrussee/caveman@
|
|
314
|
+
path: "github:JuliusBrussee/caveman@63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
315
315
|
description: "Ultra-compressed communication mode.",
|
|
316
316
|
source: {
|
|
317
317
|
kind: "github",
|
|
318
318
|
repo: "JuliusBrussee/caveman",
|
|
319
|
-
ref: "
|
|
319
|
+
ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
320
320
|
},
|
|
321
321
|
},
|
|
322
322
|
{
|
|
@@ -337,7 +337,7 @@ describe("searchPlugins", () => {
|
|
|
337
337
|
source: {
|
|
338
338
|
source: "github",
|
|
339
339
|
repo: "JuliusBrussee/caveman",
|
|
340
|
-
ref: "
|
|
340
|
+
ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
341
341
|
},
|
|
342
342
|
},
|
|
343
343
|
],
|
|
@@ -357,8 +357,8 @@ describe("searchPlugins", () => {
|
|
|
357
357
|
expect(result.matches).toEqual([]);
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
-
test("
|
|
361
|
-
// GIVEN
|
|
360
|
+
test("a marketplace entry owns a name shared by an in-repo stub dir", async () => {
|
|
361
|
+
// GIVEN a marketplace entry named "caveman"
|
|
362
362
|
const manifest = {
|
|
363
363
|
name: "vellum-assistant",
|
|
364
364
|
plugins: [
|
|
@@ -367,11 +367,13 @@ describe("searchPlugins", () => {
|
|
|
367
367
|
source: {
|
|
368
368
|
source: "github",
|
|
369
369
|
repo: "JuliusBrussee/caveman",
|
|
370
|
-
ref: "
|
|
370
|
+
ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
371
371
|
},
|
|
372
372
|
},
|
|
373
373
|
],
|
|
374
374
|
};
|
|
375
|
+
// AND a same-named in-repo directory, which is caveman's adapter stub
|
|
376
|
+
// rather than a standalone first-party plugin
|
|
375
377
|
const fetch: FetchLike = (async (input: RequestInfo | URL) => {
|
|
376
378
|
const url = typeof input === "string" ? input : input.toString();
|
|
377
379
|
if (url.includes("marketplace.json")) {
|
|
@@ -394,12 +396,17 @@ describe("searchPlugins", () => {
|
|
|
394
396
|
// WHEN we search
|
|
395
397
|
const result = await searchPlugins({ query: "caveman" }, { fetch });
|
|
396
398
|
|
|
397
|
-
// THEN
|
|
399
|
+
// THEN the name surfaces once, as the external marketplace entry — the
|
|
400
|
+
// stub dir is its adapter overlay, not a competing first-party listing
|
|
398
401
|
expect(result.matches).toEqual([
|
|
399
402
|
{
|
|
400
403
|
name: "caveman",
|
|
401
|
-
path: "
|
|
402
|
-
source: {
|
|
404
|
+
path: "github:JuliusBrussee/caveman@63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
405
|
+
source: {
|
|
406
|
+
kind: "github",
|
|
407
|
+
repo: "JuliusBrussee/caveman",
|
|
408
|
+
ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
409
|
+
},
|
|
403
410
|
},
|
|
404
411
|
]);
|
|
405
412
|
});
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* fetched with a shallow `git` clone at that ref — one network operation
|
|
10
10
|
* regardless of repo size, immune to GitHub's unauthenticated API
|
|
11
11
|
* rate-limit, and recording the exact resolved commit for provenance.
|
|
12
|
+
* When we curate an adapter stub for the plugin (an
|
|
13
|
+
* `experimental/plugins/<name>/` directory in this repo with a
|
|
14
|
+
* `scripts.postinstall` command), the stub is overlaid onto the clone and
|
|
15
|
+
* its postinstall runs to translate a foreign-ecosystem layout into the
|
|
16
|
+
* shape Vellum's loader runs (see {@link applyAdapterStub}).
|
|
12
17
|
* 2. Otherwise the first-party convention
|
|
13
18
|
* `vellum-ai/vellum-assistant/experimental/plugins/<name>/` at the
|
|
14
19
|
* configured ref, fetched via the GitHub Contents API (a small handful of
|
|
@@ -29,14 +34,16 @@ import {
|
|
|
29
34
|
existsSync,
|
|
30
35
|
mkdirSync,
|
|
31
36
|
readdirSync,
|
|
37
|
+
readFileSync,
|
|
32
38
|
renameSync,
|
|
33
39
|
rmSync,
|
|
34
40
|
statSync,
|
|
35
41
|
writeFileSync,
|
|
36
42
|
} from "node:fs";
|
|
37
|
-
import { dirname, join } from "node:path";
|
|
43
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
38
44
|
import { promisify } from "node:util";
|
|
39
45
|
|
|
46
|
+
import { ensureBun } from "../../util/bun-runtime.js";
|
|
40
47
|
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
41
48
|
import {
|
|
42
49
|
fetchMarketplaceEntries,
|
|
@@ -82,6 +89,18 @@ export type GitRunner = (
|
|
|
82
89
|
opts: { readonly cwd: string },
|
|
83
90
|
) => Promise<{ readonly stdout: string }>;
|
|
84
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Runs a plugin's postinstall adapter script in `cwd`. Injected so tests can
|
|
94
|
+
* assert the adapter is invoked (and simulate its effects) without spawning a
|
|
95
|
+
* real subprocess; production callers fall back to {@link defaultPostinstallRunner}.
|
|
96
|
+
*/
|
|
97
|
+
export type PostinstallRunner = (opts: {
|
|
98
|
+
/** The staged install directory the adapter transforms in place. */
|
|
99
|
+
readonly cwd: string;
|
|
100
|
+
/** Absolute path to the adapter script to execute. */
|
|
101
|
+
readonly script: string;
|
|
102
|
+
}) => Promise<void>;
|
|
103
|
+
|
|
85
104
|
/** Options that control which plugin to install and how. */
|
|
86
105
|
export interface InstallPluginOptions {
|
|
87
106
|
readonly name: string;
|
|
@@ -100,6 +119,8 @@ export interface InstallPluginDeps {
|
|
|
100
119
|
readonly workspacePluginsDir?: string;
|
|
101
120
|
/** Override the git runner used to clone external plugin sources. Falls back to {@link defaultGitRunner}. */
|
|
102
121
|
readonly runGit?: GitRunner;
|
|
122
|
+
/** Override the runner used to execute a plugin's postinstall adapter. Falls back to {@link defaultPostinstallRunner}. */
|
|
123
|
+
readonly runPostinstall?: PostinstallRunner;
|
|
103
124
|
}
|
|
104
125
|
|
|
105
126
|
/** Successful install result. */
|
|
@@ -123,6 +144,22 @@ export class InvalidPluginNameError extends Error {
|
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
/**
|
|
148
|
+
* A plugin's curated postinstall adapter failed — its `scripts.postinstall`
|
|
149
|
+
* command was malformed/unsupported, its script was missing, or the script
|
|
150
|
+
* exited non-zero. The install is aborted and rolled back rather than
|
|
151
|
+
* materializing a half-transformed, non-functional plugin.
|
|
152
|
+
*/
|
|
153
|
+
export class PluginPostinstallError extends Error {
|
|
154
|
+
constructor(
|
|
155
|
+
readonly pluginName: string,
|
|
156
|
+
detail: string,
|
|
157
|
+
) {
|
|
158
|
+
super(`Postinstall adapter for "${pluginName}" failed: ${detail}`);
|
|
159
|
+
this.name = "PluginPostinstallError";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
126
163
|
/** A plugin with the same name is already installed and `--force` was not passed. */
|
|
127
164
|
export class PluginAlreadyInstalledError extends Error {
|
|
128
165
|
constructor(
|
|
@@ -204,40 +241,18 @@ function firstPartySource(name: string, ref: string): PluginFetchSource {
|
|
|
204
241
|
};
|
|
205
242
|
}
|
|
206
243
|
|
|
207
|
-
/**
|
|
208
|
-
* Probe whether a first-party plugin directory exists at the given source.
|
|
209
|
-
*
|
|
210
|
-
* A transient listing failure resolves to `false` so a marketplace-claimed
|
|
211
|
-
* name still reaches its external source — the rare collision guarantee gives
|
|
212
|
-
* way to keeping the common external-only install path working under flaky
|
|
213
|
-
* network conditions.
|
|
214
|
-
*/
|
|
215
|
-
async function firstPartyPluginExists(
|
|
216
|
-
source: PluginFetchSource,
|
|
217
|
-
fetchFn: FetchLike,
|
|
218
|
-
): Promise<boolean> {
|
|
219
|
-
try {
|
|
220
|
-
const entries = await listDir(
|
|
221
|
-
source.owner,
|
|
222
|
-
source.repo,
|
|
223
|
-
source.rootPath,
|
|
224
|
-
source.ref,
|
|
225
|
-
fetchFn,
|
|
226
|
-
);
|
|
227
|
-
return entries !== null && entries.length > 0;
|
|
228
|
-
} catch {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
244
|
/**
|
|
234
245
|
* Resolve a plugin name to concrete GitHub coordinates.
|
|
235
246
|
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
247
|
+
* A name claimed by the curated marketplace resolves to its pinned external
|
|
248
|
+
* repo; any other name resolves to the first-party `experimental/plugins/<name>`
|
|
249
|
+
* convention. The marketplace is external-only by construction — a same-named
|
|
250
|
+
* `experimental/plugins/<name>` directory is the plugin's optional *adapter
|
|
251
|
+
* stub* (a curated `package.json` + postinstall script overlaid onto the clone
|
|
252
|
+
* to translate it into Vellum's shape; see {@link applyAdapterStub}), not a
|
|
253
|
+
* standalone first-party plugin. So letting the marketplace win the name is
|
|
254
|
+
* what makes the stub apply to the external clone, and the search catalog
|
|
255
|
+
* surfaces the same name as external — install and search stay in agreement.
|
|
241
256
|
*
|
|
242
257
|
* A missing or malformed manifest degrades to first-party resolution — the
|
|
243
258
|
* whitelist is supplementary and must never block installing a first-party
|
|
@@ -259,12 +274,7 @@ async function resolvePluginSource(
|
|
|
259
274
|
// Degrade to first-party resolution below.
|
|
260
275
|
}
|
|
261
276
|
|
|
262
|
-
|
|
263
|
-
if (!resolved) return firstParty;
|
|
264
|
-
|
|
265
|
-
if (await firstPartyPluginExists(firstParty, fetchFn)) {
|
|
266
|
-
return firstParty;
|
|
267
|
-
}
|
|
277
|
+
if (!resolved) return firstPartySource(name, marketplaceRef);
|
|
268
278
|
|
|
269
279
|
return {
|
|
270
280
|
kind: "external",
|
|
@@ -363,6 +373,14 @@ export async function installPlugin(
|
|
|
363
373
|
);
|
|
364
374
|
fileCount = cloned.fileCount;
|
|
365
375
|
commit = cloned.commit;
|
|
376
|
+
// An external clone is often a foreign-ecosystem plugin (e.g. a Claude
|
|
377
|
+
// Code plugin) that the Vellum loader can't run as-is. When we curate an
|
|
378
|
+
// adapter stub for it, overlay the stub and run its transform so the
|
|
379
|
+
// materialized tree is a valid Vellum plugin. Raw clones (no stub) are
|
|
380
|
+
// left untouched.
|
|
381
|
+
if (fileCount > 0) {
|
|
382
|
+
await applyAdapterStub(name, marketplaceRef, stagingDir, deps);
|
|
383
|
+
}
|
|
366
384
|
} else {
|
|
367
385
|
fileCount = await copyDir(
|
|
368
386
|
source.owner,
|
|
@@ -372,6 +390,27 @@ export async function installPlugin(
|
|
|
372
390
|
stagingDir,
|
|
373
391
|
deps.fetch,
|
|
374
392
|
);
|
|
393
|
+
// We only land in the first-party branch for this name when the
|
|
394
|
+
// marketplace lookup returned no claim. A *healthy* marketplace that
|
|
395
|
+
// claims the name routes to the external+adapter branch above; reaching
|
|
396
|
+
// here for a directory that is actually an adapter stub (declares a
|
|
397
|
+
// `scripts.postinstall`) therefore means the marketplace failed to load
|
|
398
|
+
// (rate-limit / 5xx / malformed) and we degraded past it. A stub has no
|
|
399
|
+
// hooks/tools of its own — it only transforms an external clone — so
|
|
400
|
+
// installing it alone would materialize a non-functional plugin. Fail
|
|
401
|
+
// loudly and retryably instead of silently shipping a broken plugin.
|
|
402
|
+
// Genuine first-party plugins (no postinstall) install normally.
|
|
403
|
+
if (
|
|
404
|
+
fileCount > 0 &&
|
|
405
|
+
resolvePostinstallScript(name, stagingDir) !== null
|
|
406
|
+
) {
|
|
407
|
+
throw new PluginPostinstallError(
|
|
408
|
+
name,
|
|
409
|
+
"resolved to a first-party adapter stub, but its marketplace entry " +
|
|
410
|
+
"could not be read to locate the external source it adapts — the " +
|
|
411
|
+
"marketplace lookup likely failed transiently. Retry the install.",
|
|
412
|
+
);
|
|
413
|
+
}
|
|
375
414
|
}
|
|
376
415
|
} catch (err) {
|
|
377
416
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
@@ -446,7 +485,7 @@ async function copyExternalViaGit(
|
|
|
446
485
|
// transient GitHub outage — is retryable, so map it to a 503.
|
|
447
486
|
if (isGitRefNotFound(err)) return { fileCount: 0, commit: null };
|
|
448
487
|
throw new PluginSourceUnavailableError(
|
|
449
|
-
`git clone failed for ${sourceLabel(source)} @ ${source.ref}: ${
|
|
488
|
+
`git clone failed for ${sourceLabel(source)} @ ${source.ref}: ${subprocessErrorText(err)}`,
|
|
450
489
|
503,
|
|
451
490
|
);
|
|
452
491
|
}
|
|
@@ -462,6 +501,18 @@ async function copyExternalViaGit(
|
|
|
462
501
|
commit = null;
|
|
463
502
|
}
|
|
464
503
|
|
|
504
|
+
// Defense in depth: external marketplace refs are full commit SHAs (the
|
|
505
|
+
// manifest schema rejects mutable tags/branches), so the checked-out
|
|
506
|
+
// commit must equal the requested ref. If it ever diverges, refuse the
|
|
507
|
+
// install rather than materialize and `import()` unexpected code.
|
|
508
|
+
if (commit && commit.toLowerCase() !== source.ref.toLowerCase()) {
|
|
509
|
+
throw new PluginSourceUnavailableError(
|
|
510
|
+
`git checkout of ${sourceLabel(source)} resolved to ${commit}, ` +
|
|
511
|
+
`which does not match the pinned commit ${source.ref}`,
|
|
512
|
+
502,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
465
516
|
const srcRoot = source.rootPath
|
|
466
517
|
? join(cloneDir, source.rootPath)
|
|
467
518
|
: cloneDir;
|
|
@@ -476,6 +527,172 @@ async function copyExternalViaGit(
|
|
|
476
527
|
}
|
|
477
528
|
}
|
|
478
529
|
|
|
530
|
+
/** Cap on a postinstall adapter; the curated transforms are fast and file-only. */
|
|
531
|
+
const POSTINSTALL_TIMEOUT_MS = 60_000;
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Overlay our curated adapter stub onto a freshly cloned external plugin and
|
|
535
|
+
* run its postinstall transform, returning whether a transform ran.
|
|
536
|
+
*
|
|
537
|
+
* The stub lives at `experimental/plugins/<name>/` in our own repo and carries
|
|
538
|
+
* a `package.json` (with a `scripts.postinstall` adapter command) plus the
|
|
539
|
+
* adapter script it names. We fetch it via the Contents API — a couple of
|
|
540
|
+
* small files, well within the rate limit — and copy it over the clone, which
|
|
541
|
+
* deliberately overwrites the clone's `package.json` so the postinstall we run
|
|
542
|
+
* is ours, never the upstream repo's lifecycle script. Absent a stub (the
|
|
543
|
+
* common case for a plugin already in Vellum shape), nothing is overlaid and
|
|
544
|
+
* the clone is installed as-is.
|
|
545
|
+
*
|
|
546
|
+
* On any adapter failure the error propagates so {@link installPlugin} rolls
|
|
547
|
+
* back staging — better to fail loudly than ship a half-transformed plugin.
|
|
548
|
+
*/
|
|
549
|
+
async function applyAdapterStub(
|
|
550
|
+
name: string,
|
|
551
|
+
ref: string,
|
|
552
|
+
stagingDir: string,
|
|
553
|
+
deps: InstallPluginDeps,
|
|
554
|
+
): Promise<boolean> {
|
|
555
|
+
const stubFileCount = await copyDir(
|
|
556
|
+
PLUGIN_SOURCE_OWNER,
|
|
557
|
+
PLUGIN_SOURCE_REPO,
|
|
558
|
+
`${PLUGIN_SOURCE_PATH_PREFIX}/${name}`,
|
|
559
|
+
ref,
|
|
560
|
+
stagingDir,
|
|
561
|
+
deps.fetch,
|
|
562
|
+
);
|
|
563
|
+
if (stubFileCount === 0) return false;
|
|
564
|
+
|
|
565
|
+
const script = resolvePostinstallScript(name, stagingDir);
|
|
566
|
+
if (script === null) return false;
|
|
567
|
+
|
|
568
|
+
const run = deps.runPostinstall ?? defaultPostinstallRunner;
|
|
569
|
+
try {
|
|
570
|
+
await run({ cwd: stagingDir, script });
|
|
571
|
+
} catch (err) {
|
|
572
|
+
throw new PluginPostinstallError(name, subprocessErrorText(err));
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Resolve the absolute path of the adapter script named by the (overlaid stub)
|
|
579
|
+
* `package.json`'s `scripts.postinstall`, or `null` when there is no stub
|
|
580
|
+
* package.json / postinstall script.
|
|
581
|
+
*
|
|
582
|
+
* Curated adapters declare a single `bun <script>` invocation; bun is resolved
|
|
583
|
+
* via {@link ensureBun} at execution time (see {@link defaultPostinstallRunner})
|
|
584
|
+
* so the `bun` token marks the convention without hard-coding the binary path.
|
|
585
|
+
* Anything else — extra args, a shell pipeline, a non-script file — is rejected
|
|
586
|
+
* rather than executed, and the script path is constrained to a file inside the
|
|
587
|
+
* staging dir so a stub can never escape it.
|
|
588
|
+
*/
|
|
589
|
+
function resolvePostinstallScript(
|
|
590
|
+
name: string,
|
|
591
|
+
stagingDir: string,
|
|
592
|
+
): string | null {
|
|
593
|
+
const pkgPath = join(stagingDir, "package.json");
|
|
594
|
+
if (!existsSync(pkgPath)) return null;
|
|
595
|
+
|
|
596
|
+
let parsed: unknown;
|
|
597
|
+
try {
|
|
598
|
+
parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
599
|
+
} catch {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const scripts =
|
|
604
|
+
typeof parsed === "object" && parsed !== null && "scripts" in parsed
|
|
605
|
+
? (parsed as { scripts?: unknown }).scripts
|
|
606
|
+
: undefined;
|
|
607
|
+
const command =
|
|
608
|
+
typeof scripts === "object" && scripts !== null && "postinstall" in scripts
|
|
609
|
+
? (scripts as { postinstall?: unknown }).postinstall
|
|
610
|
+
: undefined;
|
|
611
|
+
if (typeof command !== "string" || command.trim() === "") return null;
|
|
612
|
+
|
|
613
|
+
const match = /^bun\s+(\S+)$/.exec(command.trim());
|
|
614
|
+
if (!match) {
|
|
615
|
+
throw new PluginPostinstallError(
|
|
616
|
+
name,
|
|
617
|
+
`unsupported postinstall command ${JSON.stringify(command)} — ` +
|
|
618
|
+
"curated adapters must be a single `bun <script>` invocation",
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
let rel = match[1]!;
|
|
623
|
+
if (rel.startsWith("./")) rel = rel.slice(2);
|
|
624
|
+
if (!/\.(?:ts|mts|cts|mjs|cjs|js)$/.test(rel)) {
|
|
625
|
+
throw new PluginPostinstallError(
|
|
626
|
+
name,
|
|
627
|
+
`postinstall script ${JSON.stringify(rel)} must be a ` +
|
|
628
|
+
".ts/.mts/.cts/.mjs/.cjs/.js file",
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
for (const segment of rel.split("/")) {
|
|
632
|
+
assertSafeFilename("postinstall script segment", segment);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const abs = resolve(stagingDir, rel);
|
|
636
|
+
if (
|
|
637
|
+
abs !== resolve(stagingDir) &&
|
|
638
|
+
!abs.startsWith(`${resolve(stagingDir)}${sep}`)
|
|
639
|
+
) {
|
|
640
|
+
throw new PluginPostinstallError(
|
|
641
|
+
name,
|
|
642
|
+
`postinstall script ${JSON.stringify(rel)} escapes the plugin directory`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
if (!existsSync(abs)) {
|
|
646
|
+
throw new PluginPostinstallError(
|
|
647
|
+
name,
|
|
648
|
+
`postinstall script ${JSON.stringify(rel)} was not found in the plugin`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return abs;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Production postinstall runner: executes the adapter with a real `bun` binary
|
|
656
|
+
* resolved via {@link ensureBun}, under a stripped environment and a timeout.
|
|
657
|
+
*
|
|
658
|
+
* `process.execPath` is unusable here: inside a `bun build --compile` binary it
|
|
659
|
+
* is the compiled assistant app, not the bun CLI (see `util/bun-runtime.ts`),
|
|
660
|
+
* so passing the adapter script to it would launch the daemon rather than
|
|
661
|
+
* interpret the script. `ensureBun()` locates (or downloads) a standalone bun
|
|
662
|
+
* the same way every other subsystem that spawns bun does. The minimal env
|
|
663
|
+
* (bun's dir + standard bins, `HOME` only) keeps the adapter from inheriting
|
|
664
|
+
* surprising config while still finding the runtime.
|
|
665
|
+
*/
|
|
666
|
+
export const defaultPostinstallRunner: PostinstallRunner = async ({
|
|
667
|
+
cwd,
|
|
668
|
+
script,
|
|
669
|
+
}) => {
|
|
670
|
+
const bun = await ensureBun();
|
|
671
|
+
await execFileAsync(bun, [script], {
|
|
672
|
+
cwd,
|
|
673
|
+
encoding: "utf8",
|
|
674
|
+
timeout: POSTINSTALL_TIMEOUT_MS,
|
|
675
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
676
|
+
env: pluginPostinstallEnv(bun),
|
|
677
|
+
});
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
function pluginPostinstallEnv(bun: string): NodeJS.ProcessEnv {
|
|
681
|
+
const env: NodeJS.ProcessEnv = {
|
|
682
|
+
PATH: [
|
|
683
|
+
dirname(bun),
|
|
684
|
+
"/opt/homebrew/bin",
|
|
685
|
+
"/usr/local/bin",
|
|
686
|
+
"/usr/bin",
|
|
687
|
+
"/bin",
|
|
688
|
+
]
|
|
689
|
+
.filter(Boolean)
|
|
690
|
+
.join(":"),
|
|
691
|
+
};
|
|
692
|
+
if (process.env.HOME) env.HOME = process.env.HOME;
|
|
693
|
+
return env;
|
|
694
|
+
}
|
|
695
|
+
|
|
479
696
|
/**
|
|
480
697
|
* Recursively copy regular files from `srcRoot` into `destDir`, skipping the
|
|
481
698
|
* top-level `.git` directory and any symlinks. Returns the file count.
|
|
@@ -509,7 +726,7 @@ function copyTreeSkippingGit(srcRoot: string, destDir: string): number {
|
|
|
509
726
|
|
|
510
727
|
/** True when a git fetch failed because the repo or ref is unreachable. */
|
|
511
728
|
function isGitRefNotFound(err: unknown): boolean {
|
|
512
|
-
const text =
|
|
729
|
+
const text = subprocessErrorText(err).toLowerCase();
|
|
513
730
|
return [
|
|
514
731
|
"could not find remote ref",
|
|
515
732
|
"couldn't find remote ref",
|
|
@@ -523,7 +740,7 @@ function isGitRefNotFound(err: unknown): boolean {
|
|
|
523
740
|
}
|
|
524
741
|
|
|
525
742
|
/** Extract a stderr/message blob from a spawn error for classification/logging. */
|
|
526
|
-
function
|
|
743
|
+
function subprocessErrorText(err: unknown): string {
|
|
527
744
|
if (err instanceof Error) {
|
|
528
745
|
const withStreams = err as Error & { stderr?: unknown };
|
|
529
746
|
const stderr =
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* `package.json` fields the manifest doesn't carry.
|
|
15
15
|
*
|
|
16
16
|
* Name-collision precedence matches {@link ./search-plugins} and
|
|
17
|
-
* {@link ./install-from-github}: a
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* {@link ./install-from-github}: a marketplace entry owns its name, so the
|
|
18
|
+
* detail page advertises the external source the catalog and installer use. A
|
|
19
|
+
* same-named `experimental/plugins/<name>/` directory is that plugin's adapter
|
|
20
|
+
* stub, not a standalone first-party plugin, so it does not override the claim.
|
|
20
21
|
*
|
|
21
22
|
* Designed for direct programmatic use with an injected `fetch`, mirroring the
|
|
22
23
|
* sibling plugin libraries.
|
|
@@ -130,22 +131,28 @@ export async function getPluginDetails(
|
|
|
130
131
|
const local = readLocalPlugin(pluginsDir, name);
|
|
131
132
|
|
|
132
133
|
const marketplaceEntry = await findMarketplaceEntry(name, ref, fetchFn);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
|
|
135
|
+
// A marketplace entry owns its name (the same-named in-repo directory, if
|
|
136
|
+
// any, is its adapter stub) — so only probe the first-party directory when
|
|
137
|
+
// the name is unclaimed, which also spares a GitHub request in the common
|
|
138
|
+
// external case.
|
|
139
|
+
const firstPartyEntries =
|
|
140
|
+
marketplaceEntry === null
|
|
141
|
+
? await listDirSafe(
|
|
142
|
+
PLUGIN_SOURCE_OWNER,
|
|
143
|
+
PLUGIN_SOURCE_REPO,
|
|
144
|
+
`${PLUGIN_SOURCE_PATH_PREFIX}/${name}`,
|
|
145
|
+
ref,
|
|
146
|
+
fetchFn,
|
|
147
|
+
)
|
|
148
|
+
: null;
|
|
140
149
|
const firstPartyExists = firstPartyEntries !== null;
|
|
141
150
|
|
|
142
151
|
if (!local.installed && !firstPartyExists && !marketplaceEntry) {
|
|
143
152
|
throw new PluginDetailsNotFoundError(name, ref);
|
|
144
153
|
}
|
|
145
154
|
|
|
146
|
-
|
|
147
|
-
// honouring a marketplace claim — the same precedence the catalog applies.
|
|
148
|
-
const useExternal = !firstPartyExists && marketplaceEntry !== null;
|
|
155
|
+
const useExternal = marketplaceEntry !== null;
|
|
149
156
|
|
|
150
157
|
const source: PluginMatchSource = useExternal
|
|
151
158
|
? {
|
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
* Like the first-party plugin listing, the manifest is fetched from the repo
|
|
13
13
|
* at a git ref (via the GitHub Contents API) rather than bundled into the
|
|
14
14
|
* assistant build — so the whitelist can grow without shipping a new release.
|
|
15
|
-
* Every external source pins an explicit `ref`
|
|
16
|
-
*
|
|
15
|
+
* Every external source pins an explicit `ref` that MUST be a full commit SHA:
|
|
16
|
+
* the fetched code is locked to an immutable revision. Tags and branches are
|
|
17
|
+
* rejected because they are mutable — an upstream owner could retag/repoint
|
|
18
|
+
* them to attacker code that the daemon would then `import()` (the install
|
|
19
|
+
* tree is dynamically loaded). A SHA cannot be repointed, so the reviewed
|
|
20
|
+
* manifest fully determines what gets executed.
|
|
17
21
|
*
|
|
18
22
|
* Designed for direct programmatic use with an injected `fetch`, mirroring
|
|
19
23
|
* {@link ./search-plugins} and {@link ./install-from-github}.
|
|
@@ -39,6 +43,13 @@ const MARKETPLACE_FILE_PATH = "experimental/plugins/marketplace.json";
|
|
|
39
43
|
const REPO_SLUG_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
40
44
|
/** Install name: a single kebab-case path segment (same rule as the CLI). */
|
|
41
45
|
const PLUGIN_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
46
|
+
/**
|
|
47
|
+
* Full Git commit SHA — 40 hex chars (SHA-1) or 64 (SHA-256). External
|
|
48
|
+
* marketplace refs must be a complete object name so the install is pinned to
|
|
49
|
+
* an immutable revision; abbreviated SHAs, tags, and branches are all mutable
|
|
50
|
+
* or ambiguous and are rejected.
|
|
51
|
+
*/
|
|
52
|
+
const COMMIT_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
42
53
|
|
|
43
54
|
const githubSourceSchema = z.object({
|
|
44
55
|
/** Discriminator. Only GitHub sources are resolved today. */
|
|
@@ -57,10 +68,17 @@ const githubSourceSchema = z.object({
|
|
|
57
68
|
)
|
|
58
69
|
.optional(),
|
|
59
70
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
71
|
+
* Immutable revision to fetch the plugin from. Must be a full commit SHA:
|
|
72
|
+
* tags and branches are mutable, so allowing them would let an upstream
|
|
73
|
+
* owner retarget a curated entry at code the daemon later `import()`s (RCE).
|
|
74
|
+
* A full SHA pins the install to exactly the reviewed bytes.
|
|
62
75
|
*/
|
|
63
|
-
ref: z
|
|
76
|
+
ref: z
|
|
77
|
+
.string()
|
|
78
|
+
.regex(
|
|
79
|
+
COMMIT_SHA_RE,
|
|
80
|
+
"expected a full commit SHA (40 or 64 hex chars); tags and branches are mutable and not allowed",
|
|
81
|
+
),
|
|
64
82
|
});
|
|
65
83
|
|
|
66
84
|
const marketplaceEntrySchema = z.object({
|
|
@@ -200,8 +200,22 @@ export async function loadPluginCatalog(
|
|
|
200
200
|
|
|
201
201
|
const matches: PluginSearchMatch[] = [];
|
|
202
202
|
const seen = new Set<string>();
|
|
203
|
+
|
|
204
|
+
// Whitelisted external entries own their name. A same-named
|
|
205
|
+
// `experimental/plugins/<name>/` directory is that plugin's curated adapter
|
|
206
|
+
// stub (overlaid onto the clone at install time), not a standalone
|
|
207
|
+
// first-party plugin — so it must surface once, as the external entry.
|
|
208
|
+
for (const entry of marketplace) {
|
|
209
|
+
if (seen.has(entry.name)) continue;
|
|
210
|
+
matches.push(marketplaceMatch(entry));
|
|
211
|
+
seen.add(entry.name);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// First-party plugins are the in-repo directories whose name the marketplace
|
|
215
|
+
// does not claim.
|
|
203
216
|
for (const entry of entries) {
|
|
204
217
|
if (entry.type !== "dir") continue;
|
|
218
|
+
if (seen.has(entry.name)) continue;
|
|
205
219
|
matches.push({
|
|
206
220
|
name: entry.name,
|
|
207
221
|
path: entry.path,
|
|
@@ -210,14 +224,6 @@ export async function loadPluginCatalog(
|
|
|
210
224
|
seen.add(entry.name);
|
|
211
225
|
}
|
|
212
226
|
|
|
213
|
-
for (const entry of marketplace) {
|
|
214
|
-
// First-party plugins win a name collision — the curated manifest is
|
|
215
|
-
// additive, never an override of what ships in-repo.
|
|
216
|
-
if (seen.has(entry.name)) continue;
|
|
217
|
-
matches.push(marketplaceMatch(entry));
|
|
218
|
-
seen.add(entry.name);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
227
|
matches.sort((a, b) => a.name.localeCompare(b.name));
|
|
222
228
|
|
|
223
229
|
return { ref, matches };
|
|
@@ -9,9 +9,9 @@ const FROZEN_EMPTY_ARGS = Object.freeze([] as string[]) as unknown as string[];
|
|
|
9
9
|
/**
|
|
10
10
|
* Default ACP agent profiles that ship with the assistant.
|
|
11
11
|
*
|
|
12
|
-
* When
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* When the user has not provided a config entry for an agent id, the resolver
|
|
13
|
+
* falls back to this map so common agents like `claude` and `codex` Just Work
|
|
14
|
+
* without requiring per-user config.
|
|
15
15
|
*
|
|
16
16
|
* Keyed by agent id. Deeply frozen — the outer object, each profile, and the
|
|
17
17
|
* `args` arrays — so mutation throws in strict mode rather than silently
|
package/src/config/acp-schema.ts
CHANGED
|
@@ -20,12 +20,6 @@ const AcpAgentConfigSchema = z
|
|
|
20
20
|
|
|
21
21
|
export const AcpConfigSchema = z
|
|
22
22
|
.object({
|
|
23
|
-
enabled: z
|
|
24
|
-
.boolean()
|
|
25
|
-
.default(false)
|
|
26
|
-
.describe(
|
|
27
|
-
"Whether the Agent Communication Protocol (ACP) system is enabled",
|
|
28
|
-
),
|
|
29
23
|
maxConcurrentSessions: z
|
|
30
24
|
.number()
|
|
31
25
|
.int()
|
|
@@ -40,7 +34,7 @@ export const AcpConfigSchema = z
|
|
|
40
34
|
.describe("Map of agent names to their configurations"),
|
|
41
35
|
})
|
|
42
36
|
.describe(
|
|
43
|
-
"Agent Communication Protocol (ACP) —
|
|
37
|
+
"Agent Communication Protocol (ACP) — inter-agent communication and delegation",
|
|
44
38
|
);
|
|
45
39
|
|
|
46
40
|
export type AcpConfig = z.infer<typeof AcpConfigSchema>;
|