Skip to content

Commit cb28cc3

Browse files
committed
refactor(spec): remove unenforced agent visibility field (ADR-0056 D8, #1901)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gckUwoUMWvTWfGHFgWAob
1 parent 865ccb9 commit cb28cc3

8 files changed

Lines changed: 49 additions & 22 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
refactor(spec): remove unenforced agent `visibility` field (ADR-0056 D8, #1901)
6+
7+
The agent `visibility` (`global`/`organization`/`private`) field is **removed**
8+
from `AgentSchema`. It was never enforced: the chat-access evaluator excluded it
9+
and the agent list route did not filter by it, so setting `private` never hid an
10+
agent. Per ADR-0049 / ADR-0056 D8 ("design+enforce or remove"), a security-shaped
11+
field with no runtime consumer is a liability — authors who set `private` believe
12+
they've restricted an agent when they have not.
13+
14+
Unlike `field-encryption` (kept `[EXPERIMENTAL]` — it has a stable schema shape on
15+
a real roadmap), correct `visibility` enforcement is undesigned: it needs
16+
owner/org anchors that do not exist today. `agent.tenantId` was already removed
17+
(#2377), agents carry no owner field, and the `EXTERNAL` posture rung is defined
18+
but never derived — so `organization` vs `global` is runtime-indistinguishable.
19+
The semantics, not just the plumbing, are unresolved, so the field is dropped
20+
rather than carried marked.
21+
22+
- `AgentSchema` is not `.strict()`, so existing metadata that still sets
23+
`visibility` parses cleanly — the unknown key is stripped, not rejected.
24+
- Use `access` / `permissions` to restrict who can use an agent — both **enforced**
25+
at the chat route (#1884).
26+
- Re-introduce `visibility` when the agent listing surface gains real owner/org
27+
semantics; tracked in #1901.
28+
29+
Also updated: authoring form (`agent.form.ts`), liveness ledger
30+
(`liveness/agent.json`), the ADR-0056 D10 authz-conformance matrix (moved from
31+
`experimental` to `removed`), and the generated schema reference docs.

content/docs/permissions/authorization.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ one of two doors, each writing only what it owns:
210210
| 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 |
211211
| 3 · Environment composition / assignment | Platform-owned assignment records (`sys_user_position` etc.); anti-escalation; union semantics | ADR-0057 D4 |
212212
| 4 · Runtime enforcement | The six-gate chain above; ~18 primitives enforced and CI-guarded | ADR-0056 D10 matrix |
213-
| 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 |
213+
| 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 |
214214

215215
## Explaining a decision (ADR-0090 D6)
216216

content/docs/references/ai/agent.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ const result = AIKnowledge.parse(data);
8383
| **active** | `boolean` || |
8484
| **access** | `string[]` | optional | Who can chat with this agent |
8585
| **permissions** | `string[]` | optional | Required permission-set capabilities |
86-
| **visibility** | `Enum<'global' \| 'organization' \| 'private'>` || [EXPERIMENTAL — NOT ENFORCED, #1901] Intended listing scope. No runtime consumer yet; use access/permissions for real gating. |
8786
| **planning** | `{ maxIterations: integer }` | optional | Autonomous reasoning and planning configuration |
8887
| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | Agent memory management |
8988
| **guardrails** | `{ maxTokensPerInvocation?: integer; maxExecutionTimeSec?: integer; blockedTopics?: string[] }` | optional | Safety guardrails for the agent |

packages/qa/dogfood/test/authz-conformance.matrix.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,10 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [
154154
// ── Experimental — declared, NOT enforced (ADR-0049/0056 D8) ───────────
155155
{ id: 'field-encryption', summary: 'at-rest field encryption', state: 'experimental',
156156
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.' },
157-
{ id: 'agent-visibility', summary: 'AI agent `visibility` listing scope (#1901)', state: 'experimental',
158-
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.' },
159157

160158
// ── Removed — by ADR-0056 D8 "design+enforce or remove" (2026-07) ──────
159+
{ id: 'agent-visibility', summary: 'AI agent `visibility` listing scope (#1901)', state: 'removed',
160+
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.' },
161161
{ id: 'compliance-configs', summary: 'GDPR/HIPAA/PCI configs', state: 'removed',
162162
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.' },
163163
{ id: 'data-masking', summary: 'role-based data masking', state: 'removed',

packages/spec/liveness/agent.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,6 @@
6464
"evidence": "packages/services/service-ai/src/routes/agent-access.ts:50",
6565
"note": "evaluateAgentAccess() — caller must hold ALL required permissions/roles; enforced at the chat route. #1884."
6666
},
67-
"visibility": {
68-
"status": "experimental",
69-
"evidence": "packages/services/service-ai/src/routes/agent-access.ts:41",
70-
"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."
71-
},
7267
"knowledge": {
7368
"status": "live",
7469
"evidence": "objectui: packages/app-shell/src/views/metadata-admin/previews/AgentPreview.tsx:69 (d.knowledge; KnowledgeSummary renders sources/indexes)",

packages/spec/src/ai/agent.form.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export const agentForm = defineForm({
5353
collapsible: true,
5454
collapsed: true,
5555
fields: [
56-
{ field: 'visibility', helpText: 'EXPERIMENTAL — not enforced yet (#1901): setting "private" does not hide the agent. Use Access / Permissions below for real gating.' },
5756
{ field: 'access', widget: 'string-tags', helpText: 'User IDs or role names who can chat with this agent' },
5857
{ field: 'permissions', widget: 'string-tags', helpText: 'Required permissions to use this agent' },
5958
{ field: 'guardrails', type: 'composite', helpText: 'Safety rules and content policies' },

packages/spec/src/ai/agent.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,6 @@ describe('defineAgent', () => {
755755
instructions: 'Testing agent.',
756756
});
757757
expect(result.active).toBe(true);
758-
expect(result.visibility).toBe('organization');
759758
});
760759

761760
it('should accept agent with tools', () => {

packages/spec/src/ai/agent.zod.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,17 +167,21 @@ export const AgentSchema = lazySchema(() => z.object({
167167
/** Permission-set capabilities required to use this agent */
168168
permissions: z.array(z.string()).optional().describe('Required permission-set capabilities'),
169169

170-
// `tenantId` removed in the 16.x line (#2377, ADR-0049): it had no runtime
171-
// reader and did NOT scope the agent to a tenant — tenancy comes from the
172-
// request context (resolveAuthzContext), not this field.
173-
// ⚠️ EXPERIMENTAL — NOT ENFORCED (#1901, ADR-0049). The chat-access evaluator
174-
// deliberately excludes `visibility` (agent-access.ts) and the agent list
175-
// route does not filter by it — setting `private` does NOT hide the agent.
176-
// Use `access` / `permissions` (both ENFORCED at the chat route, #1884) to
177-
// actually restrict who can use an agent. Enforcement needs owner/org
178-
// semantics on the listing surface first; tracked in #1901.
179-
visibility: z.enum(['global', 'organization', 'private']).default('organization')
180-
.describe('[EXPERIMENTAL — NOT ENFORCED, #1901] Intended listing scope. No runtime consumer yet; use access/permissions for real gating.'),
170+
// Two agent-scoping fields were REMOVED as unenforced security properties
171+
// (ADR-0049 / ADR-0056 D8 "design+enforce or remove"), not merely marked:
172+
// • `tenantId` — 16.x line (#2377): had no runtime reader and did NOT scope
173+
// the agent to a tenant. Tenancy comes from the request context
174+
// (resolveAuthzContext), never a field on the artifact.
175+
// • `visibility` (`global`/`organization`/`private`) — removed 2026-07 (#1901).
176+
// No runtime consumer ever read it: the chat-access evaluator excluded it
177+
// and the agent list route did not filter by it, so `private` never hid an
178+
// agent. Enforcing it correctly needs owner/org anchors that do not exist
179+
// yet (agents have no owner field; the `EXTERNAL` posture rung is never
180+
// derived — see #1901). Carrying a security-shaped field that lies is a
181+
// liability, so it was dropped rather than left marked. Restrict who can
182+
// use an agent with `access` / `permissions` — both ENFORCED at the chat
183+
// route (#1884). Re-introduce `visibility` when the listing surface gains
184+
// real owner/org semantics.
181185

182186
/** Autonomous Reasoning */
183187
planning: z.object({

0 commit comments

Comments
 (0)