Repository navigation
Commit efdf11a
feat(security): re-open a dismissed or revoked suggested audience binding (#27353)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgKURKuRGF1ewEVVZKx7U1i4)_
Fixes #27149
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:Studio 访问设置里「推荐绑定」被忽略或撤销后,管理员现在可以把它重新打开,回到待定状态,再按原流程接受。
- 为什么改:之前点错一次「忽略」或「撤销」就无法在产品里反悔,只能手改 everyone 定义。
- 风险与回滚:重新打开只把记录改回待定,不授予任何权限;接受仍要走原来的高风险检查。回滚即撤掉这一个 PR。
-
先看:`packages/plugins/plugin-security/src/suggested-audience-bindings.ts`(reopen
函数)、`packages/spec/src/contracts/security-service.ts`(新增可选成员)、`packages/rest/src/security-routes.ts`(第二个线上入口)。
- 两分钟验证:`pnpm --filter @objectstack/plugin-security exec vitest run
src/suggested-audience-bindings.test.ts`,应看到 reopen 一组 5 条用例全部通过。
- 需要你做的:无,席位落地(spec 契约成员按 triage 的跨域例外裁决处理)。
## What changes
A dismissed audience-binding suggestion (a declined one, or a revoked
acceptance) was terminal: `confirmAudienceBindingSuggestion` accepts
only pending rows, and `syncAudienceBindingSuggestions` never moves a
dismissed row. This adds one door that takes the "no" back.
- **`POST /api/v1/security/suggested-bindings/:id/reopen`** moves a
`dismissed` row to `pending`, writing exactly what the sync's own
re-open arm writes (`deployment_decision: false`, `resolved_by` /
`resolved_at` null). The row then reads, and is reconciled, as a
suggestion never answered.
- **It is not an implicit accept** (triage ruling on #27149). A pending
row is not an input of `readAcceptedEveryoneSets`, so nothing is
re-derived: a revoked set stays out of the `everyone` definition until
the existing confirm accepts it again, with ADR-0090 D7's high-risk
gate.
- **Refusals**, all before any write: the deployment authority as for
dismiss (`403`); the walled-host refusal naming
`acceptedDefaultPermissionSets` and the no-record-owner refusal (`409`);
unknown id (`404`); a row that is not `dismissed` (`409
SUGGESTION_STATE`, the message names its status); a set its package no
longer suggests (`409`). The last one keeps the decision on record:
re-opened, such a row would be pruned by the next sync.
- **Contract**: `ISecurityService` gains the OPTIONAL
`reopenAudienceBindingSuggestion(callerContext, id)`. Optional so an
existing implementer still satisfies the contract; absence is a defined
state (every dismissal stands, nothing granted). Both mounts
feature-detect it and answer `501 NOT_IMPLEMENTED` naming the door.
- **Both mounts** serve the route, as for confirm/dismiss:
`packages/rest/src/security-routes.ts` (RouteManager) and
`packages/runtime/src/domains/security.ts` (dispatcher). Rows added to
both route ledgers (`disposition: 'sdk'`), and the SDK gains
`client.security.suggestedBindings.reopen(id)`.
- `confirm` and `reopen` share one helper for the "is the set still
suggested by this package" check (`requireStillSuggested`); confirm's
messages are unchanged except the verb slot.
- Generated/census rows: the tenant-audit census page and counts file
(one new write call site: 244 → 245, with its prose figures), and the
runtime route-ledger census sentence (105 → 106 rows).
- Docs: `content/docs/permissions/permission-sets.mdx` lists the door
and what re-opening does.
## Tests
- plugin-security `suggested-audience-bindings.test.ts`, new
`reopenAudienceBindingSuggestion` block: dismiss → re-open → pending,
listed as pending by the list (which reconciles first, and does not undo
it), then accepted through the existing confirm; revoke → re-open grants
nothing (definition, re-derive, sync and `readAcceptedEveryoneSets` all
agree); the re-accept still runs the high-risk gate (403); pending,
accepted and observed rows refused by name (409, nothing written);
unknown id 404; a no-longer-suggested dismissal refused and kept as
history. Authority and walled-host cases extended to reopen.
- spec contract test: reopen is optional (the unguarded call does not
compile) and a served member answers.
- plugin-security members pin: reopen declared `optional`.
- runtime: dispatcher reopen passes the caller context and id, 501 when
the member is absent, 409 mapping; anonymous deny covers reopen.
- rest: envelope family extended to reopen (501 not registered, 501
member absent, typed 403/404/409, untyped 500 `INTERNAL_ERROR`, one
skeleton, 200 success); write-route ledgers carry the row.
- client: `reopen` POSTs to the encoded id subroute.
## Acceptance notes
- File surface (cross-domain exception, triage comment 6484879688; seat
claim 6485403386): the spec face is exactly one OPTIONAL
`ISecurityService` member and its contract-test pin. Files beyond the
claim's literal list, each required by a gate or pinning a touched door,
cleared by the seat before push: runtime `http-dispatcher.test.ts` and
`domains/security-anonymous-deny-ordering.test.ts`; rest
`security-suggested-bindings-envelope.test.ts`,
`rest-write-route-hook-refusal-sentence.ledger.test.ts` and
`rest-write-response-internal-fields.tripwire.test.ts` (the last two
enumerate every write route); client `admin-surfaces.test.ts`;
`content/docs/permissions/tenant-audit-census.mdx` and
`docs/audits/2026-08-tenant-audit-write-call-sites.counts.md`
(regenerated by `scripts/tenant-audit-census.mjs --write`, because
`check-tenant-audit-census` reds on the new write call site, plus its
prose figures).
- The untyped-fault terminal of the REST reopen route is the standard
`INTERNAL_ERROR` rather than a family code `SUGGESTION_REOPEN_FAILED`:
registering a family code is an edit to the spec error-code ledger and
its generated JSON schemas, beyond the one contract member. If the spec
seat prefers family symmetry, that is a small follow-up.
- The Studio control (a re-grant button in the suggestions panel) is the
ui follow-up #26846 left out; this PR builds the server door only.
## Verification
All readings on head `ce4b73d2f` (rebased on `origin/main` `6d7634a92`).
- Build: full `turbo run build` after the rebase, 116/116 tasks, lock
VERDICT command-exit 0.
- Typecheck (`tsc --noEmit` + `check:test-typecheck`) of spec,
plugin-security, runtime, rest and client: green (pre-rebase tree; the
rebase brought one cli-only commit).
- Full test suites of the five packages: green on the pre-rebase tree
(748 + 236 + 334 + 468 + 52 files, 0 failures). Targeted suites re-run
on this head: spec contract 26/26, plugin-security 58/58, runtime
315/315, rest 640/640, client 8/8.
- Ablation (once, then restored): reopen's write changed to `status:
'confirmed', deployment_decision: true` (an implicit accept) via
`scripts/ablation-replace.mjs` (anchor 1 to 0, blob `014d57b4cf14` to
`121c7e0c7aca`): 3 reopen cases red (dismiss-then-accept, revoke grants
nothing, re-accept runs the gate). Restored with `git checkout HEAD`;
the file's blob equals the HEAD blob `014d57b4cf14`, `git diff HEAD`
empty.
- `eslint --no-inline-config` over the changed `.ts` files: exit 0.
- `node scripts/check-platform-checklist.mjs --changed-from
origin/main`: OK, 0 lost.
- Derived gates: `node scripts/pm/dispatch-gates.mjs --commands` on this
head = 109 families, all run, all exit 0; `--ran` reconciliation: 109
derived, 109 run, 0 NOT-MEASURED, 0 UNRUN.
<details><summary>109 derived gates, exit codes</summary>
```text
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:api-surface :: exit 0
pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0
pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0
pnpm --filter @objectstack/spec run check:export-origins :: exit 0
pnpm --filter @objectstack/spec run check:exported-any :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:llms-txt :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:skill-refs :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-stale-fill :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:route-envelope :: exit 0
pnpm check:route-ledger-census :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:spec-docblock-symbol-anchors :: exit 0
pnpm check:spec-parsed-alias :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CaofYKDcV5XtynJUdP)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9ac5ae5 commit efdf11a
22 files changed
Lines changed: 462 additions & 56 deletions
File tree
- .changeset
- content/docs/permissions
- docs/audits
- packages
- client/src
- plugins/plugin-security/src
- rest/src
- runtime/src
- domains
- spec/src/contracts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
281 | 281 | | |
282 | 282 | | |
283 | 283 | | |
| 284 | + | |
284 | 285 | | |
285 | 286 | | |
286 | 287 | | |
287 | 288 | | |
288 | 289 | | |
289 | 290 | | |
290 | 291 | | |
291 | | - | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
292 | 300 | | |
293 | 301 | | |
294 | 302 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
| 96 | + | |
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
135 | | - | |
| 135 | + | |
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
| |||
197 | 197 | | |
198 | 198 | | |
199 | 199 | | |
200 | | - | |
| 200 | + | |
201 | 201 | | |
202 | | - | |
203 | | - | |
| 202 | + | |
| 203 | + | |
204 | 204 | | |
205 | 205 | | |
206 | 206 | | |
| |||
210 | 210 | | |
211 | 211 | | |
212 | 212 | | |
213 | | - | |
214 | | - | |
| 213 | + | |
| 214 | + | |
215 | 215 | | |
216 | 216 | | |
217 | 217 | | |
218 | 218 | | |
219 | 219 | | |
220 | | - | |
| 220 | + | |
221 | 221 | | |
222 | 222 | | |
223 | 223 | | |
224 | | - | |
| 224 | + | |
225 | 225 | | |
226 | 226 | | |
227 | 227 | | |
| |||
233 | 233 | | |
234 | 234 | | |
235 | 235 | | |
236 | | - | |
237 | | - | |
| 236 | + | |
| 237 | + | |
238 | 238 | | |
239 | | - | |
| 239 | + | |
240 | 240 | | |
241 | | - | |
| 241 | + | |
242 | 242 | | |
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
248 | | - | |
| 248 | + | |
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | 252 | | |
253 | | - | |
| 253 | + | |
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | 257 | | |
258 | | - | |
| 258 | + | |
259 | 259 | | |
260 | 260 | | |
261 | 261 | | |
| |||
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
310 | | - | |
| 310 | + | |
311 | 311 | | |
312 | 312 | | |
313 | 313 | | |
314 | | - | |
315 | | - | |
316 | | - | |
317 | | - | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
318 | 318 | | |
319 | 319 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| |||
183 | 183 | | |
184 | 184 | | |
185 | 185 | | |
186 | | - | |
| 186 | + | |
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
119 | 129 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6228 | 6228 | | |
6229 | 6229 | | |
6230 | 6230 | | |
| 6231 | + | |
| 6232 | + | |
| 6233 | + | |
| 6234 | + | |
| 6235 | + | |
| 6236 | + | |
| 6237 | + | |
| 6238 | + | |
| 6239 | + | |
6231 | 6240 | | |
6232 | 6241 | | |
6233 | 6242 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
97 | 98 | | |
98 | 99 | | |
99 | 100 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| 82 | + | |
82 | 83 | | |
83 | 84 | | |
84 | 85 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
| 79 | + | |
79 | 80 | | |
80 | 81 | | |
81 | 82 | | |
| |||
370 | 371 | | |
371 | 372 | | |
372 | 373 | | |
373 | | - | |
| 374 | + | |
| 375 | + | |
374 | 376 | | |
375 | 377 | | |
376 | 378 | | |
377 | 379 | | |
378 | 380 | | |
379 | 381 | | |
| 382 | + | |
| 383 | + | |
380 | 384 | | |
381 | 385 | | |
382 | 386 | | |
| |||
0 commit comments