# 03c · Workspace, Team, Persona, Settings, Profile & Pins API ## Purpose This section is the contract for the management-plane of Waggle OS: how the frontend creates and inspects **workspaces** (and their pre-configured **templates**), manages **teams** and members, lists/creates **personas**, reads/writes **settings** (models, budgets, autonomy, tiers), maintains the **user profile** (identity, writing-style, brand), and pins/favorites chat messages. Every endpoint below is served by the **local Fastify sidecar** (the Node process bundled into the Tauri app, default loopback port `3333`); the frontend talks to it over HTTP with **no auth header** (loopback-trust model). All persistence is local — SQLite (`teams.db`, per-workspace `*.mind`) or JSON files under the data dir (`~/.waggle` by default). > Source files read for this section: > `packages/server/src/local/routes/{workspaces.ts, workspace-templates.ts, team.ts, personas.ts, settings.ts, profile.ts, pins.ts}` and the prompt-builder helpers `packages/server/src/local/{workspace-sessions.ts, workspace-state.ts}` plus `packages/server/src/local/routes/workspace-context.ts`. > > **Important framing for the rebuild:** `workspace-context.ts` and `workspace-state.ts` are **NOT HTTP routes**. They are internal prompt-builder modules (`buildWorkspaceNowBlock`, `buildWorkspaceState`, `formatWorkspaceNowPrompt`) consumed by `chat.ts`, `commands.ts`, and `workspaces.ts` to assemble the agent's system prompt. Their output reaches the frontend only as a sub-object on `GET /api/workspaces/:id/context` (the `workspaceState` field) — see [Workspace "Now"/state model](#the-workspace-now--state-model-internal). The frontend never calls them directly. --- ## 1. Endpoint Reference (every route) All paths are relative to the sidecar base URL (e.g. `http://127.0.0.1:3333`). Responses are JSON unless noted. Tier gating uses the `requireTier(...)` preHandler from `middleware/assert-tier.ts`. ### 1.1 Workspaces — lifecycle (`workspaces.ts`) | Method | Path | Purpose | |---|---|---| | GET | `/api/workspaces` | List all workspaces. Optional `?group=` and `?teamId=` filters. Returns a bare array. | | POST | `/api/workspaces` | Create a workspace (enforces tier `workspaceLimit`). Returns the created workspace (201). | | GET | `/api/workspaces/:id` | Get one workspace by id (404 if missing). | | GET | `/api/workspaces/:id/context` | **The "Workspace Now" catch-up block** — summary, recent threads, suggested prompts, stats, greeting, structured state. Used when opening a workspace. | | GET | `/api/workspaces/:id/files` | List ingested/registered files for the workspace (newest first). | | PUT | `/api/workspaces/:id` | Update workspace (full-ish). `personaId: null` is ignored on PUT (kept). Validates `model`. | | PATCH | `/api/workspaces/:id` | Partial update. `personaId: null` **clears** the persona on PATCH. | | DELETE | `/api/workspaces/:id` | Delete workspace + its mind DB (204). | | GET | `/api/workspaces/:id/export` | Export workspace. `?format=briefing` → markdown; default → JSON dump (memories, pins, sessions). | | GET | `/api/workspaces/:id/cost` | Per-workspace spend vs budget, status (`ok`/`warning`/`exceeded`), 7-day history. | | GET | `/api/workspaces/:id/storage` | Virtual/linked storage stats for the workspace. | | GET | `/api/workspaces/:id/storage/files` | List files in workspace storage (optional `?dir=`). | | GET | `/api/workspaces/:id/storage/read` | Read a file. `?path=` required; `?raw=true` returns raw bytes, else a JSON wrapper. | | POST | `/api/workspaces/:id/storage/write` | Write a file. `?path=` required, body `{ content }`. Returns 201. | | DELETE | `/api/workspaces/:id/storage/delete` | Delete a file. `?path=` required. Returns 204. | > **Persona switching** is done through these workspace update routes: set `personaId` on the workspace (PUT/PATCH). There is no dedicated `/api/personas/active` endpoint. A per-request persona override is also accepted by the chat route (`persona` field in the chat body, out of scope here). The workspace's `personaId` is the persistent default. ### 1.2 Workspace Templates (`workspace-templates.ts`) | Method | Path | Purpose | |---|---|---| | GET | `/api/workspace-templates` | List all templates: 15 built-in + user-created. Returns `{ templates, count }`. | | POST | `/api/workspace-templates` | Create a custom template (validated). Returns the template (with generated `id`). | | PUT | `/api/workspace-templates/:id` | Update a custom template. 403 if `id` is built-in, 404 if not found. | | DELETE | `/api/workspace-templates/:id` | Delete a custom template. 403 if built-in, 404 if not found. Returns `{ ok: true }`. | | POST | `/api/workspace-templates/generate` | AI-generate a template config from a prompt (needs Anthropic key in `settings.json`). | ### 1.3 Team (`team.ts`) Two sub-families: **team-server connection** (proxy to a remote Teams server) and **local team CRUD** (works solo with a local user id, stored in `teams.db`). | Method | Path | Purpose | |---|---|---| | POST | `/api/team/connect` | Connect to a remote team server (validate token via its `/health`). **Tier-gated: `TEAMS`.** Stores config; never echoes token. | | POST | `/api/team/disconnect` | Clear stored team-server config. Returns `{ disconnected: true }`. | | GET | `/api/team/status` | Current connection status `{ connected, serverUrl?, userId?, displayName? }`. | | GET | `/api/team/teams` | List teams from the remote server (proxied). 401 if not connected. | | GET | `/api/team/members` | List members from remote server, or local fallback `[{ id:'local', name:'You', status:'online' }]`. | | GET | `/api/team/presence` | Presence (`?workspaceId=`). Remote proxy, else self-as-online fallback. Emits `presence_update` on the event bus. | | GET | `/api/team/activity` | Recent activity (`?workspaceId=&limit=`, max 50). Maps remote `memory_frame` entities to activity items. Empty if disconnected. | | GET | `/api/team/messages` | Recent WaggleDance messages (`?workspaceId=&limit=`, max 50). Emits a `message` notification if any. | | GET | `/api/team/governance/permissions` | Effective capability permissions (`?workspaceId=`). **Tier-gated: `ENTERPRISE`.** 5-min in-memory cache; returns stale on fetch failure. | | GET | `/api/team/memory/search` | Search team memory frames (`?q=&limit=`, max 50). 400 if not connected or `q` missing. Client-side keyword filter. | | POST | `/api/teams` | Create a local team. Body `{ name, description? }`. Auto-adds creator as `owner`. Returns 201 with members. | | GET | `/api/teams` | List teams the local user belongs to `{ teams: [...] }`. | | GET | `/api/teams/:id` | Team detail + members + linked workspaces. 404 if missing. | | PUT | `/api/teams/:id` | Update team name/description. Requires `owner` or `admin`. | | DELETE | `/api/teams/:id` | Delete team (owner only). Unlinks workspaces. Returns 204. | | POST | `/api/teams/:id/members` | Add/invite member. Body `{ userId?, email?, displayName?, role? }`. Requires owner/admin. 409 if already member. | | PUT | `/api/teams/:id/members/:userId` | Change member role. **Owner only.** | | PATCH | `/api/teams/:id/members/:userId` | Change member role (alias for PUT). **Owner or admin.** | | DELETE | `/api/teams/:id/members/:userId` | Remove member. Owner/admin can remove anyone; a member can remove self. Cannot remove the owner. | | GET | `/api/teams/:id/activity` | Aggregated audit events across the team's workspaces (`?limit=` max 200, `?from=` ISO; defaults to last 7 days). | > Note: a **second, different** `teamRoutes` exists at `packages/server/src/routes/teams.ts` registered by the *non-local* server `index.ts`. This section documents the **local** sidecar version (`local/routes/team.ts`), which is what the desktop frontend hits. ### 1.4 Personas (`personas.ts`) | Method | Path | Purpose | |---|---|---| | GET | `/api/personas` | List persona catalog (no system prompts). Returns `{ personas: [...] }`. | | POST | `/api/personas` | Create a custom persona. **Tier-gated: `PRO`.** Requires `name` + `systemPrompt`. 409 if id collides with built-in. Returns 201. | | PATCH | `/api/personas/:id` | Update a custom persona (merge). 403 for built-ins, 404 if custom not found. | | POST | `/api/personas/generate` | AI-generate a persona from a prompt. **Tier-gated: `PRO`.** 503 if LLM unavailable. | | DELETE | `/api/personas/:id` | Delete a custom persona. 403 for built-ins, 404 if not found. Returns `{ deleted: true, id }`. | ### 1.5 Settings, Tier, Budget, Cloud Sync, Admin (`settings.ts`) | Method | Path | Purpose | |---|---|---| | GET | `/api/settings` | Read config: models, budgets, providers (API keys **masked**), `mindPath`, `dataDir`, `litellmUrl`, `onboardingCompleted`. | | PUT | `/api/settings` | Update models/budgets/providers. Secrets go to the encrypted vault; non-secrets to `config.json`. | | PATCH | `/api/settings` | Partial merge for non-provider settings (e.g. `onboardingCompleted`). | | POST | `/api/settings/test-key` | Validate an API key's **format** (no network call). Body `{ provider, apiKey }`. | | GET | `/api/settings/permissions` | Read `{ defaultAutonomy, externalGates, workspaceOverrides }`. | | PUT | `/api/settings/permissions` | Save permission settings. Accepts new `defaultAutonomy` enum or legacy `yoloMode` boolean. | | GET | `/api/tier` | **Authoritative tier source** for the frontend (effective tier, trial days, capabilities, usage, legacy `limits`). | | PATCH | `/api/tier` | Dev/test tier override. **403 unless `WAGGLE_ALLOW_TIER_OVERRIDE=1`** (fail-closed). | | POST | `/api/tier/start-trial` | Atomically start the 15-day TRIAL. **409 if already started** (idempotent — one trial per install). | | GET | `/api/cloud-sync` | Cloud-sync availability/enabled/connected status. | | POST | `/api/cloud-sync/toggle` | Enable/disable cloud sync. **Tier-gated: `TEAMS`.** Body `{ enabled }`. | | GET | `/api/admin/overview` | Admin dashboard data (usage, workspaces, plugins). **Tier-gated: `TEAMS`.** | | GET | `/api/admin/audit-export` | Export audit log. **Tier-gated: `TEAMS`.** `?format=json|csv`, `?from=&to=` ISO. | ### 1.6 User Profile (`profile.ts`) | Method | Path | Purpose | |---|---|---| | GET | `/api/profile` | Full profile (identity, writingStyle, brand, interests, meta). | | PUT | `/api/profile` | Partial-merge update. Also mirrors identity into personal memory frames. | | POST | `/api/profile/analyze-style` | LLM-analyze a writing sample (min 50 chars) → fills `writingStyle`. | | POST | `/api/profile/analyze-brand` | LLM-extract brand colors/fonts from a description → fills `brand`. | | GET | `/api/profile/style` | Writing-style summary (for agent injection). | | GET | `/api/profile/brand` | Brand profile (for document-generation tools). | | POST | `/api/profile/research` | LLM-research the user/company → writes a bio. 400 if no name/company set. | ### 1.7 Pins (`pins.ts`) | Method | Path | Purpose | |---|---|---| | GET | `/api/workspaces/:id/pins` | List pinned messages for a workspace `{ pins }`. | | POST | `/api/workspaces/:id/pins` | Add a pin. Body `{ messageContent, messageRole, label? }`. Returns 201 `{ pin }`. | | PATCH | `/api/workspaces/:id/pins/:pinId` | Update a pin's `status` (`draft`/`final`) or `label`. 404 if not found. | | DELETE | `/api/workspaces/:id/pins/:pinId` | Remove a pin. 404 if not found. Returns `{ ok: true }`. | --- ## 2. Data Shapes (real field names & types) ### 2.1 Workspace object (returned by GET/POST/PUT `/api/workspaces`) The workspace object is produced by `server.workspaceManager`. Observed/used fields: | Field | Type | Notes | |---|---|---| | `id` | string | Workspace id. Used as a path segment (validated by `assertSafeSegment`). | | `name` | string | Required on create. | | `group` | string | Required on create. Filterable via `?group=`. | | `icon` | string? | | | `model` | string? | Validated against `/^[a-zA-Z0-9][\w./-]*$/`. | | `personaId` | string? | The workspace's default persona. `PATCH ... { personaId: null }` clears it. | | `agentGroupId` | string? | | | `directory` | string? | Linked local filesystem dir (if any). | | `tone` | `'professional'\|'casual'\|'technical'\|'legal'\|'marketing'`? | | | `templateId` | string? | Set from `template`/`templateId` on create. | | `storageType` | `'virtual'\|'local'\|'team'`? | | | `storagePath` / `storageConfig` | string? / object? | For local/team storage. | | `teamId` / `team` | string? | Link to a team (`team` is a legacy alias; both are checked). | | `teamServerUrl` / `teamRole` / `teamUserId` | string? / role? / string? | Team-server linkage. | | `budget` | number? | Per-workspace dollar budget (used by `/cost`). | | `created` | string (ISO) | | **POST `/api/workspaces` request body** accepts all of: `name`(req), `group`(req), `icon`, `model`, `personaId`, `directory`, `template`, `templateId`, `tone`, `storageType`, `storagePath`, `storageConfig`, `teamId`, `teamServerUrl`, `teamRole`, `teamUserId`. **Create side effects (frontend should expect these to "just happen"):** auto-create file dir structure; auto-install starter skills (first time); seed `template.starterMemory` as visible memory frames (unless `blank`); install a template capability-pack (best-effort); register on team server if `teamId`+`teamServerUrl`; emit `workspace_create` audit event + telemetry. ### 2.2 `GET /api/workspaces/:id/context` response (the catch-up block) ```jsonc { "workspace": { "id", "name", "group", "model", "directory", "templateId", "personaId" }, "summary": "string", // narrative; falls back to a generic line for empty ws "recentThreads": [ { "id", "title", "lastActive" } ], // top 5 by mtime "recentDecisions": [ { "content", "date" } ], // up to 5 "suggestedPrompts": ["string"], // contextual; onboarding prompts if brand-new "recentMemories": [ { "content", "importance", "date" } ], "progressItems": [ { "type", "content", ... } ], // up to 10 "stats": { "memoryCount", "sessionCount", "fileCount" }, "lastActive": "ISO", "greeting": "string", // time-aware + inactivity-aware "pendingTasks": ["string"], // up to 5 (tasks + blockers) "upcomingSchedules": ["string"], // next 3 cron schedules, "name at