Skip to content

test(flows): extend the scheduled org-partition guard to update_record - #1373

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-1363-org-partition-update-record
Aug 27, 2026
Merged

test(flows): extend the scheduled org-partition guard to update_record#1373
os-steve merged 2 commits into
mainfrom
claude/issue-1363-org-partition-update-record

Conversation

@claude

@claude claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #1363

Extends the scheduled-sweep organization guard from create_record to update_record, in the same walk, through the platform's own region-slot map.

The question an update needs

The existing half asks whether organization_id is declared on a created row. An update_record targets a row that already has an organization, so that question does not apply — and the card names the one that does: where did the written value come from?

Every scheduled sweep runs runAs: 'system', and a system execution context is the one context the driver's organization predicate does not constrain. That is the platform's design (ADR-0049) and nothing here asks it to change; test/saas-composition.test.ts already measures it on a real engine. The consequence is that a sweep's reads span every organization and its writes are accepted against any of them, so an update_record can stamp one tenant's value into another tenant's row — no NULL partition, no index violation, nothing to catch it but a reviewer's eye.

The rule

Each interpolation token in an update_record's config.fields is resolved back to its source and must be organization-neutral:

  • a literal — carries no tenant;
  • a template function such as {NOW()} / {TODAY()} — reads no row;
  • a value taken off the swept row, the row the node's own filter.id names — by construction already in that row's organization;
  • or a fetch whose filter pins organization_id to an already-proven source. This is forecast_snapshot's {ownerAnyDeal.organization_id} precedent inverted, and it is the only proof mechanism admitted — a second one is deliberately not invented.

Provenance resolves transitively through assignment, loop and get_record bindings, because the interesting cases are never one hop: {firstUser.id} is an assignment off {userList.0} off a get_record on sys_user. A spelling check sees an ordinary local variable; this names the whole chain.

The general form turned out to be tractable, so the degraded "flag anything not literally the swept row" fallback was not used. The three binding constructs were measured rather than assumed — across src/flows/ the only binding keys are outputVariable (38), iteratorVariable (12) and assignments (12) — and a fourth would leave its variable unbound, which the classifier reports as unproven. Conservative direction, and a loud one.

Exemptions are argued and, where possible, measured

A flagged node is not automatically a defect, but it must be argued in ORGANIZATION_NEUTRALITY_EXEMPTIONS. The register is held from both sides: an unexplained write fails, and so does an exemption that has stopped matching anything, so it cannot rot into claims about flows that have since changed. Staleness is checked per field, not per entry.

  • demo_bootstrap (12 nodes, owner_id) — a real crossing, and the reason the flow is not registered in the multi-organization composition. The exemption is machine-checked: the test reads the flow's absence out of the SaaS composition, so re-registering it revokes the exemption automatically.
  • forecast_snapshot (write_snapshot, 4 amount columns) — right in the single-organization shape it was written for, not proven in general, because sys_user is a global identity. Filed separately as [finding] forecast_snapshot sums an owner's deals across every organization into one snapshot row #1372 rather than quietly fixed from here; this branch's surface is the guard.

Proving it red — the part that makes "all green" mean something

"Added the rule, everything green" is indistinguishable from "the rule matches nothing", so both were measured, each with the mutation proven on disk and the restore proven by state (blob hash equal to the HEAD blob and git diff HEAD empty), never by exit code.

Ablation 1 — empty the exemption register. Disk proof: register entries 1 → 0, marker absent → present. Result: red, printing the full inventory the register covers — 16 violations, 12 in demo_bootstrap and 4 in forecast_snapshot, each with its chain:

demo_bootstrap · stamp_leads · fields.owner_id = {firstUser.id}
    `firstUser` <- `bind_first_user` : `userList` is an independently-fetched
    sys_user collection (`get_user`) — its filter pins no `organization_id`

forecast_snapshot · write_snapshot · fields.pipeline_amount = {pipelineTotal}
    `pipelineTotal` <- `add_pipeline` : `current_pipeline` <- `sum_pipeline` :
    `pipelineOpps` is an independently-fetched crm_opportunity collection
    (`find_pipeline`) — its filter pins no `organization_id`

Restore proof: NOW_BLOB == HEAD_BLOB, git diff HEAD empty, marker occurrences 0.

Ablation 2 — turn a real shipped sweep into a cross-row writer. campaign_completion gained an independent sys_user fetch and wrote its id onto every campaign it touches — demo_bootstrap's defect shape, on shipped metadata reached through the barrel rather than through a fixture. Disk proof: injected text 0 → 2 occurrences, replaced text 1 → 0. Result: red.

campaign_completion · mark_completed · fields.owner_id = {ablationUser.0.id}
    `ablationUser` is an independently-fetched sys_user collection
    (`query_owner`) — its filter pins no `organization_id`

Restore proof: NOW_BLOB == HEAD_BLOB, git diff HEAD empty, injected text 0.

Against vacuity, permanently. Two guards-of-the-guard ship with the rule: it asserts it locates a non-zero number of scheduled update_record nodes, and that it classifies a non-zero number of tokens with at least one real variable reference. A committed fixture sweep pins the red and the negative direction — the literal, the template function and the swept-row read in the same node must come back clean, so a rule that flagged everything fails here too. A last case pins the escape the rule offers authors: pin the source fetch to the target's organization and the same node clears, through the same code path.

Verification

pnpm verify green at 58fd2a3, the tip of this branch, run after the final commit: validate, typecheck, lint, i18n gate, source hygiene, token ratchet, build, and 147 test files / 3074 passed, 1 skipped.

The diff is test/flow-scheduled-org-partition.test.ts plus the changeset, and nothing under src/ — checked rather than assumed, which is why the changeset carries empty frontmatter (the sanctioned "releases nothing" declaration). No platform behaviour was changed, and no in-app workaround was added.

Generated by Claude Code


Generated by Claude Code

claude added 2 commits August 27, 2026 14:31
The guard walked every schedule-bound flow's create_record nodes and required
organization_id to be declared. It never inspected update_record at all, and
for an update that is not the right question anyway: the target row already
carries an organization. The question is where the WRITTEN VALUE came from.

Every scheduled sweep runs runAs:'system', and a system execution context is
the one context the driver's organization predicate does not constrain (the
platform's design, ADR-0049, unchanged here). test/saas-composition.test.ts
measures it on a real engine. So a sweep reads across every organization and
its writes are accepted against any of them, and an update_record can stamp
one tenant's value into another tenant's row with no NULL partition and no
index violation to catch it.

Extend the same walk. Each interpolation token in an update_record's
config.fields is resolved back to its source, transitively through assignment,
loop and get_record bindings, and must be organization-neutral: a literal, a
template function, a value taken off the swept row the node's own filter.id
names, or a fetch pinning organization_id to an already-proven source, which is
forecast_snapshot's {ownerAnyDeal.organization_id} precedent inverted and the
only proof mechanism admitted.

Anything else needs a written exemption, and the register is held from both
sides so it cannot rot: an unexplained write fails, and so does an exemption
that no longer matches. demo_bootstrap's rests on a machine-checked reading of
its absence from the multi-organization composition.

Part of #1363

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMzCeNC4SZcPNBpE2zCVCg
The register entry recorded the residual (a global sys_user identity lets one
owner's deals span organizations, so the four accumulators can sum across them)
but named nothing to follow. Reference #1372, where it is filed, so the note is
traceable rather than terminal.

Part of #1363

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

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Aug 27, 2026 2:43pm

Request Review

@github-actions github-actions Bot added the ci/cd CI plumbing and the verification pipeline label Aug 27, 2026
@os-steve
os-steve marked this pull request as ready for review August 27, 2026 14:47
@os-steve
os-steve added this pull request to the merge queue Aug 27, 2026
Merged via the queue into main with commit 6c538dd Aug 27, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline

Projects

None yet

2 participants