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 guardassertEngineOwnedWriteAllowed (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.
  • Dogfooddelegation-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

vercelBot commented Jul 19, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specBuildingBuildingPreview, CommentJul 19, 2026 5:48pm

Request Review

@github-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude