From cb28cc3b0ac036b1a2a909fb4943d313d0edf9da Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 14:08:30 +0000 Subject: [PATCH] refactor(spec): remove unenforced agent `visibility` field (ADR-0056 D8, #1901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent `visibility` (global/organization/private) was declared but never enforced: the chat-access evaluator excluded it and the agent list route did not filter by it, so `private` never hid an agent. Per ADR-0049 / ADR-0056 D8 ("design+enforce or remove"), a security-shaped field with no runtime consumer is a liability — authors who set `private` believe they've restricted an agent when they have not. Correct enforcement is undesigned, not just unplumbed: agents have no owner field (`agent.tenantId` was already removed in #2377) and the `EXTERNAL` posture rung is never derived, so `organization` vs `global` is runtime-indistinguishable. Unlike field-encryption (kept EXPERIMENTAL — stable schema shape on a real roadmap), there is no shape worth preserving, so the field is dropped rather than carried marked. - Remove `visibility` from AgentSchema (`agent.zod.ts`) and the authoring form (`agent.form.ts`); drop the default-value assertion in `agent.test.ts`. - AgentSchema is not `.strict()`, so existing metadata still setting `visibility` parses cleanly (unknown key stripped, not rejected). - Liveness ledger: drop the `visibility` entry (`liveness/agent.json`). - Authz-conformance matrix: move `agent-visibility` from experimental to removed (ADR-0056 D10). - Regenerate schema reference docs; update the authorization posture table. Use `access` / `permissions` to restrict agent use — both enforced at the chat route (#1884). Re-introduce `visibility` when the listing surface gains real owner/org semantics; tracked in #1901. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012gckUwoUMWvTWfGHFgWAob --- .changeset/remove-agent-visibility.md | 31 +++++++++++++++++++ content/docs/permissions/authorization.mdx | 2 +- content/docs/references/ai/agent.mdx | 1 - .../dogfood/test/authz-conformance.matrix.ts | 4 +-- packages/spec/liveness/agent.json | 5 --- packages/spec/src/ai/agent.form.ts | 1 - packages/spec/src/ai/agent.test.ts | 1 - packages/spec/src/ai/agent.zod.ts | 26 +++++++++------- 8 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 .changeset/remove-agent-visibility.md diff --git a/.changeset/remove-agent-visibility.md b/.changeset/remove-agent-visibility.md new file mode 100644 index 0000000000..173ad6ba8b --- /dev/null +++ b/.changeset/remove-agent-visibility.md @@ -0,0 +1,31 @@ +--- +'@objectstack/spec': minor +--- + +refactor(spec): remove unenforced agent `visibility` field (ADR-0056 D8, #1901) + +The agent `visibility` (`global`/`organization`/`private`) field is **removed** +from `AgentSchema`. It was never enforced: the chat-access evaluator excluded it +and the agent list route did not filter by it, so setting `private` never hid an +agent. Per ADR-0049 / ADR-0056 D8 ("design+enforce or remove"), a security-shaped +field with no runtime consumer is a liability — authors who set `private` believe +they've restricted an agent when they have not. + +Unlike `field-encryption` (kept `[EXPERIMENTAL]` — it has a stable schema shape on +a real roadmap), correct `visibility` enforcement is undesigned: it needs +owner/org anchors that do not exist today. `agent.tenantId` was already removed +(#2377), agents carry no owner field, and the `EXTERNAL` posture rung is defined +but never derived — so `organization` vs `global` is runtime-indistinguishable. +The semantics, not just the plumbing, are unresolved, so the field is dropped +rather than carried marked. + +- `AgentSchema` is not `.strict()`, so existing metadata that still sets + `visibility` parses cleanly — the unknown key is stripped, not rejected. +- Use `access` / `permissions` to restrict who can use an agent — both **enforced** + at the chat route (#1884). +- Re-introduce `visibility` when the agent listing surface gains real owner/org + semantics; tracked in #1901. + +Also updated: authoring form (`agent.form.ts`), liveness ledger +(`liveness/agent.json`), the ADR-0056 D10 authz-conformance matrix (moved from +`experimental` to `removed`), and the generated schema reference docs. diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx index cfe813a974..25c3810033 100644 --- a/content/docs/permissions/authorization.mdx +++ b/content/docs/permissions/authorization.mdx @@ -210,7 +210,7 @@ one of two doors, each writing only what it owns: | 2 · Distribution / install / upgrade / uninstall | Install-consent scopes (ADR-0025 — consent ≠ RBAC grants); namespaced, collision-free composition; provenance axis makes uninstall well-defined | ADR-0025/0028/0048/0086 | | 3 · Environment composition / assignment | Platform-owned assignment records (`sys_user_position` etc.); anti-escalation; union semantics | ADR-0057 D4 | | 4 · Runtime enforcement | The six-gate chain above; ~18 primitives enforced and CI-guarded | ADR-0056 D10 matrix | -| 5 · Production / enterprise | ADR-0056 D8 dispositions settled (2026-07): compliance configs, data masking, and the global RLSConfig were **removed** (never enforced); field encryption stays honestly `[EXPERIMENTAL]` (roadmap); agent `visibility` is marked `[EXPERIMENTAL]` pending #1901. Enterprise authentication hardening staged per ADR-0069 | ADR-0049/0056 D8, ADR-0069 | +| 5 · Production / enterprise | ADR-0056 D8 dispositions settled (2026-07): compliance configs, data masking, the global RLSConfig, and agent `visibility` were **removed** (never enforced — for `visibility`, correct owner/org enforcement is undesigned, so it was dropped rather than carried; re-introduce under #1901); field encryption stays honestly `[EXPERIMENTAL]` (roadmap — stable schema shape). Enterprise authentication hardening staged per ADR-0069 | ADR-0049/0056 D8, ADR-0069 | ## Explaining a decision (ADR-0090 D6) diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx index b044d413e9..b29d73317a 100644 --- a/content/docs/references/ai/agent.mdx +++ b/content/docs/references/ai/agent.mdx @@ -83,7 +83,6 @@ const result = AIKnowledge.parse(data); | **active** | `boolean` | ✅ | | | **access** | `string[]` | optional | Who can chat with this agent | | **permissions** | `string[]` | optional | Required permission-set capabilities | -| **visibility** | `Enum<'global' \| 'organization' \| 'private'>` | ✅ | [EXPERIMENTAL — NOT ENFORCED, #1901] Intended listing scope. No runtime consumer yet; use access/permissions for real gating. | | **planning** | `{ maxIterations: integer }` | optional | Autonomous reasoning and planning configuration | | **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | Agent memory management | | **guardrails** | `{ maxTokensPerInvocation?: integer; maxExecutionTimeSec?: integer; blockedTopics?: string[] }` | optional | Safety guardrails for the agent | diff --git a/packages/qa/dogfood/test/authz-conformance.matrix.ts b/packages/qa/dogfood/test/authz-conformance.matrix.ts index 882526643d..e2f058c6f0 100644 --- a/packages/qa/dogfood/test/authz-conformance.matrix.ts +++ b/packages/qa/dogfood/test/authz-conformance.matrix.ts @@ -154,10 +154,10 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [ // ── Experimental — declared, NOT enforced (ADR-0049/0056 D8) ─────────── { id: 'field-encryption', summary: 'at-rest field encryption', state: 'experimental', note: 'no crypto provider reads the config; marked [EXPERIMENTAL] (D8). Deliberately KEPT (2026-07 D8 disposition): at-rest encryption is a real enterprise roadmap item with a stable schema shape — removing and re-adding would cost more (ADR-0087) than carrying it marked.' }, - { id: 'agent-visibility', summary: 'AI agent `visibility` listing scope (#1901)', state: 'experimental', - note: 'Intentionally NOT enforced — the chat-access evaluator excludes it (service-ai agent-access.ts) and the agent list route does not filter by it. Schema + authoring form carry EXPERIMENTAL banners (2026-07) so authors are told `private` does not hide the agent; `access`/`permissions` ARE enforced at the chat route (#1884). Enforce when the agent listing surface gains owner/org semantics — #1901.' }, // ── Removed — by ADR-0056 D8 "design+enforce or remove" (2026-07) ────── + { id: 'agent-visibility', summary: 'AI agent `visibility` listing scope (#1901)', state: 'removed', + note: 'REMOVED from spec (agent.zod.ts `visibility` deleted, #1901). Never enforced — the chat-access evaluator excluded it and the agent list route did not filter by it, so `private` never hid an agent. Unlike field-encryption it has NO stable schema shape to preserve: correct enforcement needs owner/org anchors that do not exist (agents carry no owner field; the `EXTERNAL` posture rung is never derived), so the semantics — not just the plumbing — are undesigned. Per D8 a security-shaped field that lies is dropped, not carried marked. `access`/`permissions` ARE enforced at the chat route (#1884); re-introduce `visibility` when the listing surface gains real owner/org semantics.' }, { id: 'compliance-configs', summary: 'GDPR/HIPAA/PCI configs', state: 'removed', note: 'REMOVED from spec (system/compliance.zod.ts deleted). Compliance-grade config must never merely look live: a parsed-but-dead `gdpr:` block is a liability in an audit. A real compliance subsystem will be designed top-down (data-subject rights engine, retention enforcer) when scheduled.' }, { id: 'data-masking', summary: 'role-based data masking', state: 'removed', diff --git a/packages/spec/liveness/agent.json b/packages/spec/liveness/agent.json index b5c8372759..2d8be5ce38 100644 --- a/packages/spec/liveness/agent.json +++ b/packages/spec/liveness/agent.json @@ -64,11 +64,6 @@ "evidence": "packages/services/service-ai/src/routes/agent-access.ts:50", "note": "evaluateAgentAccess() — caller must hold ALL required permissions/roles; enforced at the chat route. #1884." }, - "visibility": { - "status": "experimental", - "evidence": "packages/services/service-ai/src/routes/agent-access.ts:41", - "note": "intentionally NOT enforced — the chat-access evaluator excludes it and the GET /ai/agents list route does not filter by visibility. Needs owner/org semantics before it can gate listing." - }, "knowledge": { "status": "live", "evidence": "objectui: packages/app-shell/src/views/metadata-admin/previews/AgentPreview.tsx:69 (d.knowledge; KnowledgeSummary renders sources/indexes)", diff --git a/packages/spec/src/ai/agent.form.ts b/packages/spec/src/ai/agent.form.ts index 3ff50a2186..fd42c1d8ba 100644 --- a/packages/spec/src/ai/agent.form.ts +++ b/packages/spec/src/ai/agent.form.ts @@ -53,7 +53,6 @@ export const agentForm = defineForm({ collapsible: true, collapsed: true, fields: [ - { field: 'visibility', helpText: 'EXPERIMENTAL — not enforced yet (#1901): setting "private" does not hide the agent. Use Access / Permissions below for real gating.' }, { field: 'access', widget: 'string-tags', helpText: 'User IDs or role names who can chat with this agent' }, { field: 'permissions', widget: 'string-tags', helpText: 'Required permissions to use this agent' }, { field: 'guardrails', type: 'composite', helpText: 'Safety rules and content policies' }, diff --git a/packages/spec/src/ai/agent.test.ts b/packages/spec/src/ai/agent.test.ts index 2979fa47e0..7754a04bc3 100644 --- a/packages/spec/src/ai/agent.test.ts +++ b/packages/spec/src/ai/agent.test.ts @@ -755,7 +755,6 @@ describe('defineAgent', () => { instructions: 'Testing agent.', }); expect(result.active).toBe(true); - expect(result.visibility).toBe('organization'); }); it('should accept agent with tools', () => { diff --git a/packages/spec/src/ai/agent.zod.ts b/packages/spec/src/ai/agent.zod.ts index 4958dc11c2..6494974562 100644 --- a/packages/spec/src/ai/agent.zod.ts +++ b/packages/spec/src/ai/agent.zod.ts @@ -167,17 +167,21 @@ export const AgentSchema = lazySchema(() => z.object({ /** Permission-set capabilities required to use this agent */ permissions: z.array(z.string()).optional().describe('Required permission-set capabilities'), - // `tenantId` removed in the 16.x line (#2377, ADR-0049): it had no runtime - // reader and did NOT scope the agent to a tenant — tenancy comes from the - // request context (resolveAuthzContext), not this field. - // ⚠️ EXPERIMENTAL — NOT ENFORCED (#1901, ADR-0049). The chat-access evaluator - // deliberately excludes `visibility` (agent-access.ts) and the agent list - // route does not filter by it — setting `private` does NOT hide the agent. - // Use `access` / `permissions` (both ENFORCED at the chat route, #1884) to - // actually restrict who can use an agent. Enforcement needs owner/org - // semantics on the listing surface first; tracked in #1901. - visibility: z.enum(['global', 'organization', 'private']).default('organization') - .describe('[EXPERIMENTAL — NOT ENFORCED, #1901] Intended listing scope. No runtime consumer yet; use access/permissions for real gating.'), + // Two agent-scoping fields were REMOVED as unenforced security properties + // (ADR-0049 / ADR-0056 D8 "design+enforce or remove"), not merely marked: + // • `tenantId` — 16.x line (#2377): had no runtime reader and did NOT scope + // the agent to a tenant. Tenancy comes from the request context + // (resolveAuthzContext), never a field on the artifact. + // • `visibility` (`global`/`organization`/`private`) — removed 2026-07 (#1901). + // No runtime consumer ever read it: the chat-access evaluator excluded it + // and the agent list route did not filter by it, so `private` never hid an + // agent. Enforcing it correctly needs owner/org anchors that do not exist + // yet (agents have no owner field; the `EXTERNAL` posture rung is never + // derived — see #1901). Carrying a security-shaped field that lies is a + // liability, so it was dropped rather than left marked. Restrict who can + // use an agent with `access` / `permissions` — both ENFORCED at the chat + // route (#1884). Re-introduce `visibility` when the listing surface gains + // real owner/org semantics. /** Autonomous Reasoning */ planning: z.object({