|
| 1 | +--- |
| 2 | +"@objectstack/metadata-protocol": patch |
| 3 | +--- |
| 4 | + |
| 5 | +feat(metadata-protocol): publishing a platform-level scheduled `create_record` flow is refused on a multi-organization deployment unless it declares `organization_id` (#6285) |
| 6 | + |
| 7 | +A scheduled flow that creates records now has to say which organization those |
| 8 | +records belong to — but only where the answer matters, and only where nothing |
| 9 | +else can supply it. |
| 10 | + |
| 11 | +## What was open |
| 12 | + |
| 13 | +`ScheduleTrigger` builds its context as |
| 14 | +`{ event: 'schedule', params: { jobId, flowName, schedule } }` — no `tenantId`. |
| 15 | +PR #6153 closed the engine half of #5494 on the rule "stamp what the engine |
| 16 | +KNOWS": a run whose trigger resolved an organization carries it through, and the |
| 17 | +driver's tenant machinery fills `organization_id` on rows that omit it. A |
| 18 | +schedule resolves none, so nothing fills anything — and the dominant production |
| 19 | +shape of the whole issue is a nightly sweep, which fires on a schedule and not |
| 20 | +by hand. Every row it created was born `organization_id` NULL. |
| 21 | + |
| 22 | +That is not a cosmetic NULL. A `(organization_id, …)` unique index does not |
| 23 | +constrain across NULL and an org-scoped query does not see the row, so the |
| 24 | +damage is duplicate and invisible records — hotcrm#698's duplicate numbering — |
| 25 | +in a stored shape no later fix can retroactively repartition. |
| 26 | + |
| 27 | +## What now happens |
| 28 | + |
| 29 | +At the runtime publish gate, this exact combination is refused with the existing |
| 30 | +422 `INVALID_METADATA` envelope (`code` + `status` + `issues[]`, ADR-0112): |
| 31 | + |
| 32 | +- the deployment enforces an organization wall |
| 33 | + (`postureEnforcesWall(resolveTenancyPosture())` — `group` or `isolated`, |
| 34 | + ADR-0105 D1), **and** |
| 35 | +- the flow is platform-level (the write carries no organization), **and** |
| 36 | +- it binds to the **schedule** trigger, **and** |
| 37 | +- it contains a `create_record` node, **and** |
| 38 | +- that node declares no `fields.organization_id`. |
| 39 | + |
| 40 | +Every limb's negation still publishes: a single-organization deployment, an |
| 41 | +org-scoped write, any other trigger, a flow that creates nothing, and — the |
| 42 | +fix an author actually applies — a node that declares |
| 43 | +`config.fields.organization_id`. That key is not new: `CreateRecordConfigSchema` |
| 44 | +has always carried `fields`, and #6153's fill-only stamping already guarantees |
| 45 | +an author-supplied value wins over any engine fill. One issue is reported per |
| 46 | +offending node, including nodes nested inside `loop` / `try_catch` / `parallel` |
| 47 | +regions, each addressed at the key the author must write. |
| 48 | + |
| 49 | +Drafts are never gated (#4463 D1) and the draft to active promotion is, so the |
| 50 | +draft door is not a bypass. `OS_ALLOW_UNLINTED_METADATA_WRITES=1` degrades the |
| 51 | +refusal to a loud log exactly as it does for the 26 shared rules, and |
| 52 | +`os migrate meta --stored` stays carved out. |
| 53 | + |
| 54 | +## Where the judgement lives, and why |
| 55 | + |
| 56 | +Runtime publish gate only; `os validate` / `os build` / `os lint` do **not** |
| 57 | +judge this. Both inputs the rule needs are facts about the **deployment**, and |
| 58 | +the CLI runs on a build machine — a shared rule would sentence every |
| 59 | +single-organization repository on whatever `OS_TENANCY_POSTURE` happened to be |
| 60 | +exported in CI. The gate's caller performs the two readings and passes them as |
| 61 | +arguments, so the judgement itself stays a pure function of its inputs. |
| 62 | + |
| 63 | +Migration note for a multi-organization deployment: an existing scheduled flow |
| 64 | +keeps running untouched — the gate blocks new writes only, never stored rows — |
| 65 | +but the next time one is republished it will be refused until the |
| 66 | +`organization_id` is declared, which is the same edit that stops it writing |
| 67 | +outside the organization partition. |
0 commit comments