Skip to content

feat(lint,cli): flag flow update_record writes to readonly fields at design time (#3425) - #3465

Merged
os-zhuang merged 1 commit into
mainfrom
feat/3425-readonly-flow-lint
Jul 24, 2026
Merged

feat(lint,cli): flag flow update_record writes to readonly fields at design time (#3425)#3465
os-zhuang merged 1 commit into
mainfrom
feat/3425-readonly-flow-lint

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What & why

Closes the design-time half of #3425 (D1 + D2 from the triage plan).

A flow update_record node that writes a field the target object declares readonly: true, under the default runAs: 'user', is a silent no-op: the objectql engine strips static-readonly fields from a non-system UPDATE payload (#2948), so the intended write never lands — yet the step still reports success. #3407/#3413 made the strip observable at run time (step warning + droppedFields); this PR shifts discovery left to os validate / os build, so an author finds the mismatch at design time instead of by reading server WARN logs days later.

Decision recorded on the issue

readonly semantics are kept as-is and formalized, not loosened to exempt the flow engine (that would turn any user-triggerable flow into a readonly-bypass gadget and undo #2948). The formal contract:

Writerstatic readonlyreadonlyWhen
REST/UI, runAs:'user' flow (default)strippedstripped when predicate TRUE
runAs:'system' flow, system hooks, seedswrittenwritten
INSERT (all callers, engine path)exemptexempt

"Users can't edit this, but automation maintains it" = declare the field readonlyand run the maintaining flow runAs:'system'.

Changes

  • New rulevalidateReadonlyFlowWrites (@objectstack/lint) — pure (stack) => Finding[] (ADR-0019). Static readonly + literal field under runAs !== 'system'error (gates); readonlyWhenwarning (advisory). Deliberately narrow to be false-positive-free: skips create_record (INSERT is engine-exempt from the strip), runAs:'system' flows, templated object names, and non-literal fields maps.
  • Wired into os validate and os compile/os build, mirroring the security-posture gate (errors fail; advisories print dimmed).
  • Docs — the contract table + "use runAs:'system'" guidance added to the objectstack-data and objectstack-automation skills.

Verification

  • 16 unit tests (positive + every "must-not-flag" boundary). Red-proof: neutering the rule fails exactly the 8 positive tests.
  • Full @objectstack/lint suite green (283); DTS build clean; edited CLI files typecheck clean.
  • No false positives on app-crm, app-showcase, app-todo (os validate passes).
  • End-to-end red: pointing app-crm's crm_convert_lead_wizard (runAs:'user') update_record at crm_opportunity.approval_status makes os validate fail with Readonly flow-write check failed … flows[0].nodes[6].config.fields.approval_status (reverted).

Out of scope (tracked separately)

🤖 Generated with Claude Code

…design time (#3425)
A flow `update_record` node that writes a field the target object declares
`readonly: true`, under the default `runAs: 'user'`, is a silent no-op: the
objectql engine strips static-`readonly` fields from a non-system UPDATE
payload (#2948) so the write never lands, while the step still reports success.
#3407/#3413 surfaced the strip at run time; this shifts discovery left to
`os validate` / `os build`.
- New `@objectstack/lint` rule `validateReadonlyFlowWrites`: static readonly +
literal field under runAs!=='system' → error (gates); readonlyWhen → warning.
Skips create_record (INSERT is engine-exempt), runAs:'system' flows, templated
object names, and non-literal fields maps to stay false-positive-free.
- Wired into `os validate` and `os compile`/`os build` (mirrors the security
posture gate). Verified: reds on a violating app-crm flow, clean on all
example apps.
- Documents the formal contract in the objectstack-data / -automation skills:
readonly governs the user/API surface; system writers (runAs:'system', hooks,
seeds) maintain it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercelBot commented Jul 24, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specBuildingBuildingPreview, CommentJul 24, 2026 3:33pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Jul 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/lint.

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

  • content/docs/ai/skills-reference.mdx(via packages/cli)
  • content/docs/api/client-sdk.mdx(via @objectstack/cli)
  • content/docs/api/data-flow.mdx(via @objectstack/cli)
  • content/docs/api/environment-routing.mdx(via @objectstack/cli)
  • content/docs/api/error-catalog.mdx(via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx(via packages/cli)
  • content/docs/deployment/backup-restore.mdx(via @objectstack/cli)
  • content/docs/deployment/cli.mdx(via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx(via @objectstack/cli)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx(via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx(via packages/cli)
  • content/docs/permissions/authentication.mdx(via @objectstack/cli)
  • content/docs/permissions/authorization.mdx(via @objectstack/lint)
  • content/docs/plugins/packages.mdx(via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx(via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx(via @objectstack/cli)
  • content/docs/releases/v16.mdx(via @objectstack/cli)

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.

@os-zhuang
os-zhuang merged commit 4340f13 into mainJul 24, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the feat/3425-readonly-flow-lint branch July 24, 2026 16:04
os-zhuang added a commit that referenced this pull request Jul 25, 2026
…ly, never [object Object] (#3450) (#3470)
A flow string field embedding an object-valued token — notably the engine's
`$error` ({nodeId, message, ...}) in a fault handler's notify body — rendered as
[object Object]: interpolateString's multi-token branch and notify-node both
coerced via String().
- New shared stringifyForTemplate helper (builtin/template.ts): objects/arrays
JSON-serialized (legible, still carries the message), primitives pass through,
null/undefined → ''.
- interpolateString embedded branch + notify-node title/body use it. Sole-token
branch still returns the raw value (typed fields keep their type); {$error.message}
still resolves to the message string.
Split from #3425 (readonly-strip half shipped in #3465).
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 25, 2026
#3465 follow-up) (#3480)
`validateReadonlyFlowWrites` warnings (the `readonlyWhen` advisory added in
#3465) were printed in human mode but left out of the `--json` summary's
`warnings` array, where every other advisory category is aggregated. So
`os validate --json` consumers (CI, editors) silently missed them. Add
`...readonlyWriteWarnings` to the summary array so JSON and human output agree.
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang