Repository navigation
[P0] Metadata property liveness audit: ~half of all spec properties are dead; a cluster of security props is parsed-but-unenforced #1878
Description
Summary
A protocol-level audit of every metadata type cross-referenced each schema property's spec definition against its actual consumers (file:line) in the runtime + renderers, to determine (1) does it take effect? and (2) is it necessary/reasonable? Liveness = code consumption, not browser observation.
Result: roughly half of all metadata properties are DEAD — defined in the protocol but read by neither the runtime nor the renderers, so authoring them is a silent no-op. The full evidence (17 per-type reports + cross-type synthesis, each with file:line) is in PR #1864 → docs/audits/ (README.md is the index).
This issue is the umbrella; the checklists below are scoped so they can be split into separate issues/ADRs.
🔴 P0 — Parsed-but-UNENFORCED security props (latent access-control gaps)
Properties that imply a security boundary but enforce nothing. Either enforce or remove — shipping them as-is is a compliance/security liability.
-
PolicySchemais 100% dead — password complexity, session idle/absolute timeout,forceMfa, IP allow-list (trustedRanges/vpnRequired), audit retention/redaction all parsed, none enforced; not even registered as a metadata type.better-auth(auth-manager.ts:458) runs hardcoded defaults instead → false sense of compliance. - Permission lifecycle ops ungated —
allowTransfer/allowRestore/allowPurgeomitted fromOPERATION_TO_PERMISSION(permission-evaluator.ts:8-16); ownership transfer, undelete, and hard-delete/GDPR purge are not gated by RBAC. - Agent access control is a no-op —
agent.permissions/visibility/accessnever enforced; the chat route hardcodes['ai:chat','ai:agents'](agent-routes.ts:109). "Who can chat with this agent" enforces nothing. - Flow
runAsnever switches execution identity (service-automationengine ignores it) — a flow declaringrunAs: systemdoes not elevate/de-elevate. - Object
apiEnabled/apiMethodsnot enforced by REST — an object cannot actually be hidden from the API via these flags. - Action
disabled(CEL form) silently ignored — renderers read a non-specenabled(action-button.tsx:56); the predicate form ofdisabledhas zero consumers. - Role
parentdead (team-graph.ts:27does not walk it) — the documented "managers see subordinates' data" rollup is unimplemented. -
SharingRuleSchemadisconnected from the live engine — runtime enforces a divergentsys_sharing_rulemodel (JSONcriteria_json, recipient enumuser/team/department/role/queue); the spec's CELcondition+role_and_subordinates/guestrecipients are never compiled (unparsable CEL → "match nothing").
🔴 P1 — Finish the ADR-0021 analytics migration
Spec moved to dataset+values/dimensions, but renderers/Studio still read the removed legacy shape:
- Chart view variant dead-on-arrival —
ListView.tsx:1413/ObjectView.tsx:754read legacyxAxisField/yAxisFields/aggregation; spec exposesdataset/dimensions/values→ a spec-compliant chart renders nothing. - Dashboard renderer + Studio
WidgetConfigPanelstill author/read legacyobject/categoryField/valueField/aggregatewhiledashboard.zod.ts:153declaresdataset+valuesrequired → Studio emits widgets the spec rejects. - Report
chartdead on the dataset path;ReportColumnSchema/ReportGroupingSchema/ReportChartSchemaare obsolete type-only re-exports. - (Same debt that invalidated the showcase dashboard/report seeds.)
🟠 P2 — Spec hygiene
- Naming drift → silent no-ops: field
maxLength/minLength/referenceFilters/maxRating; pagetype→pageType&label→title&visibility; dashboardtitlevslabel; app readsaccentColor/badgeVariant/separatornot declared in spec; flowhttpvs deprecatedhttp_request(Studio authors the alias); skillrequiredPermissionsvspermissions; agentknowledge.{topics→sources}; webhookobject→object_name/isActive→active. - Write-only / disconnected metadata:
toolmetadata is a one-way projection (can't author a working tool as metadata — noimplementation/handlerfield, no executor loads it);EmailTemplateSchemais registered but the runtime reads a differently-shapedsys_email_template; thespec-bridgepage/dashboard bridges are orphaned (their exclusive props dead). - Aspirational config (rich spec, zero runtime) — prune or mark
experimental: field enhanced-type configs (barcode/qr/slider/rating/color/location) + governance (encryption/masking/audit/dataQuality); objectenable/versioning/partitioning/cdc/softDelete/search; agent autonomy (memory/guardrails/structuredOutput/lifecycle, exceptplanning.maxIterations); tooloutputSchema; jobretryPolicy/timeout; themespacing/breakpoints/logo/rtl/density; translationmessageFormat:'icu'(no ICU engine),supportedLocales,cache; PortalSchema (entire — not registered, no route, no renderer). - Renderer reads UNDECLARED props (inverse drift) — dashboard
component/data/rowField/columnField; viewObjectViewform-adapter keys; appaccentColor/badgeVariant/separator. These break a strictSchema.parse(). - Designer authoring gaps (live prop, no Studio editor) — dataset
filter/format/derived/dateGranularity; skilltriggerConditions(the activation-critical field); flownotify(absent from the static palette).
Healthiest vs worst (from the audit)
- Healthiest (near-total liveness):
hook,validation. - Worst:
policy(100% dead),portal(100% dead),tool(write-only).
Evidence + per-property recommendations: PR #1864, docs/audits/*.md.
🤖 Generated with Claude Code
Activity
os-zhuang commentedon Jun 15, 2026
Split into sub-issues
This umbrella has been decomposed into tracked sub-issues (each links back here, labeled by priority):
🔴 P0 — security enforcement (
priority:p0+security)🔴 P1 — analytics (
priority:p1)🟠 P2 — spec hygiene (
priority:p2)The audit's suggested 3-ADR grouping maps to: Security enforcement (the P0 set), Finish ADR-0021 (#1890), Spec hygiene (the P2 set).
os-zhuang commentedon Jun 15, 2026
Staged plan for the P0 security cluster — ADR-0049
Decision recorded in ADR-0049 (
docs/adr/0049-no-unenforced-security-properties.md): a spec property naming a security boundary must be enforced, explicitlyexperimental, or absent — never parsed-but-silently-unenforced (that's a false sense of compliance).Given the platform's pre-MVP stage, the cluster splits by whether the feature already exists — we do not build enforcement for features that don't exist yet:
experimental/ removerunAsPR A (no-regret, shipped on branch
fix/p0-unenforced-security-props-1878)PermissionEvaluatorfails closed for the destructive op class (transfer/restore/purge) instead of default-allow.allowTransfer/allowRestore/allowPurgemarked[EXPERIMENTAL — not enforced]([security][立项位] M2 权限生命周期(undelete/purge)功能与 allowRestore/allowPurge RBAC 同批建设(evaluator 已 fail-closed,allowTransfer 已 enforced) #1883).PR B (next) — real gaps where the feature is live
permissions/visibility/access.apiEnabled/apiMethods→ gate REST routes.disabledCEL → evaluate predicate in renderer.Deferred to M2/production (heavy, feature + enforcement together)
parent).runAsidentity switching.os-zhuang commentedon Jun 15, 2026
PRs open for the P0 cluster (ADR-0049)
allowTransfer/Restore/Purgemarked experimental, [security][立项位] M2 权限生命周期(undelete/purge)功能与 allowRestore/allowPurge RBAC 同批建设(evaluator 已 fail-closed,allowTransfer 已 enforced) #1883) + PR B ([P0][security] Agent access control is a no-op (permissions/visibility/access) #1884 agent access enforcement, [P0][security] Object apiEnabled/apiMethods not enforced by REST #1889 objectapiEnabled/apiMethodsenforcement).disabled(boolean|CEL) in action button (framework#1885) objectui#1743 — [P0][security] Action disabled (CEL form) silently ignored #1885 actiondisabled(CEL) now consumed by the renderer.Deferred to M2 (feature + enforcement together): #1882 Policy, #1887 SharingRule, #1888 flow
runAs, #1886 roleparent.P0 security: enforce-or-remove unenforced security props (ADR-0049) (#…
docs(spec): mark feature-absent security props EXPERIMENTAL — Policy,…
os-zhuang commentedon Jun 16, 2026
Closing the umbrella — audit delivered, disposition fully split out
The audit this issue tracked is done and has been operationalized into a standing system, and every checklist item has been split into its own labeled, independently-tracked sub-issue. Keeping this umbrella open now only duplicates that tracking, so closing it.
What the audit became (standing system, not a one-off)
packages/spec/liveness/*.jsonledgers +packages/spec/scripts/liveness/check-liveness.mts, wired as the blockingSpec property livenessCI check. Every authorable property of 10 governed types is classified live/experimental/dead with evidence; the ratchet blocks new undeclared surface. This is the living source of truth that replaces the static audit docs.objectstack compilenow warns when an author/AI sets a misleading dead/experimental property (enable.feeds,field.columnName, …) with a corrective hint. Measurement → gate → drift-cron → author-prevention.Shipped against the checklist
apiEnabled/apiMethodsenforced by REST (feat(rest): enforce object enable.apiEnabled / apiMethods (ADR-0049 #1889) #1937) —enforceApiAccess, 404/405.evaluateAgentAccessat the chat route; ledger corrected (the audit predated it).enable.clonewas dead; now a real runtime (feat: record clone — wire object.enable.clone to a real runtime #1961), reclassified live.recordTypes+cdcremoved (chore(spec): cut orphaned object.recordTypes and object.cdc stubs #1960).Remaining items — each tracked in its own open issue (nothing orphaned)
parent[P0][security] Role parent dead — manager-rollup unimplemented #1886 · SharingRule [P0][security] SharingRuleSchema disconnected from the live engine #1887 · flowrunAs[P0][security] Flow runAs never switches execution identity #1888disabledCEL — objectui renderer side.Closing as completed (deliverable shipped + operationalized); execution continues in the sub-issues above.
chore: bump objectui to a515c8181c76
fix(security): enforce flow runAs execution identity (#1888)
fix(security): enforce flow runAs execution identity (#1888)
fix(security): enforce flow runAs execution identity (#1888) (#2302)
fix(identity): reconcile managedBy:'better-auth' apiMethods with the …
25 remaining items
os-zhuang commentedon Jul 27, 2026
SharingRule loose end from the recheck (#3503) closed by #3557: authoring
ShareRecipientTypereconciled with the enforced runtime —grouprenamed →team(wired to the liveTeamGraphServiceexpansion),business_unitadded,guestpruned (anonymous access = public-form grant / share links), and the never-enforced owner-type rules removed (return if membership-reactive re-materialisation is designed). Every authorable recipient/rule type now enforces (ADR-0078); proven on a real boot — the migrated showcase rule seeds (ruleCount 4→5) withcriteria_json{done:false}. Remaining from the recheck: AuditConfig-cluster prune decision (needs an owner call on dead-vs-roadmap) and per-org IP allow-list (#2571).feat(spec)!: remove the dead AuditConfig cluster from @objectstack/sp…
feat(spec)!: remove the dead static capabilities-descriptor cluster (#…
os-zhuang commentedon Jul 27, 2026
2026-07 dead-cluster sweep — disposition record (8 PRs)
Follow-through of the recheck above: the report slice, the security-props cluster, and three chained dead-config clusters are now fully dispositioned. Recording here so triage of the remaining audit sections doesn't re-excavate.
ReportColumn/ReportGrouping+ objectuiSpecReport*re-exports,chart.groupBy, legacyReportViewerchart fallback)DatasetReportRendereraria/performance+ chart dogfood + stale audit refreshreport.chartconfirmed live (audit's "dead" claim corrected); showcase chart report addedflow-run-asreclassified enforcedAuditConfigcluster (system/audit.zod.ts, whole module)trackHistory+ lifecycleauditretention + settings overridesObjectQL/ObjectUI/Kernel/ObjectStack/ObjectOS CapabilitiesSchema)default(false)while enforced); live discovery =GET /api/v1/discovery(declared===enforced) +/.well-knownFeatureFlagSchemamodule (kernel/feature.zod.ts)feature_flagssettings manifest +PUBLIC_AUTH_FEATURESPattern note for the remaining sections (§3 naming drift, §4 aspirational, §5 write-only): these clusters chain — each removal exposed the next orphan (
AuditConfig→auditLoggingboolean → capabilities descriptor →FeatureFlagSchema). Worth pulling each thread to its end rather than pruning single symbols. All removals shipped asminorper the launch-window policy, each changeset carrying a dead-knob → live-surface migration table.Next confirmed actionable from the recheck: the objectui
action.disabledwire-rename (spec declaresdisabledCEL; the renderer reads non-specenabled— authored predicates silently no-op).docs(audits): recheck the §3 naming-drift list; fix the os doctor loo…
feat(showcase): nav separator + badge specimens; correct the §3 item-…
docs(liveness): stop counting authoring/preview renderers as runtime …
feat(spec)!: remove SkillSchema.permissions — it never gated anything (…
feat(spec): cover all three fold-and-drop aliases, not just `execute` (…
docs(ui): correct the navigation item type count and document `separa…
feat(spec): fail CI when a schema variant is missing from its hand-wr…
docs(permissions,protocol): owner-type sharing rules are rejected at …
docs(permissions,protocol): owner-type sharing rules are rejected at …
fix(spec)!: TimeUpdateInterval retires its three sub-day intervals an…
fix(core,app-shell): the two runtime strings that cite an objectstack…