SDK and ACP-registry agent grid rows
Original work item (verbatim): "Providers and their auth type is a standard plan feature. Refactor current implementations"
Operator decisions (verbatim, PRD §1):
| # | Question (short) | Answer (verbatim) |
|---|---|---|
| D1 | What should the system default + per-org Providers tab become? | "Plan grid + org override (Recommended) — operator comment: Also must be universal with custom agents" |
| D2 | Fate of the plan switch `non_claude_engines_allowed` | "Retire it, grid decides (Recommended)" |
| D3 | Operator's own orgs (ungoverned today) | "Governed by its plan/override" |
| D4 | Pairs a later build adds | "OFF until enabled (Recommended)" |
| D5 | What "universal with custom agents" means | "Agent kinds as grid rows, Each registry agent as a provider" |
| D6 | Where the grid sits | "New 'Providers' tab (Recommended)" |
| D7 | Data migration | "Preserve effective access (Recommended)" |
| D8 | Existing accounts on a now-disallowed pair | "Stop bullshit". Read as: no change from today, so keep current behaviour (never edited or deleted, refused at dispatch). |
## Architecture decisions (verbatim)
| # | Question | Answer (verbatim) | What it changes |
|---|---|---|---|
| Q1 | What does the grid's "SDK agent" row replace? `agents_enabled` gates more than running SDK agents: the Agents pages, agent CRUD, connectors, the project default agent and the task agent pick. | "All of it, retire switch" | `agents_enabled` is retired completely (it overrides PRD A2's partial reading). The `sdk` row gates every manifest agent, connectors included. The Agents pages show when any agent row (`sdk` or any `acp:*`) is ON. |
| Q2 | How does the migration keep registry access where `acp_registry_allowed` was ON? | "All OFF, operator enables" | Every `acp:*` cell starts OFF on `(default)`, on every plan and in every override. **This overrides D7/R7 for registry rows only.** Built-in pairs and the `sdk` row still keep exactly the access they had. |
| Q3 | Which registry agents appear as grid rows? | "Installable ones + stored" | A registry entry is a row when the install route would accept it, or when any plan, `(default)` or override already stores it. |
| Q4 | Delivery order | "Yes, 3 children" | Three children: C1 builds the term and the built-in cutover. C2 (agent rows) and C3 (Providers tab UI) both depend on C1 and build in parallel. |
## Requirements (PRD §3, verbatim — this child delivers the parts its scope names)
**R1 — One plan term, the provider grid.**
- What an org may run is ONE plan term in `app/plan_terms.py`: a set of allowed rows × auth types.
- It resolves the standard way: org override → plan → `(default)` → code constant.
- A plan may inherit it from `(default)`: a new `INHERITABLE` entry, e.g. `providers`.
- The code constant is today's `SEED` (Claude + API key).
- `plan_terms.py` is the single home of the resolution. `provider_auth_policy.py` may remain as the door-facing API (`is_allowed`, `require_allowed`), but it reads only the plan term.
**R2 — Rows of the grid are universal (D1, D5).**
The grid has one row per runnable engine:
- (a) each built-in provider in `agent_providers.PROVIDERS`, with that provider's `auth_kinds`;
- (b) the agent kind "SDK agent" (manifest agents), as one row;
- (c) each ACP-registry agent, as a provider row of its own, keyed by its registry id. Its auth type is the key route registry agents use: API key via ACP `authenticate`.
One resolver answers "may org O run row R with auth type K" for all three.
**R3 — Retire the old mechanisms (D1, D2, D5).** Removed, migrated first (R7):
- the system default (`system_state` `provider_auth_defaults`, Settings › Providers › Default providers, its API routes);
- the org Providers tab and `orgs.provider_auth_override`;
- the plan switch `non_claude_engines_allowed` (SWITCHES row, plan column, org override column, console twin `planSwitches.tsx`, Features-grid entry, `CONSOLE_SWITCHES`, dev seed);
- the switches the grid's agent rows replace: `agents_enabled`'s gate on running/installing SDK agents and `acp_registry_allowed`.
Columns are dropped only per the additive-migration rule: they stay until no release reads them (`docs/standards/coding-standards.md`).
**R4 — Every door reads the grid.** Every place that today reads tiers 1–4 reads the grid term instead:
- account create;
- dispatch candidate pick (`auth_pair_disallowed`);
- sign-in;
- task create/update provider pin;
- automations;
- project default provider;
- console provider pick;
- agent picker built-ins;
- SDK-agent run/install;
- ACP-registry browse/install/bind/dispatch, including the registry agent's bound key account at bind time and at dispatch.
A provider is "on the plan" iff at least one of its pairs is allowed (D2). Refusals name the row and the auth type, and point to Billing for a tenant: "<Row> with <auth type> is not on your plan. Upgrade in Billing."
**R5 — Operator orgs governed (D3).** Operator-plane orgs resolve the grid like tenants:
- an operator-plane org has no plan, so it reads `(default)` plus its own org override;
- the "not governed" carve-out in `provider_auth_policy.py` / `tenancy` is removed for this term;
- the term is overridable on operator-plane orgs (`OPERATOR_PLANE_SWITCHES` equivalent).
**R6 — New rows OFF (D4).** A pair, provider, or registry agent that a later build or a registry refresh adds is OFF on every plan, `(default)` and org until an operator turns it on. It is never ON by inference.
**R7 — Migration preserves effective access (D7).** After the migration every org — tenant, operator-plane, staging (via its parent) — can use exactly the set of built-in pairs it could use before. Computed per org as `(org override ?? system default, or all supported pairs for operator-plane) ∩ non_claude switch`. The rest of the migration:
- `(default)` = today's system default;
- each plan = that list filtered by the plan's effective Non-Claude switch, or inherits `(default)` when the filter changes nothing;
- an org whose effective set differs from its plan's gets an org override equal to its effective set today;
- SDK-agent and registry rows: ON exactly where `agents_enabled` / `acp_registry_allowed` were effectively ON;
- the migration is idempotent and reversible per the repo's migration rules.
**R8 — Existing accounts (D8).** Unchanged from today: an account whose pair the terms disallow is never edited or deleted. It is shown off-plan and never leased.
**R9 — UI, Providers tab (D6).** A new **Providers** tab beside Price / Limits / Features / Products, in both:
- the plan editor (Products › plan), with "Inherit from (default)" per the standard inherit control;
- the org's Overrides, with "Use plan" / "Override" and "Overridden — plan: X" like every other term.
It shows one grid: rows = built-ins, SDK agent, registry agents; columns = auth types. Not-applicable cells are absent, not disabled checkboxes.
On the org Overrides it is offered for operator-plane orgs too (R5). The tenant-facing account page (`AccountEditPage.tsx`) names hidden rows via the plan, not "policy", and its remedy points to the Providers override tab.
Follow `docs/standards/ui-style-guide.md` and `docs/standards/ui-copy-standards.md`.
**R10 — Docs single-homed.**
- `plan_terms.py`'s header lists the new term and drops "non-Claude engines";
- `provider_auth_policy.py`'s header is rewritten or deleted to match;
- reader docs (`apps/docs`) and router rows that mention Default providers / Providers tab / Non-Claude engines describe the present;
- no copy of the resolution rule outside its home.
## This child's scope and approach (Architecture §2, verbatim)
### C2: Agent rows, SDK agent and ACP registry
*Depends on: C1 (resolver, `Cell`, catalogue endpoint).*
**Scope:**
- The `sdk` and `acp:*` rows: decode rule, `validated`, and the catalogue `agents`/`registry` groups with the extracted `install_refusal` predicate.
- §1.5 rows marked C2, including the **new dispatch re-check** for registry rows and the bound key account's pair, at bind time and at dispatch.
- §1.6 C2 removals (`agents_enabled`, `acp_registry_allowed`), web included.
- Migration M2 and its test.
- `plan_terms` header bullets.
- R10 docs for the agent switches: every `apps/docs` page, router row and doc that names the Agent catalog switch, ACP registry switch, `agents_enabled` or `acp_registry_allowed`.
- The PR body and release note state the Q2 consequence: installed registry agents stop dispatching until an operator enables their row.
**Acceptance it owns:**
- AC4: agent switches.
- AC6: registry bound key.
- AC8.
- AC10: `acp:*` unknown.
- AC11: amended oracle, "built-in pairs and `sdk` access identical before and after; every `acp:*` cell reads OFF everywhere after M2".
- AC13.
- AC14: widened grep, `rg -n "Agent catalog|ACP registry switch|agents_enabled|acp_registry_allowed" docs apps/docs apps/api/app apps/web/src` returns no live prose or reader.
**Tests:** `test_acp_registry.py`, `test_agents_plan_switch.py` (renamed to `test_agent_rows_plan.py`), `test_agent_task_credentials.py`, `RegistryPage.test.tsx`, `App.test.tsx`.
## Pinned design (Architecture §1, verbatim — built by C1; build against it, do not re-decide)
### 1.1 The term: a set of cells
The term is a **cell**: `(row, auth)`, stored as `[row, auth]`.
| Row key | Rows | `auth` values |
|---|---|---|
| built-in provider value (`claude`, `codex`, `deepseek`, `antigravity`, `pi`) | `agent_providers.PROVIDERS` | that provider's `auth_kinds` (`oauth`, `api_key`) |
| `sdk` | the "SDK agent" row: manifest agents and catalog connectors | `own` (own credentials; the agent leases no account) |
| `acp:<registry id>` | one row per ACP-registry agent | `api_key` (the key reaches the agent through ACP `authenticate`) |
Registry rows get the `acp:` prefix so a registry id can never collide with a built-in value. For example, the registry has `pi-acp` and `codex-acp` beside the built-ins `pi` and `codex`.
**Storage** is two nullable JSONB columns:
- `billing_plans.providers`: the list of cells, in catalogue order (`encoded`).
- `orgs.providers_override`: NULL means "use plan"; a list replaces the plan's value outright.
`INHERITABLE` gains `"providers": ("providers",)`. The code constant is `SEED = {("claude", "api_key")}`.
### 1.2 Resolution: single-homed in `plan_terms.py` (R1, R5)
- `PlanTerms` gains `providers: Term[frozenset[Cell]]`. It is built in `effective()` exactly like `products`: org override, else `field(plan, dflt, "providers")`, else `SEED`.
- Staging orgs resolve through `terms_org` with nothing extra: they get the parent's plan value and the parent's override.
- **An operator-plane org reads `(default)` plus its own override, never its `plan_key`'s plan.**
- **`org_id is None`** (no org: system callers) resolves to the `(default)` row's value, else `SEED`.
- **Plan includes.** `includes_plan_key` does not apply to this term.
- A fast reader `providers_of(session, org_id, *, memo=None) -> frozenset[Cell]` returns only the value, for hot paths.
- **Decode rule (AC10, R6).** A stored cell is kept when:
- its row is built-in and the pair is in `supported_pairs()`, or
- its row is `sdk` with `own`, or
- its row matches `acp:<_ID>` with `api_key`.
Anything else is dropped on read. The resolver **never fetches the registry**: dispatch has no CDN dependency. A cell is ON only if it is stored, so a new pair, provider or registry agent reads OFF everywhere until an operator stores it.
### 1.3 The door-facing API: `app/provider_auth_policy.py`
```python
Cell = tuple[str, str]
SDK_ROW: Final = "sdk"; OWN: Final = "own"; REGISTRY_PREFIX: Final = "acp:"
def builtin_cells() -> tuple[Cell, ...] # renamed supported_pairs(); acp_runtime.py keeps calling supported_pairs (alias kept)
def validated(cells) -> frozenset[Cell] # 422 naming the first malformed/unsupported cell; shape-only for acp:* (no CDN)
def encoded(cells) -> list[list[str]] # stable order: built-ins (catalogue), sdk, acp:* sorted
def allowed(session, org_id, *, memo=None) -> frozenset[Cell]
def is_allowed(session, org_id, row, auth, *, memo=None) -> bool
def provider_on_plan(session, org_id, row, *, memo=None) -> bool # any cell of `row` allowed (D2); replaces engine_on_plan
def refusal(session, org_id, row, auth=None) -> str
def require_allowed(session, org_id, row, auth) -> None # 422 refusal(...) + " Nothing was created."
```
`refusal` returns:
- tenant: `"<Row label> with <OAuth|API key|own credentials> is not on your plan. Upgrade in Billing."`
- operator-plane org: `"… is off in this organization's Providers override."`
- with no `auth`: `"<Row label> is not on your plan. …"`
Row labels come from the provider label, "SDK agent", or the registry entry's name (falling back to its id).
### 1.4 API seams (C2 extends items 3–5)
3. **Grid catalogue**: `GET /api/provider-grid`, readable by whoever may read plans.
```json
{"columns": [{"key":"oauth","label":"OAuth"},{"key":"api_key","label":"API key"},{"key":"own","label":"Own credentials"}],
"groups": [{"key":"builtin","label":"Providers","rows":[{"key":"claude","label":"Claude","auth":["oauth","api_key"]}, ...]},
{"key":"agents","label":"Agents","rows":[{"key":"sdk","label":"SDK agent","auth":["own"]}]},
{"key":"registry","label":"ACP registry","rows":[{"key":"acp:cline","label":"Cline","auth":["api_key"],"note":null}, ...]}],
"registry_unavailable": false}
```
- C1 serves the `builtin` group only. C2 adds `agents` and `registry`.
- Registry rows are the entries that pass the **same predicate the install route uses** (extracted from `acp_registry.py:223-302` into one function, `install_refusal(entry) -> Optional[str]`), plus every `acp:*` id stored anywhere (`note: "Not in the registry"` when the live list lacks it).
- If the CDN is down, the response carries the stored ids plus `registry_unavailable: true`, and the UI says so.
4. **Identity payload** (`routes/auth.py`):
- `providers: list[list[str]]` is the principal's org's allowed cells.
- It replaces `non_claude_engines_allowed` (C1) and `agents_enabled` and `acp_registry_allowed` (C2).
- Web helper `apps/web/src/lib/providerGrid.ts`: `cellOn(p, row, auth)`, `providerOn(p, row)`, `agentsOn(p)` (true when `sdk` or any `acp:*` is ON), `registryOn(p)`. These helpers become the only console readers.
5. **Org roster `plan_switches`**: the act-as narrowing on the Agents pages now keys on the roster's `providers` (per org), read through the same `agentsOn`.
### 1.5 Doors this child moves to the grid (Architecture §1.5, C2 rows verbatim)
| Door | File (today) | New call | Child |
|---|---|---|---|
| SDK agent run | `agent_resolution.py:131-134` (`AGENTS_OFF`) | `is_allowed(sdk, own)` for v1 / manifest-2 / connector rows | C2 |
| SDK agent CRUD, catalog, connectors | `routes/agents.py:301-310` `_ON_PLAN` | create/install needs `sdk/own`; listing/page needs `agentsOn` | C2 |
| Project default agent, task agent pick | `routes/projects.py:1934`, `routes/tasks/create.py:166` | the picked agent's cell | C2 |
| Registry browse | `routes/acp_registry.py:37-49` | any `acp:*` allowed; entries off-plan hidden for a tenant, shown as "Off" to an operator | C2 |
| Registry install | `routes/acp_registry.py:143-179` | `require_allowed(acp:<id>, api_key)` | C2 |
| Registry key bind | `acp_registry.key_account_refusal` (`:405`), via `routes/agents.py:394-415` | plus `is_allowed(account.provider, account.auth_kind)` → refusal names the pair | C2 |
| Registry dispatch | `agent_resolution._unrunnable` (`:131-154`), `lease_drain.py:1493` | `is_allowed(acp:<slug>, api_key)` **and** the bound account's pair (a new check: today dispatch re-checks neither) | C2 |
| Console places tag | `support_agent/console_places.py:186-188` | `agentsOn` equivalent | C2 |
| Web readers (agent ones) | `App.tsx:336-339`, `directoryTabs.ts:62`, `agentsTabs.tsx:43`, `RegistryPage.tsx:112` (and any agent reader in `AgentPicker.tsx:126-182`) | `lib/providerGrid.ts` | C2 |
### 1.6 Retired mechanisms — C2 part (verbatim)
**C2 removes** `agents_enabled` and `acp_registry_allowed` the same way: SWITCHES, CONSOLE_SWITCHES, models, identity, planSwitches, Features grid, the dev seed, and their `plan_terms` header bullets.
**Columns are dropped later.** The DB columns stay, unmapped and read by nothing, until the contract step (follow-up F1). The removed ORM mappings are nullable columns, so inserts keep working.
### 1.7 Migrations (shared rules + M2, verbatim)
Each child migrates what it cuts over, in the same PR as the cutover, so no window lets the two sources drift. Each is a plain Alembic revision `YYYYMMDDHHMM_<slug>` in `apps/api/migrations/versions/` with `SET lock_timeout = '3s'`. Each is **expand-only** (additive rule, `docs/standards/coding-standards.md`) and **frozen**: no `app.*` import (`test_frozen_migration_imports.py`), so the built-in pair list and the switch-chain logic are copied into the revision as constants.
**M2 (C2), `<ts>_provider_grid_agents`:**
Goal: every org's effective SDK access equals its effective `agents_enabled` today (the `_switch` semantics, include-chain and operator plane included).
1. Set `(default)`'s `sdk/own` cell from `(default).agents_enabled`, else the code default, which is OFF.
2. **Inheriting plans.** For each plan whose `inherits` holds `"providers"`, compute its effective `agents_enabled` (chain OR, else own value, else `(default)`'s). If it differs from step 1's cell:
- remove `"providers"` from `inherits`;
- store `(default).providers` plus or minus `sdk/own`;
- record the plan key in a marker list, `system_state['provider_grid_m2']`, so the downgrade can restore it.
3. **Own-value plans.** For each plan with its own `providers`, set or clear `sdk/own` to match its effective `agents_enabled`.
4. **Orgs.** For each non-staging org, `old` = its effective `agents_enabled` (org override, else the plan value or chain, else `(default)`; operator plane reads `(default)` plus its override). `new` = whether `sdk/own` is in the value it now resolves to.
- An org that has a `providers_override` gets the cell set to `old`.
- An org without one gets an override (its resolved set plus or minus `sdk/own`) only when `old != new`. Its id is added to the `provider_grid_m2` marker.
5. **No `acp:*` cell is written anywhere (Q2).**
6. Idempotent: each step sets the cell to a computed target, so a re-run changes nothing.
7. Downgrade:
- remove `sdk/own` from every stored list;
- for plans in the marker list, put `"providers"` back in `inherits` and NULL their `providers`;
- for orgs in the marker list, set `providers_override` back to NULL;
- delete the marker.
**Migration tests** follow the pattern in `tests/test_migration_202610171200_project_memory_switch.py`. They seed the PRD AC11 matrix and assert that effective sets are equal before and after, that downgrade restores them, and that a second upgrade is a no-op. The matrix covers:
- a tenant with an override and a tenant without one,
- the plan switch on and off, including through `includes_plan_key`,
- an org switch override,
- an operator org,
- a staging org,
- a dangling `plan_key`,
- a NULL plan switch reading `(default)`.
Risks (verbatim, Architecture §4, the ones this child carries):
| Risk | Mitigation |
|---|---|
| **Q2 consequence.** Every registry agent an org has **installed** stops dispatching after C2 deploys, until an operator turns its `acp:<id>` cell on. Today dispatch never re-checks the switch. The operator chose this. | C2's release note and PR body name it. The Providers tab shows installed ids (stored or not) via the catalogue `note`, so they are easy to find. |
| A generic refusal ("not allowed") passes the status-code tests but does not tell the user the remedy. | A single `refusal()` sentence. Tests assert that the row label and auth label appear in the detail. |
| Registry CDN outage breaks the plan editor or dispatch. | The resolver never fetches. The catalogue falls back to stored ids with `registry_unavailable`. |
| A stale console bundle reads the removed identity flags. | Web and API deploy together. A missing flag reads as off, so the failure is fail-closed. |
| AC4's `rg` oracle matches migration **tests**, which must name the old columns. | Amend the oracle to `--glob '!**/migrations/**' --glob '!**/test_migration_*'`. |
Credentials & reach:
| Oracle | Credential / identity / environment | Status |
|---|---|---|
| All `./dc test`, `./dc check quick`, migration tests, `check_context.py` (AC5–AC8, AC10–AC14 API parts) | nothing beyond the worktree | verified (worktree-only) |
| Browser flows AC4 (agent switches), AC8 | dev control plane at `https://app.dev.jaah.ai`; credential `me/jaah/debug/main/operator-login` (operator, can switch to tenants) | verified: on the roster, `credential_get` succeeded, read-only HTTP probe from node h5-3 (Architecture §6) |
| AC6 browser bind refusal (C2) | The same operator credential on `app.dev.jaah.ai`. Setup is created in-product during C2's verification: install one registry agent (with its row ON), create an API-key account (a dummy key is enough for the bind refusal, since bind checks the pair before any probe), then turn that pair OFF and attempt the bind. | verified: the credential and reach are the same as above. The setup is not pre-staged; C2 creates it through the console and deletes it after |
| Merge and deploy watch | `jaah-forge` (pre-authenticated per task) | verified by design (runner charter) |
Out of scope:
PRD §5 (verbatim) — do NOT do any of these:
- MUST NOT change the ACP-vs-legacy runtime switch (`app/acp_runtime.py`, Settings acp-runtime). It is an operator cutover control, not a plan term.
- MUST NOT change `jaah_ai_enabled` / the Jaah AI account lane.
- MUST NOT change other plan switches, caps, quotas, products or pricing.
- MUST NOT edit, disable or delete any existing account (D8).
- MUST NOT add new providers, auth kinds, or registry distribution rules (`acp_registry.pick_distribution`, key routes).
- MUST NOT change OAuth-first spillover or account picking beyond the pair verdict.
- MUST NOT touch the node daemon (`apps/jaahd`) except, if needed, reading `agent_providers` metadata. No node wire change.
- MUST NOT keep any of the retired mechanisms as a parallel source of truth (no "system default" fallback beside `(default)`).
- Non-runnable parts of the agent catalog (e.g. connectors, if `agents_enabled` gates them) stay behind `agents_enabled` unless the Architect shows the switch then decides nothing. See A2. (Architecture Q1 settled this: `agents_enabled` is retired completely; the `sdk` row gates connectors too.)
- Architecture boundary: do NOT build the Providers tab UI (child C3). Do NOT change the C1-pinned resolver/seam signatures beyond extending them with the `sdk`/`acp:*` rows and catalogue groups.
- Q2: M2 writes NO `acp:*` cell anywhere.
- Do NOT drop any DB column (deferred F1); do NOT edit `apps/jaahd/agent_providers.py` (deferred F2).
States to cover in tests (PRD, verbatim):
| Subject | States |
|---|---|
| Term | plan own / plan inherits `(default)` / no `(default)` row (code constant) / org override / staging (parent's) / operator-plane org (no plan) |
| Pair | known+on / known+off / unknown to stored terms (off) / no longer supported by build (dropped) |
| Account | on allowed pair / on disallowed pair |
Done means:
| # | Criterion | Verify |
|---|---|---|
| AC4 | The old surfaces are gone: no Settings › Providers › Default providers, no org Providers tab, no "Non-Claude engines" in any Features grid or override, no ACP-registry or Agent catalog switch where the grid replaced it (R3). Old deep links redirect or 404 cleanly, never a blank page. | Browser: search Settings for "Default providers" → none. Open org T tabs → no "Providers" tab outside Overrides. Plan Features → no "Non-Claude engines". `rg -n "non_claude_engines\|provider_auth_override\|provider_auth_defaults" apps/ --glob '!**/migrations/**'` → no live reader. |
| AC6 | Dispatch never leases an account on a disallowed pair, including a registry agent's bound key account. | `./dc test` on the fleet candidate and registry-binding tests, with a new case "bound key on disallowed pair → refused, named". Browser: bind a registry agent to an account whose pair is off → bind refused with the reason. |
| AC8 | SDK-agent row OFF → the org cannot run or install SDK agents and the picker offers none. ON → it can. Each registry agent row OFF → that agent cannot be installed or dispatched. ON → it can. | Browser: toggle SDK agent off for T, open the agent picker → no SDK agents. Toggle one registry agent off → its Install is refused or hidden. API tests green. |
| AC10 | A pair or registry agent unknown to the stored terms reads OFF everywhere. | Unit test: a pair added to the catalogue and absent from stored terms → `is_allowed` False for plan, `(default)`, override, operator org. |
| AC11 | Migration preserves effective access: every org's allowed built-in pair set, and its SDK/registry agent access, is identical before and after. | Migration test: seed orgs covering tenant with override, tenant without, plan switch on/off, org switch override, operator org, staging org. Snapshot effective sets → upgrade → equal. Downgrade → restores. Also on dev: before deploy, record 3 orgs' Accounts › Add offerings; after deploy → same. |
| AC13 | Gates green: quick gate and full gate, context checker. | `./dc check quick`; merge lane `./dc check full`; `python3 tools/check_context.py` → pass. |
| AC14 | Docs describe the present (R10). | `rg -n "Default providers\|Non-Claude engines" docs apps/docs apps/api/app apps/web/src` → no stale prose. `check_context.py` green. |
Ownership in this child (Architecture §2 C2, verbatim): AC4: agent switches. AC6: registry bound key. AC8. AC10: `acp:*` unknown. AC11: amended oracle, "built-in pairs and `sdk` access identical before and after; every `acp:*` cell reads OFF everywhere after M2". AC13. AC14: widened grep, `rg -n "Agent catalog|ACP registry switch|agents_enabled|acp_registry_allowed" docs apps/docs apps/api/app apps/web/src` returns no live prose or reader.
- AC4 amended oracle for this child: `rg -n "Agent catalog|ACP registry switch|agents_enabled|acp_registry_allowed" apps/ docs apps/docs --glob '!**/migrations/**' --glob '!**/test_migration_*'` → no live reader or prose; Settings/plan Features/org Overrides show no Agent catalog or ACP registry switch.
- Browser steps that toggle grid cells (AC6 bind setup, AC8) need the Providers tab from the sibling child "Providers tab in plans and overrides". Run them through that tab once it is deployed on dev; if it has not merged when this PR is ready, mark them `(deferred — Finish verifies after both children deploy)` and do NOT edit dev through the API. The API tests for AC6/AC8 are mandatory either way.
- Dev-state rule: the hub rule is "Operate Jaah only through the Jaah UI" — never change dev plans/overrides through raw API calls. Any dev value a browser oracle changes is restored to its prior value afterwards, and the restore is cited. This rule overrides the architecture's line that the grid is "editable only through the plans/org-plan API on dev" between C1 and C3: such checks wait for the Providers tab or defer to Finish.
- AC6 browser setup per Credentials & reach: install one registry agent (row ON), create an API-key account (dummy key), turn its pair OFF, attempt the bind → refused with the pair named; delete the setup afterwards.
- PR body and release note state the Q2 consequence: installed registry agents stop dispatching until an operator enables their row.
- Quality floor (planner tightening): bind and dispatch refusals name the registry agent / pair and remedy; a generic refusal FAILS AC6/AC8.
Activity
- Draft
- Ready
- Runner started