Skip to content

Add ACP registry provider support#2439

Draft
juliusmarminge wants to merge 6 commits intomainfrom
t3code/acp-registry
Draft

Add ACP registry provider support#2439
juliusmarminge wants to merge 6 commits intomainfrom
t3code/acp-registry

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented May 1, 2026

Summary

  • Adds a new ACP Registry provider driver backed by the ACP registry flow, including session startup and model selection handling.
  • Extends provider instance metadata to carry iconUrl through server snapshot construction and driver creation.
  • Updates the Cursor ACP adapter to support provider-specific event labeling, custom spawn behavior, model normalization, and optional Cursor model option handling.
  • Expands the web UI and shared contracts so ACP registry instances can be created, displayed, and selected consistently across settings and model pickers.

Testing

  • Not run

Note

High Risk
High risk because it introduces a new provider driver that spawns external ACP agent processes and adds server RPCs that download/extract/install binaries from remote registry URLs, expanding the attack surface and potential platform-specific failure modes.

Overview
Adds first-class ACP Registry support end-to-end: a new AcpRegistryDriver (multi-instance) built on the existing ACP/Cursor session adapter, plus contracts/settings updates to persist ACP registry configuration and defaults.

Extends provider instance/snapshot metadata to carry iconUrl through the server registry, unavailable snapshots, and UI rendering, and updates several drivers to accept/pass through iconUrl.

Generalizes makeCursorAdapter so it can be reused by non-Cursor ACP providers (custom provider kind, spawn command/args/env, model normalization, optional Cursor-specific model-option application, and customizable “ready” reason).

Adds server websocket RPCs server.listAcpRegistry and server.installAcpRegistryBinary that fetch the ACP registry index and optionally download/extract an agent binary into the server state dir, and updates the web UI wizard/model pickers to browse registry agents, install binaries, and create ACP registry-backed provider instances (including env var editing and hiding the default ACP registry catalog entry from model selection).

Reviewed by Cursor Bugbot for commit 46fc8ff. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add ACP Registry provider support with binary install and model picker integration

  • Introduces a new acpRegistry provider driver that spawns a configured command as an ACP agent, registers it as a built-in driver, and exposes it in settings with configurable command and args fields.
  • Adds two WS RPC endpoints (server.listAcpRegistry and server.installAcpRegistryBinary) so the client can browse a remote registry index, resolve per-platform launch specs, and install binary agents.
  • Extends the Add Provider Instance dialog with a Registry step: users can search, select, or install ACP agents; selecting an agent populates command, args, env vars, and icon; env vars can be marked sensitive and are validated before save.
  • Adds isModelPickerProviderInstanceEntry to exclude the default ACP Registry catalog instance from model selection while keeping explicitly imported agents visible.
  • Propagates optional iconUrl through provider snapshots, ProviderInstanceConfig, driver create inputs, and UI components (ProviderInstanceIcon, ModelListRow, ModelPickerContent, etc.) with image-load fallback to glyph or initials.
  • Risk: args fields in existing provider instance configs are now stored and parsed as string[]; any code reading args as a plain string will need updating.
📊 Macroscope summarized 121f70b. 5 files reviewed, 1 issue evaluated, 0 issues filtered, 1 comment posted

🗂️ Filtered Issues

- Introduce ACP Registry driver and client scaffolding
- Thread provider icon URLs through server and UI models
- Expand Cursor ACP adapter hooks for alternate providers
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 27e25597-36f3-4f5a-8507-d420d338d20d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/acp-registry

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels May 1, 2026
@juliusmarminge juliusmarminge marked this pull request as draft May 1, 2026 08:39
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: ACP Registry agents use incorrect Cursor-specific auth
    • Added authMethodId and clientCapabilities to CursorAdapterLiveOptions, forwarded them through makeCursorAdapter to makeCursorAcpRuntime, and set AcpRegistryDriver to use authMethodId: "none" with empty clientCapabilities instead of the Cursor-specific defaults.

