# Mitosis SDK — Full Reference for LLM Agents

> Source of truth for `@mitosislabs/sdk` v0.15.1.
> Exhaustive, deterministic, copy-pasteable. No marketing prose.
> Written for autonomous coding agents that need precise method signatures,
> route paths, request/response shapes, and error semantics.

When in doubt, read the source. The structure of this file mirrors
`src/` so you can map any claim back to a file.

================================================================================
READ THIS FIRST — THE USER'S MEMORY LIVES HERE
================================================================================

The user's organizational memory — their email, calendar, documents, files,
and the people/projects connected to them — is ingested into their Mitosis
office and queryable through this SDK. For ANY question about the user's
life, work, schedule, contacts, or history, query it BEFORE answering from
general knowledge or saying you lack access.

Pick the surface that matches your runtime:

# exec()-style agents (you can run shell commands): use the CLI.
mi cortex ask "<question>" --office <id>        # hybrid retrieval (vector +
                                                # full-text + graph), rendered
                                                # as a cited context block
mi cortex ask "<question>" --office <id> --json # same, raw JSON w/ scores
mi cortex recall "<query>" --office <id>        # semantic-only; hits carry
                                                # source + content excerpt
mi cortex remember "<fact>" --office <id> --agent <you> \
    --source <universalId>...                   # write a derived fact back,
                                                # provenance edges required
mi cortex get <universalId> --office <id>       # original source row
mi cortex status --office <id>                  # coverage + freshness

# MCP runtimes: register the bundled server instead of shelling out.
#   .mcp.json → { "mcpServers": { "cortex-memory": { "command": "mi-cortex-mcp",
#     "env": { "OFFICE_ID": "...", "AGENT_NAME": "...", "OM_ENDPOINT": "..." } } } }
# Tools: cortex_ask / cortex_remember / cortex_freshness.

# TypeScript consumers: same loop, typed (see § CortexAPI).
#   client.cortex(officeId).answer(officeId, { query })  — one-call hybrid
#   client.cortex(officeId).forAgent(officeId, name)     — ask()/remember()

Every result carries a `universal_id` tracing to the exact source row —
cite it. Check `mi cortex status` → `embed_coverage` / `graph_coverage`
to know how complete the memory is before treating an empty result as
"doesn't exist".

================================================================================
0. PACKAGE FACTS
================================================================================

name           : @mitosislabs/sdk
version        : 0.7.3
license        : MIT
type           : module        (ES modules only — Node.js, not browser)
node engines   : >=18
entry main     : dist/index.js
entry types    : dist/index.d.ts
bin            : { "mi": "dist/cli/index.js" }
build          : tsc            (rootDir=src, outDir=dist)
tests          : vitest         (npm test)
runtime deps   : @xmtp/node-sdk, commander, ethereum-cryptography, viem
dev deps       : typescript, vitest, @types/node

Default endpoint:   https://m.mitosislabs.ai     (office-manager API)
Auth (public):      OAuth API key (mi_…)          (run `mi login`; sent as Bearer)
Auth (first-party): office-manager JWT_SECRET     (master key — internal services only)
Keystore root:      ~/.os1                        (config + per-agent keys + sessions)

================================================================================
1. PUBLIC EXPORTS  (`src/index.ts`)
================================================================================

Classes
  MitosisClient                       main entry point, see § 3
  Transport                            authenticated HTTP transport, see § 4
  Keystore                             local secret storage, see § 5
  XMTPChannel                          per-client XMTP session manager, see § 6
  XMTPSession                          single XMTP session handle, see § 6
  OfficesAPI, EmployeesAPI, TasksAPI,
  FilesAPI, CreditsAPI, XMTPAPI,
  IntegrationsAPI, ExtensionsAPI,
  EventsAPI, CallbacksAPI, BackupsAPI,
  EnvAPI, DelegatesAPI, MessagesAPI,
  WorkspaceAPI, RolesAPI, TransferAPI,
  LLMPingAPI, WhatsAppAPI, ChromiumAPI,
  CapabilitiesAPI, ProxyAPI            see § 7
  OS1Error                             extends Error, fields { status, code }

Helpers
  generateJWT(secret, payload, ttl?)   build HS256 JWT
  verifyJWT(token, secret)             verify + decode, throws on bad/expired
  authorizationHeader(secret, userId)  → "Bearer <jwt>"
  generateKeyPair()                    secp256k1 → { publicKey: hex(uncompressed,
                                       04-prefixed,130 chars), privateKey: Uint8Array(32) }
  publicKeyFromPrivate(privateKey)     → hex string
  signRequest(agentId, method, path,
              privateKey)              → { 'X-Agent-Id', 'X-Timestamp', 'X-Signature' }
  verifySignature(timestamp, method,
                  path, sigHex, pubHex) → boolean

Types
  Every interface in `src/types/index.ts` is re-exported with `export type *`.
  See § 9 for the complete list.

================================================================================
2. AUTHENTICATION  (`src/auth/`)
================================================================================

There are two cooperating modes; the `Transport` decides which to use per call.

────────────────────────────────────────────────────────────────────────────────
2.1 JWT (⚠️ FIRST-PARTY / INTERNAL ONLY — master secret; public callers use the OAuth API key)
────────────────────────────────────────────────────────────────────────────────

File:           src/auth/jwt.ts
Algorithm:      HS256 (HMAC-SHA256), base64url, no padding.
Mirrors:        office-manager `internal/auth/jwt.go`.

Header:         { "alg": "HS256", "typ": "JWT" }
Payload type:
  interface JWTPayload {
    botId: string;
    instanceId?: string;
    privateIp?: string;
    userId: string;
    role?: string;
    iat: number;          // unix seconds
    exp: number;          // unix seconds
  }

Functions:
  generateJWT(secret: string,
              payload: Omit<JWTPayload, 'iat' | 'exp'>,
              ttlSeconds = 3600): string
  verifyJWT(token: string, secret: string): JWTPayload
       throws "invalid token format" | "invalid signature" | "token expired"
  authorizationHeader(secret: string,
                      userId: string,
                      botId = ''): string         // returns "Bearer <jwt>"

Implementation notes:
  • Signature comparison uses node:crypto.timingSafeEqual (constant-time).
  • Tokens are always single-shot (no caching). Generated per request.
  • Default TTL on Transport-generated tokens is 3600 s.

────────────────────────────────────────────────────────────────────────────────
2.2 secp256k1 (per-agent ECDSA)
────────────────────────────────────────────────────────────────────────────────

File:           src/auth/secp256k1.ts
Library:        @noble/secp256k1
Mirrors:        office-manager `internal/auth/pubkey_auth.go`.

Canonical signed payload (literal, with real \n newlines):
    {unix_timestamp_seconds}\n{HTTP_METHOD_UPPER}\n{path_with_query_string}

Hash:           SHA-256
Signature:      ECDSA on the secp256k1 curve.
Wire format:    64-byte compact r||s, hex-encoded (toCompactHex()).
Server accepts: compact 64-byte OR DER-encoded (any length).
Time window:    server rejects timestamps off by >60 s.

Headers attached by signRequest:
    X-Agent-Id    : <agentId>
    X-Timestamp   : <unix seconds, decimal>
    X-Signature   : <hex>

Functions:
  generateKeyPair(): { publicKey: string,            // 130-char hex, 04-prefixed
                       privateKey: Uint8Array(32) }
  publicKeyFromPrivate(privateKey: Uint8Array): string
  signRequest(agentId, method, path, privateKey): Promise<SignedHeaders>
  verifySignature(timestamp, method, path, sigHex, pubHex): boolean

Path note: include query string when signing; the server reconstructs
   request URI exactly as the client signed it.

