Skip to content

feat(security): split managedBy:'system' into engine-owned vs admin-writable, guard engine-owned writes (#3220)#3315

Merged
os-zhuang merged 1 commit into
mainfrom
claude/managedby-taxonomy-split-yder9o
Jul 19, 2026
Merged

feat(security): split managedBy:'system' into engine-owned vs admin-writable, guard engine-owned writes (#3220)#3315
os-zhuang merged 1 commit into
mainfrom
claude/managedby-taxonomy-split-yder9o

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes the root cause tracked in #3220 (follow-through on #3222 / #1591 / #3213). ADR-0103.

Problem

managedBy: 'system' conflated two incompatible write policies:

  1. Engine-owned — rows a platform service owns end to end, written only via isSystem / a service SYSTEM_CTX / a context-less engine call (jobs, notifications, approvals, sharing, automation runs, the messaging delivery pipeline, settings/secrets/audit).
  2. Platform-schema, admin/user-writable data — the RBAC link tables (DelegatedAdminGate-governed), sys_user_preference, sys_approval_delegation, and the messaging config grids surfaced in Setup.

The bucket carried the same all-false affordance row as better-auth/append-only but, unlike better-auth, had no engine enforcement — a wildcard admin could raw-write engine-owned rows through the generic data API (ADR-0049 gap). #3222 could only patch two latent holes; this is the taxonomy fix.

Approach — resolved affordance is the policy (no new enum value)

A new managedBy value would fall through to the fully-editable platform default on already-deployed Console clients. Instead, write policy is resolveCrudAffordances (bucket default + userActions), and engine-owned ≔ a system/append-only object granting no write. Guard, apiMethods reconciliation, and the /me/permissions clamp all key off that one contract.

What's in the diff

  • Writable set declares userActions{create,edit,delete}sys_user_position, sys_user_permission_set, sys_position_permission_set, sys_user_preference, sys_approval_delegation, sys_notification_preference / _subscription / _template. Affordance only — DelegatedAdminGate / RLS / permission sets stay the authz.
  • Engine-owned objects locked to apiMethods: ['get','list'] where absent. sys_secret read-locked explicitly (an empty apiMethods array fails open).
  • sys_import_job stays engine-owned — the REST import route writes its job rows isSystem-elevated (attribution preserved via the explicit created_by stamp).
  • New engine write guard assertEngineOwnedWriteAllowed (plugin-security) — fail-closed on user-context generic writes to engine-owned objects; isSystem / context-less writes bypass. Wired into the security middleware alongside the other data-layer gates.
  • reconcileManagedApiMethods now runs for every managed bucket (was better-auth-only) — the drift backstop.
  • clampManagedObjectWrites now clamps system/append-only too.

Audit finding (write-context, the guard's precondition)

Confirmed every engine-owned writer uses isSystem / SYSTEM_CTX / a context-less engine call (incl. the metadata-protocol repository, whose transaction context carries no userId). The audit expanded the writable set from 5 to 8: notification_preference / _subscription / _template are Setup nav grids users/admins author, so they joined the writable set rather than being locked down.

⚠️ Not to be confused with the row-level managed_by provenance vocabulary (platform/package/admin, ADR-0066) — a different axis, untouched.

Verification

All green:

  • spec (6795) · objectql (1023, incl. new reconcile cases) · plugin-security (521, incl. new system-write-guard suite) · plugin-auth identity-write-guard (15) · plugin-hono-server (92, updated fold/clamp) · rest (323) · platform-objects / plugin-approvals / service-messaging / service-automation / plugin-sharing / metadata-core.
  • Dogfood delegation-of-duty + showcase-permission-zoo (18) — confirm delegated administration and direct permission-set grants under user context still work with the guard in place.

Potentially breaking

A downstream/third-party system object that advertised generic write verbs relying on today's fail-open will have those verbs stripped (with a warning) and user-context generic writes rejected — declare userActions opening the verbs it legitimately takes. better-auth keeps plugin-auth's identity guard unchanged.

The Console copy refinements (badge/empty-state for the writable-system case) ship in the paired objectui PR; they are not required for correctness (the UI already honours userActions).

🤖 Generated with Claude Code


Generated by Claude Code

…ritable, guard engine-owned writes (#3220)

The `system` bucket conflated two incompatible write policies — engine-owned
rows (never user-written) and platform-schema, admin/user-writable data — under
one all-false affordance row with no engine enforcement (only better-auth had a
write guard). This left a wildcard admin able to raw-write engine-owned rows
through the generic data API (ADR-0049 gap).

Rather than add a new managedBy enum value (which falls through to fully-editable
platform defaults on deployed Console clients), the write policy is now the
resolved affordance (resolveCrudAffordances = bucket default + userActions), and
"engine-owned" is defined as a system/append-only object granting no write:

- Writable set declares userActions{create,edit,delete}: the RBAC link tables,
  sys_user_preference, sys_approval_delegation, and the messaging config grids
  (notification preference/subscription/template). Affordance only — the
  DelegatedAdminGate / RLS / permission sets remain the authz.
- Engine-owned objects locked to apiMethods ['get','list'] where absent (jobs,
  notifications, approvals, record-share, automation-run, mail/settings/secret
  audit, messaging delivery pipeline). sys_secret is read-locked explicitly (an
  empty apiMethods array fails open).
- sys_import_job stays engine-owned: the REST import route writes job rows
  isSystem-elevated (attribution preserved via explicit created_by).
- New engine write guard (assertEngineOwnedWriteAllowed, plugin-security)
  fail-closed rejects user-context generic writes keyed off resolved
  affordances; isSystem/context-less writes bypass. Wired into the security
  middleware alongside the other data-layer gates.
- reconcileManagedApiMethods (objectql registry) now runs for every managed
  bucket, stripping advertised write verbs the affordances forbid.
- /me/permissions clamp (plugin-hono-server) now clamps system/append-only too.

ADR-0103. Refs #3220, follows #3222 / ADR-0049 / ADR-0092.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fp9yZxRQ3mb7p4vVwqFXKE
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jul 19, 2026 5:48pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling size/l labels Jul 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 10 package(s): @objectstack/objectql, @objectstack/platform-objects, @objectstack/plugin-approvals, @objectstack/plugin-auth, @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/rest, packages/services, @objectstack/spec.

114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/plugin-approvals, packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-auth, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql, @objectstack/plugin-auth, @objectstack/plugin-hono-server)
  • content/docs/permissions/authorization.mdx (via @objectstack/plugin-security, packages/plugins/plugin-sharing, @objectstack/spec)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security, packages/plugins/plugin-sharing, @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/plugin-auth, @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/platform-objects, @objectstack/plugin-approvals, @objectstack/plugin-auth, @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/rest, packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/plugins/plugin-sharing, packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/plugin-approvals, @objectstack/plugin-auth, @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/plugin-approvals, @objectstack/plugin-auth, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

documentation Improvements or additions to documentation protocol:data size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants