Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/remove-agent-visibility.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion content/docs/permissions/authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 0 additions & 1 deletion content/docs/references/ai/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 2 additions & 2 deletions packages/qa/dogfood/test/authz-conformance.matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 0 additions & 5 deletions packages/spec/liveness/agent.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
1 change: 0 additions & 1 deletion packages/spec/src/ai/agent.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
1 change: 0 additions & 1 deletion packages/spec/src/ai/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
26 changes: 15 additions & 11 deletions packages/spec/src/ai/agent.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down