Skip to main content

Agents

The Agents module (apps/agents) is Constellation's agent control plane — the future home of unified agent-run observability and the agent problem-report inbox, per ADR-026's control-plane boundary.

Current state: zone scaffold​

The module is a registered fifth zone (PLT-739), carrying the capability-gap inbox's routes and triage page (below). What exists:

  • A Next.js sub-zone served at /agents through the Directory shell (production) and standalone on port 3030 (local dev / preview), following the platform's multi-zone conventions.
  • The canonical tenant-auth wiring (authedRoute / authedRouteWithParams), the shared /api/auth/* surface (mock login, logout, me, memberships), and auth middleware with the shared principal-liveness check.
  • The agents app entitlement: identity.tenant_apps accepts it, and the app.agents.read access permission is seeded by migration 048. The migration grants it to no one — a role grant is an audit-critical authority change — so admin-shaped operator roles acquire it through the audited default-role reconciliation, which for existing tenants means running the backfill workflow (see Operations step 3).
  • The shared cross-module navbar lists Agents in every app's switcher.
  • A required CI smoke (E2E · agents) proving the authenticated shell serves.

Capability-gap inbox (PLT-694, in progress)​

Agents report platform capabilities that are missing, broken or repeatedly worked around as closed, structured evidence (SPEC-plt-694). The data layer is @constellation-platform/coordinator's (coordinator migration 022); the zone serves the routes and the triage page:

RoutePurpose
POST /api/capability-gapsFile one report, { report, delivery }. Every outcome after authentication is a typed filed / not_filed body with a retryDisposition. Filing is gated shut — see below.
GET /api/capability-gapsOne keyset page of a lane: ?status=open (default), acknowledged, resolved, wont_fix, or ?lane=attention (resolved / won't-fix cases that recurred). ?auditRef=<uuid> maps a lifecycle-audit pseudonym to at most one case.
GET /api/capability-gaps/{id}One case and a page of its occurrences, newest first.
PATCH /api/capability-gaps/{id}One declared transition, { from, to }, with an optional linkedIssueKey (null unlinks).
/capability-gaps (page)The triage page — attention lane above the oldest-first queue.

Who can do what. Reads and transitions require agents.problem_reports.triage from a GLOBAL role assignment of a GLOBAL role (new tenants' default roles, and for existing tenants the audited post-deploy repair, grant it to the canonical GLOBAL Admin and Project Manager — Directory migration 053 only defines it); a PROJECT-, PROGRAMME- or MODULE-scoped assignment of the same role is refused with a 403. Filing requires agents.problem_reports.file from any assignment. A proposal-backed case is invisible to a triager whose clearance does not admit its classified sidecar row — the coordinator decides that inside each query.

Filing is dark. Until the retention worker (PLT-839) can delete what is filed, every POST answers the retryable not_filed: backend_unconfigured; the readiness heartbeat that opens the gate lands with the MCP activation slice. There is no environment switch.

Cross-zone reads. A report's taskRef, a triager's linkedIssueKey and an optional resourceRef are resolved under the caller's own credential through the owning zone's canonical reads, then checked against the caller's tenant. The zone needs PROJECTS_ZONE_URL, WIKI_ZONE_URL and CATALOG_ZONE_URL (server-side), with PROJECTS_ZONE_BASE_PATH / WIKI_ZONE_BASE_PATH / CATALOG_ZONE_BASE_PATH set to "" when an origin serves a standalone build at /. An unset zone URL fails closed: a resource reference is dropped, and a task reference counts as an upstream failure — the retryable not_filed: upstream_unavailable when no verified session remains. Task and issue links on the triage page use NEXT_PUBLIC_PROJECTS_URL in local dev and the /projects multi-zone path in production.

What lands next​

WorkScope
PLT-837Run-report ingest (coordinator.run_reports, agent registry, ingest bindings), the unified run feed, and reporter adapters
PLT-694The rest of the problem-report inbox: the MCP tools (report_problem, list_capability_gaps) and activation
PLT-839Retention worker for capability-gap occurrences
PLT-840MCP problem-report retry ledger and receipt contract

The module owns no database schema: control-plane tables live in the coordinator schema owned by packages/platform/coordinator, which is the sole access path for app code (ADR-026's transitional-ownership rule).

Operations​

A fresh Vercel project inherits no environment from the sibling zones, so provisioning constellation-agents has two halves:

  1. Mirror the standard sub-zone environment the other zones already carry (the constellation-wiki project is the closest template): the database DSNs (DATABASE_URL, DIRECT_URL — the runtime DSN must be the non-bypass constellation_app role, or the assertNonBypassRole startup guard refuses to boot; that role must also hold EXECUTE on projects.enter_tenant_scope(text) and on projects.rls_attested_tenant_scope(), which constellation_app does, because the capability-gap session reads enter Project Tracker's attested tenant scope (ADR-068) — a role without them answers a case read with a 503 and every filing with the retryable not_filed: backend_unconfigured), AUTH_PROVIDER plus the selected provider's credentials (the middleware 503s without a provider), NEXT_PUBLIC_AUTH_PROVIDER (the production client bundle resolves to unknown and fails closed without it), and JWT_SECRET. NEXT_PUBLIC_DIRECTORY_URL is optional: in production the zone redirects an unauthenticated page request to the Directory sign-in page relative to the shared origin (/login?redirect=/agents/…), and it is only needed when the Directory zone lives on another origin (local dev: http://localhost:3001) — without it a standalone preview serves a public sign-in-required page instead of looping (PLT-992).

  2. Add the four agents-specific settings:

    • NEXT_PUBLIC_BASE_PATH=/agents on the agents Vercel project (Production only — previews serve standalone at /).

    • AGENTS_ZONE_URL on the Directory shell's Vercel project, pointing at the agents deployment.

    • API_KEY_JWT_SECRET on the agents project, rotated in lockstep with Directory's copy — API-key JWTs are rejected without it, and a divergent rotation 401s every bearer caller.

    • NEXT_PUBLIC_AGENTS_ZONE_ENABLED=1 on every zone's Vercel project — directory, projects, catalog, wiki and agents — because each renders its own copy of the shared app switcher. This is the last step of the two halves, not part of them: until it is set, the switcher does not offer Agents at all, which is what keeps a registered-but-undeployed zone from advertising a link whose upstream does not exist. Setting it before AGENTS_ZONE_URL reinstates exactly that broken link, so set it after, and set it everywhere — a zone that misses it silently drops the entry only for users who happen to be in that zone.

      Setting the variable is not enough — every zone needs a FRESH git-based deployment afterwards, and vercel redeploy is not one. Next inlines NEXT_PUBLIC_* at build time, so the bundles already serving each zone were built while this flag was absent and will keep hiding Agents no matter what the Vercel project settings say. vercel redeploy re-serves an existing build and has been observed not to pick a new value up at all — see .ai/lessons/platform-build-and-deploy.md (2026-07-18). Push a commit, or run a CLI deploy from a real clone checked out on the target branch; never from a per-task worktree, which carries no branch attribution and so applies no branch-scoped variables. The flag is declared in turbo.json's tasks.build.env alongside NEXT_PUBLIC_BASE_PATH and NEXT_PUBLIC_AUTH_PROVIDER, so the Turborepo cache treats a change to it as build-invalidating rather than relying on framework inference to notice.

      The gate is an explicit flag rather than an inference from NEXT_PUBLIC_AGENTS_URL, which is a local-dev variable that production deliberately leaves unset (the multi-zone shell serves relative paths), or from AGENTS_ZONE_URL, which is server-only and unreadable from the switcher. The zone the user is already inside is exempt: the menu always lists the module it is rendered in.

  3. Activate the permission for existing tenants. Migration 048 seeds app.agents.read but deliberately grants it to no role (a grant must co-commit its audit entry, domain event and authority-cache invalidation, which migration SQL cannot do). Newly provisioned tenants are covered — provisioning reconciles their default roles — but existing tenants' canonical Admin roles acquire the permission only when role reconciliation next runs. So after the migration applies, run the Default role backfill workflow once per environment, staging first.

    Follow the canonical runbook — .ai/runbooks/default-role-production-repair.md — for the procedure itself, rather than the summary below: it owns the staging rehearsal, the production canary, and the evidence requirements, and a second copy here would drift from it.

    Two properties of the workflow are worth stating anyway, because getting either wrong looks like success:

    • A run dispatched with the default inputs changes nothing. write defaults to false, so the repair job is skipped and the run still reports green. Read the plan artifact and confirm the Repair (<Environment>) job actually ran — not the tick.
    • all-active is never the first production write. The runbook requires starting with target_mode=tenant for a single canary tenant, confirming a clean and idempotent result, and only then planning and writing all-active. Dispatching all-active first would put every active tenant behind one unreviewed blast radius.

    For this activation the inputs are environment (Staging, then Production), target_mode (tenant with a tenant_id for the canary, all-active afterwards), and write (false for each plan pass, true for each repair pass). A Production dispatch must come from main — the verify-ref gate rejects any other ref — and each repair pass needs an approval on the protected <Environment> environment from a second operator (self-approval, and an approval on the separate … Plan environment, are both rejected). Skipping the activation is silent today and becomes a lockout for those tenants' admins the day PLT-440's fail-closed resolver starts enforcing app entitlement.