Skip to content

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

Closed

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.

Activity

  1. changed the title [-][P1][security] Enforce agent `visibility` (organization/private) — needs tenant/owner in request context[/-] [+][P2][security] Enforce agent `visibility` (organization/private) — context now has tenant (ADR-0095); needs owner/org anchor on the agent[/+] on Jul 18, 2026
  2. added and removed
    priority:p1High: required for production / M2
    on Jul 18, 2026
  3. os-zhuang commented on Jul 18, 2026

    @os-zhuang
    ContributorAuthor

    Re-evaluated against main (2026-07-18, 865ccb9) and rewrote the body. Summary of what changed:

    • Original blocker cleared: ADR-0095 landed (2026-07-14) — resolveAuthzContext now carries tenantId + a capability-derived posture rung, and tenant isolation is Layer 0. The "needs tenant/owner in request context" framing is stale.
    • New blocker: agent.tenantId was removed from the spec in [11.0][A2] Remove dead author-facing metadata properties (ADR-0049 enforce-or-remove) #2377 (no runtime reader), and agents still have no owner field — so the gate as originally specified can't be built. The open question moved from the request side to the agent side (what anchors organization/private on a metadata artifact).
    • Also relevant: the EXTERNAL posture rung is defined but never derived (no external principal type yet), so organization vs global is runtime-indistinguishable until external principals exist.
    • Priority P1 → P2: since the 2026-07 ADR-0056 D8 sweep, visibility carries EXPERIMENTAL/NOT-ENFORCED banners in the schema, authoring form, liveness registry, and QA conformance matrix — the silent-false-security risk that justified P1 is gone; what remains is a documented unimplemented feature with a design decision pending.

    Generated by Claude Code

  4. os-zhuang commented on Jul 18, 2026

    @os-zhuang
    ContributorAuthor

    Resolved by removal (ADR-0056 D8 "design+enforce or remove"), not enforcement — closing.

    Re-evaluation showed the "enforce" path is not buildable today: agent.tenantId was already removed (#2377), agents carry no owner field, and the EXTERNAL posture rung is never derived — so private has no anchor and organization vs global is runtime-indistinguishable. The semantics, not just the plumbing, are undesigned. Carrying a security-shaped field that never gates anything is exactly the D8 liability, so visibility is dropped rather than left marked [EXPERIMENTAL].

    Implemented in:

    Real gating is unchanged: access / permissions remain enforced at the chat route (#1884). Re-introduce visibility under a fresh issue when the agent listing surface gains real owner/org semantics (external principals + an agent owner field).

    Both PRs carry Closes #1901; closing now as the development is complete.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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