Routed by the PM from #1429 (PR #1433). Human floor: this changes a permission boundary. Not dispatchable; recording the measurement so the ruling has it attached.
What #1429 measured
The readonly strip on the UPDATE path is one branch — if (!opCtx.context?.isSystem) — applied to caller-supplied keys only. So it is about the caller's context, not about which kind of writer you are:
- a
beforeUpdate hook's own ctx.input.data stamp survives in every context;
- INSERT is exempt entirely;
- a flow write survives exactly when that flow's effective
runAs is 'system' — and the engine default is runAs = flow.runAs ?? 'user'.
is_escalated has three writers, and they disagree:
| writer |
runAs |
write to a readonly field |
case_escalation |
'system' |
survives |
case_sla_monitor |
'system' |
survives |
escalate_case (src/flows/case-actions.flow.ts) |
none declared ⇒ 'user' |
stripped |
The least-privileged writer decides. One flow declaring no runAs is the entire reason is_escalated cannot be declared readonly, and therefore the entire reason PR #1427's STAMPED_NOT_TYPED guard exemption exists.
In-repo corroboration that the mechanism is already understood and relied on: close_case already declares runAs: 'system' precisely because it writes is_closed, which is readonly: true.
The decision
Adding runAs: 'system' to escalate_case would:
That last point is why this is not a mechanical fix. close_case is the precedent, but a precedent for one flow is not a policy for all of them, and "make it system so the declaration works" is a pattern that gets copied.
四象限
|
给 escalate_case 加 runAs: 'system' |
不加 |
| 该加而加 |
三个标志可以诚实地声明为 readonly,门禁豁免连同反向 pin 一起删掉。close_case 已经是同样的先例、同样的理由。声明与执行重新对齐,读代码的人不再需要读一条注释才知道为什么这个字段可写。 |
— |
| 该加而未加 |
— |
豁免长期留着,理由正确但绕。每个新来的人都要重新理解「为什么这三个字段明明没人手输却不是 readonly」,而答案藏在另一个文件的一个缺失声明里。 |
| 不该加而加 |
— |
一个用户点出来的界面流程从此以系统身份运行全部写入,不只是这三个标志。下游任何读取调用者身份的东西都拿不到真实用户。而且这个模式会被复制 —— 下一个人遇到 readonly 被剥离时,学到的解法是「把流程改成 system」。 |
| 不该加而未加 |
— |
现状。代价是一条有据可查的豁免,收益是界面动作仍以发起人的身份运行。 |
My input, as input only
I lean against it as stated, and toward a narrower third option nobody has costed: leave escalate_case as 'user' and ask upstream whether a flow can declare per-node elevation, so the one update_record that stamps the flags runs elevated while the rest of the flow keeps the acting user. That keeps both properties. But I have not measured whether the platform offers it, and I am not going to guess — if this direction interests you, say so and I will have it established before anything is built.
⛔ Nothing is being changed in the meantime. The exemption stands with the corrected reason PR #1433 landed, which is accurate under the current declarations.
Refs #1429 · PR #1433 · PR #1427 · objectstack-ai/objectstack#9107 · #3425
Generated by Claude Code
Routed by the PM from #1429 (PR #1433). Human floor: this changes a permission boundary. Not dispatchable; recording the measurement so the ruling has it attached.
What #1429 measured
The
readonlystrip on the UPDATE path is one branch —if (!opCtx.context?.isSystem)— applied to caller-supplied keys only. So it is about the caller's context, not about which kind of writer you are:beforeUpdatehook's ownctx.input.datastamp survives in every context;runAsis'system'— and the engine default isrunAs = flow.runAs ?? 'user'.is_escalatedhas three writers, and they disagree:runAsreadonlyfieldcase_escalation'system'case_sla_monitor'system'escalate_case(src/flows/case-actions.flow.ts)'user'The least-privileged writer decides. One flow declaring no
runAsis the entire reasonis_escalatedcannot be declaredreadonly, and therefore the entire reason PR #1427'sSTAMPED_NOT_TYPEDguard exemption exists.In-repo corroboration that the mechanism is already understood and relied on:
close_casealready declaresrunAs: 'system'precisely because it writesis_closed, which isreadonly: true.The decision
Adding
runAs: 'system'toescalate_casewould:readonly— the honest declaration, since no user types them;STAMPED_NOT_TYPEDexemption and its counter-pin outright. Removing a guard exemption is strictly better than documenting one, and it was the outcome Measure whatreadonly: trueactually strips on 17.1.0 — the app's blanket "the platform drops writes to readonly fields" now carries a guard exemption, and upstream contradicts half of it #1429 was filed hoping for;escalate_caseis invoked by a person from the UI. Running it assystemmeans every write it makes bypasses the readonly strip — not only the three flags — and, more importantly, it stops carrying the acting user's context for anything downstream that reads it.That last point is why this is not a mechanical fix.
close_caseis the precedent, but a precedent for one flow is not a policy for all of them, and "make it system so the declaration works" is a pattern that gets copied.四象限
escalate_case加runAs: 'system'readonly,门禁豁免连同反向 pin 一起删掉。close_case已经是同样的先例、同样的理由。声明与执行重新对齐,读代码的人不再需要读一条注释才知道为什么这个字段可写。My input, as input only
I lean against it as stated, and toward a narrower third option nobody has costed: leave
escalate_caseas'user'and ask upstream whether a flow can declare per-node elevation, so the oneupdate_recordthat stamps the flags runs elevated while the rest of the flow keeps the acting user. That keeps both properties. But I have not measured whether the platform offers it, and I am not going to guess — if this direction interests you, say so and I will have it established before anything is built.⛔ Nothing is being changed in the meantime. The exemption stands with the corrected reason PR #1433 landed, which is accurate under the current declarations.
Refs #1429 · PR #1433 · PR #1427 · objectstack-ai/objectstack#9107 · #3425
Generated by Claude Code