diff --git a/src/gateway/server-methods/agents-mutate.test.ts b/src/gateway/server-methods/agents-mutate.test.ts index fb0ebdf10f36..87ae081db0a9 100644 --- a/src/gateway/server-methods/agents-mutate.test.ts +++ b/src/gateway/server-methods/agents-mutate.test.ts @@ -2947,19 +2947,36 @@ describe("agents.files.list", () => { // Pins the derivation: a private copy of this list in the gateway is what let a // retired file linger in the Control UI as a permanently-missing tab. - it("lists exactly the canonical workspace bootstrap filenames", async () => { + it("lists the canonical workspace filenames except IDENTITY.md", async () => { const names = await listAgentFileNames(); - expect(names).toStrictEqual([...WORKSPACE_BOOTSTRAP_FILENAMES]); + expect(names).toStrictEqual( + WORKSPACE_BOOTSTRAP_FILENAMES.filter((name) => name !== "IDENTITY.md"), + ); }); it("lists the canonical filenames without BOOTSTRAP.md once setup completed", async () => { mocks.isWorkspaceSetupCompleted.mockResolvedValue(true); const names = await listAgentFileNames(); expect(names).toStrictEqual( - WORKSPACE_BOOTSTRAP_FILENAMES.filter((name) => name !== "BOOTSTRAP.md"), + WORKSPACE_BOOTSTRAP_FILENAMES.filter( + (name) => name !== "IDENTITY.md" && name !== "BOOTSTRAP.md", + ), ); }); + // The identity form owns this file via agents.update; raw writes stay available + // so removing the editor tab does not remove the capability. + it("still accepts direct IDENTITY.md writes even though it is not listed", async () => { + const { respond, promise } = makeCall("agents.files.set", { + agentId: "main", + name: "IDENTITY.md", + content: "- Name: Ada\n", + }); + await promise; + + expectRespondOk(respond, { ok: true }); + }); + it("rejects writes to retired HEARTBEAT.md workspace files", async () => { const { respond, promise } = makeCall("agents.files.set", { agentId: "main", diff --git a/src/gateway/server-methods/agents.ts b/src/gateway/server-methods/agents.ts index 604d947bc694..e899a05ed72c 100644 --- a/src/gateway/server-methods/agents.ts +++ b/src/gateway/server-methods/agents.ts @@ -101,7 +101,13 @@ import type { GatewayRequestHandlers, RespondFn } from "./types.js"; // Derived from the canonical workspace list so retiring a bootstrap file cannot // leave the Control UI advertising a file the runtime no longer reads. -const CORE_FILE_NAMES = WORKSPACE_BOOTSTRAP_FILENAMES; +// IDENTITY.md is excluded: it is a parsed record that `agents.update` rewrites via +// mergeIdentityMarkdownContent, so a second freeform editor would clobber fields +// (Creature/Vibe, unfilled placeholders) that the identity form round-trips. +// It stays writable through agents.files.set for clients that want raw access. +const CORE_FILE_NAMES = WORKSPACE_BOOTSTRAP_FILENAMES.filter( + (name) => name !== DEFAULT_IDENTITY_FILENAME, +); const CORE_FILE_NAMES_POST_ONBOARDING = CORE_FILE_NAMES.filter( (name) => name !== DEFAULT_BOOTSTRAP_FILENAME, ); @@ -131,8 +137,10 @@ export const testing = { }, }; -// Gateway file mutations are intentionally capped to the workspace files the UI owns. -const ALLOWED_FILE_NAMES = new Set(CORE_FILE_NAMES); +// Writes stay capped to canonical workspace files, and deliberately remain wider +// than the listed core files: IDENTITY.md is not offered as an editor tab but is +// still writable for clients that manage it directly. +const ALLOWED_FILE_NAMES = new Set(WORKSPACE_BOOTSTRAP_FILENAMES); function resolveAgentWorkspaceFileOrRespondError( params: Record,