Skip to content

A sandboxed hook body reaching through a caller-supplied readonly key aborts the whole write with a bare TypeError that names nothing actionable #17219

Description

@os-sam

What an author sees

Since #16344 (PR #17195) a caller can no longer place a static readonly value on the hook's record. A sandboxed body that reaches through such a key — e.g. ctx.input.locked_meta.who = 'hook' where locked_meta is a caller-supplied readonly json key — now throws:

SandboxError: hook 'guard_task_body' threw: TypeError: cannot set property 'who' of undefined

A body's default onError is abort, so the caller's entire write is refused where it previously succeeded.

⚠️ The refusal itself is correct and is NOT what this card asks to change

What the refusal replaced is worse: the write succeeded and persisted a value derived from the caller's forged input. Trading a silent bad write for a loud refusal is the right direction, and #16344 exists precisely to close that laundering route.

Do not "fix" this by restoring the old behaviour. The defect here is the diagnostic, not the decision.

The defect

cannot set property 'who' of undefined names nothing an author can act on:

  • it does not say which key was readonly;
  • it does not say the value was withheld by the platform rather than absent by accident;
  • it does not name the remedy;
  • and it surfaces as a SandboxError wrapping a raw TypeError, which reads like a bug in the hook rather than a contract the platform is enforcing.

The remedy that exists is ctx.previous, and it is documented in PR #17195's changeset under Who is affected. An author hitting the raw TypeError has no path from the message to that remedy.

Why it was deliberately not fixed in PR #17195

Two reasons, both recorded there:

  1. Scope. fix(objectql)!: beforeUpdate receives the persist image; the caller submission moves to ctx.submitted (#16344) #17195 already spans four packages under a maintainer-confirmed ruling (5582066157, confirmed 5582245356). Widening it again for a diagnostic would repeat, in the opposite direction, the fence error the seat made on that same card.
  2. The obvious fix was measured and rejected on its merits. Marshalling ctx.submitted onto the sandbox face was considered and refused: that face is assembled key by key, and dispatch.scope is the standing precedent for that assembly discipline. ⛔ Whoever takes this card should not reach for that shape without re-arguing it against that precedent.

What a fix probably looks like

Not prescribed — the landing site is a measurement for whoever takes this. But the shape of a good answer is an error that names the key, says the platform withheld it because it is readonly, and points at ctx.previous. ⚠️ Re-derive the current behaviour on your own head before writing anything; ⛔ do not trust the message text quoted above to still be exact.

⚠️ One thing to check first

#17195 was still in contract review when this was filed. If that review changes the refusal semantics, this card's premise moves with it. ⛔ Read #17195's final verdict before starting.

Refs

#16344 / PR #17195 (the change that introduced it) · the seat ruling accepting the direction and deferring the diagnostic: PR #17195 comment 5606540131 · #14760 (the sandbox write-back provenance case whose test this surfaced through).

⛔ Unassigned and ungraded — domain:*, type and priority are triage's write.

Filed by the PM dispatch seat · domain:engine · session session_01XTBcV7zZHmokdyQgXjbyEU · R1

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions