diff --git a/.changeset/20260903010000-drop-agent-metadata.md b/.changeset/20260903010000-drop-agent-metadata.md new file mode 100644 index 000000000..ee4b52f4d --- /dev/null +++ b/.changeset/20260903010000-drop-agent-metadata.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge": patch +--- + +Drop unused `agent.metadata`; remote identity is stored in `external_id`. diff --git a/packages/trueforge/src/db/agentStore.ts b/packages/trueforge/src/db/agentStore.ts index 3fdd9ab20..abc16dd76 100644 --- a/packages/trueforge/src/db/agentStore.ts +++ b/packages/trueforge/src/db/agentStore.ts @@ -5,7 +5,6 @@ * Implementations: PostgresAgentStore and SqliteAgentStore. */ import { AgentSpecSchema, type AgentSpec } from '@truefoundry/trueforge-core/agent-session'; -import type { AgentMetadata } from '../schemas/agentMetadata'; import type { ResourceName } from '../schemas/common'; export interface AgentRecord { @@ -13,7 +12,6 @@ export interface AgentRecord { tenant_id: string; name: ResourceName; manifest: AgentSpec; - metadata: AgentMetadata; external_id: string | null; /** ISO-8601 UTC instant. */ created_at: string; @@ -40,14 +38,13 @@ export interface CreateAgentInput { } /** - * Patch an existing agent by immutable id. At least one of `manifest`, `metadata`, or `external_id` is required. + * Patch an existing agent by immutable id. At least one of `manifest` or `external_id` is required. * Provided fields replace the stored column; omitted fields are left unchanged. */ export interface UpdateAgentInput { tenant_id: string; id: string; manifest?: AgentSpec; - metadata?: AgentMetadata; external_id?: string | null; } @@ -87,7 +84,7 @@ export interface IAgentStore { getAgent(input: GetAgentInput, transaction?: TTransaction): Promise; /** Inserts a new agent with a generated ULID. Throws AgentNameConflictError or AgentExternalIdConflictError on unique clash. */ createAgent(input: CreateAgentInput, transaction?: TTransaction): Promise; - /** Patches `manifest`, `metadata`, and/or `external_id`. Throws AgentExternalIdConflictError on unique clash. Returns undefined if missing. */ + /** Patches `manifest` and/or `external_id`. Throws AgentExternalIdConflictError on unique clash. Returns undefined if missing. */ updateAgent(input: UpdateAgentInput, transaction?: TTransaction): Promise; /** Deletes by immutable id. Idempotent if already missing. */ deleteAgent(input: DeleteAgentInput, transaction?: TTransaction): Promise; diff --git a/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts b/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts index 97f58c8c7..629c30c79 100644 --- a/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts +++ b/packages/trueforge/src/db/postgres/agent-store/PostgresAgentStore.ts @@ -1,5 +1,4 @@ import type { Kysely, Selectable, Transaction } from 'kysely'; -import { EMPTY_AGENT_METADATA } from '../../../schemas/agentMetadata'; import { newId } from '../../../utils/id'; import { AgentExternalIdConflictError, @@ -23,7 +22,6 @@ function toRecord(row: Selectable): AgentRecord { tenant_id: row.tenant_id, name: row.name, manifest: parseStoredAgentSpec(row.manifest), - metadata: row.metadata, external_id: row.external_id, created_at: row.created_at.toISOString(), updated_at: row.updated_at.toISOString(), @@ -83,7 +81,6 @@ export class PostgresAgentStore implements IAgentStore> { tenant_id: input.tenant_id, name: input.name, manifest: json(input.manifest), - metadata: json(EMPTY_AGENT_METADATA), external_id: input.external_id, created_at: now(), updated_at: now(), @@ -105,8 +102,8 @@ export class PostgresAgentStore implements IAgentStore> { } async updateAgent(input: UpdateAgentInput, transaction?: Transaction): Promise { - if (input.manifest === undefined && input.metadata === undefined && input.external_id === undefined) { - throw new Error('updateAgent requires manifest, metadata, and/or external_id'); + if (input.manifest === undefined && input.external_id === undefined) { + throw new Error('updateAgent requires manifest and/or external_id'); } const db = transaction ?? this.#db; try { @@ -114,7 +111,6 @@ export class PostgresAgentStore implements IAgentStore> { .updateTable('agent') .set({ ...(input.manifest === undefined ? {} : { manifest: json(input.manifest) }), - ...(input.metadata === undefined ? {} : { metadata: json(input.metadata) }), ...(input.external_id === undefined ? {} : { external_id: input.external_id }), updated_at: now(), }) diff --git a/packages/trueforge/src/db/postgres/migrations/20260903_000001_drop_agent_metadata.ts b/packages/trueforge/src/db/postgres/migrations/20260903_000001_drop_agent_metadata.ts new file mode 100644 index 000000000..a1097a112 --- /dev/null +++ b/packages/trueforge/src/db/postgres/migrations/20260903_000001_drop_agent_metadata.ts @@ -0,0 +1,17 @@ +import { sql, type Kysely } from 'kysely'; + +/** + * Drop agent.metadata — identity lives in external_id; column was never on the public Agent API. + */ +export async function up(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await sql`ALTER TABLE agent DROP COLUMN IF EXISTS metadata`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await sql` + ALTER TABLE agent + ADD COLUMN metadata jsonb NOT NULL DEFAULT '{}'::jsonb + `.execute(db); +} diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index 20f2603ca..c3025bb4b 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -21,7 +21,6 @@ import type { } from '@truefoundry/trueforge-core/core'; import type { CurrentContextUsage } from '@truefoundry/trueforge-core/core/runtime/contextUsage'; import type { ColumnType, Generated, JSONColumnType } from 'kysely'; -import type { AgentMetadata } from '../../schemas/agentMetadata'; import type { McpServerManifest } from '../../schemas/mcpServer'; import type { ModelProviderManifest } from '../../schemas/modelProvider'; import type { SandboxBuildMetadata, SandboxBuildStatus, SandboxProviderManifest } from '../../schemas/sandboxProvider'; @@ -376,8 +375,6 @@ export interface AgentTable { name: string; /** AgentSpec document; replaced whole on every upsert */ manifest: JSONColumnType; - /** `agent.metadata` jsonb; default `{}` for existing rows */ - metadata: JSONColumnType; external_id: string | null; created_at: Date; updated_at: Date; diff --git a/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts b/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts index f9fb814b3..722735475 100644 --- a/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts +++ b/packages/trueforge/src/db/sqlite/agent-store/SqliteAgentStore.ts @@ -1,6 +1,5 @@ import type { AgentSpec } from '@truefoundry/trueforge-core/agent-session'; import type { ExpressionBuilder, Kysely, Transaction } from 'kysely'; -import { EMPTY_AGENT_METADATA, type AgentMetadata } from '../../../schemas/agentMetadata'; import { newId } from '../../../utils/id'; import { AgentExternalIdConflictError, @@ -24,7 +23,6 @@ function recordColumns(eb: ExpressionBuilder) { 'tenant_id' as const, 'name' as const, jsonText(eb.ref('manifest')).as('manifest'), - jsonText(eb.ref('metadata')).as('metadata'), 'external_id' as const, 'created_at' as const, 'updated_at' as const, @@ -36,7 +34,6 @@ function toRecord(row: { tenant_id: string; name: AgentRecord['name']; manifest: AgentSpec; - metadata: AgentMetadata; external_id: string | null; created_at: string; updated_at: string; @@ -85,7 +82,6 @@ export class SqliteAgentStore implements IAgentStore> { tenant_id: input.tenant_id, name: input.name, manifest: jsonbBind(input.manifest), - metadata: jsonbBind(EMPTY_AGENT_METADATA), external_id: input.external_id, created_at: timestamp, updated_at: timestamp, @@ -108,8 +104,8 @@ export class SqliteAgentStore implements IAgentStore> { } async updateAgent(input: UpdateAgentInput, transaction?: Transaction): Promise { - if (input.manifest === undefined && input.metadata === undefined && input.external_id === undefined) { - throw new Error('updateAgent requires manifest, metadata, and/or external_id'); + if (input.manifest === undefined && input.external_id === undefined) { + throw new Error('updateAgent requires manifest and/or external_id'); } const db = transaction ?? this.#db; try { @@ -117,7 +113,6 @@ export class SqliteAgentStore implements IAgentStore> { .updateTable('agent') .set({ ...(input.manifest === undefined ? {} : { manifest: jsonbBind(input.manifest) }), - ...(input.metadata === undefined ? {} : { metadata: jsonbBind(input.metadata) }), ...(input.external_id === undefined ? {} : { external_id: input.external_id }), updated_at: nowIso(), }) diff --git a/packages/trueforge/src/db/sqlite/migrations/20260903_000001_drop_agent_metadata.ts b/packages/trueforge/src/db/sqlite/migrations/20260903_000001_drop_agent_metadata.ts new file mode 100644 index 000000000..4391f2925 --- /dev/null +++ b/packages/trueforge/src/db/sqlite/migrations/20260903_000001_drop_agent_metadata.ts @@ -0,0 +1,116 @@ +import { type Kysely, sql } from 'kysely'; +import { AGENT_EXTERNAL_ID_UQ } from '../../indexes'; + +/** + * Drop agent.metadata. Rebuild: ADD/DROP COLUMN cannot take DEFAULT (jsonb(...)) / STRICT drop. + * Keeps external_id + partial unique index from 20260902_000002. + * DROP TABLE agent needs FKs off (`schedule` REFERENCES it). + * PRAGMA foreign_keys is a no-op inside a txn. + */ +export async function up(db: Kysely): Promise { + await sql`PRAGMA foreign_keys = OFF`.execute(db); + try { + await db.transaction().execute(async trx => { + await sql` + CREATE TABLE agent_new ( + id TEXT NOT NULL, + tenant_id TEXT NOT NULL, + name TEXT NOT NULL, + manifest BLOB NOT NULL, + external_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (id), + UNIQUE (tenant_id, name) + ) STRICT + `.execute(trx); + + await sql` + INSERT INTO agent_new ( + id, + tenant_id, + name, + manifest, + external_id, + created_at, + updated_at + ) + SELECT + id, + tenant_id, + name, + manifest, + external_id, + created_at, + updated_at + FROM agent + `.execute(trx); + + await sql`DROP TABLE agent`.execute(trx); + await sql`ALTER TABLE agent_new RENAME TO agent`.execute(trx); + await sql` + CREATE UNIQUE INDEX ${sql.raw(AGENT_EXTERNAL_ID_UQ)} + ON agent (tenant_id, external_id) + WHERE external_id IS NOT NULL + `.execute(trx); + }); + } finally { + await sql`PRAGMA foreign_keys = ON`.execute(db); + } +} + +export async function down(db: Kysely): Promise { + await sql`PRAGMA foreign_keys = OFF`.execute(db); + try { + await db.transaction().execute(async trx => { + await sql`DROP INDEX IF EXISTS ${sql.raw(AGENT_EXTERNAL_ID_UQ)}`.execute(trx); + await sql` + CREATE TABLE agent_old ( + id TEXT NOT NULL, + tenant_id TEXT NOT NULL, + name TEXT NOT NULL, + manifest BLOB NOT NULL, + metadata BLOB NOT NULL DEFAULT (jsonb('{}')), + external_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + PRIMARY KEY (id), + UNIQUE (tenant_id, name) + ) STRICT + `.execute(trx); + + await sql` + INSERT INTO agent_old ( + id, + tenant_id, + name, + manifest, + metadata, + external_id, + created_at, + updated_at + ) + SELECT + id, + tenant_id, + name, + manifest, + jsonb('{}'), + external_id, + created_at, + updated_at + FROM agent + `.execute(trx); + + await sql`DROP TABLE agent`.execute(trx); + await sql`ALTER TABLE agent_old RENAME TO agent`.execute(trx); + await sql` + CREATE UNIQUE INDEX ${sql.raw(AGENT_EXTERNAL_ID_UQ)} + ON agent (tenant_id, external_id) + WHERE external_id IS NOT NULL + `.execute(trx); + }); + } finally { + await sql`PRAGMA foreign_keys = ON`.execute(db); + } +} diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index 8cd9c0184..57343d86a 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -24,7 +24,6 @@ import type { } from '@truefoundry/trueforge-core/core'; import type { CurrentContextUsage } from '@truefoundry/trueforge-core/core/runtime/contextUsage'; import type { ColumnType, Generated, JSONColumnType } from 'kysely'; -import type { AgentMetadata } from '../../schemas/agentMetadata'; import type { McpServerManifest } from '../../schemas/mcpServer'; import type { ModelProviderManifest } from '../../schemas/modelProvider'; import type { SandboxBuildMetadata, SandboxBuildStatus, SandboxProviderManifest } from '../../schemas/sandboxProvider'; @@ -222,8 +221,6 @@ export interface AgentTable { name: string; /** AgentSpec document; replaced whole on every upsert */ manifest: JsonbColumn; - /** `agent.metadata` jsonb; default `{}` for existing rows */ - metadata: JsonbColumn; external_id: string | null; created_at: string; updated_at: string; diff --git a/packages/trueforge/src/schemas/agent.ts b/packages/trueforge/src/schemas/agent.ts index dd14dac44..0c484c67f 100644 --- a/packages/trueforge/src/schemas/agent.ts +++ b/packages/trueforge/src/schemas/agent.ts @@ -15,7 +15,7 @@ export const CreateAgentRequestSchema = z .strict() .openapi('CreateAgentRequest'); -/** PUT body: full manifest replacement only (metadata is store-internal, not on the wire). */ +/** PUT body: full manifest replacement only. */ export const UpdateAgentRequestSchema = z .object({ manifest: AgentSpecSchema, diff --git a/packages/trueforge/src/schemas/agentMetadata.ts b/packages/trueforge/src/schemas/agentMetadata.ts deleted file mode 100644 index 3b9f7e7cd..000000000 --- a/packages/trueforge/src/schemas/agentMetadata.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Agent-row metadata jsonb (internal only; not on the public Agent API). - * Empty until keys are whitelisted here. - */ -export type AgentMetadata = Record; - -export const EMPTY_AGENT_METADATA: AgentMetadata = {}; diff --git a/packages/trueforge/tests/db/agentStoreContractSuite.ts b/packages/trueforge/tests/db/agentStoreContractSuite.ts index 46c91cb38..157ec3f8f 100644 --- a/packages/trueforge/tests/db/agentStoreContractSuite.ts +++ b/packages/trueforge/tests/db/agentStoreContractSuite.ts @@ -31,7 +31,6 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { expect(created.name).toBe('research'); expect(created.id.length).toBeGreaterThan(0); expect(created.manifest).toEqual(manifest()); - expect(created.metadata).toEqual({}); expect(created.external_id).toBeNull(); expect(created.created_at).toMatch(ISO_UTC); expect(created.updated_at).toBe(created.created_at); @@ -49,7 +48,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { expect(await store.getAgent({ tenant_id: TENANT, name: 'missing' })).toBeUndefined(); }); - it('updateAgent by id replaces manifest but keeps id, name, metadata, and created_at', async () => { + it('updateAgent by id replaces manifest but keeps id, name, and created_at', async () => { const store = getStore(); const created = await store.createAgent({ tenant_id: TENANT, @@ -70,7 +69,6 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { id: created.id, name: 'research', manifest: replacement, - metadata: {}, created_at: created.created_at, }), ); @@ -83,49 +81,6 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void { expect(await store.getAgent({ tenant_id: TENANT, name: 'research' })).toEqual(updated); }); - it('updateAgent can patch metadata without changing manifest', async () => { - const store = getStore(); - const created = await store.createAgent({ - tenant_id: TENANT, - name: 'research', - manifest: manifest(), - external_id: null, - }); - - const updated = await store.updateAgent({ - tenant_id: TENANT, - id: created.id, - metadata: {}, - }); - - expect(updated).toEqual( - expect.objectContaining({ - id: created.id, - name: 'research', - manifest: created.manifest, - metadata: {}, - created_at: created.created_at, - }), - ); - expect(updated).toBeDefined(); - if (updated === undefined) { - throw new Error('expected updateAgent to return a record'); - } - expect(Date.parse(updated.updated_at)).toBeGreaterThanOrEqual(Date.parse(created.updated_at)); - expect(await store.getAgent({ tenant_id: TENANT, id: created.id })).toEqual(updated); - }); - - it('updateAgent returns undefined for unknown ids when patching metadata', async () => { - const store = getStore(); - expect( - await store.updateAgent({ - tenant_id: TENANT, - id: 'missing', - metadata: {}, - }), - ).toBeUndefined(); - }); - it('updateAgent returns undefined for unknown ids', async () => { const store = getStore(); expect( diff --git a/packages/trueforge/tests/unit/apis/agents.test.ts b/packages/trueforge/tests/unit/apis/agents.test.ts index 0305c84d7..14808cdd6 100644 --- a/packages/trueforge/tests/unit/apis/agents.test.ts +++ b/packages/trueforge/tests/unit/apis/agents.test.ts @@ -109,9 +109,6 @@ describe('agents router', () => { }); expect(createdJson.data).not.toHaveProperty('metadata'); - const beforePut = await agentStore.getAgent({ tenant_id: 'default', id: createdJson.data.id }); - expect(beforePut?.metadata).toEqual({}); - const updated = await router.request(`/${createdJson.data.id}`, jsonInit('PUT', updateBody)); expect(updated.status).toBe(200); const updatedJson = (await updated.json()) as { data: WireAgent }; @@ -119,9 +116,6 @@ describe('agents router', () => { expect(updatedJson.data.name).toBe('research'); expect(updatedJson.data.manifest.instructions).toBe('Updated instructions.'); expect(updatedJson.data).not.toHaveProperty('metadata'); - - const afterPut = await agentStore.getAgent({ tenant_id: 'default', id: createdJson.data.id }); - expect(afterPut?.metadata).toEqual(beforePut?.metadata); }); it('PUT rejects metadata in the request body', async () => {