================================================================================
3. MitosisClient  (`src/client.ts`)
================================================================================

Constructor:
    new MitosisClient(config: ClientConfig)

ClientConfig:
    {
      endpoint: string;                              // base URL (no trailing slash)
      jwt?:   { jwtSecret: string; userId?: string };
      agent?: { agentId:   string; signingKey: Uint8Array };
      timeout?: number;                              // ms, default 30000
    }

If only `jwt` is set            → all requests use JWT.
If only `agent` is set          → all requests use secp256k1.
If both are set                 → JWT by default; secp256k1 only when a call
                                   is explicitly marked `asAgent: true`
                                   (see Transport.request below).
Neither set                     → throws on first request:
                                   "No authentication configured."

Static factories:
  MitosisClient.fromConfig(): Promise<MitosisClient>
       – reads ~/.os1/config.json for endpoint
       – reads ~/.os1/keys/jwt.key for the HMAC secret
       – defaults endpoint to https://m.mitosislabs.ai if missing
       – throws if jwt.key not present
  MitosisClient.asAgent(officeId, agentName, endpoint?): Promise<MitosisClient>
       – loads ~/.os1/keys/<officeId>/<agentName>.key
       – returns a client configured with `agent` auth

Instance methods (besides API namespaces):
  health():            Promise<boolean>          GETs `${endpoint}/healthz` (no auth)
  verifyAuth():        Promise<{ ok: boolean; method: string; error?: string }>
                                                      attempts `offices.list()`
  close():             Promise<void>             closes all open XMTP sessions

Members (every one is a typed instance):
  transport, keystore,
  offices, employees, tasks, files, credits, xmtpApi, integrations, extensions,
  events, callbacks, backups, env, delegates, messages, workspace, roles,
  transfer, llmPing, whatsapp, chromium, capabilities, proxy,
  xmtp                  // the XMTPChannel manager

================================================================================
4. Transport  (`src/transport.ts`)
================================================================================

Responsibilities:
  • Inject auth headers (JWT or secp256k1 per call).
  • JSON-encode bodies (skipped for FormData uploads).
  • Apply `timeout` via AbortController.
  • Decode JSON responses (or text, or raw Response).
  • Throw OS1Error on non-2xx (extracts message/error/code from JSON body).

Public methods:
  request<T>(method, path, options?): Promise<T>
      options:
        body?:    unknown                         (JSON or FormData)
        query?:   Record<string, string|number|undefined>
        asAgent?: boolean                         force secp256k1 even with JWT
        raw?:     boolean                         return Response, do not parse
  get<T>(path, query?)
  post<T>(path, body?)
  put<T>(path, body?)
  patch<T>(path, body?)
  delete<T>(path)
  upload<T>(path, filename, data: Buffer|Uint8Array)
                                                  multipart/form-data, field=`file`
  stream(path, query?): AsyncGenerator<{ event?: string; data: string }>
                                                  parses `event:` / `data:` SSE lines
  get endpoint: string                            base URL

Header shape on the wire:
  JWT mode:     Authorization: Bearer <token>
  Agent mode:   X-Agent-Id, X-Timestamp, X-Signature      (no Authorization)

Body precedence:
  body instanceof FormData   → sent as-is, no Content-Type override
  body present otherwise     → JSON.stringify, Content-Type: application/json
  body absent                → no body

================================================================================
5. Keystore  (`src/auth/keystore.ts`)
================================================================================

Default base:   ~/.os1/keys           (override: new Keystore({ basePath: '...' }))
Config file:    ~/.os1/config.json    (NOT inside basePath; lives one level up)
Permissions:    directories 0700, files 0600 (chmod after write)

Layout
  ~/.os1/
  ├── config.json                     # arbitrary JSON ({ endpoint, … })
  ├── keys/
  │   ├── jwt.key                     # HMAC secret (string)
  │   └── <officeId>/<agentName>.key  # secp256k1 private key (32 bytes raw)
  └── sessions/                       # active session JSON files (currently
                                        unused by the SDK itself — reserved)

Methods:
  storeAgentKey(officeId, agentName, privateKey: Uint8Array): Promise<void>
  loadAgentKey(officeId, agentName): Promise<Uint8Array>
       throws "No signing key found for <agent> in office <id>." on ENOENT
  hasAgentKey(officeId, agentName): Promise<boolean>
  generateAndStore(officeId, agentName): Promise<KeyPair>
  storeJWTSecret(secret: string): Promise<void>
  loadJWTSecret(): Promise<string>     throws "No JWT secret found." on ENOENT
  getPublicKey(officeId, agentName): Promise<string>
  listAgentKeys(officeId): Promise<string[]>      // names, .key stripped
  deleteAgentKey(officeId, agentName): Promise<void>
  storeConfig(config: Record<string, unknown>): Promise<void>
  loadConfig(): Promise<Record<string, unknown>>  // {} if missing

================================================================================
6. XMTP — sessions and channels  (`src/xmtp/`)
================================================================================

XMTPChannel is the manager. XMTPSession is the per-conversation handle.
Both use the office-manager's `/api/v1/offices/:officeId/xmtp/...` endpoints
under the hood (they do NOT speak XMTP directly from the SDK process).

────────────────────────────────────────────────────────────────────────────────
6.1 XMTPChannel  (`src/xmtp/channel.ts`)
────────────────────────────────────────────────────────────────────────────────

class XMTPChannel {
  constructor(transport: Transport)

  openSession(officeId, agentName): Promise<XMTPSession>
      // returns existing open session if one exists, else creates a new one
  negotiateSession(officeId, agentName, timeoutMs?): Promise<SessionNegotiation>
      // openSession + session.negotiate(timeoutMs)
  send(officeId, agentName, content): Promise<void>
      // openSession; negotiate if needed; session.send(content)
  getSession(officeId, agentName): XMTPSession | undefined
  closeSession(officeId, agentName): Promise<void>
  closeAll(): Promise<void>
  listSessions(): Array<{ officeId, agentName, sessionId }>   // only open + negotiated
}

Internal key:   `${officeId}:${agentName}`         (deduplicates sessions)

────────────────────────────────────────────────────────────────────────────────
6.2 XMTPSession  (`src/xmtp/session.ts`)
────────────────────────────────────────────────────────────────────────────────

Control message constants (sent JSON-encoded as `content`):
  __SESSION_START__   { type, session_id, capabilities_request, timestamp }
  __SESSION_ACK__     { type, session_id, capabilities? }
  __SESSION_END__     { type, session_id, timestamp }

class XMTPSession {
  constructor(transport, officeId, agentName)
      // sessionId = randomUUID()

  negotiate(timeoutMs = 30_000): Promise<SessionNegotiation>
      1. GET   /api/v1/offices/{officeId}/xmtp/conversations
         find conversation where peer === agentName
      2. If none, conversationId := agentName (chat-server creates lazily)
      3. POST  /api/v1/offices/{officeId}/xmtp/conversations/{conversationId}/messages
              body: { agent_id: 'admin-sdk',
                      content: JSON.stringify(__SESSION_START__) }
      4. Poll  GET .../messages  every 1 s until SESSION_ACK or deadline.
      5. Either way returns SessionNegotiation; if no ACK, capabilities=[].

  send(content: string): Promise<void>
      throws "Session is closed" or "Session not negotiated"
      POST .../messages with { agent_id: 'admin-sdk', content }

  receive(opts?: { limit?: number }): Promise<XMTPMessage[]>
      • GET .../messages?limit=<n||20>
      • returns only messages from agentName whose created_at > cursor
      • advances cursor to newest returned timestamp
      • filters out control frames (anything whose JSON .type starts with "__SESSION_")

  stream(pollIntervalMs = 2_000): AsyncGenerator<XMTPMessage>
      // forever, until close() — calls receive() then sleeps

  close(): Promise<void>
      // best-effort POST __SESSION_END__; sets isOpen → false; idempotent

  get negotiation: SessionNegotiation | null
  get isOpen:      boolean
}

SessionNegotiation:
  {
    sessionId:      string;      // uuid
    officeId:       string;
    agentName:      string;
    conversationId: string;
    capabilities?:  string[];
    startedAt:      string;      // ISO timestamp
  }

────────────────────────────────────────────────────────────────────────────────
6.3 Underlying XMTPAPI routes (used by sessions and exposed for direct use)
────────────────────────────────────────────────────────────────────────────────

GET     /api/v1/offices/:officeId/xmtp/conversations
GET     /api/v1/offices/:officeId/xmtp/messages/{agentName}/{peerName}
POST    /api/v1/offices/:officeId/xmtp/send
GET     /api/v1/offices/:officeId/xmtp/groups
POST    /api/v1/offices/:officeId/xmtp/groups
GET     /api/v1/offices/:officeId/xmtp/groups/{groupId}/messages
POST    /api/v1/offices/:officeId/xmtp/groups/{groupId}/send
POST    /api/v1/offices/:officeId/xmtp/groups/{groupId}/members
DELETE  /api/v1/offices/:officeId/xmtp/groups/{groupId}/members/{agentName}
PATCH   /api/v1/offices/:officeId/xmtp/groups/{groupId}

XMTPSession also uses POST/GET on:
  /api/v1/offices/:officeId/xmtp/conversations/{conversationId}/messages

================================================================================
7. API NAMESPACES  (`src/api/*.ts`)
================================================================================

Notation:
  • Each method's request/response types are listed where non-obvious.
  • All `<officeId>` are UUID strings.
  • All response bodies are JSON unless noted.

────────────────────────────────────────────────────────────────────────────────
7.1 OfficesAPI         (`src/api/offices.ts`)   prefix /api/v1/offices
────────────────────────────────────────────────────────────────────────────────

create(req: CreateOfficeRequest)            POST    /                       → Office
                                                NOTE: routed through dashboard, not OM (CLA-904).
                                                The dashboard dedupes per-user: if the caller
                                                already owns a non-archived colony, the response
                                                is that existing colony with `existing: true` and
                                                the requested `name` is IGNORED. Pass
                                                `forceCreate: true` to bypass.
list()                                       GET     /                       → Office[]
get(officeId)                                GET     /{id}                   → Office
getSettings(officeId)                        GET     /{id}/settings          → OfficeSettings
updateSettings(officeId, settings)           PATCH   /{id}/settings          → OfficeSettings
delete(officeId)                             DELETE  /{id}                   → void
status(officeId)                             GET     /{id}/status            → ClusterStatus
kubeconfig(officeId)                         GET     /{id}/kubeconfig        → string (text)
transfer(officeId, newOwnerId)               POST    /{id}/transfer          → void
                                                body: { new_owner_id }
rotateSecret(officeId)                       POST    /{id}/rotate-secret     → { secret }
setSecret(officeId, secretName, value)       PUT     /{id}/secrets/{name}    → void
                                                body: { value }
suspend(officeId)                            POST    /{id}/suspend           → void
resume(officeId)                             POST    /{id}/resume            → void

CreateOfficeRequest = { name: string, owner_id?: string, forceCreate?: boolean }
ClusterStatus.phase = 'ready' | 'degraded' | 'failed' | 'provisioning'

# Agent-handling rule for offices.create
# When the response contains `existing: true`, the user already owns a colony
# and your requested name was discarded. Do NOT pretend you created what was
# asked for. Surface the existing colony to the user, ask whether to keep
# using it or create a second one, and only retry with `forceCreate: true`
# after explicit confirmation. The dashboard's office-selector UI uses the
# same `forceCreate: true` for its explicit "create new colony" button.

────────────────────────────────────────────────────────────────────────────────
7.2 EmployeesAPI       (`src/api/employees.ts`)
                       prefix /api/v1/offices/:officeId/employees
────────────────────────────────────────────────────────────────────────────────

hire(officeId, req: HireRequest)             POST    /                       → Employee
list(officeId)                               GET     /                       → Employee[]
get(officeId, name)                          GET     /{name}                 → Employee
update(officeId, name, req)                  PATCH   /{name}                 → Employee
delete(officeId, name)                       DELETE  /{name}                 → void
logs(officeId, name, { tail? })              GET     /{name}/logs?tail=…     → EmployeeLogs
activity(officeId, name, ActivityQuery?)     GET     /{name}/activity        → ActivityEvent[]
presence(officeId, name)                     GET     /{name}/presence        → { online, last_seen? }
lastError(officeId, name)                    GET     /{name}/last-error      → { error?, timestamp? }
action(officeId, name, EmployeeAction)       POST    /{name}/action          → unknown
promote(officeId, name, PromoteRequest)      POST    /{name}/promote         → Employee
setSkills(officeId, name, SkillsRequest)     POST    /{name}/skills          → Employee
logStatusEvent(officeId, name, event)        POST    /{name}/events          → void
                                                event: { type, message? }
archive(officeId, name)                      POST    /{name}/archive         → void
restore(officeId, name)                      POST    /{name}/restore         → void
rotateCredentials(officeId, name)            POST    /{name}/credentials/rotate → void
lifecycle(officeId, name, action: string)    POST    /{name}/lifecycle       → void
                                                body: { action }
events(officeId, name)                       GET     /{name}/events          → unknown[]
chatSessions(officeId, name)                 GET     /{name}/chat-sessions   → ChatSession[]

ActivityQuery       = { limit?: number, offset?: number, category?: string, since?: string }
HireRequest         = see § 9 (rich object: name, role, modelTier, skills, channels, …)
PromoteRequest      = { modelTier, provider?, resources? }
SkillsRequest       = { add?: string[], remove?: string[] }

────────────────────────────────────────────────────────────────────────────────
7.3 TasksAPI           (`src/api/tasks.ts`)
                       prefix /api/v1/offices/:officeId/tasks
────────────────────────────────────────────────────────────────────────────────

create(officeId, CreateTaskRequest)          POST    /                       → Task
list(officeId)                               GET     /                       → Task[]
get(officeId, taskId)                        GET     /{id}                   → Task
stats(officeId)                              GET     /stats                  → TaskStats

CreateTaskRequest = { title, description?, kind?, priority?, assigned_to? }
TaskStats         = { total, queued, running, completed, failed }

────────────────────────────────────────────────────────────────────────────────
7.4 FilesAPI           (`src/api/files.ts`)
                       prefix /api/v1/offices/:officeId/files
────────────────────────────────────────────────────────────────────────────────

upload(officeId, filename, data)             POST    /                       → void
                                                multipart, field=`file`
list(officeId)                               GET     /                       → FileInfo[]
download(officeId, filename)                 GET     /{filename}             → Response (raw)
delete(officeId, filename)                   DELETE  /{filename}             → void
changes(officeId, since?)                    GET     /_changes?since=…       → FileChangesResponse
getPermissions(officeId, filename)           GET     /{filename}/permissions → FilePermission[]
setPermissions(officeId, filename, perms)    PUT     /{filename}/permissions → void

FileInfo            = { name, size, modifiedAt }
FileChange          = { type: 'upload' | 'delete', name, timestamp }
FileChangesResponse = { events: FileChange[], serverTime, full_refresh? }
FilePermission      = { agent_name, access: 'none' | 'read' | 'write' }

`download` returns the raw `Response`. To get a buffer:
    const r = await mi.files.download(officeId, name);
    const buf = Buffer.from(await r.arrayBuffer());

────────────────────────────────────────────────────────────────────────────────
7.5 CreditsAPI         (`src/api/credits.ts`)
                       mixed prefixes under /api/v1/offices/:officeId
────────────────────────────────────────────────────────────────────────────────

balance(officeId)                            GET     /credits                → CreditBalance
add(officeId, AddCreditsRequest)             POST    /credits                → CreditBalance
history(officeId, { limit?, offset? })       GET     /credits/history        → CreditHistoryEntry[]
usage(officeId)                              GET     /usage                  → unknown
usageCurrent(officeId)                       GET     /usage/current          → unknown
usageSummary(officeId)                       GET     /usage/summary          → UsageSummary
llmUsage(officeId)                           GET     /usage/llm              → unknown
llmUsageSummary(officeId)                    GET     /usage/llm/summary      → LLMUsageSummary
agentUsage(officeId, name)                   GET     /employees/{name}/usage → unknown
quota(officeId)                              GET     /quota                  → Quota
setQuota(officeId, SetQuotaRequest)          PUT     /quota                  → Quota

CreditBalance      = { office_id, balance }
AddCreditsRequest  = { amount: number, reason: string }
CreditHistoryEntry = { id, office_id, amount, balance_after, reason, created_at }
UsageSummary       = { cpu_core_hours, memory_gib_hours, total_credits, pods[] }
LLMUsageSummary    = { total_tokens, total_cost, total_credits, agents[] }
Quota              = { cpu, memory, pods, tier }
SetQuotaRequest    = { tier, cpu?, memory?, pods? }

────────────────────────────────────────────────────────────────────────────────
7.6 XMTPAPI            (`src/api/xmtp.ts`)
                       prefix /api/v1/offices/:officeId/xmtp
────────────────────────────────────────────────────────────────────────────────

listConversations(officeId)                  GET     /conversations          → XMTPConversation[]
getMessages(officeId, agent, peer, {limit?}) GET     /messages/{agent}/{peer} → XMTPMessage[]
send(officeId, SendXMTPMessageRequest)       POST    /send                   → void
listGroups(officeId)                         GET     /groups                 → XMTPGroup[]
createGroup(officeId, CreateGroupRequest)    POST    /groups                 → XMTPGroup
getGroupMessages(officeId, gid, {limit?})    GET     /groups/{gid}/messages  → XMTPMessage[]
sendGroupMessage(officeId, gid, req)         POST    /groups/{gid}/send      → void
addMember(officeId, gid, agentName)          POST    /groups/{gid}/members   → void
removeMember(officeId, gid, agentName)       DELETE  /groups/{gid}/members/{agent} → void
renameGroup(officeId, gid, name)             PATCH   /groups/{gid}           → void
                                                body: { name }

SendXMTPMessageRequest = { agent_id, content }
CreateGroupRequest     = { name?, members: string[] }

────────────────────────────────────────────────────────────────────────────────
7.7 IntegrationsAPI    (`src/api/integrations.ts`)
                       /api/v1/offices/:officeId/{provider-models,integrations}
────────────────────────────────────────────────────────────────────────────────

listProviderModels(officeId)                 GET     /provider-models        → ModelInfo[]
ensureSecret(officeId, integrationId, key)   POST    /integrations/{id}/secret → void
                                                body: { key }
deleteSecret(officeId, integrationId)        DELETE  /integrations/{id}/secret → void
toggleAgent(officeId, integrationId,
            agentName, enabled)              POST    /integrations/{id}/agents/{agent} → void
                                                body: { enabled }
syncClaudeCodeKey(officeId)                  POST    /integrations/claude-code/sync-key → void

ModelInfo         = { provider, model_id, name, available }
IntegrationSecret = { provider, has_key, updated_at? }
SetSecretRequest  = { provider, key }    // (used by some legacy flows)

────────────────────────────────────────────────────────────────────────────────
7.8 ExtensionsAPI      (`src/api/extensions.ts`)
                       per-office /api/v1/offices/:officeId/extensions
                       global   /api/v1/marketplace
────────────────────────────────────────────────────────────────────────────────

register(officeId, CreateExtensionRequest)   POST    /extensions             → Extension
list(officeId)                               GET     /extensions             → Extension[]
get(officeId, extId)                         GET     /extensions/{id}        → Extension
delete(officeId, extId)                      DELETE  /extensions/{id}        → void
install(officeId, { extId })                 POST    /extensions/install     → void
uninstall(officeId, extId)                   DELETE  /extensions/{id}/uninstall → void
marketplaceList()                            GET     /api/v1/marketplace     → MarketplaceItem[]
marketplaceGet(extId)                        GET     /api/v1/marketplace/{id}→ MarketplaceItem
marketplacePublish(data)                     POST    /api/v1/marketplace/publish → void

CreateExtensionRequest = { name, manifest?, panel_html? }

────────────────────────────────────────────────────────────────────────────────
7.9 EventsAPI / CallbacksAPI / BackupsAPI / EnvAPI / DelegatesAPI /
    MessagesAPI / WorkspaceAPI / RolesAPI / TransferAPI / LLMPingAPI /
    WhatsAppAPI / ChromiumAPI / CapabilitiesAPI / ProxyAPI
                       (all in `src/api/events.ts`)
────────────────────────────────────────────────────────────────────────────────

EventsAPI    /api/v1/offices/:officeId
  list()                                      GET     /events                 → unknown[]
  chatEvents(officeId, sessionKey)            GET     /events/chat/{key}      → unknown[]
  sshEvents(officeId)                         GET     /events/ssh             → unknown[]

CallbacksAPI
  podEvent(officeId, PodEventRequest)         POST    /callbacks/pod-event    → void
  list(officeId)                              GET     /callbacks              → PodCallback[]
  get(officeId, callbackId)                   GET     /callbacks/{id}         → PodCallback
  PodEventRequest = { type, payload? }

BackupsAPI
  list(officeId, { employee? })               GET     /backups                → Backup[]
  get(officeId, backupId)                     GET     /backups/{id}           → Backup
  delete(officeId, backupId)                  DELETE  /backups/{id}           → void

EnvAPI
  list(officeId)                              GET     /env                    → EnvVar[]
  values(officeId)                            GET     /env/values             → Record<string,string>
  set(officeId, vars)                         PUT     /env                    → void
                                                body: Record<string,string>
  delete(officeId, key)                       DELETE  /env/{key}              → void
  getAgentEnv(officeId, name)                 GET     /employees/{name}/env   → Record<string,string>
  setAgentEnv(officeId, name, key, value)     PUT     /employees/{name}/env/{key} → void
                                                body: { value }

DelegatesAPI
  list(officeId)                              GET     /delegates              → Delegate[]
  get(officeId, agentId)                      GET     /delegates/{id}         → Delegate
  create(officeId, agentId, CreateDelegateRequest)
                                              POST    /delegates/{id}         → Delegate
  update(officeId, agentId, partial)          PATCH   /delegates/{id}         → Delegate
  delete(officeId, agentId)                   DELETE  /delegates/{id}         → void
  CreateDelegateRequest = { permissions: string[] }

MessagesAPI
  send(officeId, { to, content })             POST    /messages/send          → void
  poolStats(officeId)                         GET     /messages/pool-stats    → PoolStats
  stream(officeId)                            GET     /messages/stream        → SSE generator
                                                yields { event?, data }

WorkspaceAPI
  exec(officeId, { command, timeout_ms? })    POST    /workspace/exec         → ExecResponse
                                                ExecResponse = { stdout, stderr, exit_code }
  health(officeId)                            GET     /workspace/health       → { healthy }