Create PR

Or push these changes by commenting:

@cursor push 78fb05040a
Preview (78fb05040a)
diff --git a/apps/server/src/provider/Drivers/AcpRegistryDriver.ts b/apps/server/src/provider/Drivers/AcpRegistryDriver.ts
--- a/apps/server/src/provider/Drivers/AcpRegistryDriver.ts
+++ b/apps/server/src/provider/Drivers/AcpRegistryDriver.ts
@@ -137,6 +137,8 @@
           readyReason: "ACP session ready",
           applyCursorModelOptions: false,
           normalizeModel: (model) => model?.trim() || "default",
+          authMethodId: "none",
+          clientCapabilities: {},
           ...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
           spawn: ({ cwd, environment: spawnEnv }) => ({
             command: effectiveConfig.command.trim(),

diff --git a/apps/server/src/provider/Layers/CursorAdapter.ts b/apps/server/src/provider/Layers/CursorAdapter.ts
--- a/apps/server/src/provider/Layers/CursorAdapter.ts
+++ b/apps/server/src/provider/Layers/CursorAdapter.ts
@@ -101,6 +101,8 @@
   };
   readonly normalizeModel?: (model: string | null | undefined) => string;
   readonly applyCursorModelOptions?: boolean;
+  readonly authMethodId?: string;
+  readonly clientCapabilities?: EffectAcpSchema.InitializeRequest["clientCapabilities"];
   /**
    * Selections are honored when `modelSelection.instanceId` matches this value.
    * Defaults to the legacy built-in instance id (`cursor`).
@@ -546,6 +548,10 @@
             cwd,
             ...(resumeSessionId ? { resumeSessionId } : {}),
             clientInfo: { name: "t3-code", version: "0.0.0" },
+            ...(options?.authMethodId ? { authMethodId: options.authMethodId } : {}),
+            ...(options?.clientCapabilities
+              ? { clientCapabilities: options.clientCapabilities }
+              : {}),
             ...acpNativeLoggers,
           }).pipe(
             Effect.provideService(Scope.Scope, sessionScope),

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 46fc8ff. Configure here.

readonly env?: NodeJS.ProcessEnv;
};
readonly normalizeModel?: (model: string | null | undefined) => string;
readonly applyCursorModelOptions?: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACP Registry agents use incorrect Cursor-specific auth

Medium Severity

The CursorAdapterLiveOptions interface was extended with provider, spawn, normalizeModel, and applyCursorModelOptions but not with authMethodId or clientCapabilities. The AcpRegistryDriver calls makeCursorAdapter which internally calls makeCursorAcpRuntime, where these default to "cursor_login" and CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES. Generic ACP registry agents don't support Cursor login, so session initialization will use incorrect auth and capability negotiation. The CursorAcpRuntimeInput was extended with optional authMethodId and clientCapabilities in this same PR, but the adapter layer doesn't forward them.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 46fc8ff. Configure here.

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented May 1, 2026

Approvability

Verdict: Needs human review

This PR introduces a significant new feature (ACP Registry provider support) with binary download/installation capabilities and extensive new UI workflows. There is also an unresolved medium-severity bug regarding incorrect auth configuration for ACP agents. The scope and nature of these changes warrant human review.

You can customize Macroscope's approvability policy. Learn more.

- Route ACP registry sessions through a generic ACP runtime
- Make ACP auth optional and skip Cursor-only extensions for generic providers
Comment thread apps/server/src/provider/Drivers/AcpRegistryDriver.ts Outdated
- Route ACP registry through the shared generic adapter
- Simplify cursor adapter session setup and model selection
- Preserve cursor-specific extension handling in the cursor layer
interface PendingApproval {
readonly decision: Deferred.Deferred<ProviderApprovalDecision>;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low Layers/GenericAcpAdapter.ts:80

respondToUserInput reads from ctx.pendingUserInputs (line 666), but nothing in the adapter ever writes entries to this map. The map is created at line 241 but remains empty, so every call to respondToUserInput returns "Unknown pending user-input request". Unlike pendingApprovals which is populated in the handleRequestPermission callback, the corresponding population logic for user inputs is missing. If this adapter is intended to support user input requests, the handler that populates pendingUserInputs needs to be added.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/GenericAcpAdapter.ts around line 80:

`respondToUserInput` reads from `ctx.pendingUserInputs` (line 666), but nothing in the adapter ever writes entries to this map. The map is created at line 241 but remains empty, so every call to `respondToUserInput` returns "Unknown pending user-input request". Unlike `pendingApprovals` which is populated in the `handleRequestPermission` callback, the corresponding population logic for user inputs is missing. If this adapter is intended to support user input requests, the handler that populates `pendingUserInputs` needs to be added.

Evidence trail:
apps/server/src/provider/Layers/GenericAcpAdapter.ts line 241 (map creation), line 397 (stored in ctx), line 666 (read via .get()). git_grep for `pendingUserInputs.set(` returns no results. git_grep for `pendingApprovals.set(` returns line 304, confirming the parallel pattern exists for approvals but is missing for user inputs. All references to pendingUserInputs: lines 92, 120, 123, 191, 241, 397, 631, 666 — none are writes.

- Move ACP binary install and launch resolution into a shared module
- Switch stream exposure to a getter so runtime events stay fresh
- Keep WebSocket RPC handlers thin and reuse registry install helpers
}
}).pipe(Effect.scoped);

const extractArchive = (archivePath: string, destinationDir: string) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low acp/AcpRegistryBinaryInstaller.ts:220

On Windows, extractArchive calls PowerShell with -LiteralPath and -DestinationPath as separate arguments, but -Command joins all arguments with spaces into a single command string. If archivePath or destinationDir contain spaces, the paths are split incorrectly and Expand-Archive fails. For example, a path like C:\Users\John Smith\Temp\file.zip becomes two tokens (C:\Users\John and Smith\Temp\file.zip). Consider passing the entire command as a single quoted string to -Command, or using proper escaping.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/acp/AcpRegistryBinaryInstaller.ts around line 220:

On Windows, `extractArchive` calls PowerShell with `-LiteralPath` and `-DestinationPath` as separate arguments, but `-Command` joins all arguments with spaces into a single command string. If `archivePath` or `destinationDir` contain spaces, the paths are split incorrectly and `Expand-Archive` fails. For example, a path like `C:\Users\John Smith\Temp\file.zip` becomes two tokens (`C:\Users\John` and `Smith\Temp\file.zip`). Consider passing the entire command as a single quoted string to `-Command`, or using proper escaping.

Evidence trail:
- `apps/server/src/provider/acp/AcpRegistryBinaryInstaller.ts` lines 225-233 (REVIEWED_COMMIT): PowerShell invocation passes archivePath and destinationDir as separate args after `-Command`
- `https://github.com/PowerShell/PowerShell` `src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs` line 1552: `_commandLineCommand = string.Join(' ', args, i, args.Length - i);` — confirms args after `-Command` are joined with spaces from the already-parsed (quotes-stripped) argv
- Same file lines 1041-1048: `_commandHasArgs` is only true for `-commandwithargs`/`-cwa`, not for `-command`/`-c`
- Same file line 1629: `private bool _commandHasArgs;` — defaults to false

Comment on lines +186 to +204
const stopSessionInternal = (ctx: GenericAcpSessionContext) =>
Effect.gen(function* () {
if (ctx.stopped) return;
ctx.stopped = true;
yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals);
yield* settlePendingUserInputsAsEmptyAnswers(ctx.pendingUserInputs);
if (ctx.notificationFiber) {
yield* Fiber.interrupt(ctx.notificationFiber);
}
yield* Effect.ignore(Scope.close(ctx.scope, Exit.void));
sessions.delete(ctx.threadId);
yield* offerRuntimeEvent({
type: "session.exited",
...(yield* makeEventStamp()),
provider: providerKind,
threadId: ctx.threadId,
payload: { exitKind: "graceful" },
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/GenericAcpAdapter.ts:186

threadLocksRef leaks memory: getThreadSemaphore creates and stores a Semaphore for each unique threadId, but stopSessionInternal only removes the session from sessions without removing the corresponding semaphore entry. Over time the map grows unboundedly with every stopped session. Consider deleting the semaphore entry when stopping a session, or using a cleanup mechanism like Effect.addFinalizer in withThreadLock.

     const stopSessionInternal = (ctx: GenericAcpSessionContext) =>
       Effect.gen(function* () {
         if (ctx.stopped) return;
         ctx.stopped = true;
+        yield* SynchronizedRef.update(threadLocksRef, (map) => {
+          const next = new Map(map);
+          next.delete(ctx.threadId);
+          return next;
+        });
         yield* settlePendingApprovalsAsCancelled(ctx.pendingApprovals);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/GenericAcpAdapter.ts around lines 186-204:

`threadLocksRef` leaks memory: `getThreadSemaphore` creates and stores a `Semaphore` for each unique `threadId`, but `stopSessionInternal` only removes the session from `sessions` without removing the corresponding semaphore entry. Over time the map grows unboundedly with every stopped session. Consider deleting the semaphore entry when stopping a session, or using a cleanup mechanism like `Effect.addFinalizer` in `withThreadLock`.

Evidence trail:
apps/server/src/provider/Layers/GenericAcpAdapter.ts lines 148-149 (sessions map and threadLocksRef creation), lines 158-168 (getThreadSemaphore adding entries), lines 186-203 (stopSessionInternal deleting from sessions but not threadLocksRef). git_grep for 'threadLocksRef' confirms no removal of entries anywhere in the codebase.

- Convert ACP registry binary path helpers to Effect-based access
- Reuse shared path resolution for install previews and installs
- Keep archive command validation and manifest lookup behavior intact
Comment on lines +177 to +181
const json = yield* Effect.try({
try: () => JSON.parse(raw.value) as unknown,
catch: () => null,
});
if (json === null) return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium acp/AcpRegistryBinaryInstaller.ts:177

When JSON.parse throws on invalid JSON, Effect.try maps the exception to null as an error value, not a success value. The yield* then short-circuits the generator with failure rather than assigning null to json. This contradicts the intended behavior of treating invalid manifest files as "no manifest" — instead, parse errors propagate and can fail the entire listAcpRegistryAgents call. Consider using Effect.option (like on line 182-184) or Effect.either to catch and handle the parse error locally.

-    const json = yield* Effect.try({
-      try: () => JSON.parse(raw.value) as unknown,
-      catch: () => null,
-    });
-    if (json === null) return null;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/acp/AcpRegistryBinaryInstaller.ts around lines 177-181:

When `JSON.parse` throws on invalid JSON, `Effect.try` maps the exception to `null` as an *error* value, not a success value. The `yield*` then short-circuits the generator with failure rather than assigning `null` to `json`. This contradicts the intended behavior of treating invalid manifest files as "no manifest" — instead, parse errors propagate and can fail the entire `listAcpRegistryAgents` call. Consider using `Effect.option` (like on line 182-184) or `Effect.either` to catch and handle the parse error locally.

Evidence trail:
File: apps/server/src/provider/acp/AcpRegistryBinaryInstaller.ts lines 165-200 (viewed at REVIEWED_COMMIT). Effect.try API behavior confirmed via Effect-TS community patterns on GitHub (e.g., https://github.com/jpb06/effect-errors, https://github.com/Effect-TS/effect/issues/3563) showing catch callback returns the error channel value, not the success channel value.

- Normalize search tokens and score field matches
- Sort dialog results by best query match instead of substring order
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant