From c6d2813f9c406e9ecdc2a9f69325d335196aa21d Mon Sep 17 00:00:00 2001 From: Skolte <52416805+freezscholte@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:59:01 +0100 Subject: [PATCH 1/5] feat(policy): limits.providers.allowedAcpAgents limits which ACP agents run allowedDrivers can only allow or forbid the whole ACP Registry, so an administrator could not approve one agent. The catalog now hides and refuses agents outside the list, and the list takes commandPath and commandArgs from users, since either would run something else. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/policy/ManagedPolicy.test.ts | 36 +++++++++++++++ apps/server/src/policy/ManagedPolicy.ts | 7 ++- apps/server/src/policy/managedProviders.ts | 12 +++++ apps/server/src/policy/policyTemplates.ts | 8 ++++ apps/server/src/provider/ProviderHostLive.ts | 1 + docs/user/managed-policy.md | 10 +++- packages/contracts/src/managedPolicy.ts | 12 +++++ .../src/server/AcpRegistrySupport.test.ts | 46 +++++++++++++++++++ .../src/server/AcpRegistrySupport.ts | 22 ++++++++- .../provider-core/src/server/ProviderHost.ts | 2 + packages/shared/src/managedPolicyFile.ts | 1 + 11 files changed, 152 insertions(+), 5 deletions(-) diff --git a/apps/server/src/policy/ManagedPolicy.test.ts b/apps/server/src/policy/ManagedPolicy.test.ts index 26e74f05b0372..21b8f31895266 100644 --- a/apps/server/src/policy/ManagedPolicy.test.ts +++ b/apps/server/src/policy/ManagedPolicy.test.ts @@ -6,6 +6,7 @@ import * as Path from "effect/Path"; import { DEFAULT_SERVER_SETTINGS, + managedAcpAgentDenial, ProviderDriverKind, ProviderInstanceId, } from "@t3tools/contracts"; @@ -399,6 +400,41 @@ it.layer(NodeServices.layer)("ManagedPolicy.load", (it) => { }), ); + it.effect("takes the launch command from users when it limits ACP agents", () => + Effect.gen(function* () { + const policy = yield* loadFrom( + JSON.stringify({ + schemaVersion: 1, + organization: "Example Corp", + limits: { providers: { allowedAcpAgents: ["devin"] } }, + }), + ); + const acpRegistry = ProviderDriverKind.make("acpRegistry"); + assert.deepEqual(policy.summary?.limits?.providers, { + allowedAcpAgents: ["devin"], + lockedInstanceFields: { acpRegistry: ["config.commandPath", "config.commandArgs"] }, + }); + assert.isUndefined(managedAcpAgentDenial(policy.summary, "devin")); + assert.strictEqual( + managedAcpAgentDenial(policy.summary, "other"), + "Example Corp's policy does not allow the ACP agent other.", + ); + assert.isUndefined(managedAcpAgentDenial(undefined, "other")); + + const instanceId = ProviderInstanceId.make("acp_local"); + const shown = policy.providers.applyLimits({ + ...DEFAULT_SERVER_SETTINGS, + providerInstances: { + [instanceId]: { + driver: acpRegistry, + config: { source: "local", commandPath: "/bin/sh", commandArgs: ["-c", "x"] }, + }, + }, + }); + assert.deepEqual(shown.providerInstances[instanceId]?.config, { source: "local" }); + }), + ); + it.effect("refuses provider limits it cannot enforce", () => Effect.gen(function* () { const withProviders = (providers: unknown) => diff --git a/apps/server/src/policy/ManagedPolicy.ts b/apps/server/src/policy/ManagedPolicy.ts index 01651051893c0..cf3830ae2b325 100644 --- a/apps/server/src/policy/ManagedPolicy.ts +++ b/apps/server/src/policy/ManagedPolicy.ts @@ -146,13 +146,18 @@ function summarizeLimits( ...(webhookTasks === undefined ? {} : { webhookTasks }), ...(telemetry === undefined ? {} : { telemetry }), ...(browserCookieImport === undefined ? {} : { browserCookieImport }), - ...(providers?.allowedDrivers === undefined && Object.keys(lockedInstanceFields).length === 0 + ...(providers?.allowedDrivers === undefined && + providers?.allowedAcpAgents === undefined && + Object.keys(lockedInstanceFields).length === 0 ? {} : { providers: { ...(providers?.allowedDrivers === undefined ? {} : { allowedDrivers: providers.allowedDrivers }), + ...(providers?.allowedAcpAgents === undefined + ? {} + : { allowedAcpAgents: providers.allowedAcpAgents }), ...(Object.keys(lockedInstanceFields).length === 0 ? {} : { lockedInstanceFields }), }, }), diff --git a/apps/server/src/policy/managedProviders.ts b/apps/server/src/policy/managedProviders.ts index ac390097e150d..5197e3c32edf5 100644 --- a/apps/server/src/policy/managedProviders.ts +++ b/apps/server/src/policy/managedProviders.ts @@ -352,6 +352,18 @@ export function makeManagedProviders( } } + // A launch command runs instead of the registry agent, so an allow list owns it. + if (limits.allowedAcpAgents !== undefined) { + const driver = ProviderDriverKind.make("acpRegistry"); + const rule = rules.get(driver); + rules.set(driver, { + pin: rule?.pin, + config: [...new Set([...(rule?.config ?? []), "commandPath", "commandArgs"])], + fill: rule?.fill ?? [], + environment: rule?.environment ?? "fill", + }); + } + const allowed = limits.allowedDrivers === undefined ? undefined : new Set(limits.allowedDrivers); const isDriverAllowed = (driver: ProviderDriverKind) => allowed?.has(driver) ?? true; diff --git a/apps/server/src/policy/policyTemplates.ts b/apps/server/src/policy/policyTemplates.ts index 55c15fb94f56c..9c9a883cb1971 100644 --- a/apps/server/src/policy/policyTemplates.ts +++ b/apps/server/src/policy/policyTemplates.ts @@ -175,6 +175,14 @@ const policies = (): ReadonlyArray => [ { kind: "text", name: "ProviderInstances", label: "Provider settings JSON", json: true }, ], }, + { + id: "AllowedAcpAgents", + title: "Allowed ACP agents (JSON)", + help: 'ACP Registry agents that may run, as a JSON list of IDs such as ["devin"]. Users cannot set their own launch command for an ACP agent.', + elements: [ + { kind: "text", name: "AllowedAcpAgents", label: "Allowed ACP agents JSON", json: true }, + ], + }, ]; const escapeXml = (text: string) => diff --git a/apps/server/src/provider/ProviderHostLive.ts b/apps/server/src/provider/ProviderHostLive.ts index 6398210599b61..25f951c9fbf98 100644 --- a/apps/server/src/provider/ProviderHostLive.ts +++ b/apps/server/src/provider/ProviderHostLive.ts @@ -23,6 +23,7 @@ export const layer = Layer.effect( const digest = yield* Digest.Digest; const fileSystem = yield* FileSystem.FileSystem; return ProviderHost.ProviderHost.of({ + managedPolicy: serverSettings.managedPolicy, paths: { cwd: config.cwd, baseDir: config.baseDir, diff --git a/docs/user/managed-policy.md b/docs/user/managed-policy.md index c18bb6aa93939..b9a6a913ca411 100644 --- a/docs/user/managed-policy.md +++ b/docs/user/managed-policy.md @@ -204,8 +204,8 @@ own `claude` earlier on their `PATH`. Pin an absolute `binaryPath` in `providers provider you allow. ACP agents' own mode menus have no effect under `maxRuntimeMode`, except Antigravity's, whose modes -T3 Code can place under the limit. Users still choose which ACP Registry agent runs; leave -`acpRegistry` out of `allowedDrivers` to stop that. +T3 Code can place under the limit. Users still choose which ACP Registry agent runs; list the ones +they may use in `providers.allowedAcpAgents`, or leave `acpRegistry` out of `allowedDrivers`. The limit decides which modes T3 Code asks the provider for. It is not a sandbox; see [What a policy does not cover](#what-a-policy-does-not-cover). @@ -270,6 +270,12 @@ empty. A `codex` entry that is locked, or any `codex` entry under `maxRuntimeMod your fields. In the same way, an `opencode` entry like that rules out a server URL, so OpenCode starts the binary you set. +`providers.allowedAcpAgents` lists the ACP Registry agents that may run, by registry ID, such as +`["devin"]`. Other agents disappear from the add dialog, and instances already using them show an +error. Users also cannot run a local command or override an agent's executable, since either would +run something other than the listed agent. T3 Code does not check the IDs against the registry, so +a misspelled ID matches nothing. + ### Updates - `mode`: `auto` keeps the normal update flow. `off` stops T3 Code from checking for updates, for diff --git a/packages/contracts/src/managedPolicy.ts b/packages/contracts/src/managedPolicy.ts index 7d1c7ed8b8aae..29f7f1b219668 100644 --- a/packages/contracts/src/managedPolicy.ts +++ b/packages/contracts/src/managedPolicy.ts @@ -37,6 +37,8 @@ export const ManagedProviderLimits = Schema.Struct({ /** Only these drivers run; instances of any other driver show as unavailable. */ allowedDrivers: Schema.optionalKey(Schema.NonEmptyArray(ProviderDriverKind)), instances: Schema.optionalKey(Schema.Record(ProviderDriverKind, ManagedProviderInstance)), + /** Only these ACP Registry agents run, and users cannot set their own launch command. */ + allowedAcpAgents: Schema.optionalKey(Schema.NonEmptyArray(TrimmedNonEmptyString)), }); export type ManagedProviderLimits = typeof ManagedProviderLimits.Type; @@ -99,6 +101,8 @@ export const ManagedPolicySummaryLimits = Schema.Struct({ Schema.Struct({ /** Absent means every driver is allowed. */ allowedDrivers: Schema.optionalKey(Schema.NonEmptyArray(ProviderDriverKind)), + /** Absent means every ACP Registry agent is allowed. */ + allowedAcpAgents: Schema.optionalKey(Schema.NonEmptyArray(TrimmedNonEmptyString)), /** * Per driver, the instance fields users cannot change, as dot paths into the instance * envelope: `config.binaryPath`, `config.launchArgs`, `environment.`. A bare @@ -236,6 +240,14 @@ export function isManagedDriverAllowed( return allowed === undefined || allowed.includes(driver); } +/** Why `agentId` may not run from the ACP Registry on an environment, or undefined when it may. */ +export function managedAcpAgentDenial(summary: ManagedPolicySummary | undefined, agentId: string) { + const allowed = summary?.limits?.providers?.allowedAcpAgents; + return summary === undefined || allowed === undefined || allowed.includes(agentId) + ? undefined + : managedPolicyDeniedMessage(summary.organization, `the ACP agent ${agentId}`); +} + /** Why updates are unavailable on an environment with `summary`, or undefined when allowed. */ export function managedUpdatesDenial(summary: ManagedPolicySummary | undefined) { return summary?.limits?.updates?.mode === "off" diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index 02a32ac40250d..2294685623c8b 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -841,6 +841,52 @@ describe("AcpRegistrySupport", () => { ); }); + it.effect("hides and refuses agents the policy leaves out", () => { + const allowed = makeAgent({ npx: { package: "@example/acp@1.0.0" } }); + const refused = { ...allowed, id: "refused-agent", name: "Refused Agent" }; + const denial = "Example Corp's policy does not allow the ACP agent refused-agent."; + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-registry-allowed-agents-", + }); + const toolchain = yield* makeFakeNpmToolchain(cacheDir); + const catalog = yield* AcpRegistrySupport.makeAcpRegistryCatalog({ + cacheDir, + toolsDir: `${cacheDir}/tools`, + registryUrl, + agentDenial: (agentId) => (agentId === allowed.id ? undefined : denial), + }).pipe(Effect.provideService(HostProcess.Environment, toolchain.environment)); + + const found = yield* catalog.search({ query: "agent" }); + expect(found.agents.map((agent) => agent.id)).toEqual([allowed.id]); + + const blocked = settings({ agentId: refused.id }); + const errors = [ + yield* Effect.flip(catalog.prepare({ agentId: refused.id })), + yield* Effect.flip(catalog.inspect(blocked, toolchain.environment)), + yield* Effect.flip(catalog.resolve(blocked, "/workspace", toolchain.environment)), + ]; + expect(errors.map((error) => error.detail)).toEqual([denial, denial, denial]); + const commands = yield* fileSystem + .readFileString(toolchain.logPath) + .pipe(Effect.orElseSucceed(() => "")); + expect(commands).not.toContain("install --global"); + }).pipe( + Effect.scoped, + Effect.provide( + layerResolver((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(JSON.stringify({ version: "1.0.0", agents: [allowed, refused] })), + ), + ), + ), + ), + ); + }); + it.effect("searches reuse an index fetched in the last five minutes", () => { const agent = makeAgent({ npx: { package: "@example/acp@1.2.3" } }); let requests = 0; diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts index 99b1cfdc9f958..892dc990c05da 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -2,6 +2,7 @@ import { ACP_REGISTRY_MAX_AGENTS, AcpRegistryOperationError, AcpRegistryOperationErrorReason, + managedAcpAgentDenial, TrimmedNonEmptyString, type AcpRegistryManagedBinaryUninstallInput, type AcpRegistryManagedBinaryUninstallResult, @@ -531,11 +532,12 @@ const layer = (options: AcpRegistryCatalogOptions) => export const layerFromHost = Layer.merge( Layer.unwrap( Effect.gen(function* () { - const { paths } = yield* ProviderHost.ProviderHost; + const { paths, managedPolicy } = yield* ProviderHost.ProviderHost; const path = yield* Path.Path; return layer({ cacheDir: paths.providerStatusCacheDir, toolsDir: path.join(paths.baseDir, "tools"), + agentDenial: (agentId) => managedAcpAgentDenial(managedPolicy, agentId), }); }), ), @@ -546,6 +548,8 @@ export interface AcpRegistryCatalogOptions { readonly cacheDir: string; readonly toolsDir: string; readonly registryUrl?: string; + /** Why policy keeps an agent from running, or undefined when it may. */ + readonly agentDenial?: (agentId: string) => string | undefined; } const INSTALL_LOCK_RETRY_COUNT = 300; @@ -654,6 +658,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const resolveExecutable = yield* SpawnExecutableResolution; const platformTarget = resolveAcpRegistryPlatformTarget(platform, architecture); const registryUrl = input.registryUrl ?? ACP_REGISTRY_URL; + const { agentDenial } = input; const registryDirectory = path.join(input.cacheDir, "acp-registry"); const registryCachePath = path.join(registryDirectory, "registry.json"); const installsDirectory = input.toolsDir; @@ -1594,6 +1599,13 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct ); }); + const refuseUnderPolicy = (agentId: string) => { + const detail = agentDenial?.(agentId); + return detail === undefined + ? Effect.void + : Effect.fail(new AcpRegistryError({ reason: "agent_not_found", detail })); + }; + const findAgent = Effect.fn("AcpRegistryCatalog.findAgent")(function* ( registry: AcpRegistryIndex, agentId: string, @@ -1641,7 +1653,10 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const packageManagerAvailable = packageManager === undefined || resolveExecutable(packageManager, platform, hostEnvironment) !== undefined; - return distribution === undefined || rank === undefined || !packageManagerAvailable + return distribution === undefined || + rank === undefined || + !packageManagerAvailable || + agentDenial?.(agent.id) !== undefined ? [] : [{ agent, distribution, rank }]; }); @@ -1675,6 +1690,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const prepare: AcpRegistryCatalog["Service"]["prepare"] = Effect.fn("AcpRegistryCatalog.prepare")( function* (input) { + yield* refuseUnderPolicy(input.agentId.trim()); const registry = yield* refreshRegistry(); const agent = yield* findAgent(registry, input.agentId); const distribution = yield* compatibleDistribution(agent, "auto"); @@ -1727,6 +1743,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct } const agentId = settings.agentId.trim(); if (agentId.length === 0) return { status: "unconfigured" } as const; + yield* refuseUnderPolicy(agentId); const registry = yield* loadCachedRegistry(); const agent = registry.agents.find((candidate) => candidate.id === agentId); if (agent === undefined) return { status: "not_found", agentId } as const; @@ -1855,6 +1872,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct detail: "ACP Registry provider requires a registry agent ID.", }); } + yield* refuseUnderPolicy(agentId); const registry = yield* loadRegistry(); const agent = yield* findAgent(registry, agentId); const distribution = yield* compatibleDistribution(agent, settings.distribution); diff --git a/packages/provider-core/src/server/ProviderHost.ts b/packages/provider-core/src/server/ProviderHost.ts index 298613bf13c91..ab12b19451a1b 100644 --- a/packages/provider-core/src/server/ProviderHost.ts +++ b/packages/provider-core/src/server/ProviderHost.ts @@ -1,6 +1,7 @@ import type { BackgroundScope, ChatAttachment, + ManagedPolicySummary, ServerSettings, ServerSettingsError, } from "@t3tools/contracts"; @@ -33,6 +34,7 @@ export class ProviderHost extends Context.Service< ProviderHost, { readonly paths: ProviderHostPaths; + readonly managedPolicy?: ManagedPolicySummary | undefined; readonly settings: { readonly get: Effect.Effect; /** Runs `use` on a settings snapshot that no settings write can change until it finishes. */ diff --git a/packages/shared/src/managedPolicyFile.ts b/packages/shared/src/managedPolicyFile.ts index 2f775abb8a02a..b4f268661ab06 100644 --- a/packages/shared/src/managedPolicyFile.ts +++ b/packages/shared/src/managedPolicyFile.ts @@ -115,6 +115,7 @@ export const WINDOWS_POLICY_VALUES = { Telemetry: { type: "boolean", path: ["limits", "telemetry"] }, BrowserCookieImport: { type: "boolean", path: ["limits", "browserCookieImport"] }, ProviderInstances: { type: "json", path: ["limits", "providers", "instances"] }, + AllowedAcpAgents: { type: "json", path: ["limits", "providers", "allowedAcpAgents"] }, UpdatesMode: { type: "string", path: ["limits", "updates", "mode"] }, UpdatesFeedUrl: { type: "string", path: ["limits", "updates", "feedUrl"] }, AllowNightly: { type: "boolean", path: ["limits", "updates", "allowNightly"] }, From 30f1e5802881b9b5dc6e02b378219079b0538358 Mon Sep 17 00:00:00 2001 From: Skolte <52416805+freezscholte@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:17:30 +0100 Subject: [PATCH 2/5] docs(policy): say what allowedAcpAgents leaves open Co-Authored-By: Claude Opus 5.5 --- docs/user/managed-policy.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/user/managed-policy.md b/docs/user/managed-policy.md index b9a6a913ca411..b7744fe04ef3b 100644 --- a/docs/user/managed-policy.md +++ b/docs/user/managed-policy.md @@ -276,6 +276,10 @@ error. Users also cannot run a local command or override an agent's executable, run something other than the listed agent. T3 Code does not check the IDs against the registry, so a misspelled ID matches nothing. +This limit picks the agent, not everything around it. A user can still set variables on the +provider, and `PATH` decides which `npm` or `uv` installs an agent. `maxRuntimeMode` locks those +variables too. + ### Updates - `mode`: `auto` keeps the normal update flow. `off` stops T3 Code from checking for updates, for From ca65c9a7de10f5313e419045d0b762cc20e9a0c1 Mon Sep 17 00:00:00 2001 From: Skolte <52416805+freezscholte@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:26:58 +0100 Subject: [PATCH 3/5] test(policy): prepare and resolve an allowed ACP agent Co-Authored-By: Claude Opus 5.5 --- .../src/server/AcpRegistrySupport.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index 2294685623c8b..739fb70ef1752 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -842,7 +842,7 @@ describe("AcpRegistrySupport", () => { }); it.effect("hides and refuses agents the policy leaves out", () => { - const allowed = makeAgent({ npx: { package: "@example/acp@1.0.0" } }); + const allowed = makeAgent({ npx: { package: "@example/acp@V1.2.3" } }); const refused = { ...allowed, id: "refused-agent", name: "Refused Agent" }; const denial = "Example Corp's policy does not allow the ACP agent refused-agent."; return Effect.gen(function* () { @@ -872,6 +872,11 @@ describe("AcpRegistrySupport", () => { .readFileString(toolchain.logPath) .pipe(Effect.orElseSucceed(() => "")); expect(commands).not.toContain("install --global"); + + const prepared = yield* catalog.prepare({ agentId: allowed.id }); + expect(prepared.agentId).toBe(allowed.id); + const launched = yield* catalog.resolve(settings(), "/workspace", toolchain.environment); + expect(launched.spawn.command).toBe(toolchain.executablePath); }).pipe( Effect.scoped, Effect.provide( From 6ec8aed0346fe62225a2af0dfeabba5bad9a8b69 Mon Sep 17 00:00:00 2001 From: Skolte <52416805+freezscholte@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:55:57 +0100 Subject: [PATCH 4/5] fix(policy): show why allowedAcpAgents stopped an agent A real client run showed the limit worked but explained itself badly. A refused agent read "Not found", a local instance was told to configure the command the policy had locked, and the add dialog offered local commands and saved refused agents. A refused agent or local command now reports as unavailable with the policy as its reason, through the existing availability fields. The add dialog says the limit applies, hides local commands, locks the override, and refuses a manually entered agent the policy leaves out. Co-Authored-By: Claude Opus 5.5 --- .../settings/AcpRegistrySearchStep.tsx | 16 ++++++++-- ...nstanceDialog.environment.browser.test.tsx | 32 ++++++++++++++++++- .../settings/AddProviderInstanceDialog.tsx | 21 ++++++++++-- .../settings/ProviderInstanceCard.tsx | 13 +++++++- .../settings/providerStatus.test.ts | 6 ++++ .../src/components/settings/providerStatus.ts | 6 ++++ docs/user/managed-policy.md | 9 +++--- packages/contracts/src/managedPolicy.ts | 14 ++++++++ .../src/server/AcpRegistrySupport.test.ts | 15 +++++++-- .../src/server/AcpRegistrySupport.ts | 16 ++++++++-- .../provider-acp-registry/src/server/auth.ts | 1 + .../src/server/driver.test.ts | 17 ++++++++++ .../src/server/driver.ts | 7 +++- 13 files changed, 155 insertions(+), 18 deletions(-) diff --git a/apps/web/src/components/settings/AcpRegistrySearchStep.tsx b/apps/web/src/components/settings/AcpRegistrySearchStep.tsx index a3612a609418c..03dec9b119b93 100644 --- a/apps/web/src/components/settings/AcpRegistrySearchStep.tsx +++ b/apps/web/src/components/settings/AcpRegistrySearchStep.tsx @@ -1,5 +1,9 @@ import { pipe } from "effect/Function"; -import { ACP_REGISTRY_MAX_AGENTS, AuthProvidersManageScope } from "@t3tools/contracts"; +import { + ACP_REGISTRY_MAX_AGENTS, + AuthProvidersManageScope, + managedAcpAgentsNotice, +} from "@t3tools/contracts"; import { readEnvironmentScope, useEnvironmentScope } from "../../state/session"; import { isAtomCommandInterrupted, @@ -17,6 +21,7 @@ import { type FormEvent, useState } from "react"; import { useDeadlinePassed } from "../../hooks/useDeadline"; import { serverEnvironment } from "../../state/server"; import { useEnvironmentQuery } from "../../state/query"; +import { useManagedPolicy } from "../../state/managedPolicy"; import { useAtomCommand } from "../../state/use-atom-command"; import { Alert, AlertDescription } from "../ui/alert"; import { Button } from "../ui/button"; @@ -126,7 +131,7 @@ interface AcpRegistrySearchStepProps { readonly providerInstances: Readonly>; readonly onPrepared: (agent: AcpRegistrySearchAgent) => void; readonly onManualConfiguration: () => void; - readonly onLocalConfiguration?: () => void; + readonly onLocalConfiguration?: (() => void) | undefined; } export function AcpRegistrySearchStep({ @@ -138,6 +143,7 @@ export function AcpRegistrySearchStep({ onLocalConfiguration, }: AcpRegistrySearchStepProps) { const canManageProviders = useEnvironmentScope(environmentId, AuthProvidersManageScope); + const agentsLimited = managedAcpAgentsNotice(useManagedPolicy(environmentId)) !== undefined; const { query, search, preparingId, prepareError } = registry; const handleSearch = (event: FormEvent) => { @@ -224,7 +230,11 @@ export function AcpRegistrySearchStep({ results.length === 0 ? (

No compatible agents found

-

Try a broader search.

+

+ {agentsLimited + ? "Only agents your organization allows appear here." + : "Try a broader search."} +

) : ( diff --git a/apps/web/src/components/settings/AddProviderInstanceDialog.environment.browser.test.tsx b/apps/web/src/components/settings/AddProviderInstanceDialog.environment.browser.test.tsx index 568c6a953ffc7..1251da39af887 100644 --- a/apps/web/src/components/settings/AddProviderInstanceDialog.environment.browser.test.tsx +++ b/apps/web/src/components/settings/AddProviderInstanceDialog.environment.browser.test.tsx @@ -1,4 +1,9 @@ -import { EnvironmentId, ProviderDriverKind, type AcpRegistrySearchAgent } from "@t3tools/contracts"; +import { + EnvironmentId, + ProviderDriverKind, + type AcpRegistrySearchAgent, + type ManagedPolicySummary, +} from "@t3tools/contracts"; import { userEvent } from "vite-plus/test/browser"; import { describe, expect, it, vi } from "vite-plus/test"; @@ -9,6 +14,9 @@ const actions = vi.hoisted(() => ({ onOpenChange: vi.fn(), canManageProviders: true, })); +const managed = vi.hoisted(() => ({ + policy: undefined as ManagedPolicySummary | undefined, +})); const settingsHooks = vi.hoisted(() => ({ read: vi.fn(() => ({ providerInstances: {} })), mutate: vi.fn(), @@ -46,6 +54,10 @@ vi.mock(import("../../state/session"), async (original) => { readEnvironmentScope: hasScope as never, }; }); +vi.mock(import("../../state/managedPolicy"), async (original) => ({ + ...(await original()), + useManagedPolicy: () => managed.policy, +})); vi.mock(import("../ui/toast"), async (original) => { const actual = await original(); return { ...actual, toastManager: { ...actual.toastManager, add: actions.toast } }; @@ -140,6 +152,7 @@ async function prepareCodexInstance() { } function setup() { + managed.policy = undefined; actions.canManageProviders = true; actions.toast.mockReset(); actions.onOpenChange.mockReset(); @@ -291,6 +304,23 @@ describe("AddProviderInstanceDialog environment routing", () => { expect(signInStep()).toBeNull(); }); + it("refuses a manually entered agent the policy leaves out", async () => { + setup(); + managed.policy = { + organization: "Example Corp", + lockedSettings: [], + limits: { providers: { allowedAcpAgents: ["devin"] } }, + }; + await using _screen = await render(dialog()); + await click("Show more"); + await click("Enter registry agent"); + await type("#add-provider-acpRegistry-manual-agentId", "cline"); + await click("Next"); + expect(document.body.textContent).toContain( + "Example Corp's policy does not allow the ACP agent cline.", + ); + }); + it("rejects a queued save after the provider grant is revoked", async () => { setup(); await using screen = await prepareCodexInstance(); diff --git a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx index 9b1829879405f..318067c5c2f74 100644 --- a/apps/web/src/components/settings/AddProviderInstanceDialog.tsx +++ b/apps/web/src/components/settings/AddProviderInstanceDialog.tsx @@ -18,6 +18,8 @@ import { type ProviderInstanceConfig, type ProviderInstanceEnvironmentVariable, isManagedDriverAllowed, + managedAcpAgentDenial, + managedAcpAgentsNotice, } from "@t3tools/contracts"; import { @@ -28,6 +30,7 @@ import { import { cn } from "../../lib/utils"; import { normalizeProviderAccentColor } from "../../providerInstances"; import { managedInstanceLocks, useManagedPolicy } from "../../state/managedPolicy"; +import { ManagedNotice } from "../ManagedPolicy"; import { readEnvironmentScope, useEnvironmentScope } from "../../state/session"; import { Button } from "../ui/button"; import { ChatGptConnectionButton } from "./ChatGptConnectionButton"; @@ -236,9 +239,11 @@ export function AddProviderInstanceDialog({ ? localCommandPath.length > 0 ? null : "Executable is required." - : selectedAcp !== null || (isManualAcpConfiguration && manualAgentId.length > 0) + : selectedAcp !== null ? null - : "Select an ACP or configure one manually."; + : isManualAcpConfiguration && manualAgentId.length > 0 + ? (managedAcpAgentDenial(managedPolicy, manualAgentId) ?? null) + : "Select an ACP or configure one manually."; const wizardStepSummaries = isAcpRegistry ? ([ isLocalAcp ? "Local ACP command" : (selectedAcp?.name ?? (manualAgentId || null)), @@ -366,6 +371,8 @@ export function AddProviderInstanceDialog({ // A policy that owns this driver's launch settings also rules out ChatGPT's own runtime. const driverLocks = managedInstanceLocks(managedPolicy, driver); + const acpLocks = managedInstanceLocks(managedPolicy, ACP_REGISTRY_DRIVER_KIND); + const acpAgentsNotice = managedAcpAgentsNotice(managedPolicy); const handleSave = async () => { if (!readEnvironmentScope(environmentId, AuthProvidersManageScope)) return; @@ -510,6 +517,9 @@ export function AddProviderInstanceDialog({
)} + {acpRegistryAllowed && acpAgentsNotice ? ( + {acpAgentsNotice} + ) : null} {!acpRegistryAllowed ? null : isAcpRegistry && isManualAcpConfiguration ? (
@@ -541,6 +551,9 @@ export function AddProviderInstanceDialog({ idPrefix="add-provider-acpRegistry-manual" variant="settings" onChange={setConfigDraft} + managed={ + acpLocks && { keys: acpLocks.configKeys, reason: acpLocks.reason } + } environmentId={environmentId} /> {isLocalAcp ? ( @@ -563,7 +576,9 @@ export function AddProviderInstanceDialog({ providerInstances={settings.providerInstances} onPrepared={handleAcpPrepared} onManualConfiguration={handleManualAcpConfiguration} - onLocalConfiguration={handleLocalAcpConfiguration} + onLocalConfiguration={ + acpAgentsNotice ? undefined : handleLocalAcpConfiguration + } /> {isAcpRegistry && hasAttemptedSubmit && acpSelectionError ? (

{acpSelectionError}

diff --git a/apps/web/src/components/settings/ProviderInstanceCard.tsx b/apps/web/src/components/settings/ProviderInstanceCard.tsx index b676e64399d97..ff984ad5d9fb7 100644 --- a/apps/web/src/components/settings/ProviderInstanceCard.tsx +++ b/apps/web/src/components/settings/ProviderInstanceCard.tsx @@ -49,7 +49,11 @@ import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; import { Switch } from "../ui/switch"; import { ManagedControl, ManagedNotice } from "../ManagedPolicy"; import { managedInstanceLocks, useManagedPolicy } from "../../state/managedPolicy"; -import { disallowedProviderMessage, isManagedDriverAllowed } from "@t3tools/contracts"; +import { + disallowedProviderMessage, + isManagedDriverAllowed, + managedAcpAgentsNotice, +} from "@t3tools/contracts"; import { stackedThreadToast, toastManager } from "../ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import type { @@ -772,6 +776,8 @@ export function ProviderInstanceCard({ const accentColor = normalizeProviderAccentColor(instance.accentColor); const managedPolicy = useManagedPolicy(environmentId); const instanceLocks = managedInstanceLocks(managedPolicy, instance.driver); + const acpAgentsNotice = + instance.driver === "acpRegistry" ? managedAcpAgentsNotice(managedPolicy) : undefined; const managedDriverReason = isManagedDriverAllowed(managedPolicy, instance.driver) ? undefined : disallowedProviderMessage( @@ -1335,6 +1341,11 @@ export function ProviderInstanceCard({ aria-disabled={readOnly || undefined} className={readOnly ? "opacity-50 select-none" : undefined} > + {acpAgentsNotice ? ( +
+ {acpAgentsNotice} +
+ ) : null} {runtimeFields} ) : null} diff --git a/apps/web/src/components/settings/providerStatus.test.ts b/apps/web/src/components/settings/providerStatus.test.ts index bc4b9674b674f..6bf078a7d4cc0 100644 --- a/apps/web/src/components/settings/providerStatus.test.ts +++ b/apps/web/src/components/settings/providerStatus.test.ts @@ -98,6 +98,12 @@ describe("Antigravity with unchecked sign-in", () => { headline: "Not found", status: "warning", }, + { + name: "refused by policy", + change: { installed: false, status: "error", availability: "unavailable" }, + headline: "Unavailable", + status: "error", + }, { name: "API key authentication", change: { auth: { status: "unknown", type: "gemini-api-key" } }, diff --git a/apps/web/src/components/settings/providerStatus.ts b/apps/web/src/components/settings/providerStatus.ts index a4db548906549..de7cb7736ee32 100644 --- a/apps/web/src/components/settings/providerStatus.ts +++ b/apps/web/src/components/settings/providerStatus.ts @@ -58,6 +58,12 @@ export function getProviderSummary(provider: ServerProvider | undefined) { provider.message ?? "This provider is installed but disabled for new sessions in T3 Code.", }; } + if (provider.availability === "unavailable") { + return { + headline: "Unavailable", + detail: provider.unavailableReason ?? provider.message ?? null, + }; + } if (!provider.installed) { return { headline: "Not found", diff --git a/docs/user/managed-policy.md b/docs/user/managed-policy.md index b7744fe04ef3b..8682c6c7dfaed 100644 --- a/docs/user/managed-policy.md +++ b/docs/user/managed-policy.md @@ -271,10 +271,11 @@ your fields. In the same way, an `opencode` entry like that rules out a server U starts the binary you set. `providers.allowedAcpAgents` lists the ACP Registry agents that may run, by registry ID, such as -`["devin"]`. Other agents disappear from the add dialog, and instances already using them show an -error. Users also cannot run a local command or override an agent's executable, since either would -run something other than the listed agent. T3 Code does not check the IDs against the registry, so -a misspelled ID matches nothing. +`["devin"]`. Other agents disappear from the add dialog, and instances already using them show as +unavailable with the policy as the reason. Users also cannot run a local command or override an +agent's executable, since either would run something other than the listed agent, so instances +that use a local command stop too. T3 Code does not check the IDs against the registry, so a +misspelled ID matches nothing. This limit picks the agent, not everything around it. A user can still set variables on the provider, and `PATH` decides which `npm` or `uv` installs an agent. `maxRuntimeMode` locks those diff --git a/packages/contracts/src/managedPolicy.ts b/packages/contracts/src/managedPolicy.ts index 29f7f1b219668..6e156a89fca01 100644 --- a/packages/contracts/src/managedPolicy.ts +++ b/packages/contracts/src/managedPolicy.ts @@ -248,6 +248,20 @@ export function managedAcpAgentDenial(summary: ManagedPolicySummary | undefined, : managedPolicyDeniedMessage(summary.organization, `the ACP agent ${agentId}`); } +/** Why local ACP commands may not run on an environment, or undefined when they may. */ +export function managedLocalAcpCommandDenial(summary: ManagedPolicySummary | undefined) { + return summary?.limits?.providers?.allowedAcpAgents === undefined + ? undefined + : managedPolicyDeniedMessage(summary.organization, "local ACP commands"); +} + +/** What ACP settings show while the policy limits agents, or undefined when it does not. */ +export function managedAcpAgentsNotice(summary: ManagedPolicySummary | undefined) { + return summary?.limits?.providers?.allowedAcpAgents === undefined + ? undefined + : `${summary.organization} chooses which ACP agents run here, so local commands and executable overrides are turned off.`; +} + /** Why updates are unavailable on an environment with `summary`, or undefined when allowed. */ export function managedUpdatesDenial(summary: ManagedPolicySummary | undefined) { return summary?.limits?.updates?.mode === "off" diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index 739fb70ef1752..c01e55a6f48de 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -845,6 +845,7 @@ describe("AcpRegistrySupport", () => { const allowed = makeAgent({ npx: { package: "@example/acp@V1.2.3" } }); const refused = { ...allowed, id: "refused-agent", name: "Refused Agent" }; const denial = "Example Corp's policy does not allow the ACP agent refused-agent."; + const localDenial = "Example Corp's policy does not allow local ACP commands."; return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ @@ -856,6 +857,7 @@ describe("AcpRegistrySupport", () => { toolsDir: `${cacheDir}/tools`, registryUrl, agentDenial: (agentId) => (agentId === allowed.id ? undefined : denial), + localCommandDenial: localDenial, }).pipe(Effect.provideService(HostProcess.Environment, toolchain.environment)); const found = yield* catalog.search({ query: "agent" }); @@ -864,10 +866,19 @@ describe("AcpRegistrySupport", () => { const blocked = settings({ agentId: refused.id }); const errors = [ yield* Effect.flip(catalog.prepare({ agentId: refused.id })), - yield* Effect.flip(catalog.inspect(blocked, toolchain.environment)), yield* Effect.flip(catalog.resolve(blocked, "/workspace", toolchain.environment)), ]; - expect(errors.map((error) => error.detail)).toEqual([denial, denial, denial]); + expect(errors.map((error) => error.detail)).toEqual([denial, denial]); + expect(yield* catalog.inspect(blocked, toolchain.environment)).toEqual({ + status: "denied", + message: denial, + }); + expect( + yield* catalog.inspect( + decodeAcpRegistrySettings({ source: "local" }), + toolchain.environment, + ), + ).toEqual({ status: "denied", message: localDenial }); const commands = yield* fileSystem .readFileString(toolchain.logPath) .pipe(Effect.orElseSucceed(() => "")); diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts index 892dc990c05da..42bc05d874349 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -3,6 +3,7 @@ import { AcpRegistryOperationError, AcpRegistryOperationErrorReason, managedAcpAgentDenial, + managedLocalAcpCommandDenial, TrimmedNonEmptyString, type AcpRegistryManagedBinaryUninstallInput, type AcpRegistryManagedBinaryUninstallResult, @@ -470,6 +471,7 @@ export interface ResolvedAcpRegistryAgent { export type AcpRegistryInspection = | { readonly status: "unconfigured" } + | { readonly status: "denied"; readonly message: string } | { readonly status: "not_found"; readonly agentId: string } | { readonly status: "unsupported"; @@ -538,6 +540,7 @@ export const layerFromHost = Layer.merge( cacheDir: paths.providerStatusCacheDir, toolsDir: path.join(paths.baseDir, "tools"), agentDenial: (agentId) => managedAcpAgentDenial(managedPolicy, agentId), + localCommandDenial: managedLocalAcpCommandDenial(managedPolicy), }); }), ), @@ -550,6 +553,8 @@ export interface AcpRegistryCatalogOptions { readonly registryUrl?: string; /** Why policy keeps an agent from running, or undefined when it may. */ readonly agentDenial?: (agentId: string) => string | undefined; + /** Why policy keeps a local command from running, or undefined when it may. */ + readonly localCommandDenial?: string | undefined; } const INSTALL_LOCK_RETRY_COUNT = 300; @@ -658,7 +663,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const resolveExecutable = yield* SpawnExecutableResolution; const platformTarget = resolveAcpRegistryPlatformTarget(platform, architecture); const registryUrl = input.registryUrl ?? ACP_REGISTRY_URL; - const { agentDenial } = input; + const { agentDenial, localCommandDenial } = input; const registryDirectory = path.join(input.cacheDir, "acp-registry"); const registryCachePath = path.join(registryDirectory, "registry.json"); const installsDirectory = input.toolsDir; @@ -1730,7 +1735,11 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct function* (settings, environment) { if (settings.source === "local") { const command = settings.commandPath.trim(); - if (!command) return { status: "unconfigured" } as const; + if (!command) { + return localCommandDenial === undefined + ? ({ status: "unconfigured" } as const) + : ({ status: "denied", message: localCommandDenial } as const); + } const executable = resolveExecutable(command, platform, environment ?? hostEnvironment); if (executable !== undefined) yield* validateLocalExecutable(executable); return executable === undefined @@ -1743,7 +1752,8 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct } const agentId = settings.agentId.trim(); if (agentId.length === 0) return { status: "unconfigured" } as const; - yield* refuseUnderPolicy(agentId); + const denial = agentDenial?.(agentId); + if (denial !== undefined) return { status: "denied", message: denial } as const; const registry = yield* loadCachedRegistry(); const agent = registry.agents.find((candidate) => candidate.id === agentId); if (agent === undefined) return { status: "not_found", agentId } as const; diff --git a/packages/provider-acp-registry/src/server/auth.ts b/packages/provider-acp-registry/src/server/auth.ts index 240bca67cea57..02ea59e4ab49b 100644 --- a/packages/provider-acp-registry/src/server/auth.ts +++ b/packages/provider-acp-registry/src/server/auth.ts @@ -101,6 +101,7 @@ export const makeAcpRegistryAuth = Effect.fn("makeAcpRegistryAuth")(function* (o failure("methods", "Could not inspect the selected ACP agent.", cause), ), ); + if (inspected.status === "denied") return yield* failure("methods", inspected.message); if (inspected.status !== "ready") return yield* failure("methods", "Prepare this ACP agent before signing in."); // Path overrides can change in place; cache discovery only for registry versions. diff --git a/packages/provider-acp-registry/src/server/driver.test.ts b/packages/provider-acp-registry/src/server/driver.test.ts index 7fba45fd6bec0..da372fb55b26f 100644 --- a/packages/provider-acp-registry/src/server/driver.test.ts +++ b/packages/provider-acp-registry/src/server/driver.test.ts @@ -43,6 +43,23 @@ function catalogWithInspection( } describe("acpRegistrySnapshotReadiness", () => { + it("reports a policy refusal as unavailable, not as a missing agent", () => { + const message = "Example Corp's policy does not allow the ACP agent test-agent."; + const provider = buildCheckedAcpRegistrySnapshot({ + ...identity, + settings: decodeSettings({ agentId: "test-agent" }), + checkedAt: "2026-08-13T10:00:00.000Z", + inspection: { status: "denied", message }, + }); + expect(provider).toMatchObject({ + installed: false, + status: "error", + message, + availability: "unavailable", + unavailableReason: message, + }); + }); + it("treats a live empty command advertisement as an authoritative replacement", () => { const provider = buildCheckedAcpRegistrySnapshot({ ...identity, diff --git a/packages/provider-acp-registry/src/server/driver.ts b/packages/provider-acp-registry/src/server/driver.ts index c154b7a2bc6fd..153ef1a1d05da 100644 --- a/packages/provider-acp-registry/src/server/driver.ts +++ b/packages/provider-acp-registry/src/server/driver.ts @@ -133,6 +133,8 @@ function acpRegistrySnapshotReadiness( message: "Select an ACP Registry agent or configure a local ACP executable before starting a thread.", }; + case "denied": + return { installed: false, version: null, status: "error", message: inspection.message }; case "not_found": return { installed: false, @@ -342,7 +344,7 @@ export function buildCheckedAcpRegistrySnapshot( ? `Set ${advertisedAuthMethod.envVarNames!.join(", ")} under this instance's environment variables in provider settings. T3 Code will detect it on the next provider refresh.` : `Sign in in provider settings using "${advertisedAuthMethod.name}".` : undefined; - return baseSnapshot({ + const snapshot = baseSnapshot({ ...input, ...(input.inspection.status === "ready" && input.inspection.documentationUrl ? { documentationUrl: input.inspection.documentationUrl } @@ -374,6 +376,9 @@ export function buildCheckedAcpRegistrySnapshot( ? { message: readiness.message } : {}), }); + return input.inspection.status === "denied" + ? { ...snapshot, availability: "unavailable", unavailableReason: input.inspection.message } + : snapshot; } export const checkAcpRegistryProviderStatus = Effect.fn("AcpRegistryDriver.checkProviderStatus")( From 9265ae9c85692e388c2a84f3a9cf832e0c94a4f1 Mon Sep 17 00:00:00 2001 From: Skolte <52416805+freezscholte@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:10:24 +0100 Subject: [PATCH 5/5] fix(policy): the ACP catalog refuses a configured local command The settings overlay already clears these commands, but the catalog only denied an empty one. inspect and resolve now refuse a local command or an executable override themselves while the policy limits agents. Co-Authored-By: Claude Opus 5.5 --- .../src/server/AcpRegistrySupport.test.ts | 20 +++++++++++++------ .../src/server/AcpRegistrySupport.ts | 17 +++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index c01e55a6f48de..1c29eab6ae750 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -873,12 +873,20 @@ describe("AcpRegistrySupport", () => { status: "denied", message: denial, }); - expect( - yield* catalog.inspect( - decodeAcpRegistrySettings({ source: "local" }), - toolchain.environment, - ), - ).toEqual({ status: "denied", message: localDenial }); + for (const decoded of [ + decodeAcpRegistrySettings({ source: "local" }), + decodeAcpRegistrySettings({ source: "local", commandPath: toolchain.executablePath }), + settings({ commandPath: toolchain.executablePath }), + ]) { + expect(yield* catalog.inspect(decoded, toolchain.environment)).toEqual({ + status: "denied", + message: localDenial, + }); + const error = yield* Effect.flip( + catalog.resolve(decoded, "/workspace", toolchain.environment), + ); + expect(error.detail).toBe(localDenial); + } const commands = yield* fileSystem .readFileString(toolchain.logPath) .pipe(Effect.orElseSucceed(() => "")); diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts index 42bc05d874349..c614ad6913557 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -1611,6 +1611,11 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct : Effect.fail(new AcpRegistryError({ reason: "agent_not_found", detail })); }; + const commandDenial = (settings: AcpRegistrySettings) => + settings.source === "local" || settings.commandPath.trim().length > 0 + ? localCommandDenial + : undefined; + const findAgent = Effect.fn("AcpRegistryCatalog.findAgent")(function* ( registry: AcpRegistryIndex, agentId: string, @@ -1733,13 +1738,11 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const inspect: AcpRegistryCatalog["Service"]["inspect"] = Effect.fn("AcpRegistryCatalog.inspect")( function* (settings, environment) { + const refusal = commandDenial(settings); + if (refusal !== undefined) return { status: "denied", message: refusal } as const; if (settings.source === "local") { const command = settings.commandPath.trim(); - if (!command) { - return localCommandDenial === undefined - ? ({ status: "unconfigured" } as const) - : ({ status: "denied", message: localCommandDenial } as const); - } + if (!command) return { status: "unconfigured" } as const; const executable = resolveExecutable(command, platform, environment ?? hostEnvironment); if (executable !== undefined) yield* validateLocalExecutable(executable); return executable === undefined @@ -1853,6 +1856,10 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const resolve: AcpRegistryCatalog["Service"]["resolve"] = Effect.fn("AcpRegistryCatalog.resolve")( function* (settings, cwd, environment) { + const refusal = commandDenial(settings); + if (refusal !== undefined) { + return yield* new AcpRegistryError({ reason: "agent_not_found", detail: refusal }); + } if (settings.source === "local") { const executable = settings.commandPath.trim(); if (!executable) {