Skip to content

Commit 085f518

Browse files
LivioGamaclaude
andcommitted
Add ACP Registry installer and settings page
Bundles a snapshot of the Agent Client Protocol registry and exposes a Zed-style installer at Settings → ACP Registry. Users can browse, search, install, and remove 31 ACP-conforming coding agents (everything upstream except the four overlapping with first-party drivers: claude-acp, cursor, opencode, codex-acp). - Bundled snapshot in packages/contracts/src/registry/ refreshed by a new bun run sync:acp-registry script. - Installer in apps/server/src/acpRegistry/ supports binary (download + extract + chmod), npx (bunx), and uvx distribution channels with per-platform target resolution. - Install state persists to ServerSettings.acpRegistryInstalls; cache lives under <baseDir>/acp-agents/<id>/<version>/. - Three new RPC methods (acpRegistry.list/install/uninstall) wired through WsRpcGroup, LocalApi, and WsRpcClient. - See docs/providers/acp-registry.md for the user-facing guide. The generic ACP adapter that would let installed agents appear in the chat provider picker is intentionally a follow-up — the installer and UI scaffolding land first so that adapter can layer on top without further protocol/UI changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 90eea04 commit 085f518

86 files changed

Lines changed: 3559 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ Docs:
4545

4646
- Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server
4747

48+
## ACP Registry
49+
50+
In addition to the four bespoke providers (Codex, Claude, Cursor, OpenCode),
51+
T3 Code bundles a snapshot of the
52+
[Agent Client Protocol registry](https://agentclientprotocol.com/get-started/registry)
53+
and exposes a Zed-style installer at **Settings → ACP Registry**. See
54+
[docs/providers/acp-registry.md](./docs/providers/acp-registry.md) for the
55+
install pipeline, distribution channels, and how to refresh the bundled
56+
snapshot (`bun run sync:acp-registry`).
57+
4858
## Reference Repos
4959

5060
- Open-source Codex repo: https://github.com/openai/codex
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* AcpRegistryService — Effect service exposing install/uninstall/list
3+
* operations for the bundled ACP registry. RPC handlers (see `ws.ts`) call
4+
* into this; the underlying logic lives in the framework-agnostic
5+
* `installer.ts` module.
6+
*
7+
* The service is responsible for:
8+
* - Listing each bundled entry with rolled-up install status for the UI.
9+
* - Driving install/uninstall, including settings persistence via the
10+
* `ServerSettingsService`.
11+
*
12+
* @module acpRegistry/AcpRegistryService
13+
*/
14+
import {
15+
ACP_REGISTRY,
16+
acpRegistryEntryById,
17+
type AcpRegistryEntry,
18+
type AcpRegistryEntryWithStatus,
19+
type AcpRegistryInstallState,
20+
type AcpRegistryInstallStatus,
21+
AcpRegistryError,
22+
} from "@t3tools/contracts";
23+
import * as Context from "effect/Context";
24+
import * as Effect from "effect/Effect";
25+
import * as Layer from "effect/Layer";
26+
27+
import { ServerConfig } from "../config.ts";
28+
import { ServerSettingsService } from "../serverSettings.ts";
29+
30+
import { availableChannels, installAgent, uninstallAgent } from "./installer.ts";
31+
import { resolveCurrentPlatform } from "./platform.ts";
32+
33+
export interface AcpRegistryServiceShape {
34+
readonly list: () => Effect.Effect<ReadonlyArray<AcpRegistryEntryWithStatus>, AcpRegistryError>;
35+
readonly install: (agentId: string) => Effect.Effect<AcpRegistryInstallState, AcpRegistryError>;
36+
readonly uninstall: (agentId: string) => Effect.Effect<void, AcpRegistryError>;
37+
}
38+
39+
function rollupStatus(
40+
entry: AcpRegistryEntry,
41+
installed: AcpRegistryInstallState | undefined,
42+
channels: ReadonlyArray<ReturnType<typeof availableChannels>[number]>,
43+
): AcpRegistryInstallStatus {
44+
if (channels.length === 0) return "unsupported";
45+
if (!installed) return "not_installed";
46+
if (installed.version !== entry.version) return "update_available";
47+
return "installed";
48+
}
49+
50+
export class AcpRegistryService extends Context.Service<
51+
AcpRegistryService,
52+
AcpRegistryServiceShape
53+
>()("t3/acpRegistry/AcpRegistryService") {}
54+
55+
export const layer: Layer.Layer<AcpRegistryService, never, ServerConfig | ServerSettingsService> =
56+
Layer.effect(
57+
AcpRegistryService,
58+
Effect.gen(function* () {
59+
const config = yield* ServerConfig;
60+
const settingsService = yield* ServerSettingsService;
61+
const platform = resolveCurrentPlatform();
62+
const cacheRoot = config.acpRegistryCacheDir;
63+
64+
const readInstalls = (): Effect.Effect<
65+
Readonly<Record<string, AcpRegistryInstallState>>,
66+
AcpRegistryError
67+
> =>
68+
settingsService.getSettings.pipe(
69+
Effect.map((s) => s.acpRegistryInstalls),
70+
Effect.mapError(
71+
(cause) =>
72+
new AcpRegistryError({
73+
detail: "Failed to read server settings",
74+
cause,
75+
}),
76+
),
77+
);
78+
79+
const writeInstalls = (
80+
next: Readonly<Record<string, AcpRegistryInstallState>>,
81+
): Effect.Effect<void, AcpRegistryError> =>
82+
settingsService.updateSettings({ acpRegistryInstalls: next }).pipe(
83+
Effect.asVoid,
84+
Effect.mapError(
85+
(cause) =>
86+
new AcpRegistryError({
87+
detail: "Failed to persist server settings",
88+
cause,
89+
}),
90+
),
91+
);
92+
93+
const service: AcpRegistryServiceShape = {
94+
list: () =>
95+
Effect.gen(function* () {
96+
const installs = yield* readInstalls();
97+
return ACP_REGISTRY.map((entry) => {
98+
const channels = availableChannels(entry, platform);
99+
const installed = installs[entry.id];
100+
return {
101+
entry,
102+
availableChannels: channels,
103+
status: rollupStatus(entry, installed, channels),
104+
...(installed ? { installed } : {}),
105+
} satisfies AcpRegistryEntryWithStatus;
106+
});
107+
}),
108+
109+
install: (agentId) =>
110+
Effect.gen(function* () {
111+
const entry = acpRegistryEntryById(agentId);
112+
if (!entry) {
113+
return yield* new AcpRegistryError({
114+
agentId,
115+
detail: "Unknown ACP registry agent.",
116+
});
117+
}
118+
const result = yield* Effect.tryPromise({
119+
try: () => installAgent(entry, { cacheRoot }),
120+
catch: (cause) => {
121+
if (
122+
cause &&
123+
typeof cause === "object" &&
124+
"_tag" in cause &&
125+
cause._tag === "AcpRegistryError"
126+
) {
127+
return cause as AcpRegistryError;
128+
}
129+
return new AcpRegistryError({
130+
agentId,
131+
detail:
132+
cause instanceof Error ? cause.message : `Install failed: ${String(cause)}`,
133+
cause,
134+
});
135+
},
136+
});
137+
const installs = yield* readInstalls();
138+
yield* writeInstalls({ ...installs, [agentId]: result.state });
139+
return result.state;
140+
}),
141+
142+
uninstall: (agentId) =>
143+
Effect.gen(function* () {
144+
const entry = acpRegistryEntryById(agentId);
145+
if (!entry) {
146+
return yield* new AcpRegistryError({
147+
agentId,
148+
detail: "Unknown ACP registry agent.",
149+
});
150+
}
151+
yield* Effect.tryPromise({
152+
try: () => uninstallAgent(entry, cacheRoot),
153+
catch: (cause) =>
154+
new AcpRegistryError({
155+
agentId,
156+
detail:
157+
cause instanceof Error ? cause.message : `Uninstall failed: ${String(cause)}`,
158+
cause,
159+
}),
160+
});
161+
const installs = yield* readInstalls();
162+
if (agentId in installs) {
163+
const { [agentId]: _removed, ...rest } = installs;
164+
yield* writeInstalls(rest);
165+
}
166+
}),
167+
};
168+
169+
return service;
170+
}),
171+
);

0 commit comments

Comments
 (0)