Accept inherited environments in agent setup readiness

The name and adapter dialog saves an inactive draft without an explicit environment. On managed-only and forced Kubernetes instances, setup compared that inherited null to the resolved environment ID and disabled every connection control. Treat an unchanged inherited selection as saved, while retaining preparation and unsaved-selection gates.

Verified 140 focused UI tests, the UI typecheck script, token gates, map checks and whitespace. New regressions reproduce the blocked controls and inherited preparation transition against the previous implementation. Reproduced the deployed failure with an owned draft and completed its lifecycle cleanup.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-10-27 14:48:11 -07:00
1 parent 2c70d04bf6
commit f2a5a2df76
3 files changed
+45 -4

No files matched your search

+3
View File
@@ -99,6 +99,9 @@ For an agent-owned plugin environment, verify preparation before connecting,
same-agent probes, account preservation after an environment change, failed same-agent probes, account preservation after an environment change, failed
selection-save retry, and switching back while the computer stops. Confirm the selection-save retry, and switching back while the computer stops. Confirm the
setup hold still blocks ordinary work and API key creation before finishing. setup hold still blocks ordinary work and API key creation before finishing.
Start through the name/adapter dialog with a managed-only or forced Kubernetes
policy and a draft that inherits its environment. Once preparation completes,
confirm the auth choices, Advanced gateway, and saved subscription remain usable.
The [setup lifecycle tests](../../server/src/modules/agent-lifecycle/__tests__/setup.test.ts) The [setup lifecycle tests](../../server/src/modules/agent-lifecycle/__tests__/setup.test.ts)
exercise durable transitions with synthetic plugin resources; they do not prove exercise durable transitions with synthetic plugin resources; they do not prove
live VM provisioning. live VM provisioning.
@@ -414,8 +414,9 @@ function Setup({
); );
const needsCurrentSetup = ["codex_local", "claude_local", "opencode_local", "pi_local"].includes(adapterType); const needsCurrentSetup = ["codex_local", "claude_local", "opencode_local", "pi_local"].includes(adapterType);
const preparingComputer = created?.pauseReason === "setup" && ["preparing", "pausing"].includes(created.lifecycleState ?? ""); const preparingComputer = created?.pauseReason === "setup" && ["preparing", "pausing"].includes(created.lifecycleState ?? "");
const environmentSelectionSaved = (created?.defaultEnvironmentId ?? null) // Drafts from the name/adapter dialog can inherit the policy's environment.
=== (environmentOverride || (forced.forced || managedOnly ? environmentId : null)); const environmentSelectionSaved = (!environmentOverride && created?.defaultEnvironmentId == null)
|| (created?.defaultEnvironmentId ?? null) === (environmentOverride || (forced.forced || managedOnly ? environmentId : null));
const ready = Boolean(created) && environmentSelectionSaved && !environmentSaving && !preparingComputer && connectionSettingsReady && (!needsCurrentSetup || ( const ready = Boolean(created) && environmentSelectionSaved && !environmentSaving && !preparingComputer && connectionSettingsReady && (!needsCurrentSetup || (
envs.isSuccess && !envs.isFetching envs.isSuccess && !envs.isFetching
&& settings.isSuccess && !settings.isFetching && settings.isSuccess && !settings.isFetching
+39 -2
View File
@@ -10,6 +10,42 @@ import {
} from "./NewAgent.test-support"; } from "./NewAgent.test-support";
describe("New agent setup", () => { describe("New agent setup", () => {
it.each([
["claude_local", "Claude", "plugin"], ["codex_local", "OpenAI", "plugin"],
["claude_local", "Claude", "sandbox"], ["codex_local", "OpenAI", "sandbox"],
["claude_local", "Claude", "kubernetes"], ["codex_local", "OpenAI", "kubernetes"],
] as const)("connects an inherited %s draft through the saved %s account under %s policy", async (adapter, provider, policy) => {
const id = "00000000-0000-4000-8000-000000000093";
const draft = { id, companyId: "company-1", name: "Atlas", role: "general", adapterType: "paperclip_runner",
adapterConfig: adapter === "claude_local" ? { provider: "acpx", acpxAgent: "claude" } : { provider: "codex" },
runtimeConfig: {}, lifecycleState: "paused", pauseReason: "setup", defaultEnvironmentId: null, status: "paused" };
state.params = new URLSearchParams({ name: "Atlas", adapterType: adapter, runner: "auto", setupAgentId: id });
api.get.mockResolvedValue(draft);
api.update.mockImplementation(async (_id, patch) => ({ ...draft, ...patch }));
settings.get.mockResolvedValue({ defaultEnvironmentId: "remote" });
settings.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: policy !== "kubernetes" });
settings.getGeneral.mockResolvedValue({ executionMode: policy === "kubernetes" ? "kubernetes" : "any" });
envApi.list.mockResolvedValue([{ id: "remote", name: "Remote", status: "active",
driver: policy === "plugin" ? "plugin" : "sandbox", config: policy === "plugin" ? {} : { provider: policy === "sandbox" ? "daytona" : "kubernetes" } }]);
managedApi.list.mockResolvedValue({ currentUserId: "user-1", connections: [{
id: "00000000-0000-4000-8000-000000000021", grantId: "00000000-0000-4000-8000-000000000022",
companyId: "company-1", provider: adapter === "claude_local" ? "anthropic" : "openai", method: "subscription",
name: "Saved subscription", ownership: "shared", isDefault: false, status: "connected",
}] } as any);
await renderCurrentParams();
await click("AdvancedCustom Gateway");
expect(container.querySelector('[role="combobox"][aria-label="Connection"]')).toBeTruthy();
await click(`${provider}API key`);
expect(container.querySelector('[aria-label="API key"]')).toBeTruthy();
await click(`${provider}Subscription`);
await click("Use saved subscription");
expect(container.textContent).toContain("Configure your agent");
expect(api.testEnvironment).toHaveBeenCalledExactlyOnceWith("company-1", adapter, expect.objectContaining({ agentId: id, environmentId: "remote" }));
await click("Finish setup");
expect(api.hire).toHaveBeenCalledExactlyOnceWith("company-1", expect.objectContaining({ setupAgentId: id, defaultEnvironmentId: "remote" }));
expect(api.create).not.toHaveBeenCalled();
});
it("retains the selected account while a company requires approval before preparing the computer", async () => { it("retains the selected account while a company requires approval before preparing the computer", async () => {
settings.get.mockResolvedValue({ defaultEnvironmentId: "computer" }); settings.get.mockResolvedValue({ defaultEnvironmentId: "computer" });
envApi.list.mockResolvedValue([{ id: "computer", name: "Agent computer", status: "active", driver: "plugin", config: {} }]); envApi.list.mockResolvedValue([{ id: "computer", name: "Agent computer", status: "active", driver: "plugin", config: {} }]);
@@ -92,13 +128,14 @@ describe("New agent setup", () => {
expect(api.hire).toHaveBeenCalledWith("company-1", expect.objectContaining({ setupAgentId: id, defaultEnvironmentId: "computer" })); expect(api.hire).toHaveBeenCalledWith("company-1", expect.objectContaining({ setupAgentId: id, defaultEnvironmentId: "computer" }));
}); });
it("waits for background preparation and tests the same saved computer after it completes", async () => { it.each([null, "computer"])("waits for background preparation and tests the same saved computer (environment=%s)", async (defaultEnvironmentId) => {
const id = "00000000-0000-4000-8000-000000000092"; const id = "00000000-0000-4000-8000-000000000092";
const draft = { id, companyId: "company-1", name: "Atlas", adapterType: "paperclip_runner", adapterConfig: { provider: "codex" }, const draft = { id, companyId: "company-1", name: "Atlas", adapterType: "paperclip_runner", adapterConfig: { provider: "codex" },
runtimeConfig: {}, lifecycleState: "preparing", pauseReason: "setup", defaultEnvironmentId: "computer", status: "paused" }; runtimeConfig: {}, lifecycleState: "preparing", pauseReason: "setup", defaultEnvironmentId, status: "paused" };
state.params = new URLSearchParams({ name: "Atlas", adapterType: "codex_local", setupAgentId: id }); state.params = new URLSearchParams({ name: "Atlas", adapterType: "codex_local", setupAgentId: id });
api.get.mockResolvedValue(draft); api.get.mockResolvedValue(draft);
settings.get.mockResolvedValue({ defaultEnvironmentId: "computer" }); settings.get.mockResolvedValue({ defaultEnvironmentId: "computer" });
settings.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true });
envApi.list.mockResolvedValue([{ id: "computer", name: "Agent computer", status: "active", driver: "plugin", config: {} }]); envApi.list.mockResolvedValue([{ id: "computer", name: "Agent computer", status: "active", driver: "plugin", config: {} }]);
await renderCurrentParams(); await renderCurrentParams();
expect(container.textContent).toContain("Preparing your agent computer"); expect(container.textContent).toContain("Preparing your agent computer");