RolesAPI
  list(officeId)                              GET     /roles                  → Role[]
  get(officeId, name)                         GET     /roles/{name}           → Role

TransferAPI            (agent-pod transfer between offices)
  prepare(officeId)                           POST    /agents/prepare         → { transfer_id }
  install(officeId, data)                     POST    /agents/install         → void
  start(officeId, data)                       POST    /agents/start           → void
  status(officeId, transferId)                GET     /agents/transfer/{id}   → TransferStatus

LLMPingAPI
  ping(officeId)                              POST    /llm-ping               → PingResult
  agentPing(officeId, name)                   POST    /employees/{name}/llm-ping → PingResult

WhatsAppAPI
  qrStart(officeId, name)                     POST    /employees/{name}/whatsapp/qr-start  → void
  qrStatus(officeId, name)                    GET     /employees/{name}/whatsapp/qr-status → WhatsAppStatus
  qrStop(officeId, name)                      POST    /employees/{name}/whatsapp/qr-stop   → void
  sessionStatus(officeId, name)               GET     /employees/{name}/whatsapp/status    → WhatsAppStatus
  reset(officeId, name)                       POST    /employees/{name}/whatsapp/reset     → void
  agentStatus(officeId)                       GET     /whatsapp/agents                     → unknown
  toggleAgentAccess(officeId, agent, enabled) POST    /whatsapp/agents/{agent}/access      → void
                                                body: { enabled }
  registerAgent(officeId, agent)              POST    /whatsapp/agents/{agent}/register    → void
  officeQRStart(officeId)                     POST    /whatsapp/qr-start                   → void
  officeQRStatus(officeId)                    GET     /whatsapp/qr-status                  → unknown
  officeReset(officeId)                       POST    /whatsapp/reset                      → void

ChromiumAPI            (per-office headless browser)
  start(officeId)                             POST    /chromium/start         → ChromiumInstance
  status(officeId)                            GET     /chromium/status        → ChromiumInstance
  done(officeId)                              POST    /chromium/done          → void
  delete(officeId)                            DELETE  /chromium               → void

CapabilitiesAPI
  self(officeId)                              GET     /capabilities/self      → unknown

ProxyAPI               (teardowns for code/codex IDE proxies)
  teardownCodeProxy(officeId)                 DELETE  /code-proxy             → void
  teardownCodexProxy(officeId)                DELETE  /codex-proxy            → void

================================================================================
8. CLI  (`src/cli/index.ts`)
================================================================================

Binary:        `mi`           (entry: dist/cli/index.js, shebang `#!/usr/bin/env node`)
Internal name: `os1-admin`     (printed in --help)
Library:       commander@12

Commands (verbatim):

mi init [--endpoint <url>] [--secret <secret>]
mi auth test
mi auth token [--user <id>] [--ttl <s>]

mi login                                       # browser OAuth (localhost callback OR auto-device-code on remote/non-TTY)
mi login --token <mi_key>                      # headless paste — for hosts where the user's browser cannot reach the CLI
mi login --device-code                         # force device-code flow (RFC 8628) on an interactive TTY
mi login --device-code-start [--force] [--json]    # CLA-1184: AI-agent flow, step 1 — mint + persist ~/.os1/pending-device-code.json, exit
mi login --device-code-claim [--json]              # CLA-1184: AI-agent flow, step 2 — poll once; exit 0=approved, 75=still pending, 1=terminal/missing
mi login --device-code-status [--json]             # CLA-1184: read-only state probe (never persists key, never mutates file)

# AI-agent login pattern (CLA-1184)
# Each `exec()` is a fresh process. The monolithic `mi login` won't survive
# a short-timeout exec(), so split it across calls — the AGENT owns the loop:
#
#   1. mi login --device-code-start --json
#        emits {user_code, verification_uri_complete, expires_at, ...}
#   2. Send verification_uri_complete to the human.
#   3. Loop in your scheduler (every 5-10s):
#        mi login --device-code-claim --json
#        exit 0  → logged in (key persisted to ~/.os1/config.json)
#        exit 75 → still pending; retry later
#        exit  1 → terminal (expired/denied/consumed/missing pending file)
#   4. After exit 0, all `mi backup *` commands authenticate from config.json.
#
# Pending file: ~/.os1/pending-device-code.json (0600, atomic writes, version 1).
# If --device-code-start runs and an unexpired pending file already exists,
# it is reused (same user_code printed) — pass --force to mint fresh.

mi keys generate --office <id> --agent <name>
mi keys list     --office <id>
mi keys pubkey   --office <id> --agent <name>

mi colonies list                              # alias: mi offices list
mi colonies create --name <name> [--force]    # --force bypasses per-user dedup
mi colonies status <colonyId>
mi colonies delete <colonyId>

mi agents list     --office <id>
mi agents hire     --office <id> --name <name> [--role <r>] [--model <m>]
mi agents fire     <officeId> <name>
mi agents get      <officeId> <name>
mi agents logs     <officeId> <name> [--tail <n>]
mi agents activity <officeId> <name> [--limit <n>] [--category <c1,c2>]

mi chat <officeId> <agentName>            # interactive XMTP loop
mi send <officeId> <agentName> <message>  # one-shot

mi tasks list   --office <id>
mi tasks create --office <id> --title <t> [--description <d>] [--kind <k>] [--assign <agent>]
mi tasks stats  --office <id>

mi files ls       --office <id>
mi files upload   <officeId> <localPath> [--name <remote>]
mi files download <officeId> <filename>  [--output <path>]

mi credits balance --office <id>
mi credits add     --office <id> --amount <n> --reason <text>

mi cortex ask      <query> --office <id> [--limit <n>] [--agent <name>] [--json]
                                          # hybrid retrieval → cited context block
mi cortex answer   <query> --office <id> [--limit <n>] [--include-raw]
                                          # raw JSON: RRF scores, signals, freshness
mi cortex recall   <query> --office <id> [--limit <n>]   # semantic-only + excerpts
mi cortex remember <text>  --office <id> --agent <name> [--kind <k>]
                   [--confidence <0-1>] [--source <universalId>...]
mi cortex get      <universalId> --office <id>           # original source row
mi cortex status   --office <id>                         # coverage + freshness

mi api <METHOD> <path> [--data '<json>'] [--agent <name> --office <id>]

CLI helpers:
  • `getClient()` → MitosisClient.fromConfig() — exits 1 with hint if missing.
  • `json(data)`  → console.log(JSON.stringify(data, null, 2)).
  • Interactive `chat` uses readline + a 2 s polling loop on `xmtpSession.receive()`.

================================================================================
9. TYPE DEFINITIONS  (`src/types/index.ts`)
================================================================================

Auth
  JWTAuthConfig          { jwtSecret, userId? }
  AgentAuthConfig        { agentId, signingKey: Uint8Array(32) }
  ClientConfig           { endpoint, jwt?, agent?, timeout? }
  SignedHeaders          { 'X-Agent-Id', 'X-Timestamp', 'X-Signature' }
  JWTPayload             { botId, instanceId?, privateIp?, userId, role?, iat, exp }

Keystore
  KeyPair                { publicKey: hex, privateKey: Uint8Array(32) }
  KeystoreConfig         { basePath? }

Office
  Office                 { id, name, owner_id, created_at, settings?, existing? }
                         existing?: true is set when offices.create returned
                         the caller's pre-existing colony (per-user dedup).
                         The requested name was IGNORED in that case.
  CreateOfficeRequest    { name, owner_id?, forceCreate? }
                         forceCreate?: true bypasses the per-user dedup so a
                         second colony is actually created.
  OfficeSettings         Record<string, unknown>
  ClusterStatus          { phase, operator, ingress, ingressEndpoint?, message? }
  ComponentStatus        { ready, message? }

Employee  (matches office-manager internal/models/employee.go)
  Employee               { name, role?, modelTier, modelProvider, skills?,
                           channels?, resources?, storage?, selfConfigure?,
                           autoUpdate?, chromium?, webTerminal?, backup?,
                           imageTag?, customConfig?, env?, envSecrets?,
                           systemPrompt?, status }
  EmployeeChannels       { whatsapp?, discord?, telegram?, signal?, xmtp? }
  EmployeeResources      { cpuRequest?, cpuLimit?, memoryRequest?, memoryLimit? }
  BackupConfig           { schedule?, enabled? }
  EmployeeStatus         { phase, ready, gatewayEndpoint?, accessUrl?,
                           accessToken?, lastSeen?, message? }
  AgentKitOwner          { name?, phone?, email?, context? }
  AgentKitConfig         { enabled?, taskQueue?, disableTaskQueue?,
                           neonSecret?, owner?, personality? }
  HireRequest            { name, role?, modelTier?, skills?, channels?,
                           resources?, storage?, selfConfigure?, autoUpdate?,
                           chromium?, webTerminal?, imageTag?, systemPrompt?,
                           env?, envSecrets?, customConfig?, agentKit?, botId?,
                           bedrockCredentials?: { accessKeyId, secretAccessKey, region },
                           restoreFromBackup? }
  UpdateEmployeeRequest  Partial of HireRequest minus name + agentKit
  PromoteRequest         { modelTier, provider?, resources? }
  SkillsRequest          { add?, remove? }
  EmployeeAction         { action, params? }
  EmployeeLogs           { logs, pod }

Tasks
  Task                   { id, office_id, title, description?, kind?,
                           priority?, status, assigned_to?, created_at,
                           completed_at? }
  CreateTaskRequest      { title, description?, kind?, priority?, assigned_to? }
  TaskStats              { total, queued, running, completed, failed }

Files
  FileInfo               { name, size, modifiedAt }
  FileChange             { type: 'upload' | 'delete', name, timestamp }
  FileChangesResponse    { events, serverTime, full_refresh? }
  FilePermission         { agent_name, access: 'none' | 'read' | 'write' }

Credits & usage
  CreditBalance          { office_id, balance }
  AddCreditsRequest      { amount, reason }
  CreditHistoryEntry     { id, office_id, amount, balance_after, reason, created_at }
  UsageSummary           { cpu_core_hours, memory_gib_hours, total_credits, pods[] }
  LLMUsageSummary        { total_tokens, total_cost, total_credits, agents[] }
  Quota                  { cpu, memory, pods, tier }
  SetQuotaRequest        { tier, cpu?, memory?, pods? }

XMTP
  XMTPConversation       { id, peer, last_message?, last_message_at? }
  XMTPGroup              { id, name?, members, created_at }
  XMTPMessage            { id, from_agent, content, created_at }
  SendXMTPMessageRequest { agent_id, content }
  CreateGroupRequest     { name?, members }
  SessionNegotiation     { sessionId, officeId, agentName, conversationId,
                           capabilities?, startedAt }

Activity / events
  ActivityEvent          { id, category, type, summary, timestamp, metadata? }
                         category ∈ task|message|standup|session|xmtp|
                                    lifecycle|chat|terminal
  ActivityQuery          { limit?, offset?, category?, since? }
  ChatSession            { session_key, started_at, messages }

Integrations / extensions
  ModelInfo              { provider, model_id, name, available }
  IntegrationSecret      { provider, has_key, updated_at? }
  SetSecretRequest       { provider, key }
  Extension              { id, name, office_id, manifest?, created_at }
  CreateExtensionRequest { name, manifest?, panel_html? }
  MarketplaceItem        { id, name, description?, author?, installs }

Channels & misc
  WhatsAppStatus         { connected, phone_number?, agent_id? }
  WhatsAppQRStatus       { status, qr? }
  WhatsAppAgentStatus    { agents: [{ name, whatsapp_enabled }] }
  ChromiumInstance       { id, status, vnc_url? }
  Delegate               { agent_id, permissions, created_at }
  CreateDelegateRequest  { permissions }
  SendMessageRequest     { to, content }
  PoolStats              { active, idle }
  ExecRequest            { command, timeout_ms? }
  ExecResponse           { stdout, stderr, exit_code }
  Backup                 { id, employee_name, office_id, s3_key, size_bytes, created_at }
  TransferStatus         { transfer_id, status, progress? }
  Role                   { name, permissions }
  PingResult             { model, latency_ms, success, error? }
  PodCallback            { id, type, payload, created_at }
  PodEventRequest        { type, payload? }
  EnvVar                 { key, value?, source? }
  SetEnvRequest          { key, value }
  Paginated<T>           { data, total?, offset?, limit? }
  APIError               { status, message, code? }

Errors
  class OS1Error extends Error
    status: number
    code?:  string
    name === 'OS1Error'

================================================================================
10. ERROR HANDLING
================================================================================

All non-2xx responses → `OS1Error`. The Transport tries to JSON-parse the
body and read `message`, `error`, or `code` from it. If parsing fails, the
HTTP statusText is used as the message.

Patterns:

  try {
    await mi.employees.hire(officeId, { name: 'aria' });
  } catch (e) {
    if (e instanceof OS1Error) {
      switch (e.status) {
        case 401: /* auth invalid (JWT secret mismatch or expired token) */
        case 402: /* office out of credits */
        case 403: /* user/agent forbidden */
        case 404: /* missing resource */
        case 409: /* name conflict (e.g. agent already exists) */
        case 429: /* rate limit (hire = 3/10min/office) */
      }
    }
  }

Timeouts: AbortController fires at `config.timeout` ms (default 30 000).
The thrown error is whatever `fetch()` raises on abort (DOMException name
"AbortError"); not an OS1Error.

================================================================================
11. ROUTE-BY-ROUTE MAP (FLAT)
================================================================================

Use this when you only know the URL and need the SDK call.

GET    /healthz                                                            client.health()
GET    /api/v1/offices                                                     offices.list
POST   /api/v1/offices                                                     offices.create
GET    /api/v1/offices/{id}                                                offices.get
DELETE /api/v1/offices/{id}                                                offices.delete
GET    /api/v1/offices/{id}/settings                                       offices.getSettings
PATCH  /api/v1/offices/{id}/settings                                       offices.updateSettings
GET    /api/v1/offices/{id}/status                                         offices.status
GET    /api/v1/offices/{id}/kubeconfig                                     offices.kubeconfig
POST   /api/v1/offices/{id}/transfer                                       offices.transfer
POST   /api/v1/offices/{id}/rotate-secret                                  offices.rotateSecret
PUT    /api/v1/offices/{id}/secrets/{name}                                 offices.setSecret
POST   /api/v1/offices/{id}/suspend                                        offices.suspend
POST   /api/v1/offices/{id}/resume                                         offices.resume

