Follow-up from #1884 (closed) / #1878 / ADR-0049.
Re-evaluated 2026-07-18. The original blocker has partially cleared and the remaining work changed shape; body rewritten (original framing preserved below under "History").
Status: still open, re-scoped
Agent visibility (global/organization/private) is still not enforced — deliberately, per ADR-0049 / ADR-0056 D8 it is carried as [EXPERIMENTAL] with banners at every authoring surface (all pointing at this issue):
- schema:
packages/spec/src/ai/agent.zod.ts (EXPERIMENTAL banner on visibility)
- authoring form:
packages/spec/src/ai/agent.form.ts (helpText: setting private does not hide the agent)
- liveness registry:
packages/spec/liveness/agent.json (visibility: experimental)
- QA conformance matrix:
packages/qa/dogfood/test/authz-conformance.matrix.ts (agent-visibility: experimental)
access + permissions remain the enforced gates at the chat route (#1884, evaluateAgentAccess() in service-ai agent-access.ts).
What changed since filing (2026-06-15)
✅ Original blocker resolved — tenant is now in the request context. ADR-0095 (accepted 2026-07-14, implemented): resolveAuthzContext (packages/core/src/security/resolve-authz-context.ts) is the single transport-agnostic authz resolver and carries tenantId (session activeOrganizationId or API-key principal), org_user_ids, and a monotonic posture rung PLATFORM_ADMIN > TENANT_ADMIN > MEMBER > EXTERNAL derived from capability grants. Tenant isolation is Layer 0 (plugin-security/src/tenant-layer.ts).
❌ New blocker — the agent-side anchor was removed. agent.tenantId was deleted from the spec (#2377, ADR-0049: it had no runtime reader and did not actually scope the agent), and the agent schema has no owner/creator field. The originally proposed gate — organization → caller tenant must match agent.tenantId — is therefore no longer implementable as written.
Remaining design decisions
Agents are metadata artifacts, so the anchor question is now:
organization: with no per-agent tenant id, the meaningful reading is "authenticated members only" (posture ≥ MEMBER), vs global = also anonymous/EXTERNAL. Note the EXTERNAL rung is defined but never derived yet (no external principal type — posture-ladder.ts), so organization vs global is currently indistinguishable at runtime; enforcement becomes meaningful once external principals exist.
private: needs an owner. Studio-authored agents have created_by on their sys_metadata row; file-authored agents have no owner. Either restrict private to DB-stored agents, or define it as "requires an explicit access list".
Work plan (when scheduled)
- Decide the semantics above (small ADR, or amendment to ADR-0049/0095).
- Enforce in service-ai: extend
evaluateAgentAccess() for the chat route and filter the GET /ai/agents list route by visibility (both surfaces, or private still leaks by listing).
- Add conformance coverage in the authz matrix.
- Flip liveness
experimental → live and remove the EXPERIMENTAL banners in agent.zod.ts / agent.form.ts / liveness/agent.json / authz-conformance.matrix.ts.
Priority: P1 → P2
The P1 rationale was a silent false-security gap. Since the 2026-07 ADR-0056 D8 sweep, visibility is loudly marked EXPERIMENTAL / NOT ENFORCED at every authoring surface, and real gating (access/permissions) is enforced — this is now a documented unimplemented feature, not a silent hole.
History — original issue body (2026-06-15)
#1884 enforced agent access + permissions in the chat route, but visibility (global/organization/private) is still not enforced. The route's RouteUserContext carries userId/roles/permissions but no tenant id, and the agent has no owner field — so a correct gate (organization = same tenant; private = owner only) can't be decided at the route layer today.
Needed
- Add tenant id (and, for
private, an agent owner/creator) to the authenticated request context via the auth middleware.
- Then gate the chat route:
global → open; organization → caller tenant must match agent.tenantId; private → owner only.
Until then, enforcing a partial/guessed version was intentionally avoided (would risk both lock-out and false security). See agent-access.ts for the documented gap.
Follow-up from #1884 (closed) / #1878 / ADR-0049.
Status: still open, re-scoped
Agent
visibility(global/organization/private) is still not enforced — deliberately, per ADR-0049 / ADR-0056 D8 it is carried as[EXPERIMENTAL]with banners at every authoring surface (all pointing at this issue):packages/spec/src/ai/agent.zod.ts(EXPERIMENTAL banner onvisibility)packages/spec/src/ai/agent.form.ts(helpText: settingprivatedoes not hide the agent)packages/spec/liveness/agent.json(visibility: experimental)packages/qa/dogfood/test/authz-conformance.matrix.ts(agent-visibility: experimental)access+permissionsremain the enforced gates at the chat route (#1884,evaluateAgentAccess()in service-aiagent-access.ts).What changed since filing (2026-06-15)
✅ Original blocker resolved — tenant is now in the request context. ADR-0095 (accepted 2026-07-14, implemented):
resolveAuthzContext(packages/core/src/security/resolve-authz-context.ts) is the single transport-agnostic authz resolver and carriestenantId(sessionactiveOrganizationIdor API-key principal),org_user_ids, and a monotonic posture rungPLATFORM_ADMIN > TENANT_ADMIN > MEMBER > EXTERNALderived from capability grants. Tenant isolation is Layer 0 (plugin-security/src/tenant-layer.ts).❌ New blocker — the agent-side anchor was removed.
agent.tenantIdwas deleted from the spec (#2377, ADR-0049: it had no runtime reader and did not actually scope the agent), and the agent schema has no owner/creator field. The originally proposed gate —organization→ caller tenant must matchagent.tenantId— is therefore no longer implementable as written.Remaining design decisions
Agents are metadata artifacts, so the anchor question is now:
organization: with no per-agent tenant id, the meaningful reading is "authenticated members only" (posture ≥MEMBER), vsglobal= also anonymous/EXTERNAL. Note theEXTERNALrung is defined but never derived yet (no external principal type —posture-ladder.ts), soorganizationvsglobalis currently indistinguishable at runtime; enforcement becomes meaningful once external principals exist.private: needs an owner. Studio-authored agents havecreated_byon theirsys_metadatarow; file-authored agents have no owner. Either restrictprivateto DB-stored agents, or define it as "requires an explicitaccesslist".Work plan (when scheduled)
evaluateAgentAccess()for the chat route and filter theGET /ai/agentslist route by visibility (both surfaces, orprivatestill leaks by listing).experimental → liveand remove the EXPERIMENTAL banners inagent.zod.ts/agent.form.ts/liveness/agent.json/authz-conformance.matrix.ts.Priority: P1 → P2
The P1 rationale was a silent false-security gap. Since the 2026-07 ADR-0056 D8 sweep,
visibilityis loudly marked EXPERIMENTAL / NOT ENFORCED at every authoring surface, and real gating (access/permissions) is enforced — this is now a documented unimplemented feature, not a silent hole.History — original issue body (2026-06-15)
#1884 enforced agent
access+permissionsin the chat route, butvisibility(global/organization/private) is still not enforced. The route'sRouteUserContextcarriesuserId/roles/permissionsbut no tenant id, and the agent has no owner field — so a correct gate (organization = same tenant; private = owner only) can't be decided at the route layer today.Needed
private, an agent owner/creator) to the authenticated request context via the auth middleware.global→ open;organization→ caller tenant must matchagent.tenantId;private→ owner only.Until then, enforcing a partial/guessed version was intentionally avoided (would risk both lock-out and false security). See
agent-access.tsfor the documented gap.