Commit Graph
9 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 22eeaa6f15 feat(auth): the door that can delegate no longer needs a person's session
`/mcp` — `email_send`, `slack_post`, `delegate` — authenticated with
`authenticate`, which accepts only `full`. Nothing hands it a token today, so
this cost nothing yet; the moment something did, the only credential that
worked would have been an owner's session, held by an agent runtime.

`SCOPE_AGENT_DOOR` is that credential's narrow form. `full` still works, so
the UI and every human caller are unaffected, and the route now names what it
accepts rather than accepting everything by default.

The test that matters is not that each scope opens its own route: it is that
holding one grants nothing the other has. Both tokens live where an agent can
read them.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-25 07:34:30 -05:00
Omar SobhandClaude Opus 5 2668191e30 feat(auth): a credential narrow enough to hand to an agent
`docs/TOOL-CALL-ARCHITECTURE.md` §3 calls deploying the MCP door "config,
not code". It is not, and the reason is authentication.

`/mcp/skills` authenticates with `AuthService::authenticate`, which
returns a full `AuthedUser` carrying the user's role. There is no
narrower credential in the system. So pointing a mission container at the
door means writing a bearer token into a file inside that container — and
mission agents run arbitrary `Bash` with egress and no read gate, which
is this platform's own documented security posture. An owner-scoped token
there turns "the agent runs commands in a sandbox" into "the agent drives
the whole ClawMates API as the owner".

Checked before building this rather than assumed: no such credential is
in a mission container today. The runtime's config.toml has no
`[mcp.servers]` block and no bearer, so the door would have been a NEW
exposure, not an existing one.

So: `auth_sessions.scope`, defaulting to `full`. `authenticate` now
delegates to `authenticate_scoped(token, SCOPE_FULL)`, which means **every
existing caller rejects a narrow token** and a route must opt in by naming
the scope it accepts. `/mcp/skills` is the only opt-in.

Fail closed on purpose. The likely mistake here is adding a scope and
forgetting to wire its check; this way that mistake grants nothing rather
than granting everything.

`mint_scoped` refuses to mint a `full` token — a caller reaching for it
wants a narrow credential, and handing back a full one because an
argument was wrong is exactly the failure the column exists to prevent,
and it would be invisible because the token would work.

The test that matters is not that the door accepts the token, it is that
nothing else does. Negative-controlled: removing the scope comparison
fails `a_scoped_token_is_refused_by_every_unscoped_caller`.

`.sqlx` regenerated — `authenticate` is a compile-checked query and CI
builds with SQLX_OFFLINE=true.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018i9Ten1LU4jUr5d7TAWda9
2026-08-21 11:43:25 -07:00
Omar SobhandClaude Opus 4.7 49f94a5360 mcp door: mint workspace-owner service session for team runtime bearer
ci / gates (push) Successful in 5s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 25s
ci / e2e (push) Skipped
ci / publish (push) Skipped
The runtime template's static clawmates_door bearer is rejected by
cm_auth::authenticate() (needs an auth_sessions row). Every per-team
agent was getting `unauthorized: missing or invalid bearer token` and
`0 tool(s) registered from 0 server(s)`.

Add AuthService::mint_service_session + users::owner_of_workspace and
mint a 30d service session in try_team_gateway_url; inject it into the
freshly-spawned team container's config.toml [[mcp.servers]] clawmates
Authorization header via prewrite_daemon_config_with_risk (bearer arg).

Follow-up: apply the same pattern to research::spawn (per-topic) and
per-loop spawn paths.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-07-18 13:37:27 -07:00
Omar SobhandClaude Opus 4.8 3554a3aaf2 CI: remove k8s stages, fix the Docker-level pipeline green
ci / frontend (push) Successful in 23s
ci / rust (push) Failing after 27s
ci / e2e (push) Has been skipped
ci / gates (push) Successful in 5s
Survey + fixes so the pipeline passes at the Docker level (no k8s).

- Remove k8s: drop the `sandbox-k8s` job (kind/Calico/--features k8s-tests) and the
  "Helm chart lints" gate step. release.yml was already k8s-clean.
- Rust job:
  - `cargo fmt --all` — fix pre-existing formatting drift (fmt --check was failing).
  - clippy -D warnings: fix 3 lib warnings (cm-brain sort_by_key→Reverse, cm-api
    fleet.rs doc list indentation, node_rules map_or→is_none_or).
  - Regenerate the .sqlx offline cache (was missing the cm-runtime run_loop test
    query → offline compile failed). DB-backed tests use testcontainers at runtime.
  - Set SQLX_OFFLINE=true on the rust + e2e jobs so query! macros compile against
    the committed cache deterministically (no DB needed at compile time).
- Frontend job:
  - Fix the 1 ESLint error (useAgentTelemetry: no setState-synchronously-in-effect;
    tag the slice with agentId + derive null on mismatch).
  - Fix 2 stale panel-params tests (`terminal` is a valid app id now; assert the
    current APP_IDS + use a genuinely-unknown id for the reject case).

Verified locally: fmt clean, clippy --all-targets -D warnings clean (offline),
frontend lint 0 errors, tsc clean, 86/86 frontend tests pass, build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 18:15:31 -07:00
Omar SobhandClaude Opus 4.8 e9ce368ec1 Scaling Phase 1: multi-tenant onboarding + replica-safe coordination
Decouples "many users" + "many server replicas" from "many machines" so the
platform is tenant-isolated and horizontally safe on the current single node.

- Per-signup workspaces (cm-auth): a new hosted-identity sign-in provisions and
  owns its own workspace instead of joining the first. Config-gated by
  auth.per_signup_workspace (default off); concurrent first-logins serialized by
  a per-subject advisory lock so no duplicate workspaces.
- Terminal tickets in Postgres (migration 0016, hashed, single-use): any replica
  can redeem a ticket minted by another. Drops the in-process ticket map.
- Container registry in Postgres (migration 0017, agent_containers): Terminal
  and Sandbox managers resolve an agent's container through a shared registry,
  so a 2nd replica reuses it instead of spawning a duplicate. node_id recorded
  as 'local' (Phase 2 hook). Boot reconcile removes only true orphans, so
  terminals now survive a redeploy (tmux sessions resume).
- Per-workspace quotas (cm-api/quota.rs): plan-tier caps on agents + live
  containers, enforced at agent create + terminal spin-up (reconnects allowed),
  returned as HTTP 402. New GET /api/quota surfaces usage vs limits.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 18:24:51 -07:00
Omar SobhandClaude Opus 4.8 6071e1fd61 fix(auth): make JIT provisioning idempotent under concurrent first-login
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled
On a user's first login the authed shell fires several API calls at once;
each ran the JIT-provision path and raced to INSERT the same new user row,
tripping the partial unique index on auth_subject. The losing requests
500'd and the post-login SSR errored out. Use INSERT ... ON CONFLICT
(auth_subject) DO UPDATE ... RETURNING so concurrent callers converge on
the row the winner created. Regenerated the .sqlx offline query cache.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-15 03:25:31 -07:00
Omar SobhandClaude Fable 5 447f7039d8 Compose: production README, env knobs, first-owner bootstrap — deployed & live
Made the Docker Compose route turn-key for a real self-host, then stood
the whole stack up and drove a live chat through it.

- First-owner bootstrap (cm-auth::bootstrap_owner): a fresh local-auth
  install has no users and no signup route, so the initial Owner +
  workspace are provisioned ONCE from CLAWMATES_BOOTSTRAP_* env on first
  boot — idempotent, never clobbers an existing install (keys on 'any
  workspace exists'). Two real-Postgres tests (creates + signs in;
  second call is a no-op). Wired into server boot, guarded on a
  non-empty password
- deploy/compose/README.md: full production bring-up — services, the
  security topology, every config knob, Anthropic vs local-LLM, the
  broker-key backup, ops, and TLS/SSE proxy notes
- .env.example fleshed out (bootstrap, LLM, auth mode, OTLP); compose
  uses optional env_file so only the knobs you set are injected (unset
  options never override clawmates.toml with empty strings)
- volume-init one-shot chowns the broker's named volumes so the non-root
  scratch broker can write its socket + generated master key

Deployed locally and verified end to end: all 5 containers healthy,
broker generated its key, server bootstrapped owner@…, login + /api/user/me
work, and a real message streamed a live Anthropic response through the
gateway. Captured screenshots of login, workspace home, chat, and the
Computer panel.

166 Rust tests (+2 bootstrap).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 14:06:15 -05:00
Omar SobhandClaude Fable 5 b9fdec9173 Clerk deployment smoke: validated against a real instance, both halves
Last open item from the roadmap + post-1.0 list. Run against the live
Clerk instance closing-seasnail-39.clerk.accounts.dev.

- Backend (crates/cm-auth/tests/live_clerk.rs, CM_LIVE_CLERK=1): pulls
  REAL discovery + JWKS from the live instance, mints a REAL session JWT
  via Clerk's Backend API (create user -> open session -> session token),
  and runs it through AuthService::authenticate — verify + JIT provision
  (keyed on the real sub), duplicate-subject suppression, tamper
  rejection against the live JWKS. Decodes the instance domain from the
  publishable key; cleans up the test user after. PASSING
- Frontend: built with AUTH_MODE=clerk + real keys, next start serves
  Clerk's <SignIn /> at /login wired to the instance (instance domain +
  data-clerk attributes present in the HTML). Both halves confirmed
  end to end against production Clerk
- docs/clerk.md: documented the smoke procedure for both halves

166 Rust tests (+6 live, key-gated). Keys used via env only, never
stored — rotate them (they passed through chat).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:39:26 -05:00
Omar SobhandClaude Fable 5 add4f79fed Rebrand: TeamClaw -> Clawmates (clawmates.work)
Full-depth rename per the approved plan; the 'claw' product vocabulary
(claws, /claws routes, clawId, Claw Chat) stays — it is now the brand.

- Display brand: Clawmates (manifest, titles, hero, login/rail logo
  'clawmates'); default host app.clawmates.work; registry
  ghcr.io/clawmates
- Crates tc-* -> cm-* (16 crates + all imports); binaries
  clawmates-server/broker/bundler; images clawmates/*; env prefix
  CLAWMATES_* (+ CM_TEST_DATABASE_URL / CM_LIVE_LLM); config
  clawmates.toml; helm chart deploy/helm/clawmates with clawmates-*
  resources; db names clawmates*; sockets /run/clawmates; cookie
  cm_session; kind cluster clawmates-test; seccomp node profile
  clawmates-agent-profile.json
- All 9 Playwright brand assertions updated in lockstep; historical
  spec document left untouched as the only remaining 'TeamClaw'
- Local env migrated: dev pg clawmates-dev-pg/clawmates_dev, shared
  test server clawmates-test-pg, kind cluster recreated with image +
  profile, compose images rebuilt under clawmates/*

Verified end to end: 161 Rust + 68 frontend tests, 29 Playwright
journeys, 4 live kind tests, helm/install/LOC/placeholder gates, and
the clean-room install rehearsal serving the clawmates login page from
a signed bundle of the rebuilt images.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 12:31:25 -05:00