awslabs / awslabs/cli-agent-orchestrator

[Feat] Sign in with SAML or Entra ID, and give every resource an owner

Open
#774 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Python
Stars
1.3k
Forks
267
Avg merge
1d 23h
Merged PRs (30d)
70

Description

[Feat] Sign in with SAML or Entra ID, and give every resource an owner
Part of #777 (CAO 3.0). **Depends on #778 for design, not for completion** — you need the idea of an organisation before you can put an owner inside one, but both land in the same migration, so neither closes first. See the build order in #777.

**Scope note:** an earlier version of this issue also covered organisations and ruled SAML out. Both have changed. Organisations now live in #778, and **SAML is a first-class part of 3.0** alongside Microsoft Entra ID.

## The problem, in plain terms

Today CAO assumes **one person working on one laptop**. To let a team share one CAO server, it has to know *who* is using it, and keep their work apart. Right now it does neither.

Picture Alice and Bob sharing a single CAO server.

**Bob can see and control everything Alice does.** He can see every agent she started, type into her terminals, read her workflow output, and delete her sessions. Not because of a bug — CAO simply has no concept of "Alice's things" versus "Bob's things". There's no login screen, no user account, and nothing recorded anywhere saying who created what.

**Worse, they can silently destroy each other's work.** This one is not obvious, so it's worth being concrete.

Workflows are stored under their plain name. The database table that indexes them uses `name TEXT PRIMARY KEY` — the name alone, with nothing else. The workflow's file is saved in one shared folder as `.py`.

So if Alice creates a workflow called `deploy`, and Bob later creates his own workflow called `deploy`, Bob's overwrites hers. Same primary key, same filename, one shared folder. Alice gets no warning. Her workflow is simply gone.

The same collision applies to session names, which are also global.

That is fine for a tool on your own machine. It is not fine once #745 turns CAO into one shared server.

## What "keeping them apart" needs to mean

Logging in is only the first half. Alice and Bob need **genuinely separate CAO environments**: Alice's sessions, workflows, terminals, and memories exist in her own space, and Bob cannot see them, address them, or overwrite them — even by picking the same name.

That means a workflow called `deploy` must be able to exist twice, once for each of them, without collision. The namespace is **tenant plus owner**, not owner alone: #779 also permits the same person to belong to different tenants, and their identically named resources in those tenants must stay independent.

## What already exists (and is worth keeping)

CAO isn't starting from zero. There's a real security layer in `security/auth.py`. It can check a login token issued by an identity provider. Of the server's 96 route handlers, 80 use the standard `require_any_scope` dependency. **The other 16 are not all unprotected:** the terminal WebSocket and AG-UI stream validate tokens and scopes inside their handlers (`api/main.py:1759-1784`, `:6850-6877`). Health and discovery metadata are intentionally public; the remaining resource routes need individual assessment. Counting one helper is not an authentication audit.

But it has two limits:

**It's switched off unless configured.** It only activates if an operator sets one of two environment variables. Out of the box, everything is open.

**Even switched on, it checks permissions but not people.** It answers "is this caller allowed to write?" It never answers "*which person* is this?"

So you could enable it today and still have the whole Alice-and-Bob problem. Both would hold valid tokens, both would pass every check, and both would still collide — because nothing distinguishes their data.

## The database has to change

This is the part that can't be skipped, and it's most of the work. There are **14 tables and not one records an owner**. Searching the schema-defining files for `user_id`, `owner`, `tenant_id`, `created_by`, or `principal` returns three hits, every one of them prose in a comment or docstring (all about directory permissions, not columns).

Two distinct kinds of change are needed:

**1. Tables whose existing primary IDs can remain.** `terminals`, `inbox`, `memory_metadata`, `memory_relationships`, `workflow_outcomes`, `workflow_run`, `workflow_run_step`, `workflow_run_event`, `workflow_run_seq`. Add ownership and filter every read, list, and update. This does **not** mean column additions alone suffice: both memory tables also have uniqueness rules that must change.

**2. Tables whose *key* has to change.** This is the harder group, and the reason the naive "just add a column" approach fails:

