Skip to content

[P2][security] Enforce agent visibility (organization/private) — context now has tenant (ADR-0095); needs owner/org anchor on the agent #1901

Description

@os-zhuang

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)

  1. Decide the semantics above (small ADR, or amendment to ADR-0049/0095).
  2. 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).
  3. Add conformance coverage in the authz matrix.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions