Skip to content

Commit 594465f

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24583-7c-dual-reads
2 parents 8f6bf26 + 1366740 commit 594465f

23 files changed

Lines changed: 1414 additions & 107 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): the delegable-scope report no longer offers the built-in identity positions to a platform administrator or a delegate
6+
7+
Clause-②: no
8+
9+
`GET /security/my-delegable-scope` (`describeDelegableScope`) listed `platform_admin`, `org_owner`, `org_admin` and `org_member` in `assignablePositions` for a tenant-level administrator and for a delegate holding a placing `adminScope`. `sys_user_position.position` refuses those four names from every writer with `400 VALIDATION_FAILED`, so the invite dialog and Setup's position picker offered choices that could only fail.
10+
11+
Every branch of the report (tenant-level, organization-level, delegated) now reads one position listing that leaves out the ADR-0068 D2 identity names through the same `isReservedIdentityName` predicate the write's validation is built from, beside the audience anchors it already left out. No other position leaves any branch, and placeable business units are unchanged. The write path's refusal and the reserved list are unchanged. No spec key, endpoint or configuration changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/plugin-approvals": minor
3+
---
4+
5+
feat(plugin-approvals): the decision that ends an approval tells its submitter the outcome (`approval.approved` / `approval.rejected`)
6+
7+
Clause-②: yes (widening)
8+
9+
- **What was wrong.** Approving or rejecting a request notified nobody. A send-back (`approval.returned`) and a question (`approval.request_info`) reached the submitter, but the decision itself did not, so a submitter learned that their request was approved or rejected only by opening the Approvals Inbox.
10+
- **What is new.** When a decision ends the approval, the submitter of the request it decides gets one notification on one of two new topics: `approval.approved` (title "Request approved") or `approval.rejected` (title "Request rejected"). The body names the record by its object's label and its id, as every approval notification does (`Your Deal D_123 was rejected.`), followed by the decider's comment as its own paragraph when they wrote one. It links to that request in the Approvals Inbox and, like the other approval notices, carries as its actor the person the decision's context vouches for. It goes through the installed `messaging` service like every other approval topic, so the same channels and delivery preferences apply; a preference row can mute one outcome and keep the other.
11+
- **When "ends the approval" holds.** A co-sign vote that leaves a `unanimous`, `quorum` or `per_group` request pending tells no one. A decision whose flow moves on to another approval step tells no one either; the decision that ends the run is the one that notifies. That is read from the run after the decision resumes it, not from the flow's graph, so when a decision node routes around a later step, the first decision is the outcome.
12+
- **Who is not told.** A submitter who decided their own request. And an SLA escalation's `auto_approve` or `auto_reject`: the escalation's own submitter notice (`approval.sla_breached`, naming the action taken) is the one message about it, so `escalation.notifySubmitter: false` still means the submitter hears nothing. A decision whose flow run cannot be resumed reports its failure to the decider and tells the submitter nothing, as a send-back in the same state already did.
13+
- **What to change in your flows.** If a flow ends with a `notify` node whose only job is to tell the submitter the decision, remove it, or the submitter is told twice. `notify` nodes that address anyone else are unaffected. No flow in this repository's examples has that workaround: the three showcase flows whose `notify` node reports a decision address someone other than the submitter — `showcase_invoice_signoff` and `showcase_project_closure` the record's owner, `showcase_release_signoff` a fixed address — so they are unchanged, and only a record owner who is also the person who submitted now hears both.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security): under the `group` posture, a tenant-scoped insert from a session with no active organization is refused 403 naming the missing organization, as `isolated` already answered, instead of a 500
6+
7+
Clause-②: no (no accept set moves: the insert was refused before and is refused now, with the ADR-0123 D2 answer in place of an internal error)
8+
9+
- **Before.** On `group` (any union-scope posture), a member of an organization whose session had no active organization got `500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` for a create on a tenant-scoped object, logged as an unhandled REST error.
10+
- **After.** That create gets `403 PERMISSION_DENIED` with the message `isolated` gives the same session, which names the missing active organization and tells the user to join or select one.
11+
- **Unchanged.** Nothing was written before and nothing is written now. The refusal judges the row as it is about to be stored, the same row the engine's own organization check reads, so it covers exactly the inserts that answered 500 and no insert that was admitted. Reads still span the member's organizations. Updates and deletes of rows the membership wall admits, inserts from a session with an active organization, system contexts, a platform operator on a posture-permitting object, the delegator's leg, and every `isolated` and `single` answer are as before.

‎content/docs/automation/approvals.mdx‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,8 +536,35 @@ second one for the opening.
536536

537537
If a flow has a `notify` node beside its approval node only to tell the
538538
approvers that a request is waiting, remove that node: those approvers are now
539-
told twice. Keep `notify` nodes that address someone else, such as the
540-
submitter or a watcher, or that report the decision.
539+
told twice. Keep `notify` nodes that address someone else, such as a watcher,
540+
or that report the decision to anyone but the submitter.
541+
</Callout>
542+
543+
<Callout type="info">
544+
**The decision tells the submitter.** When a decision ends the approval, the
545+
submitter of the request it decides gets one notification: `approval.approved`
546+
("Request approved") or `approval.rejected` ("Request rejected"). Its body
547+
names the record (`Deal D_123`) and the outcome, followed by the decider's
548+
comment when they wrote one, and it links to that request in the Approvals
549+
Inbox. There is one topic per outcome, so a delivery preference can mute
550+
approvals and keep rejections.
551+
552+
"Ends the approval" means the process, not the step. A co-sign vote that
553+
leaves a `unanimous`, `quorum` or `per_group` request pending tells no one,
554+
and neither does a decision whose flow moves on to another approval step: on
555+
a two-step flow the first step's decision is silent and the decision that ends
556+
the run is the one that notifies. Whether a later step opened is read from the
557+
run, not from the flow's graph, so when a decision node routes around the
558+
later step, the first decision is the outcome and notifies.
559+
560+
Two cases tell nobody by design. A submitter who decided their own request
561+
already knows. An SLA `auto_approve` or `auto_reject` is told by the
562+
escalation's own submitter notice (`approval.sla_breached`, which names the
563+
action taken), so `notifySubmitter: false` still means the submitter hears
564+
nothing about it.
565+
566+
If a flow ends with a `notify` node whose only job is to tell the submitter
567+
the decision, remove that node: the submitter is now told twice.
541568
</Callout>
542569

543570
### Mount the approvals inbox in your app

‎docs/qa/platform-checklist/RUNNER.md‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,49 @@ selector handed you has a verdict; planned ids are listed, not chased.
110110
and cite its output as the evidence; drive the browser only for what the pin doesn't
111111
cover. The reverse also holds: when a sweep hand-proves something repeatedly,
112112
propose promoting it to a permanent test and set `automated` in a revision.
113+
114+
**A pin scores only the layer it drives.** Before a pin scores a clause whose oracle
115+
is a public door's answer (an HTTP status, an error envelope, a row the door left
116+
behind), name the layer the pin reaches — a **plugin unit** (one plugin's hooks or
117+
middleware over a stub), the **engine** (a real ObjectQL engine and driver, no HTTP),
118+
or **HTTP** (a booted server answering the door) — and write that layer in the
119+
clause's evidence. Read it off the pin's own harness, never off its file name or its
120+
`automated.ref` prose: a stub terminal standing in for the engine means "lands" is
121+
"the middleware called `next()`", and nothing below it ran. A pin that stops above
122+
the layer the clause's oracle sits at scores that clause **`partial`**, naming the
123+
missing layer, and the live leg is driven, or recorded `blocked` with its fixture
124+
cited. It never scores `pass`. The instance that made this a rule is
125+
`access-security.no-active-org-session-semantics`: the `group` CONTROL of
126+
`no-active-organization-write-refusal.test.ts` "lands" a write by a member with no
127+
active organization at the plugin-security layer, two rounds scored the walled legs
128+
from it while a walled boot was refused, and on a full walled `group` boot the engine
129+
answers that same write with a 500 (#24784).
113130
7. **Verify pass for high-stakes claims.** For P0 `fail`s and any finding that would
114131
ship or block a release: a second, independent agent re-derives the verdict from the
115132
captured evidence alone (not from the first agent's narrative) before it is acted
116133
on. Disagreement → re-run the item.
134+
8. **Keep an off-clause probe budget.** Clauses only ask what someone already thought
135+
to ask. Every run keeps time AFTER its clauses have verdicts for probing the doors its
136+
selector covers past those clauses — on the same boot, as the same personas. Probe
137+
where a clause stops:
138+
- **boundary values** on the inputs the clauses drive: the empty string, a missing
139+
id, an unknown id;
140+
- **the opposite severity or posture** of a path a clause asserts: the `warning`
141+
twin of an `error` rule, `group` beside `isolated`;
142+
- **recipients or readers outside the asserted scope**: who else a notification,
143+
share or read reaches.
144+
145+
A probe is not a clause, so it scores nothing and no item verdict moves on it. A
146+
finding that passes the filing gate is extracted like any other product defect (see
147+
"Extraction obligation" below), and its card carries
148+
`QA-source: #<run record> · <area.item> · observed beside the clauses`, naming the
149+
item whose doors were probed. A probe that finds a gap the ledger should have asked
150+
about is also a checklist-accuracy finding for the wave's anchor card: the clause it
151+
was missing. The run record states how long the probes took and what they covered
152+
(doors, personas, the boundary classes tried), and says so when the budget was
153+
spent on nothing. A record that omits the probe line reads as a run that skipped it.
154+
Round 3 of the 18.0 sweep (QA run #24779) found #24781 and #24782 this way, beside
155+
items whose clauses all passed.
117156

118157
### Environment facts the runner should not re-derive
119158

@@ -717,7 +756,8 @@ The in-environment JSON scratch (RUNNER shape, never committed):
717756

718757
The issue body is: env fingerprint · scope (selector + per-item `revision`) · the
719758
per-clause verdict table (text oracle evidence) · a reproduction rule per `fail` (rule 2's
720-
carve-out excepted) · derived item verdicts + fixture gaps. The durable,
759+
carve-out excepted) · derived item verdicts + fixture gaps · the off-clause probe line
760+
(rule 8: how long, which doors and boundary classes, what it found). The durable,
721761
version-controlled truth is still the checklist under `areas/`; a run is a dated assertion
722762
about one build, and it lives in its issue, not the tree.
723763

0 commit comments

Comments
 (0)