POST   /api/v1/offices/{id}/employees                                      employees.hire
GET    /api/v1/offices/{id}/employees                                      employees.list
GET    /api/v1/offices/{id}/employees/{name}                               employees.get
PATCH  /api/v1/offices/{id}/employees/{name}                               employees.update
DELETE /api/v1/offices/{id}/employees/{name}                               employees.delete
GET    /api/v1/offices/{id}/employees/{name}/logs                          employees.logs
GET    /api/v1/offices/{id}/employees/{name}/activity                      employees.activity
GET    /api/v1/offices/{id}/employees/{name}/presence                      employees.presence
GET    /api/v1/offices/{id}/employees/{name}/last-error                    employees.lastError
POST   /api/v1/offices/{id}/employees/{name}/action                        employees.action
POST   /api/v1/offices/{id}/employees/{name}/promote                       employees.promote
POST   /api/v1/offices/{id}/employees/{name}/skills                        employees.setSkills
POST   /api/v1/offices/{id}/employees/{name}/events                        employees.logStatusEvent
GET    /api/v1/offices/{id}/employees/{name}/events                        employees.events
POST   /api/v1/offices/{id}/employees/{name}/archive                       employees.archive
POST   /api/v1/offices/{id}/employees/{name}/restore                       employees.restore
POST   /api/v1/offices/{id}/employees/{name}/credentials/rotate            employees.rotateCredentials
POST   /api/v1/offices/{id}/employees/{name}/lifecycle                     employees.lifecycle
GET    /api/v1/offices/{id}/employees/{name}/chat-sessions                 employees.chatSessions
GET    /api/v1/offices/{id}/employees/{name}/usage                         credits.agentUsage
GET    /api/v1/offices/{id}/employees/{name}/env                           env.getAgentEnv
PUT    /api/v1/offices/{id}/employees/{name}/env/{key}                     env.setAgentEnv
POST   /api/v1/offices/{id}/employees/{name}/llm-ping                      llmPing.agentPing
POST   /api/v1/offices/{id}/employees/{name}/whatsapp/qr-start             whatsapp.qrStart
GET    /api/v1/offices/{id}/employees/{name}/whatsapp/qr-status            whatsapp.qrStatus
POST   /api/v1/offices/{id}/employees/{name}/whatsapp/qr-stop              whatsapp.qrStop
GET    /api/v1/offices/{id}/employees/{name}/whatsapp/status               whatsapp.sessionStatus
POST   /api/v1/offices/{id}/employees/{name}/whatsapp/reset                whatsapp.reset

POST   /api/v1/offices/{id}/tasks                                          tasks.create
GET    /api/v1/offices/{id}/tasks                                          tasks.list
GET    /api/v1/offices/{id}/tasks/{taskId}                                 tasks.get
GET    /api/v1/offices/{id}/tasks/stats                                    tasks.stats

POST   /api/v1/offices/{id}/files                                          files.upload
GET    /api/v1/offices/{id}/files                                          files.list
GET    /api/v1/offices/{id}/files/{filename}                               files.download
DELETE /api/v1/offices/{id}/files/{filename}                               files.delete
GET    /api/v1/offices/{id}/files/_changes                                 files.changes
GET    /api/v1/offices/{id}/files/{filename}/permissions                   files.getPermissions
PUT    /api/v1/offices/{id}/files/{filename}/permissions                   files.setPermissions

GET    /api/v1/offices/{id}/credits                                        credits.balance
POST   /api/v1/offices/{id}/credits                                        credits.add
GET    /api/v1/offices/{id}/credits/history                                credits.history
GET    /api/v1/offices/{id}/usage                                          credits.usage
GET    /api/v1/offices/{id}/usage/current                                  credits.usageCurrent
GET    /api/v1/offices/{id}/usage/summary                                  credits.usageSummary
GET    /api/v1/offices/{id}/usage/llm                                      credits.llmUsage
GET    /api/v1/offices/{id}/usage/llm/summary                              credits.llmUsageSummary
GET    /api/v1/offices/{id}/quota                                          credits.quota
PUT    /api/v1/offices/{id}/quota                                          credits.setQuota

GET    /api/v1/offices/{id}/xmtp/conversations                             xmtpApi.listConversations
GET    /api/v1/offices/{id}/xmtp/messages/{agent}/{peer}                   xmtpApi.getMessages
POST   /api/v1/offices/{id}/xmtp/send                                      xmtpApi.send
GET    /api/v1/offices/{id}/xmtp/groups                                    xmtpApi.listGroups
POST   /api/v1/offices/{id}/xmtp/groups                                    xmtpApi.createGroup
GET    /api/v1/offices/{id}/xmtp/groups/{gid}/messages                     xmtpApi.getGroupMessages
POST   /api/v1/offices/{id}/xmtp/groups/{gid}/send                         xmtpApi.sendGroupMessage
POST   /api/v1/offices/{id}/xmtp/groups/{gid}/members                      xmtpApi.addMember
DELETE /api/v1/offices/{id}/xmtp/groups/{gid}/members/{agent}              xmtpApi.removeMember
PATCH  /api/v1/offices/{id}/xmtp/groups/{gid}                              xmtpApi.renameGroup

GET    /api/v1/offices/{id}/provider-models                                integrations.listProviderModels
POST   /api/v1/offices/{id}/integrations/{intg}/secret                     integrations.ensureSecret
DELETE /api/v1/offices/{id}/integrations/{intg}/secret                     integrations.deleteSecret
POST   /api/v1/offices/{id}/integrations/{intg}/agents/{agent}             integrations.toggleAgent
POST   /api/v1/offices/{id}/integrations/claude-code/sync-key              integrations.syncClaudeCodeKey

POST   /api/v1/offices/{id}/extensions                                     extensions.register
GET    /api/v1/offices/{id}/extensions                                     extensions.list
GET    /api/v1/offices/{id}/extensions/{extId}                             extensions.get
DELETE /api/v1/offices/{id}/extensions/{extId}                             extensions.delete
POST   /api/v1/offices/{id}/extensions/install                             extensions.install
DELETE /api/v1/offices/{id}/extensions/{extId}/uninstall                   extensions.uninstall
GET    /api/v1/marketplace                                                 extensions.marketplaceList
GET    /api/v1/marketplace/{extId}                                         extensions.marketplaceGet
POST   /api/v1/marketplace/publish                                         extensions.marketplacePublish

GET    /api/v1/offices/{id}/events                                         events.list
GET    /api/v1/offices/{id}/events/chat/{key}                              events.chatEvents
GET    /api/v1/offices/{id}/events/ssh                                     events.sshEvents

POST   /api/v1/offices/{id}/callbacks/pod-event                            callbacks.podEvent
GET    /api/v1/offices/{id}/callbacks                                      callbacks.list
GET    /api/v1/offices/{id}/callbacks/{cbId}                               callbacks.get

GET    /api/v1/offices/{id}/backups                                        backups.list
GET    /api/v1/offices/{id}/backups/{backupId}                             backups.get
DELETE /api/v1/offices/{id}/backups/{backupId}                             backups.delete

GET    /api/v1/offices/{id}/env                                            env.list
GET    /api/v1/offices/{id}/env/values                                     env.values
PUT    /api/v1/offices/{id}/env                                            env.set
DELETE /api/v1/offices/{id}/env/{key}                                      env.delete

GET    /api/v1/offices/{id}/delegates                                      delegates.list
GET    /api/v1/offices/{id}/delegates/{aid}                                delegates.get
POST   /api/v1/offices/{id}/delegates/{aid}                                delegates.create
PATCH  /api/v1/offices/{id}/delegates/{aid}                                delegates.update
DELETE /api/v1/offices/{id}/delegates/{aid}                                delegates.delete

POST   /api/v1/offices/{id}/messages/send                                  messages.send
GET    /api/v1/offices/{id}/messages/pool-stats                            messages.poolStats
GET    /api/v1/offices/{id}/messages/stream                                messages.stream  (SSE)

POST   /api/v1/offices/{id}/workspace/exec                                 workspace.exec
GET    /api/v1/offices/{id}/workspace/health                               workspace.health

GET    /api/v1/offices/{id}/roles                                          roles.list
GET    /api/v1/offices/{id}/roles/{name}                                   roles.get

POST   /api/v1/offices/{id}/agents/prepare                                 transfer.prepare
POST   /api/v1/offices/{id}/agents/install                                 transfer.install
POST   /api/v1/offices/{id}/agents/start                                   transfer.start
GET    /api/v1/offices/{id}/agents/transfer/{tid}                          transfer.status

POST   /api/v1/offices/{id}/llm-ping                                       llmPing.ping

GET    /api/v1/offices/{id}/whatsapp/agents                                whatsapp.agentStatus
POST   /api/v1/offices/{id}/whatsapp/agents/{agent}/access                 whatsapp.toggleAgentAccess
POST   /api/v1/offices/{id}/whatsapp/agents/{agent}/register               whatsapp.registerAgent
POST   /api/v1/offices/{id}/whatsapp/qr-start                              whatsapp.officeQRStart
GET    /api/v1/offices/{id}/whatsapp/qr-status                             whatsapp.officeQRStatus
POST   /api/v1/offices/{id}/whatsapp/reset                                 whatsapp.officeReset

POST   /api/v1/offices/{id}/chromium/start                                 chromium.start
GET    /api/v1/offices/{id}/chromium/status                                chromium.status
POST   /api/v1/offices/{id}/chromium/done                                  chromium.done
DELETE /api/v1/offices/{id}/chromium                                       chromium.delete

GET    /api/v1/offices/{id}/capabilities/self                              capabilities.self

DELETE /api/v1/offices/{id}/code-proxy                                     proxy.teardownCodeProxy
DELETE /api/v1/offices/{id}/codex-proxy                                    proxy.teardownCodexProxy

================================================================================
12. CANONICAL EXAMPLES
================================================================================

# 12.1 Bootstrapping a client (env-based, recommended for scripts)

import { MitosisClient } from '@mitosislabs/sdk';

const mi = new MitosisClient({
  endpoint: process.env.OS1_ENDPOINT ?? 'https://m.mitosislabs.ai',
  apiKey: process.env.MITOSIS_API_KEY,   // mi_… key from `mi login` / dashboard
  timeout: 15_000,
});
// …or simply: const mi = await MitosisClient.fromConfig();

const offices = await mi.offices.list();
await mi.close();

# 12.2 Iterating every office and every agent

for (const office of await mi.offices.list()) {
  const agents = await mi.employees.list(office.id);
  for (const a of agents) {
    const last = await mi.employees.lastError(office.id, a.name).catch(() => null);
    console.log(office.name, a.name, a.status.phase, last?.error ?? '');
  }
}

# 12.3 Hiring + waiting for ready

const employee = await mi.employees.hire(officeId, {
  name: 'aria', role: 'researcher', modelTier: 'sonnet',
});

while (true) {
  const e = await mi.employees.get(officeId, 'aria');
  if (e.status.ready) break;
  await new Promise(r => setTimeout(r, 5_000));
}

# 12.4 XMTP DM with capability negotiation

const session = await mi.xmtp.openSession(officeId, 'aria');
const meta    = await session.negotiate(15_000);   // SessionNegotiation
await session.send('hello aria');

(async () => {
  for await (const msg of session.stream(2_000)) {
    console.log(msg.from_agent, msg.content);
    if (msg.content === 'BYE') await session.close();
  }
})();

# 12.5 Acting as the agent (secp256k1)

import { generateKeyPair } from '@mitosislabs/sdk';
const kp = generateKeyPair();
await mi.keystore.storeAgentKey(officeId, 'aria', kp.privateKey);

const agentClient = await MitosisClient.asAgent(officeId, 'aria');
await agentClient.callbacks.podEvent(officeId, { type: 'ready' });

# 12.6 Streaming the office message bus (SSE)

for await (const evt of mi.messages.stream(officeId)) {
  console.log(evt.event ?? 'message', evt.data);
}

# 12.7 Raw call (escape hatch)

const result = await mi.transport.request<unknown>(
  'POST',
  `/api/v1/offices/${officeId}/some/new-route`,
  { body: { foo: 'bar' }, asAgent: false },
);

================================================================================
13. INVARIANTS / GOTCHAS
================================================================================

• Path signing for secp256k1 includes the query string. If you want a signed
  GET with query params, sign the path that the Transport actually sends —
  the Transport already does this, but if you call signRequest yourself,
  remember to include `?key=value`.

• `Transport.upload` field name is hard-coded to `file`. Server-side handler
  expects multipart/form-data with that field.

• `files.download` returns the raw `Response`. Use `await r.arrayBuffer()`
  or `r.body` (Node Readable). Do not call `.json()` on binary files.

• `xmtpSession.send` always uses agent_id = 'admin-sdk' as the sender alias
  (matches office-manager's expectation for SDK-originated messages). The
  underlying signing identity is whatever auth the Transport uses.

• `XMTPChannel.openSession` is sync-returning-a-promise but only does work
  when the conversation is created by the first negotiate/send.

• `Keystore` puts `config.json` outside `keys/` — at `~/.os1/config.json`.
  This is intentional so `keys/` can be backed up without leaking endpoint
  preferences, and vice versa.

• Default endpoint resolution order:
    1. `MitosisClient` constructor `endpoint` arg.
    2. `~/.os1/config.json` `endpoint` field (when using `fromConfig`/`asAgent`).
    3. Fallback: `https://m.mitosislabs.ai`.

• The CLI displays the program name `os1-admin` (from `Command.name()`),
  but the binary itself is `mi`. Help text shows `mi`-prefixed examples in
  this reference; older outputs may say `os1-admin`.

• Rate limit: agent hiring is capped server-side at ~3/10min/office. The
  SDK does not retry on 429.

• Time skew matters: secp256k1 signatures must be within ±60 s of server
  clock. The SDK uses `Date.now()` directly — sync your machine clock.

• ESM only. `require('@mitosislabs/sdk')` will fail. Use `import` or
  dynamic `await import(...)`.

================================================================================
14. SOURCE → SYMBOL INDEX
================================================================================

src/index.ts                       all public re-exports
src/client.ts                      MitosisClient
src/transport.ts                   Transport
src/auth/index.ts                  re-exports of jwt + secp256k1 + keystore
src/auth/jwt.ts                    generateJWT, verifyJWT, authorizationHeader
src/auth/secp256k1.ts              generateKeyPair, publicKeyFromPrivate,
                                    signRequest, verifySignature
src/auth/keystore.ts               Keystore
src/api/offices.ts                 OfficesAPI
src/api/employees.ts               EmployeesAPI
src/api/tasks.ts                   TasksAPI
src/api/files.ts                   FilesAPI
src/api/credits.ts                 CreditsAPI (credits + usage + quota)
src/api/xmtp.ts                    XMTPAPI
src/api/integrations.ts            IntegrationsAPI
src/api/extensions.ts              ExtensionsAPI (+ marketplace methods)
src/api/events.ts                  EventsAPI, CallbacksAPI, BackupsAPI,
                                    EnvAPI, DelegatesAPI, MessagesAPI,
                                    WorkspaceAPI, RolesAPI, TransferAPI,
                                    LLMPingAPI, WhatsAppAPI, ChromiumAPI,
                                    CapabilitiesAPI, ProxyAPI
src/xmtp/channel.ts                XMTPChannel
src/xmtp/session.ts                XMTPSession + control message constants
src/cli/index.ts                   `mi` CLI (commander program)
src/types/index.ts                 every TS interface + OS1Error class

Tests
src/../tests/auth.test.ts          JWT + secp256k1 round-trips, tamper detection
src/../tests/client.test.ts        MitosisClient construction
src/../tests/install-xmtp.test.ts  legacy XMTP installer (older code path)
src/../tests/integration.test.ts   live office-manager smoke (skipped without secret)
src/../tests/session.test.ts       XMTPSession negotiation + cursor logic

End of file.
