diff --git a/packages/shared/src/validators/instance.test.ts b/packages/shared/src/validators/instance.test.ts index 735bf026a91..e2a518e99f9 100644 --- a/packages/shared/src/validators/instance.test.ts +++ b/packages/shared/src/validators/instance.test.ts @@ -85,12 +85,6 @@ describe("instance experimental settings validators", () => { expect(settings.enableApps).toBe(false); }); - it("defaults built-in agents off", () => { - const settings = instanceExperimentalSettingsSchema.parse({}); - - expect(settings.enableBuiltInAgents).toBe(false); - }); - it("accepts worktree run execution patches", () => { expect( patchInstanceExperimentalSettingsSchema.parse({ diff --git a/server/src/__tests__/low-trust-red-team-routes.test.ts b/server/src/__tests__/low-trust-red-team-routes.test.ts index 0e9244f227d..8288f557448 100644 --- a/server/src/__tests__/low-trust-red-team-routes.test.ts +++ b/server/src/__tests__/low-trust-red-team-routes.test.ts @@ -111,6 +111,7 @@ async function deleteCompanySkillsAfterLateHeartbeatWritesDrain(db: Db) { await new Promise((resolve) => setTimeout(resolve, 25)); } } + throw lastError; } function expectNoCanary(value: unknown, ...markers: string[]) { diff --git a/server/src/__tests__/openapi-routes.test.ts b/server/src/__tests__/openapi-routes.test.ts index 3d8c85a5f0b..38168e4f044 100644 --- a/server/src/__tests__/openapi-routes.test.ts +++ b/server/src/__tests__/openapi-routes.test.ts @@ -21,6 +21,7 @@ const apiPrefixes: Record = { "assets.ts": "/api", "auth.ts": "/api/auth", "board-chat.ts": "/api", + "briefs.ts": "/api", "built-in-agents.ts": "/api", "cloud.ts": "/api/cloud", "companies.ts": "/api/companies", diff --git a/server/src/app.ts b/server/src/app.ts index fcecb322e17..226b462ec14 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -25,6 +25,7 @@ import { companySkillRoutes } from "./routes/company-skills.js"; import { companySkillPolicyRoutes } from "./routes/company-skill-policy.js"; import { inboxAgentPolicyRoutes } from "./routes/inbox-agent-policy.js"; import { builtInAgentRoutes } from "./routes/built-in-agents.js"; +import { briefsRoutes } from "./routes/briefs.js"; import { folderRoutes } from "./routes/folders.js"; import { summarySlotRoutes } from "./routes/summary-slots.js"; import { statusCardRoutes } from "./routes/status-cards.js"; @@ -419,6 +420,7 @@ export async function createApp( api.use(companySkillPolicyRoutes(db)); api.use(inboxAgentPolicyRoutes(db)); api.use(builtInAgentRoutes(db)); + api.use(briefsRoutes(db)); api.use(summarySlotRoutes(db)); api.use(statusCardRoutes(db)); api.use(teamsCatalogRoutes(db)); diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index 835c54bb769..6261998803c 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -4,6 +4,7 @@ export { companySkillRoutes } from "./company-skills.js"; export { companySkillPolicyRoutes } from "./company-skill-policy.js"; export { inboxAgentPolicyRoutes } from "./inbox-agent-policy.js"; export { builtInAgentRoutes } from "./built-in-agents.js"; +export { briefsRoutes } from "./briefs.js"; export { folderRoutes } from "./folders.js"; export { summarySlotRoutes } from "./summary-slots.js"; export { statusCardRoutes } from "./status-cards.js"; diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index 690c4883e33..c6c1a63f3b6 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -571,6 +571,10 @@ const responses = { description: "Conflict", content: { "application/json": { schema: ErrorSchema } }, }, + preconditionFailed: { + description: "Precondition failed", + content: { "application/json": { schema: ErrorSchema } }, + }, payloadTooLarge: { description: "Payload too large", content: { "application/json": { schema: ErrorSchema } }, @@ -3564,6 +3568,20 @@ registry.registerPath({ responses: { 200: r.ok(), 401: r.unauthorized }, }); +registry.registerPath({ + method: "get", + path: "/api/companies/{companyId}/briefs/overview", + tags: ["briefs"], + summary: "Get Briefs overview", + request: { params: z.object({ companyId: z.string() }) }, + responses: { + 200: r.ok(), + 401: r.unauthorized, + 403: r.forbidden, + 412: r.preconditionFailed, + }, +}); + registry.registerPath({ method: "get", path: "/api/companies/{companyId}/recovery-observability", diff --git a/server/src/services/agents.ts b/server/src/services/agents.ts index 745cca607a3..d4f6a129637 100644 --- a/server/src/services/agents.ts +++ b/server/src/services/agents.ts @@ -615,21 +615,6 @@ export function agentService(db: Db) { }); } - function assertBuiltInAgentMetadataMutationAllowed( - beforeMetadata: unknown, - afterMetadata: unknown, - options?: { allowBuiltInAgentMetadata?: boolean }, - ) { - if (options?.allowBuiltInAgentMetadata) return; - const beforeMarker = readBuiltInAgentMarker(beforeMetadata); - const afterMarker = readBuiltInAgentMarker(afterMetadata); - if (builtInAgentMarkersEqual(beforeMarker, afterMarker)) return; - throw conflict("Built-in agent marker is managed by Paperclip and cannot be edited directly", { - code: "built_in_agent_marker_readonly", - key: beforeMarker?.key ?? afterMarker?.key ?? null, - }); - } - async function updateAgent( id: string, data: Partial, diff --git a/server/src/services/authorization.ts b/server/src/services/authorization.ts index f2016cebc0e..063de1e2710 100644 --- a/server/src/services/authorization.ts +++ b/server/src/services/authorization.ts @@ -1804,21 +1804,6 @@ export function authorizationService(db: Db) { if (policyEffect.kind === "restricted") return denyRestrictedAssignmentPolicy(policyEffect); return grantDecision; } - if (input.action === "agent_config:read") { - return decideWithAgentConfigReadGrant("user", input.actor.userId); - } - if (input.action === "agent_config:update") { - return decideWithProtectedChangeGrants("user", input.actor.userId, { - direct: "agents:configure", - suggest: "agents:suggest-changes", - }); - } - if (input.action === "skill_config:update") { - return decideWithProtectedChangeGrants("user", input.actor.userId, { - direct: "skills:create", - suggest: "skills:suggest-changes", - }); - } return decidePrincipalGrant({ companyId, principalType: "user", diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 54e56fc936a..0c0e851de1d 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -6,6 +6,7 @@ export { feedbackService } from "./feedback.js"; export { companySkillService } from "./company-skills.js"; export { companySkillPolicyService, normalizeSkillPolicySourceType } from "./company-skill-policy.js"; export { folderService } from "./folders.js"; +export { briefsService } from "./briefs.js"; export { agentService, deduplicateAgentName } from "./agents.js"; export { builtInAgentService, diff --git a/ui/src/components/AgentActionButtons.tsx b/ui/src/components/AgentActionButtons.tsx index d2b9ce56131..a062a71af21 100644 --- a/ui/src/components/AgentActionButtons.tsx +++ b/ui/src/components/AgentActionButtons.tsx @@ -195,13 +195,6 @@ export function AgentActionButtons({ pauseConfirm?: { title: string; description: ReactNode }; /** Hide the Terminate action (e.g. built-in agents are undeletable). */ hideTerminate?: boolean; - /** - * When set, pausing prompts a confirmation dialog first (e.g. for built-in - * agents that power a feature). Omit for the immediate-pause default. - */ - pauseConfirm?: { title: string; description: ReactNode }; - /** Hide the Terminate action (e.g. built-in agents are undeletable). */ - hideTerminate?: boolean; /** * Optional inline error reporter. When provided it is used instead of a toast * for action failures (preserves the detail page's inline error banner). When diff --git a/ui/src/lib/queryKeys.ts b/ui/src/lib/queryKeys.ts index b4680ed1899..747de204ffe 100644 --- a/ui/src/lib/queryKeys.ts +++ b/ui/src/lib/queryKeys.ts @@ -157,6 +157,9 @@ export const queryKeys = { builtInAgents: { list: (companyId: string) => ["built-in-agents", companyId] as const, }, + briefs: { + overview: (companyId: string) => ["briefs", companyId, "overview"] as const, + }, summarySlots: { detail: (companyId: string, scopeKind: string, slotKey: string, scopeId?: string | null) => ["summary-slots", companyId, scopeKind, slotKey, scopeId ?? null] as const, diff --git a/ui/src/pages/Briefs.test.tsx b/ui/src/pages/Briefs.test.tsx index fd3b63354f2..459231c1497 100644 --- a/ui/src/pages/Briefs.test.tsx +++ b/ui/src/pages/Briefs.test.tsx @@ -34,6 +34,10 @@ const builtInAgentsApiMock = vi.hoisted(() => ({ const resumeMock = vi.hoisted(() => vi.fn()); +const instanceSettingsApiMock = vi.hoisted(() => ({ + getExperimental: vi.fn(), +})); + vi.mock("@/context/CompanyContext", () => ({ useCompany: () => companyState, })); @@ -58,6 +62,10 @@ vi.mock("@/api/agents", () => ({ agentsApi: { resume: resumeMock }, })); +vi.mock("@/api/instanceSettings", () => ({ + instanceSettingsApi: instanceSettingsApiMock, +})); + vi.mock("@/components/ConfigureBuiltInAgentModal", () => ({ ConfigureBuiltInAgentModal: ({ open }: { open: boolean }) => open ?
: null, @@ -161,7 +169,9 @@ describe("Briefs page", () => { briefsApiMock.overview.mockReset(); builtInAgentsApiMock.list.mockReset(); resumeMock.mockReset(); + instanceSettingsApiMock.getExperimental.mockReset(); breadcrumbState.setBreadcrumbs.mockReset(); + instanceSettingsApiMock.getExperimental.mockResolvedValue({ enableBuiltInAgents: true }); builtInAgentsApiMock.list.mockResolvedValue([makeBuiltInState("ready")]); briefsApiMock.overview.mockResolvedValue(makeOverview()); });