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
6 changes: 6 additions & 0 deletions .changeset/owd-posture-authoring-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@objectstack/metadata-protocol': minor
'@objectstack/plugin-security': minor
---

OWD posture is now enforced on the runtime write path (#3050). `metadata-protocol` gains the ADR-0094-addendum `registerAuthoringGate(type, gate)` seam — an awaited, throwing pre-persistence hook inside `saveMetaItem` (draft and publish-mode saves; environment writes only). `plugin-security` registers the `object` posture gate on it: an environment overlay of a packaged object may only TIGHTEN `sharingModel`/`externalSharingModel` (ADR-0086 D1 — closes the `OS_METADATA_WRITABLE=object` unvalidated-widening hole), and `externalSharingModel ≤ sharingModel` (ADR-0090 D11) is now rejected at save time instead of only by CLI lint. Write-path only — stored metadata keeps loading unchanged.
2 changes: 1 addition & 1 deletion docs/adr/0025-plugin-package-distribution.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0025: Plugin Package Distribution (Code + Dependencies)

**Status**: Proposed
**Status**: Proposed — partially implemented (2026-07-16 audit): the `.osplugin` artifact format and `os plugin build`/`sign`/`publish` CLI landed; the install flow (§3.5), `sys_plugin`/`sys_plugin_version`/`sys_plugin_installation` registry (§3.8), and install-time consent remain unimplemented.
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0003](./0003-package-as-first-class-citizen.md) (package + versioned releases), [ADR-0004](./0004-cloud-multi-kernel.md) (cloud multi-kernel), [ADR-0010](./0010-metadata-protection-model.md) (L1/L2/L3 protection), [ADR-0016](./0016-studio-package-authoring-and-publish.md) (package authoring & publish, local export/import)
**Consumers**: `@objectstack/core` (kernel, plugin-loader, security), `@objectstack/runtime` (sandbox, marketplace install), `@objectstack/cli`, `@objectstack/spec/system` (ObjectStackManifest), `@objectstack/spec/cloud`, `../objectui` (Studio)
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0047-object-ui-run-modes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0047: Two run modes for object UI — data views vs interface pages, user filters, and runtime visualization choice

**Status**: Proposed (2026-06-12) · **Revised 2026-06-19**
**Status**: Accepted (2026-06-12; revised 2026-06-19) — framework scope implemented (`UserFiltersSchema`/`InterfacePageConfigSchema` + the 2026-07-07 object-list narrowing, reference-integrity diagnostics); the PageView render + Studio panel phases live in objectui.

> **Revision (2026-06-19) — the "iron rule" is superseded.** Validating the
> Studio config panel against Airtable showed Airtable has no "inherit from a
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0049-no-unenforced-security-properties.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0049: Spec must not declare security properties the runtime does not enforce (enforce-or-remove gate)

**Status**: Proposed (2026-06-15)
**Status**: Accepted (2026-06-15) — implemented: fail-closed `DESTRUCTIVE_OPERATIONS` (`permission-evaluator.ts:37,122`), lifecycle bits RBAC-gated, `apiEnabled` enforced (`runtime/src/api-exposure.ts`), `PolicySchema` removed, EXPERIMENTAL tag convention live. Two gate-valid disposition deviations: agent access-control shipped experimental-tagged (not enforced), `flow.runAs` kept + enforced (not removed). `action.disabled` CEL enforcement to confirm in objectui.
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (artifact vs runtime overlay), [ADR-0010](./0010-metadata-protection-model.md) (package provenance), [ADR-0027](./0027-metadata-authoring-lifecycle.md) (authoring lifecycle)
**Consumers**: `@objectstack/spec` (security/identity schemas), `@objectstack/plugin-security` (`PermissionEvaluator`, `SecurityPlugin`), spec authors, the metadata-property liveness audit follow-ups (#1878 P0 cluster).
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0053-date-and-datetime-semantics.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0053: `date` is a timezone-naive calendar day; `datetime` is an instant rendered in a reference timezone

**Status**: Proposed (2026-06-16)
**Status**: Accepted (2026-06-16) — Phase 1 + addendum D-A1 implemented (`sql-driver.ts` `toDateOnly` write/read/filter normalization; analytics `coerceTemporalFilterValue`), Phase 2 landing incrementally; D-A2 (`temporalFilterValue` promotion onto the `IDataDriver` contract) still open as the ADR predicted.
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0032](./0032-unified-expression-layer.md) (unified expression layer — CEL dialect, `today()`/`daysFromNow()`), [ADR-0014](./0014-record-form-field-type.md) (field types)
**Consumers**: `@objectstack/spec` (`Field.date`/`Field.datetime`), `@objectstack/driver-sql` (`coerceFilterValue`, `formatInput`/`formatOutput`, `dateFields`/`datetimeFields`), `@objectstack/formula` (`stdlib` time functions, `cel-engine` hydration), `@objectstack/objectql` (`applyFormulaPlan`), schedule/cron executors, report/analytics date bucketing, `sys-user-preference.timezone`.
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0056-permission-model-landing-verification.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0056: Permission Model Landing Verification — Whole-Model Enforce / Prove / Reconcile Audit