- **`workflow_index`** is keyed on `name` alone. Its logical name key must include tenant, owner and name, so both different owners and the same owner in different tenants can use `deploy`.
- **`workflow_plan_approval`** matters for a different reason. It records that a workflow was approved to run. Bind an approval to the tenant and owner of the exact plan it authorizes, separately from the identity of its approver; it must not authorize another owner's or tenant's plan.
- **`idempotency_keys`** (`clients/database.py:360`) is keyed on **`key` alone** (`:362`), so every caller shares one namespace. Two people who each use a common key such as `retry` or `job-1` collide. The column comment records the same class of bug being fixed before (review on PR #634), where key reuse returned another caller's terminal. Adding ownership columns is not enough: **the key must be scoped to tenant and owner**, including when one person reuses it in two tenants. This table shipped in v2.5.1 and is absent from v2.5.0; the migration must handle both.
- **Memory uniqueness does not become owner-aware automatically.** `memory_metadata` has `uq_memory_key_scope` on `(key, scope, scope_id)` and a separate partial unique index, `uq_memory_key_scope_null`, for rows whose `scope_id` is NULL (`clients/database.py:174-185`). `memory_relationships` has `uq_memory_rel` on `(scope, scope_id, source_key, target_key, type, origin)` (`:261-269`, `:645-646`). Adding an owner column changes none of these rules: two owners using the same memory name or relationship still collide. Include tenant and owner in all three uniqueness definitions, preserving the NULL-scope handling, and update the matching lookup, upsert, delete and backfill paths together (`services/memory_service.py:298-403`, `services/memory_relationship_service.py:440-470`, `clients/database.py:850-906`). A UUID primary key does not remove these other collision points.
- **`flows`** is keyed on `name` alone (`FlowModel.name`, `clients/database.py:331`), and the API writes each flow to `flows/.flow.md`. Adding ownership columns leaves both collisions in place. Use tenant plus owner plus name for the logical key and the corresponding owned file namespace.
- **`project_aliases`** is keyed on `alias` alone (`ProjectAliasModel.alias`, `clients/database.py:297`). Scope alias uniqueness and reverse lookup to tenant and owner too; the alias key remains load-bearing.

*(`flows` and `project_aliases` were previously listed in group 1. That was wrong: both have single-column primary keys, exactly like `workflow_index`. A migration that only adds an owner column to them would report success while users still overwrite each other.)*

Existing data migrates to the designated local owner, with a stable internal identity. When that installation later enables shared sign-in, #778's operator-controlled setup binds the intended work account to that owner. The first person to visit the login page must not automatically inherit existing work.

## Files on disk need separating too

The database is only half of it. Stored content and execution names must use the same ownership boundary:

- **Workflow files**, saved under their bare name in a single shared folder. (Not only `.py`: CAO scans `.yaml`, `.yml` and `.py`, and treats same-stem files across those extensions as siblings — so the collision is on the *name*, whatever the extension.)
- **Flow files and memory content**, including wiki files and their indexes. Memory text lives in files, not in `memory_metadata`; its `file_path` must resolve within the same tenant and owner as the row. Global/federated memory currently has no scope ID, so its name alone cannot separate owners (`services/memory_service.py:438-456`, `:947-987`).
- **User-authored profiles and skills**, including discovery, source editing, installation and the files/configuration used at launch. Profiles currently resolve by name against shared directories, and profile substitutions read the installation's managed `.env` (`services/profile_store.py:57-128`, `utils/agent_profiles.py:365-373`, `utils/env.py:10-21`). Scoping a database query does not scope these paths. Resolve names and credential references in the same authorized context throughout; do not fall back to another person's files or configuration.
- **Session names**, which are global, so two people can't both have a session called `dev`.
- **Working directories**, where agents actually read and write code.

Use the shared path-resolution helpers to map the logical tenant/owner namespace to storage. For the fixed local tenant and owner, preserve the existing default physical paths rather than forcing files into new directories merely to display an owner prefix. Shared resources still need collision-free, authorized storage mappings; any required moves use #775's coordinated migration and recovery procedure.

These are boundaries between ordinary members. #779's explicit tenant-administrator permissions remain available for intervention, but can never cross a tenant.

Not everything installed with CAO is someone's private document. #778 distinguishes user-owned content from operator-controlled configuration and the read-only built-in catalogue. A user's copy of a built-in profile is private; editing it must not change the shared original.

## An honest limit

This delivers isolation **inside the application**. It is not an operating-system security boundary.

In the current local execution path, CAO and workflow scripts use the same operating-system identity. The codebase is explicit about what that means:

> *"THIS IS A SAME-USER LOCAL CONTROL, NOT A PRIVILEGE BOUNDARY. CAO runs as the invoking user and a workflow script runs as that same user…"*

Application ownership checks do not, by themselves, stop running code from accessing files underneath the API. #745 separates execution workloads from the server; their filesystem, credential and network configuration must match the intended trust boundary. Moving a subprocess or adding a container is not by itself a complete hostile-user boundary; stronger execution isolation is tracked in #784.

## What we'd need to build

**1. Let people sign in with the account they already have.** CAO should accept the organisation's identity provider rather than inventing passwords. The existing code validates incoming JWTs; it does not implement a complete interactive OIDC sign-in or SAML flow, nor CAO session issuance. Reuse that validation seam, but include sign-in/callback correlation, session creation, expiry/refresh and sign-out as actual implementation work.

**Bind a stable external identity to a stable internal user.** For OIDC, use the validated issuer and subject together, not an email address or display name. For SAML, configure a stable identifier under the validated federation rather than assuming every NameID is persistent. Provider changes or account linking require an explicitly authorized operation, not an automatic same-email merge. Tenant selection and membership are validated through #778/#779; the login hint does not grant access.

On **SAML** (an older enterprise login standard, still very common): CAO should support it **directly, as a first-class option**. Brokering it through another provider was the earlier plan, and it is genuinely less code — but in a multi-tenant deployment each organisation brings its **own** identity system, and many enterprise ones are SAML-only. Requiring every such customer to stand up a broker they don't otherwise need is a real adoption barrier, and it puts a critical piece of the login path outside the deployment's control.

Two things follow from this, and both are worth knowing before starting.

**A SAML browser sign-in does not automatically issue a CAO API credential.** For this design, CAO establishes its own application session after either SAML or OIDC sign-in. Both adapters return the same internal principal and session contract; downstream code does not consume SAML assertions. Define browser session protection and API credential handling once, rather than building two application-wide authentication paths.

**SAML is the more dangerous of the two to implement.** Its security rests on XML signature validation, which has a long history of bypasses — signature-wrapping attacks in particular have hit many implementations. Use a maintained, well-reviewed library, never hand-roll the XML handling, and treat certificate rotation per tenant as part of the work rather than an afterthought.

On **Google and Facebook login**: deliberately deferred. A company deployment almost never wants raw consumer login; it wants its employee directory. If it's ever needed, connect it through the organisation's identity system rather than directly into CAO.

**2. Make sign-in usable by the existing clients, not just the web interface.** Cover browser login, the CLI and operator MCP's supported session acquisition/credential handling, and expiry during long-running work. The `cao_workflow` client and agent tools use restricted execution credentials, not copied human login tokens. Current CLI session calls use direct `requests` calls (`cli/commands/session.py:19-41`); adding a web login module alone does not authenticate them.

**The endpoint must be reachable as well as authenticated.** Core clients currently construct an HTTP-only URL from host/port (`constants.py:372-377`), and launch also constructs one directly (`cli/commands/launch.py:275`). Support a configured server URL and normal TLS verification consistently across CLI, operator MCP, workflow clients and browser transports. Distinguish the server's listen address from internal routing and public/canonical addresses used for sign-in and discovery. Preserve the local default, but do not make an HTTPS shared deployment depend on a hidden localhost tunnel, disabled certificate checks or plaintext credential forwarding.

**3. Make the database changes above**, in one migration, coordinated with #775 so it isn't done twice.

**4. Separate the on-disk resources** described above, including custom profiles, skills and their configuration, without turning installation-wide settings into tenant-editable state.

**5. Give agents their own limited credentials.** When an agent acts for Alice, it should carry a restricted credential tied to her, not a copy of Alice's own login token handed to a worker container.

**Carry identity after the request ends.** Queued and scheduled work, retries/resumes, child agents, inbox delivery and background memory work must retain trusted tenant/owner context from the accepted resource or assignment. Recheck current authority before starting new work; a captured request context is not a permanent grant. Keep the acting principal distinct from the resource owner when an administrator intervenes. Maintenance and cleanup use explicit, bounded server authority, never an anonymous fallback to the local user. #779 defines revocation and the remaining cancellation/control path.

## Where this code should live

Login logic must not end up scattered across route handlers and UI components. It belongs in **one dedicated module on each side**, with a single choke-point everything else goes through. Adding a new identity provider should then mean writing one adapter and some configuration — never touching dozens of files.

### Backend

A `security/` package already exists (`auth.py` and `decorators.py`, about 510 lines together), and it is already the shared seam: six production modules outside the package import from it: the API (`api/main.py`), two agent-tool server modules (`mcp_server/app_tools.py`, `mcp_server/utils.py`), the MCP apps plugin (`plugins/builtin/mcp_apps.py`), the memory gateway (`services/memory_gateway.py`), and the orchestration utilities (`utils/orchestration.py`). **Extend this rather than starting a parallel module.**

It should grow to hold four things:

- **Provider adapters** — reuse a maintained OIDC implementation for compatible providers, including Entra ID, and a maintained SAML implementation. Add provider-specific code only where protocol or claim handling actually differs.
- **Token validation** — signature, issuer, audience, expiry, and key caching, in one implementation used everywhere.
- **The principal** — turning a validated token into a stable internal user identity.
- **Authorization and ownership helpers** — check the operation, tenant, owner and effective membership through one shared policy, with scoped data/file access underneath it. Include #779's distinction between managing one's own work, intervening as a tenant administrator, and changing operator-controlled installation settings.

Keep authorization declarative at the route boundary, but do not promise existing dependencies can remain unchanged. Session, terminal and workflow-spec deletion currently require `cao:admin` (`api/main.py:3134`, `:6628`, `:4367`). A member must be able to manage their own resources without receiving installation-wide powers; adding an owner filter behind the old guard does not achieve that.

### Frontend

There's no auth code at all today, so this is a new `web/src/auth/` module owning sign-in, token storage, refresh, sign-out, and attaching credentials to outgoing calls.

**The ordinary API client and specialized streaming transports must share one authentication policy.** Relevant network entry points include:

- `web/src/api.ts`
- `web/src/components/workflow/useEventFollow.ts`
- `web/src/components/TerminalView.tsx` — a WebSocket, not a `fetch`

`DashboardHome` and `MemoryGraphView` already call `api.ts`; they are not additional bypasses. In particular, DashboardHome's locally named `fetch` function calls `api.getTerminalStatus` (`DashboardHome.tsx:159-174`), while the graph component uses `api.getMemory`, `api.getGraph` and `api.exportGraph`. Do not count a function name or a comment containing `fetch` as a network call.

Share credential acquisition, refresh, expiry and revocation handling across the ordinary API client and the SSE/WebSocket adapters. Do not assume that wrapping ordinary JSON requests also authenticates long-lived streams.

The WebSocket needs its own handling: browsers don't allow custom headers on a WebSocket handshake, so it can't simply reuse the `Authorization` header approach the `fetch` calls will use.

### Why a module boundary, not just a helper function

SAML is the clearest argument for this shape, and for the opposite reason to the one an earlier draft of this issue gave. Because CAO now terminates SAML itself rather than brokering it, the codebase has to hold two genuinely different protocols: OIDC, a token exchange, and SAML, a browser POST of a signed XML assertion that ends in an assertion rather than a credential for API calls.

Without a module boundary that difference leaks outward into route handlers and UI components. With one, SAML is a single adapter that terminates the protocol and returns the same internal principal as every other provider, so nothing downstream ever needs to know which was used.

## Phased delivery

This is a large change, so it should land in reviewable, backward-compatible steps. **The order matters for safety, not just convenience:** agree the identity and ownership model, create/backfill it through the shared migration, then use it for sign-in and enforcement. Filtering cannot safely precede ownership, and session creation cannot precede its versioned store.

Each phase below keeps the default-off local behaviour intact.

### Phase 1 — Consolidate the seams. No behaviour change at all.

Shape the backend `security/` package into the module described above, preserving today's behaviour. On the frontend, connect the API, SSE and terminal WebSocket paths to the common client/authentication boundary.

This is groundwork, not a reason to rewrite components that already use the API client. Keep transport-specific streaming behavior while sharing the authentication and endpoint policy.

**Exit:** behaviour is byte-for-byte identical with auth disabled; there is one choke-point on each side; a test proves no call site reaches the backend without going through the client.

### Phase 2 — The shared identity and ownership migration.

Create the agreed identity, membership and session records together with the tenant/owner/visibility changes under #775. Classify the existing resources under #778, and backfill user-owned rows and content to the local tenant and designated local owner. Scope the keys of `workflow_index`, `workflow_plan_approval`, `flows`, `project_aliases` and `idempotency_keys`, and change the memory uniqueness rules described above. Resolve stored files in that same logical namespace while preserving the fixed local account's existing default paths.

**Plan the migration against the supported SQLite versions and the actual schema.** Follow SQLite's [documented table-rebuild procedure](https://sqlite.org/lang_altertable.html#otheralter) where the chosen key or constraint change requires it:

- **Changing a PRIMARY KEY or an inline UNIQUE constraint requires more than adding a column.** The rebuild copies data into the new definition, drops the old table and renames the replacement inside the migration transaction. Work out the physical changes per table rather than assuming a fixed number of rebuilds.
- **Backfill is not a permanent tenant default.** A non-nullable column added to a populated table needs a non-NULL default on older SQLite versions, or a rebuild. A default affects inserts that omit the value; it does not overwrite an explicitly supplied tenant. In shared operation, missing trusted tenant/owner context must be rejected, not silently assigned to the local account. SQLite 3.53.0 adds `ALTER COLUMN ... SET NOT NULL` after backfilling, but CAO cannot assume that newer capability on every supported installation.
- **Preserve and deliberately update constraints, indexes and relationships.** A replacement table creates its own PRIMARY KEY/UNIQUE indexes from its new definition; old explicit indexes are not copied. Recreate the required indexes and any affected triggers/views, check foreign-key integrity where applicable, and scope uniqueness to tenant and owner rather than restoring the old global rules unchanged.

Shared use stays disabled in this phase, and the existing local behavior remains unchanged. Splitting the migration from enforcement keeps a risky data change separate from a risky behavior change.

**Exit:** the versioned identity/session store exists, every user-owned resource has its tenant and owner, and an existing single-user install upgrades without data loss or changed default file paths.

### Phase 3 — Connect sign-in to the migrated identity store.

Turn a validated external identity into the existing internal principal and application session. Add the sign-in and client-session paths, and verify Entra ID and SAML end to end against the same versioned store.

Verify this intermediate state in an isolated integration environment. Knowing who signed in is not yet sufficient for shared access: shared deployment remains gated until separation, administration and delegated/live-access controls are integrated in phases 4 and 5.

**Exit:** both sign-in adapters and the supported clients use the migrated identity/session model, without a second store or an enabled-but-unisolated shared mode.

### Phase 4 — Enforce separation. This is the switch-flip.

Enforce tenant and owner on reads, lists and writes. Resolve workflow/session names, working directories and stored files through the corresponding authorized namespace, retaining the local path compatibility above. Integrate #779's owner actions and tenant-administrator permissions.

**Exit:** the Alice-and-Bob criteria pass — both can hold a workflow named `deploy` and a session named `dev` with no collision and no overwrite; neither can see or address the other's resources; an approval recorded for one never authorises the other. Browser/API separation alone is not the shared-use release gate: delegation, background work and #779's administration/revocation must also be integrated.

### Phase 5 — Delegation and shared agent-tool identity.

Give agents attenuated credentials tied to the user they act for, and resolve caller identity per request on the shared tool endpoint instead of one server-wide environment variable. Carry the same trusted ownership context through queued/scheduled work and execution callbacks, including after server recovery; revalidate current permissions rather than reviving a stale grant.

This phase overlaps #745's MCP hosting work and should be designed with it rather than twice.

**Exit:** an agent acting for Alice carries a restricted credential bound to her, and a shared tool endpoint serves two agents with distinct caller contexts. Scheduled/queued work stays attributed to its owner without an open browser, and #779's removal and role-change cases hold across clients, running tools and live channels.

### Sequencing with the other issues

Phase 2 is the shared migration carried by #775. Agree one implementation owner under #777's build order; #774, #778, #779 and #780 supply their parts of the same schema and storage-mapping change. Sign-in adapters can be developed against that contract in parallel, but use the migrated store before creating persisted principals or sessions. Administration and revocation are co-delivered with #779; neither whole issue must close before the other can be integrated.

Phase 5 integrates with #745's shared MCP endpoint, rather than waiting for all of #745 to close first. Phases 1 through 4 can proceed alongside the remote bridge; the completed shared-use release requires both.

## Acceptance criteria

- [ ] A user can sign in through a configured identity provider. The browser never receives an IdP client secret or CAO session-signing key.
- [ ] **Microsoft Entra ID and SAML are both tested end to end and documented**, each with its own per-tenant configuration. The SAML implementation uses a maintained library, and certificate rotation is covered.
- [ ] CAO issues its own session token, and it is the single internal format regardless of how the person signed in.
- [ ] Login tokens are properly validated — signature, issuer, audience, expiry — and unsigned tokens are rejected.
- [ ] Stable identity binding distinguishes identical subjects under different issuers and different accounts sharing an email address; changing a display name/email does not orphan the owner's work.
- [ ] Provider group/app-role mappings are tenant-configured inputs to membership. Effective permissions follow #779's membership roles and the credential's permitted operations; existing scope names are not a substitute for a person or an installation-wide grant to a tenant administrator.
- [ ] Every user-owned resource records an owner, and listing, reading, and changing enforce tenant and owner, with only the explicit tenant-administrator permissions defined in #779. Operator configuration and the built-in catalogue follow #778's separate resource classification.
- [ ] The fixed local tenant/owner keeps existing default content paths through the shared resolver. Logical namespacing does not require a physical move on the laptop; any required shared-layout changes have #775's matched data/file recovery.
- [ ] **Alice and Bob can each create a workflow named `deploy`, and a session named `dev`, with no collision and no overwrite.**
- [ ] The same person, with memberships in two tenants, can independently reuse resource names, aliases and idempotency keys. Neither lookups nor uniqueness/approval rules can merge those namespaces.
- [ ] **An approval recorded for one person's workflow never authorises another person's.**
- [ ] Ordinary members using one server cannot see, address, or overwrite each other's terminals, sessions, workflows, messages, or memories.
- [ ] Independent owners and tenants can use identical memory names and relationship tuples, including global/federated NULL scopes and project scopes. Repeated writes within the same ownership boundary retain the intended de-duplication.
- [ ] Workflow files, flow files, memory files/indexes, custom profiles/skills, session names, working directories and credential resolution use the same authorized ownership context. Two members can create, edit, discover and launch different profiles with the same name without affecting one another or the built-in original.
- [ ] Existing single-user data migrates to the local owner without loss, and #778's controlled work-account binding makes that same work accessible when shared sign-in is enabled.
- [ ] Live output streams require a login, and an expiring session is handled visibly rather than by silently dropping the connection.
- [ ] The shared agent-tool endpoint identifies each caller per request, rather than by one server-wide environment variable.
- [ ] CLI, operator MCP, agent tools and the `cao_workflow` client have supported authenticated paths to a shared server; browser-only success does not satisfy this criterion.
- [ ] Supported clients connect directly to a configured HTTPS endpoint with certificate verification, while local HTTP behavior remains unchanged. Sign-in/discovery addresses and browser WebSocket URLs match the advertised deployment rather than the server's bind address.
- [ ] Queued, scheduled and resumed work preserves its tenant/owner without a live initiating HTTP request, and cannot start with stale or missing authority. Revocation follows #779, while authorized cleanup can still complete.
- [ ] Login and authorization policy stays in the shared backend module and frontend client boundary; compatible providers reuse adapters through configuration.
- [ ] Every frontend call to the backend, including the terminal WebSocket, goes through one authenticated client. No call site bypasses it.
- [ ] Local single-user operation behaves as today. Once shared operation is explicitly enabled, missing/broken IdP configuration or a missing principal fails closed rather than restoring the implicit local administrator.
- [ ] Documentation states plainly that this is application-level separation, not an OS security boundary, and what would be required for the stronger guarantee.

## Out of scope

- **Organisations as a concept, and keeping them apart — that is #778**, which this issue depends on. This issue covers identity and per-person ownership *within* an organisation.
- Roles, member management and session revocation — #779.
- Sharing between people — #780 (its schema lands in the same migration as this issue).
- Consumer sign-in such as Google or Facebook, and automatic joiner/leaver sync (SCIM).
- CAO storing passwords or acting as an identity provider itself.
- A new fine-grained permission framework, or changing the local default of "no login required". Existing scope vocabulary can remain, but its guards must implement the owner/member/tenant-administrator policy above.
- Running each user's agents in their own container — that depends on #745.
- The local-machine hardening already tracked in #706.

## Evidence

All verified on `main` at `29b235cf62ed0f9d624bc9ad9afce09ab72f8ddf`.

| Claim | Where |
| --- | --- |
| Existing token scopes `cao:read` / `cao:write` / `cao:admin` | `security/auth.py:44-46` |
| Security layer off unless `AUTH0_DOMAIN` or `CAO_AUTH_JWKS_URI` is set | `security/auth.py:69-85` |
| Falls back to full permissions when disabled | `security/auth.py` (`get_scopes_for_local_token`) |
| 80 of 96 route handlers use `require_any_scope`; some others enforce scopes inline | `api/main.py`; AG-UI at `:1759-1784`, terminal WebSocket at `:6850-6877` |
| Standard protected-resource metadata endpoint exists | `api/main.py:1529` |
| **Workflows keyed by bare name: `name TEXT PRIMARY KEY`** | `clients/database.py` (`workflow_index`) |
| **Workflow specs share one flat folder, keyed by bare name** — `.yaml`, `.yml` and `.py` all scanned, same-stem files treated as siblings | `constants.py:875` (`WORKFLOW_SPEC_DIR`); `services/workflow_spec_service.py:409-411`, `:519-525` |
| 14 tables, none recording an owner (3 text matches, all comments) | `clients/database.py` (8 ORM) + raw `CREATE TABLE` (6 more) |
| `terminals` columns include `tmux_session`, `working_directory` — no owner | `clients/database.py` |
| Web UI has no auth library and no token handling | `web/package.json`, `web/src/` |
| Agent tool server identifies callers by `CAO_TERMINAL_ID` (18 uses) | `mcp_server/server.py` |
| *"THIS IS A SAME-USER LOCAL CONTROL, NOT A PRIVILEGE BOUNDARY"* | `services/approval_gate.py:39` |
| *"The CAO server runs on localhost by default. If exposing externally, use proper authentication and TLS."* | `SECURITY.md:220` |
| Cluster example reached by port-forward, not exposed publicly | `examples/cao-clusters/kubernetes/eks/panel.yaml:9` |

Standards: [RFC 9728 — OAuth 2.0 Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728), [OAuth 2.0 for Browser-Based Applications](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps).

Identity binding: [OpenID Connect Core section 5.7](https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability) defines issuer plus subject as the stable identifier; [Microsoft's ID token claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference) explains mutable email/name claims and the different identities used across applications and directories.

Related: #745 (per-pod execution — the path to real containment), #775 (same database migration; do it once), #706 (local-machine hardening), #736.

Contributor guide

Open the contributing guide

Research direction

Start with security/auth.py, api/main.py:1759-1784 and :6850-6877, then inspect the schema and migration paths in clients/database.py. Read the ownership-sensitive services at services/memory_service.py:298-403 and services/memory_relationship_service.py:440-470, plus the profile paths in services/profile_store.py and utils/agent_profiles.py. Done means identity-aware sign-in, tenant/owner-scoped database keys and storage paths, preserved local migration behavior, and authorized resource isolation.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
authentication, authorization, backend, databases, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.