You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: isolate managed-agent resources by workspace (#144)
## What changed
- Introduce Workspace as Mango's sole tenant and authorization boundary
without changing CMA request/response shapes.
- Authenticate opaque API keys at the HTTP edge and attach exactly one
Workspace scope; all keys in a Workspace have equal access.
- Scope Agents, Environments, Sessions, Files, Skills, Memory Stores,
Vaults, Deployments, child resources, lists, mutations, and S3 object
keys.
- Recover and propagate Workspace scope across scheduled Deployment,
Temporal activity, NATS, lifecycle, and reconciliation paths.
- Add local operator commands for Workspace and API-key lifecycle. No
user model, role hierarchy, per-resource ACL, OpenFGA, or administration
HTTP/Console surface is introduced.
- Keep pre-tenancy data in `wrkspc_default` and preserve stored legacy
object keys for cleanup.
- Make system-store tenant dependency reads and writes fail closed
without an explicit Workspace. Single-tenant embedding/tests use the
explicitly named default-Workspace store.
## Verification
- `go test ./...`
- `go test -race ./...`
- `go vet ./...`
- `make lint` — 0 issues
- `make security` — no reachable Go vulnerabilities; npm audit passed
with the existing explicitly accepted upstream `image-size` build
advisories
- `make test-service` — real PostgreSQL, Temporal, NATS, MinIO, and
sandbox suites passed
- website typecheck and production build
- Compose configuration validation
- PostgreSQL isolation tests cover top-level reads/lists, child access,
API-key sharing/revocation, cross-Workspace Deployment dependencies,
fail-closed system access, and validated composite constraints
## Compatibility
Workspace identity stays credential-derived and is not added to CMA
JSON. Existing SDK/wire semantics, durable Session snapshots,
provider-native web search/fetch behavior, and the v1.63.1 SDK surface
remain unchanged.
The server accepts the documented `x-api-key` form and the documented
OpenAPI Bearer alternative; sending both is rejected. Health, readiness,
and the embedded OpenAPI document remain public.
## Durability and security
Migration `00032_workspaces.sql` creates Workspaces and digest-only API
keys, backfills every existing root into `wrkspc_default`, adds non-null
ownership and indexes, and installs then validates the cross-Workspace
Deployment constraints. Session Agent/Environment fields intentionally
remain durable snapshots; public admission locks their same-Workspace
dependencies.
Public handlers use a scoped Store. Privileged reconcilers can enumerate
global work, but tenant dependency reads and tenant writes require
recovered scope and never silently fall back to the default Workspace.
Object-store keys are Workspace-prefixed while persisted legacy keys
remain directly cleanable.
`MANAGED_AGENT_DATABASE_URL` remains an operator/root credential.
Workspace/API-key administration is deliberately local CLI-to-PostgreSQL
only and is not exposed by the HTTP server.
## Local Claude Code review
Review was run locally with Claude Code 2.1.234, Opus
(`claude-opus-4-7`), `--permission-mode bypassPermissions`, `--bare`,
and slash commands disabled. The review inspected `origin/main..HEAD`
and produced 20 findings. Each was independently checked against the
actual call graph:
| # | Claude finding | Disposition |
|---|---|---|
| 1 | Bearer auth should be removed | Not changed: Bearer is an explicit
documented/OpenAPI alternative, both-header requests are rejected, and
credentials are opaque in either transport. |
| 2 | Unscoped system Agent reads silently select `wrkspc_default` |
Fixed: system tenant dependency reads now return `ErrMissingScope`;
explicit scope is tested. |
| 3 | Environment `Put` changed from upsert to insert-only |
Intentional: `Put` is Create-only, mutable updates use `Update`, and
insert-only prevents a global-ID collision from overwriting another
Workspace. |
| 4 | Session Resource/File ownership needs another Workspace FK | Not
actionable: API admission creates the scoped File and child atomically,
IDs are globally unique, parent access is asserted, and reconciliation
uses the persisted BlobKey. A direct corrupt foreign reference fails
scoped File lookup rather than deleting another tenant's blob. |
| 5 | System writes can silently land in `wrkspc_default` | Fixed:
`NewSystemStore` writes fail closed without scope;
`NewDefaultWorkspaceStore` makes single-tenant intent explicit. Full
service tests verify async scope recovery. |
| 6 | Local key administration has no independent admin policy |
Intentional boundary: there is no admin HTTP route; CLI access requires
the PostgreSQL root credential and is documented as operator access. |
| 7 | Restart reasserts a revoked bootstrap key | Intentional
desired-state behavior: while `MANAGED_AGENT_API_KEY` remains
configured, startup rotates/reasserts the fixed bootstrap credential.
Operators remove the env setting to stop managing it this way. |
| 8 | `NOT VALID` constraints are not validated; add Session dependency
FKs | Partially fixed: all three installed composite constraints are now
validated in the migration and tested. Agent/Environment Session fields
remain snapshots by documented durable-admission design. |
| 9 | Prepared Session File transition ignores zero affected rows |
Already protected: `insertPreparedSessionResources` checks
`RowsAffected() == 1` and aborts the transaction otherwise. |
| 10 | Session ownership assertions add a database round-trip | Deferred
performance idea; correctness/security is fail-closed and this PR
prioritizes a small auditable boundary. |
| 11 | Legacy File BlobKeys are orphaned | False positive: a persisted
legacy `files/... ` key is deleted directly; fallback reconstruction is
used only when the File row/key is already absent. |
| 12 | Legacy Skill BlobKeys are not cleanable | False positive: Skill
cleanup always uses the BlobKey persisted on the version, including
legacy unprefixed keys. |
| 13 | Optional-scope SQL may reduce index use | Deferred low-risk
optimization; HTTP list paths build direct `workspace_id = ...`
predicates and service tests passed. |
| 14 | File delete may disclose foreign reference state | False positive
under current schema/call paths; the review itself notes the required
cross-Workspace state is impossible, and foreign IDs return not found. |
| 15 | Active-key count could be exposed later | Not actionable: it is
called only during process startup and no HTTP route exposes it. |
| 16 | `last_used_at` is not updated | Non-blocking follow-up candidate
for operator observability; it does not affect authentication,
revocation, or isolation and avoiding per-request writes is preferable
here. |
| 17 | Internal prepared BlobKey could carry another prefix | Not
actionable at the public boundary: keys are generated internally from
the authenticated context before the scoped DB insert; legacy keys also
preclude a blanket DB prefix check. |
| 18 | A corrupt Deployment Workspace could mis-scope reconciliation |
Addressed by validated non-null Workspace and composite dependency
constraints; claims propagate the stored authoritative Workspace. |
| 19 | Boot-time File reconciliation could race serving | False
positive: reconciliation completes before the HTTP server starts, as the
review also observed. |
| 20 | Workspace might leak into durable JSON later | Current structs
use `json:"-"` and SDK/wire golden tests guard shape; hypothetical
future fields are outside this change. |
The two accepted security findings (2/5, one root cause) and the valid
portion of finding 8 were fixed before this PR was opened. All
verification above was rerun afterward.
## Checklist
- [x] Tests cover the changed behavior.
- [x] Public behavior is documented.
- [x] Compatibility claims are limited to the existing pinned SDK/wire
surface.
- [x] No credentials, generated build output, or local databases are
included.
addr:=fs.String("addr", defaultAddr, "listen address (default binds to loopback; use e.g. :8080 to expose on all interfaces)")
409
-
strict:=fs.Bool("strict", false, "require Claude API wire headers (auth, version, beta, content-type) to be present and valid; this is header validation, NOT authentication")
413
+
strict:=fs.Bool("strict", false, "require Claude API version, beta, and content-type headers; API-key authentication is always enforced")
0 commit comments