**Status**: Proposed (2026-06-20)
**Status**: Accepted (2026-06-20) — landed per the implementation-status addendum (D2 anonymous default-deny, D4 fail-closed RLS compile, D7 app default set, D10 conformance matrix CI-gated). Caveat: the addendum's D6 row is stale — `RoleGraphService` never shipped; role-hierarchy widening was superseded by the ADR-0090 position/business-unit model (positions are flat, rollup lives on the BU tree).
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove), [ADR-0054](./0054-runtime-proof-for-authorable-surface.md) (prove-it-runs), [ADR-0055](./0055-master-detail-controlled-by-parent.md) (controlled-by-parent)
**Consumers**: `@objectstack/plugin-security`, `@objectstack/plugin-sharing`, `@objectstack/plugin-org-scoping`, `@objectstack/rest`, `@objectstack/runtime`, `@objectstack/spec`, `@objectstack/verify`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0057: ERP-Grade Authorization Core — Business-Unit Partitioning, Scope-Depth Grants, and Hierarchy Rollup

**Status**: Proposed (2026-06-21)
**Status**: Accepted in part (2026-06-21) — D1–D3/D8 implemented (scope-depth enum + evaluator, `sys_business_unit` tree, hierarchy-resolver seam, conformance rows); **D4–D7 superseded by ADR-0090 D3 / ADR-0095**: the `sys_role`/`sys_user_role`/`role_and_subordinates` vocabulary never shipped — the code realizes them as flat `sys_position`/`sys_user_position` with rollup on the BU tree. This ADR's own implementation-status table predates that supersession; read it with the rename applied.
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0010](./0010-metadata-protection.md) (metadata protection / object ownership),
[ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove),
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0058-expression-and-predicate-surface.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0058: The Expression & Predicate Surface — One Authoring Language, Two Backends, and the Pushdown-Compiler Reconciliation (#1887)

**Status**: Proposed (2026-06-21)
**Status**: Accepted (2026-06-21) — implemented: canonical CEL→Filter compiler (`formula/src/cel-to-filter.ts`), RLS + sharing cutover (`rls-compiler.ts`, `bootstrap-declared-sharing-rules.ts`), `check` enforced on writes, expression-surface conformance ledger CI-gated (`dogfood/test/expression-conformance.ledger.ts`).
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0049](./0049-no-unenforced-security-properties.md) (enforce-or-remove), [ADR-0054](./0054-runtime-proof-for-authorable-surface.md) (runtime proof), [ADR-0055](./0055-master-detail-controlled-by-parent.md) (RLS reuses pre-resolved membership IN-form; **no compiler subquery**), [ADR-0056](./0056-permission-model-landing-verification.md) (permission-model landing), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (ERP authz core)
**Consumers**: `@objectstack/formula`, `@objectstack/objectql`, `@objectstack/plugin-security`, `@objectstack/plugin-sharing`, `@objectstack/service-analytics`, `@objectstack/service-automation`, `@objectstack/spec`, `@objectstack/verify`
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0062-external-datasource-runtime.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0062: External Datasource Runtime — connection lifecycle, credentials, visibility & query completeness

**Status**: Proposed — recommended for acceptance; consolidates the runtime gaps surfaced while implementing ADR-0015 federation. Some requirements are already shipped (marked ✅ below); the open decisions are D1–D8 (2026-06-22).
**Status**: Accepted (2026-06-22) — D1–D8 implemented (`service-datasource` connection service + opt-in-safe gate + fail-closed connect policy; native-SQL declines external per D6; D7 lint in `validate-expressions.ts`).

**Supersedes the runtime portions of**: ADR-0015 §18 addendum (kept as the historical record). ADR-0015 remains the canonical spec/binding decision; this ADR is the canonical *runtime* decision.

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0066-unified-authorization-model.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0066: Unified authorization model — capability registry, secure-by-default posture, resource→capability contracts, dual-surface gates

**Status**: Proposed (2026-06-23)
**Status**: Accepted (2026-06-23) — D1–D5 + refinements ⑤/⑨ implemented (capability registry + `defineCapability` seeding, `access.default` posture with posture-gated superuser bypass, `requiredPermissions` AND-gates on object/field/action, `crudBucketForOperation`, `validateCapabilityReferences` lint); D4's UI half enforced in objectui ActionRunner.
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`readScope`/`writeScope` depth: own/unit/unit_and_below/org), [ADR-0058](./0058-expression-and-predicate-surface.md) (CEL predicate surface used by RLS); relates to cloud ADR-0016 (authz open/paid boundary)
**Consumers**: `@objectstack/spec` (object/app/permission schemas), `@objectstack/plugin-security` (RLS/FLS compiler + enforcement), `../objectui` (ActionRunner + app/nav gating), `../cloud` (control-plane objects, e.g. `sys_license`)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0068: Unified user-context contract & built-in identity roles — one `current_user` shape across formula/RLS/client, identities as roles not booleans

**Status**: Proposed (2026-06-24)
**Status**: Accepted (2026-06-24) — implemented under the position model: `EvalUser` factory + `current_user` mount (`spec/identity/eval-user.zod.ts`, `formula/stdlib.ts`), built-in identity rows seeded (`bootstrap-builtin-positions.ts`), derived `isPlatformAdmin`. Note: this ADR's `roles[]` is realized as `current_user.positions[]` per ADR-0090 D3 / ADR-0095.
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`sys_role` is platform-native, decoupled from better-auth; `ExecutionContext.roles` = union of `sys_member.role` + `sys_user_role.role`), [ADR-0066](./0066-unified-authorization-model.md) (capability/assignment/requirement separation; resources declare a capability, never "who"), [ADR-0058](./0058-expression-and-predicate-surface.md) (CEL predicate surface)
**Consumers**: `@objectstack/spec` (the `EvalUser` contract), `@objectstack/plugin-security` (`resolve-execution-context`, RLS), `@objectstack/plugin-auth` (`customSession` bridge), `@objectstack/formula` (`buildScope`), `@objectstack/runtime`, `../objectui` (AuthProvider / ExpressionProvider / predicate scope), `../cloud` (control-plane metadata — consumer only)
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0081-trusted-react-page-tier.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0081: A trusted `kind:'react'` page tier — real React executed in the main tree, gated by a host capability; and renaming `kind:'jsx'` → `kind:'html'`

**Status**: Proposed (2026-06-30)
**Status**: Accepted (2026-06-30) — framework scope implemented (`html`/`react` page kinds + `jsx` alias, lint split `validate-jsx-pages`/`validate-react-pages`, `OS_PAGE_REACT=off` server toggle); the `@object-ui/react-runtime` executor + `CAP_REACT_PAGES` gate live in objectui.
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0080](./0080-ai-authored-ui-jsx-source.md) (AI authors a *constrained* JSX text, parsed-never-executed, into the SDUI tree; the component registry is the contract; capability ≠ contract), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (AI writes metadata via draft-gated `saveMetaItem` — the human-review boundary), [ADR-0077](./0077-authoring-surface-boundary-hook-flow-validation.md) (route by intent; verifiability tier = friction tier; **code is the flagged escape hatch**), [ADR-0078](./0078-no-silently-inert-metadata.md) (no silently-inert metadata).
**Consumers**: `@objectstack/spec` (`packages/spec/src/ui/page.zod.ts` — `kind` gains `'html'` and `'react'`; `'jsx'` kept as a deprecated alias), `@objectstack/lint` (`validate-jsx-pages` — lints `html`/`jsx`, **skips** `react`), `../objectui` `@object-ui/react-runtime` (**new** — the trusted runtime-React executor) + `@object-ui/core` (the host capability gate) + `@object-ui/components` (PageRenderer routing + the full HTML tag set).
Expand Down
1 change: 1 addition & 0 deletions packages/metadata-protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
export { ObjectStackProtocolImplementation, ConcurrentUpdateError, normalizeViewMetadata } from './protocol.js';
export type { UninstallCleanup, UninstallCleanupOutcome } from './protocol.js';
export type { MetadataMutationEvent, MetadataMutationProjector, MutationProjectionOutcome } from './protocol.js';
export type { MetadataAuthoringGate, MetadataAuthoringGateContext } from './protocol.js';

export { SysMetadataRepository, resetEnvWritableMetadataTypes } from './sys-metadata-repository.js';
export type {
Expand Down
70 changes: 70 additions & 0 deletions packages/metadata-protocol/src/mutation-listeners.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,73 @@ describe('ObjectStackProtocolImplementation.registerMutationProjector (ADR-0094)
expect(out).toEqual({ success: false, error: 'projection boom' });
});
});

// #3050 — the pre-persistence AUTHORING GATE seam (ADR-0094 addendum). The
// inverse contract of the projector: it runs BEFORE persistence and a throw
// PROPAGATES (rejecting the write) instead of being swallowed. saveMetaItem
// invokes it for env writes only, both draft and publish-mode saves; the
// domain-gate behavior itself (OWD posture) is pinned in plugin-security's
// object-posture-gate suite.
describe('ObjectStackProtocolImplementation.registerAuthoringGate (#3050)', () => {
const save = (over: Record<string, unknown> = {}) => ({
type: 'object', name: 'crm_account', state: 'active' as const, body: { sharingModel: 'private' }, ...over,
});

it('dispatches the registered gate with the body and state', async () => {
const p = makeProtocol();
const seen: any[] = [];
p.registerAuthoringGate('object', (ctx) => { seen.push(ctx); });
await (p as any).runAuthoringGate(save({ state: 'draft' }));
expect(seen).toHaveLength(1);
expect(seen[0]).toMatchObject({
type: 'object', name: 'crm_account', state: 'draft',
body: { sharingModel: 'private' }, isArtifactBacked: false,
});
});

it('normalizes plural registrations to the singular type', async () => {
const p = makeProtocol();
const gate = vi.fn();
p.registerAuthoringGate('objects', gate);
await (p as any).runAuthoringGate(save());
expect(gate).toHaveBeenCalledTimes(1);
});

it('a gate throw PROPAGATES with its status/code (the write is rejected)', async () => {
const p = makeProtocol();
p.registerAuthoringGate('object', () => {
const err: any = new Error('[owd_widening_forbidden] no widening');
err.code = 'owd_widening_forbidden'; err.status = 403;
throw err;
});
await expect((p as any).runAuthoringGate(save())).rejects.toMatchObject({
code: 'owd_widening_forbidden', status: 403,
});
});

it('is a no-op for types with no registered gate', async () => {
const p = makeProtocol();
await expect((p as any).runAuthoringGate(save({ type: 'view' }))).resolves.toBeUndefined();
});

it('resolves isArtifactBacked + declaredBody from the artifact registry', async () => {
const declared = { name: 'crm_account', sharingModel: 'private', _packageId: 'crm' };
const engine = { registry: { getItem: (_t: string, n: string) => (n === 'crm_account' ? declared : undefined) } };
const p = new ObjectStackProtocolImplementation(engine as any);
const seen: any[] = [];
p.registerAuthoringGate('object', (ctx) => { seen.push(ctx); });
await (p as any).runAuthoringGate(save());
expect(seen[0].isArtifactBacked).toBe(true);
expect(seen[0].declaredBody).toBe(declared);
});

it('a second registration replaces the first (idempotent re-init)', async () => {
const p = makeProtocol();
const first = vi.fn(); const second = vi.fn();
p.registerAuthoringGate('object', first);
p.registerAuthoringGate('object', second);
await (p as any).runAuthoringGate(save());
expect(first).not.toHaveBeenCalled();
expect(second).toHaveBeenCalledTimes(1);
});
});
Loading