diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 2b87a556..665b47c4 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -253,8 +253,10 @@ export { export { createCompanySchema, updateCompanySchema, + updateCompanyBrandingSchema, type CreateCompany, type UpdateCompany, + type UpdateCompanyBranding, createAgentSchema, createAgentHireSchema, updateAgentSchema, diff --git a/packages/shared/src/validators/company.ts b/packages/shared/src/validators/company.ts index bb4851f4..d3e77af3 100644 --- a/packages/shared/src/validators/company.ts +++ b/packages/shared/src/validators/company.ts @@ -22,3 +22,13 @@ export const updateCompanySchema = createCompanySchema }); export type UpdateCompany = z.infer; + +/** Branding-only subset that CEO agents may update. */ +export const updateCompanyBrandingSchema = z.object({ + name: z.string().min(1).optional(), + description: z.string().nullable().optional(), + brandColor: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(), + logoAssetId: logoAssetIdSchema, +}); + +export type UpdateCompanyBranding = z.infer; diff --git a/packages/shared/src/validators/index.ts b/packages/shared/src/validators/index.ts index ed9dcdd0..6979e467 100644 --- a/packages/shared/src/validators/index.ts +++ b/packages/shared/src/validators/index.ts @@ -15,8 +15,10 @@ export { export { createCompanySchema, updateCompanySchema, + updateCompanyBrandingSchema, type CreateCompany, type UpdateCompany, + type UpdateCompanyBranding, } from "./company.js"; export { portabilityIncludeSchema, diff --git a/server/src/routes/companies.ts b/server/src/routes/companies.ts index bb6585a2..f11e7cae 100644 --- a/server/src/routes/companies.ts +++ b/server/src/routes/companies.ts @@ -6,11 +6,13 @@ import { companyPortabilityPreviewSchema, createCompanySchema, updateCompanySchema, + updateCompanyBrandingSchema, } from "@paperclipai/shared"; import { forbidden } from "../errors.js"; import { validate } from "../middleware/validate.js"; import { accessService, + agentService, budgetService, companyPortabilityService, companyService, @@ -58,9 +60,12 @@ export function companyRoutes(db: Db) { }); router.get("/:companyId", async (req, res) => { - assertBoard(req); const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); + // Allow agents (CEO) to read their own company; board always allowed + if (req.actor.type !== "agent") { + assertBoard(req); + } const company = await svc.getById(companyId); if (!company) { res.status(404).json({ error: "Company not found" }); @@ -144,23 +149,44 @@ export function companyRoutes(db: Db) { res.status(201).json(company); }); - router.patch("/:companyId", validate(updateCompanySchema), async (req, res) => { - assertBoard(req); + router.patch("/:companyId", async (req, res) => { const companyId = req.params.companyId as string; assertCompanyAccess(req, companyId); - const company = await svc.update(companyId, req.body); + + const actor = getActorInfo(req); + let body: Record; + + if (req.actor.type === "agent") { + // Only CEO agents may update company branding fields + const agentSvc = agentService(db); + const actorAgent = req.actor.agentId ? await agentSvc.getById(req.actor.agentId) : null; + if (!actorAgent || actorAgent.role !== "ceo") { + throw forbidden("Only CEO agents or board users may update company settings"); + } + if (actorAgent.companyId !== companyId) { + throw forbidden("Agent key cannot access another company"); + } + body = updateCompanyBrandingSchema.parse(req.body); + } else { + assertBoard(req); + body = updateCompanySchema.parse(req.body); + } + + const company = await svc.update(companyId, body); if (!company) { res.status(404).json({ error: "Company not found" }); return; } await logActivity(db, { companyId, - actorType: "user", - actorId: req.actor.userId ?? "board", + actorType: actor.actorType, + actorId: actor.actorId, + agentId: actor.agentId, + runId: actor.runId, action: "company.updated", entityType: "company", entityId: companyId, - details: req.body, + details: body, }); res.json(company); }); diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index 054c6a87..5335dcbe 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -266,6 +266,34 @@ PATCH /api/agents/{agentId}/instructions-path | List agents | `GET /api/companies/:companyId/agents` | | Dashboard | `GET /api/companies/:companyId/dashboard` | | Search issues | `GET /api/companies/:companyId/issues?q=search+term` | +| Get company details (CEO/board) | `GET /api/companies/:companyId` | +| Update company branding (CEO/board) | `PATCH /api/companies/:companyId` | +| Upload company logo | `POST /api/companies/:companyId/logo` (multipart file upload) | + +## Company Branding (CEO) + +CEO agents can read and update their company's branding. Board users have full access to all company fields. + +**Readable fields** (via `GET /api/companies/:companyId`): + +All company fields including `name`, `description`, `brandColor`, `logoUrl`, `issuePrefix`. + +**Updatable fields** (CEO agents, via `PATCH /api/companies/:companyId`): + +| Field | Type | Notes | +| ------------- | ------------------------ | ----------------------------------------- | +| `name` | string | Company display name | +| `description` | string \| null | Company description | +| `brandColor` | string \| null | Hex color, e.g. `#FF5733` | +| `logoAssetId` | UUID string \| null | Set after uploading via the logo endpoint | + +**Protected fields** (board-only): `status`, `budgetMonthlyCents`, `spentMonthlyCents`, `requireBoardApprovalForNewAgents`. The `issuePrefix` (company slug) cannot be changed via API. + +**Logo upload flow:** + +1. Upload: `POST /api/companies/:companyId/logo` with `Content-Type: multipart/form-data`, field name `file`. Accepts PNG, JPEG, WebP, GIF, SVG (max 10 MB). +2. Set: `PATCH /api/companies/:companyId` with `{ "logoAssetId": "" }`. +3. Clear: `PATCH /api/companies/:companyId` with `{ "logoAssetId": null }`. ## Searching Issues diff --git a/skills/paperclip/references/api-reference.md b/skills/paperclip/references/api-reference.md index cbf5ef05..6e4ae4cb 100644 --- a/skills/paperclip/references/api-reference.md +++ b/skills/paperclip/references/api-reference.md @@ -280,6 +280,26 @@ GET /api/companies/{companyId}/dashboard — health summary: agent/task counts, Use the dashboard for situational awareness, especially if you're a manager or CEO. +## Company Branding (CEO / Board) + +CEO agents can update branding fields on their own company. Board users can update all fields. + +``` +GET /api/companies/{companyId} — read company (CEO agents + board) +PATCH /api/companies/{companyId} — update company fields +POST /api/companies/{companyId}/logo — upload logo (multipart, field: "file") +``` + +**CEO-allowed fields:** `name`, `description`, `brandColor` (hex e.g. `#FF5733` or null), `logoAssetId` (UUID or null). + +**Board-only fields:** `status`, `budgetMonthlyCents`, `spentMonthlyCents`, `requireBoardApprovalForNewAgents`. + +**Not updateable:** `issuePrefix` (used as company slug/identifier — protected from changes). + +**Logo workflow:** +1. `POST /api/companies/{companyId}/logo` with file upload → returns `{ assetId }`. +2. `PATCH /api/companies/{companyId}` with `{ "logoAssetId": "" }`. + ## OpenClaw Invite Prompt (CEO) Use this endpoint to generate a short-lived OpenClaw onboarding invite prompt: