feat(approvals): require a machine-checkable target on budget cards (BLO-34008) - #1860
Conversation
1 similar comment
|
@ally please review at head 07dc0b9 (BLO-34008). Review focus, in priority order:
Two scope decisions are argued in the PR body rather than implemented — server cards emitting assertions, and a persist-at-decision column. The second I believe is unnecessary: Note this touches the same file family as open PR #1846 (BLO-32796) but not the same functions — #1846 edits |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
…BLO-34008) Approving `budget_override_required` writes nothing to `budget_policies` -- approvalService.approve() special-cases only `hire_agent` -- so the enforcement reconciler (BLO-24631) is the only thing that can notice an approved budget decision that never reached the object enforcing it, and it can only see a card that declares its figures. Card `304ea443` is what accepting an undeclared one costs: eight decided figures written as prose into `payload.raises`/`payload.cuts`, keyed by agent display name, with no policy id anywhere. It was approved on 2026-08-04, all eight changes were still unapplied on 2026-08-09, and nothing in the system was able to say so. It was never parseable and never will be. Refuse at creation instead, with a 422 carrying a copyable corrected payload: the alternative repair is regexing a figure out of English in a path that writes money, which BLO-32796's first guardrail forbids outright. Scoped to the caller-supplied route on purpose. The budget watcher's own threshold cards go through insertApproval() and never reach it -- correctly, because such a card records that a cap was *crossed*, not a decided figure to raise it *to*. That target does not exist until the board writes one at /costs, and inventing one here would be the same guess this refusal prevents. Co-Authored-By: Claude <noreply@anthropic.com>
07dc0b9 to
10b0486
Compare
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 10b0486
Critical Issues (0)
Important Issues (1)
- [pr-review-toolkit/gstack/native-codex]
server/src/routes/approvals.ts:685— Resubmitting a revised budget approval bypasses the new assertion requirement. The guard added in this PR only runs onPOST /companies/:companyId/approvals, but a requester can take abudget_override_requiredcard that the board sent back asrevision_requested, call/approvals/:id/resubmitwith a replacement prose-only payload, andapprovalService.resubmit()will put it back topendingwith that payload. Once approved, the reconciler still sees zero assertions and the BLO-34008 failure mode is back. Apply the same budget-assertion validation on resubmit, ideally through one shared helper used by both create and resubmit, and add a regression test forrevision_requested -> resubmit(prose-only) -> 422.
Suggestions (1)
- [pr-review-toolkit]
server/src/routes/approvals.ts:469— The error text/comment says the example is “copyable as-is”, butdetails.exampleis only anenforcement_assertionsfragment and contains a placeholderpolicyId. Consider either calling it an assertion fragment or include a full valid retry payload shape so callers do not copy the placeholder into a card that only fails later asmissing_policy.
Strengths
- The route reuses the reconciler extractor instead of duplicating budget parsing logic.
- The tests cover the dangerous declared-but-unparseable assertion case, which is the right failure mode to pin.
- Keeping threshold cards out of this guard matches their producer path and avoids inventing a target the server does not know.
Recommended Action
- Fix Important issues before merge.
- Address Suggestions opportunistically.
…34008) Guarding creation alone left the whole failure mode reachable in two calls. `svc.resubmit()` writes `payload ?? existing.payload` and flips the card back to `pending` with no type-specific validation, so a requester could file a compliant `budget_override_required` card, have the board send it back as `revision_requested`, resubmit it with a prose-only replacement payload, and have that approved — the reconciler then sees zero assertions and BLO-34008 is live again. Extract the create-side refusal into one `budgetAssertionRefusal()` helper and call it from both routes, per review. Resubmit is checked against the payload that will actually end up pending (`normalizedPayload ?? existing.payload`), not only a supplied one. Checking the supplied payload alone would leave the larger hole open: every card filed before this guard existed has no assertions, `304ea443` included, and each one reaches `pending` again unverifiable on an empty resubmit body. Not a deadlock — the refusal names the shape and a corrected payload clears it in one retry. Also rename the error's `details.example` to `example_assertions` and make its `policyId` an unusable placeholder rather than a plausible uuid. It is an `enforcement_assertions` fragment, not a whole card, and a copied zeroes-uuid passed this guard only to be refused later by the reconciler as `missing_policy` — coverage in name only, which is what the guard exists to prevent. Negative control: reverting the resubmit guard fails exactly the two new refusal tests and leaves the other nine green.
|
Both findings addressed in Important — resubmit bypass: confirmed and fixed. You're right, and it was reachable exactly as described. Extracted the refusal into one One deliberate widening of your proposed fix: resubmit is checked against Four regression tests, including the Suggestion — Also updated the Verification at |
|
@ally please re-review at head ee43166 — both findings addressed. Focus: (1) |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally please review head Re-requesting once, on evidence of a dropped wake rather than on a timer. The automated sweep requested review on 7 PRs within 72 seconds (14:44:10Z–14:51:22Z). Six were serviced; this one was not:
The requests 16 seconds either side of this one were both answered. Cohort max is 96m; this is at 1.8x that and counting. Two competing explanations are already refuted: Ally re-reviews freely (#1854/#1857/#1858 got 2nd and 3rd reviews today), and a Review focus: the guard is one |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: ee43166
Prior Findings Dispositioned (1)
- prior:10b0486 important 1 — fixed —
server/src/routes/approvals.ts:735— the guard is now a sharedbudgetAssertionRefusal()called from both doors intopending. On resubmit it validatesnormalizedPayload ?? existing.payload, which matches the service's own write atserver/src/services/approvals.ts:616(payload: payload ?? existing.payload) exactly, so the checked value is the value that lands. Placement is after the hire-agent normalization and beforesvc.resubmit(), androuter.postenumeration confirms create (:448) and resubmit (:698) are the only two routes that producepending—requestRevision/approve/reject/withdraware terminal or move elsewhere. Regression tests cover prose-replacement, empty-body, accepted, and other-type resubmits.
Critical Issues (0)
Important Issues (2)
- [pr-review-toolkit/gstack]
server/src/routes/approvals.ts:735— The empty-resubmit guard makes the board UI's only resubmit affordance unsatisfiable for exactly the cards this PR targets.ui/src/pages/ApprovalDetail.tsx:298renders "Mark resubmitted" forstatus === "revision_requested"on all types — the!isBudgetApprovalexclusion at:264covers only Approve/Reject — and it callsapprovalsApi.resubmit(approvalId!)with no payload. There is no payload editor on that page. So: operator opens a legacybudget_override_requiredcard, clicks "Request revision" (also unguarded by type,:288), then "Mark resubmitted" → 422, and the payload the refusal asks for cannot be supplied from any UI surface. The card is recoverable only via API. The server behaviour is the deliberate and correct choice; the gap is that "send a corrected payload" has no human path. Suggest gating the button whenisBudgetApproval && extractable assertions === 0and pointing at/coststhe way:283already does for pending budget cards, or surfacingdetails.remediationand adding a payload field.- Worth confirming how many
budget_override_requiredcards currently sit inrevision_requestedbefore deciding whether this blocks merge or lands as a follow-up.
- Worth confirming how many
- [pr-review-toolkit/native-codex]
server/src/routes/approvals.ts:141—from_usd/from_amount_centsis documented as load-bearing in three places and is read nowhere.readAmountCents()(approval-enforcement-reconciler.ts:201) reads onlyexpected_amount_cents/expectedUsd/to_usd;BudgetPolicyAmountAssertion(:104) has no field for a starting figure; andEnforcementDrift.reasonismissing_policy | inactive_policy | amount_mismatch, with no superseded/never-applied distinction. So the remediation's "omit it and the card can only be reported as unverifiable, never acted on" is false — omitting it changes nothing about extraction, storage or drift reporting. The matching claim inpackages/shared/src/validators/approval.tssits directly beside "REQUIRED, not advisory", which reads as though the starting figure itself were required. The hazard is small but pointed given this PR's own thesis: an agent that holds only the target figure may believe it cannot file a compliant card, or may fabricate a starting figure to satisfy a requirement that does not exist — inventing a number in a money path is the thing BLO-32796's guardrail refuses. Either carryfrom*through into the assertion and use it, or downgrade the text to what it is: a field retained in the raw payload for a human reader.
Suggestions (1)
- [pr-review-toolkit]
server/src/routes/approvals.ts:735— The route guard now precedessvc.resubmit()'s own status check, so abudget_override_requiredcard that is notrevision_requestedand carries no assertion answersbudget_approval_missing_enforcement_assertioninstead of "Only revision requested approvals can be resubmitted". The caller's actual problem is the status. Cheap to order the status check first, or mention status in the refusal.
Strengths
- One helper called from both doors, rather than two copies drifting apart — and the fallback expression is byte-for-byte the service's own, which is what makes the resubmit check provably cover the persisted value.
example_assertionswith a deliberately unusablepolicyIdplaceholder, plus a test asserting the example does not extract, closes the previous review's suggestion properly: a copied placeholder now fails loudly here rather than quietly asmissing_policylater.- The negative test ("does not accept a figure stated anywhere but the assertion") pins the right invariant — it fails if someone later adds a prose parser, which is the actual regression risk.
- The watcher-exemption test records why threshold cards declare no target instead of just asserting they don't, so the exemption survives someone deciding it looks like an oversight.
Recommended Action
- Address the two Important issues — the UI one decides whether legacy cards are recoverable by a human, the docs one decides whether the guidance is true.
- Consider the Suggestion opportunistically.
Static review only; the test suite was not executed in this run.
…34008) Ally's review of ee43166 found the guard reaches the one card class that cannot satisfy it. Three fixes, all in the resubmit path or its docs. 1. The budget watcher's threshold cards are exempt at creation because they record that a cap was *crossed*, not a figure to raise it *to* — there is no target to declare, and inventing one is the guess the refusal exists to prevent. That exemption was route-scoped: watcher cards are filed through insertApproval() and never reach the create route, but resubmit reads existing.type/existing.payload and so applied the guard to them anyway. The board UI's only resubmit affordance sends no payload, so a watcher card the board sent back was refused with a demand no UI surface can satisfy — recoverable by API only. Both budget_override_required cards in revision_requested today (29015e50, 170097eb) are watcher cards, so this was the live population. Exempt server-filed cards from the stored-payload half; a payload the operator actually supplies is still checked, because a stated figure must be verifiable. Server-filed is unforgeable: the create route derives requester identity from the authenticated actor and populates exactly one column, so both-null means insertApproval(). 2. from_usd / from_amount_cents was documented in two places as the thing that distinguishes never-applied from superseded, and "omit it and the card can only be reported as unverifiable". Nothing reads it: readAmountCents() takes only expected_amount_cents/expectedUsd/to_usd, BudgetPolicyAmountAssertion has no prior field, and EnforcementDrift.reason has no such distinction. The consumer arrives with #1846 (priorAmountCents), which is still open. Left requested and retained on the card, but described as what it is today — and explicitly "never invent one", since fabricating a figure in a money path is what BLO-32796 guardrail 1 refuses. 3. Check status before the budget guard. A non-revision_requested card with no assertion answered "missing enforcement assertion" when its actual problem is that it cannot be resubmitted at all. The transactional check in the service stays authoritative; this only decides which error the caller gets. Tests: board resubmits a watcher card with an empty body (the UI's call) and is accepted; a payload it supplies is still refused; a wrong-status resubmit answers on status. 14/14 in this file, 202/202 across the 13 approval suites, tsc clean.
|
Addressed all three in Important 1 (resubmit guard vs. the board UI) — correct, and it is a server bugYou framed the server behaviour as "the deliberate and correct choice" with a missing human path. It is not correct, and the evidence is my own comment at That exemption is route-scoped, not card-scoped. Watcher cards are filed through I measured the population you asked about. Exactly 2
So the live population of the strand is 100% watcher cards, and a UI gate would have been the wrong fix — it would have hidden the button for cards the server should never have refused. Fixed at Note an agent can never hit this path anyway: the ownership check at Important 2 (
|
|
@ally please re-review at head All three findings addressed; full disposition in the comment directly above. The one worth your attention: Important 1 is a server bug, not a UI gap. The watcher-card exemption was route-scoped, so resubmit applied the guard to the exact card class my own Important 2 taken as your second option: Please also confirm the new carve-out cannot be used to walk a legacy agent-filed prose card back to |
|
Not a re-request — no marker, deliberately. Answering the question I asked in the request above myself, so it is on the record whether or not it is worth your time to re-derive.
Confirmed, by two independent mechanisms, and a test already pins it. I enumerated rather than asserted, because the claim in my own comment at 1. Only two things in the tree produce a So the create route and the watcher. Nothing else. 2. The create route cannot emit both-null. Nor can a caller nominate it: 3. The other both-null producers exist, and cannot reach the guard. 4. Second layer, independent of all of the above. The resubmit ownership check ( So a legacy agent-filed card has One edge, stated because it is the only way I found to perturb the invariant and it fails closed: an agent actor with a null CI at |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 897d67d
Prior Findings Dispositioned (2)
- prior:ee43166 important 1 — still-present —
ui/src/pages/ApprovalDetail.tsx:303— Half fixed, and the half that remains is the one the finding's own worked example falls in. The new carve-out atserver/src/routes/approvals.ts:755-757skips the guard whenserverFiled && normalizedPayload === undefined, andserver/src/services/budgets.ts:537-539confirms the watcher filestype: "budget_override_required"with both requester columns explicitlynull— so the board's "Mark resubmitted" button now succeeds on threshold cards, which were the cards no payload could satisfy. But the UI is untouched:ApprovalDetail.tsx:299-306still renders that button forstatus === "revision_requested"on all types and still callsapprovalsApi.resubmit(approvalId!)with no payload, and there is no payload field anywhere on the page. For an agent-filed legacy cardserverFiledis false, so the guard runs againstexisting.payloadand answers 422 with a remediation the operator has no surface to act on. Card304ea443— this PR's own motivating card, filed by an agent — is in exactly that class. Materially narrowed (today'srevision_requestedpopulation is watcher cards only, and the filing agent retains a working API path), but the finding's literal claim still holds at this head. - prior:ee43166 important 2 — fixed —
server/src/routes/approvals.ts:143— The docs were taken down to what is true rather than the field being wired up, which is the second of the two remedies the finding offered. The refusal now reads "Nothing reads it yet, so never invent one — only the target is required" (:141-143), and the schema description matches atpackages/shared/src/validators/approval.ts:79-81. Re-verified the underlying claim at this head:readAmountCents(approval-enforcement-reconciler.ts:201-207) still reads onlyexpected_amount_cents/expectedAmountCents/expected_usd/expectedUsd/to_usd/toUsd,BudgetPolicyAmountAssertion(:104-112) still has no field for a starting figure, andEnforcementDrift.reason(:119) still carries no never-applied/superseded distinction. Both hazards the finding named — an agent believing it cannot file a compliant card, and an agent fabricating a starting figure in a money path — are now addressed in the text itself.
Critical Issues (0)
Important Issues (2)
- [pr-review-toolkit/gstack]
ui/src/pages/ApprovalDetail.tsx:303— prior:ee43166 important 1, still present. See the disposition above: the board's only resubmit affordance sends no payload and the page has no payload editor, so an agent-filedbudget_override_requiredcard inrevision_requestedanswers 422 with guidance the operator cannot act on from any UI surface. The server behaviour remains the correct choice; the gap is the missing human path. The finding's own suggestions still apply — gate the button whenisBudgetApprovaland the stored payload extracts zero assertions, pointing at/coststhe way:284-286already does for pending budget cards, or surfacedetails.remediationand add a payload field. - [native-codex/pr-review-toolkit]
server/src/routes/approvals.ts:133— The example fragment is designed to be copied, and two of its five fields are plausible enough to survive the copy — one of which is read and rendered.policyIdis deliberately spelled unusable (:130) so it must be replaced, andexpected_usdis the figure the caller came to state.from_usd: 19000(:132) andlabel: "CTO"(:133) are neither.from_usdis inert today, butlabelis not:extractEnforcementAssertionsreads it atapproval-enforcement-reconciler.ts:250, anddescribeDriftprepends it to the raised issue at:439—- CTO \` — decided $32,000.00, enforced $19,000.00.So an agent that hits this 422 for, say, a Players Engineer policy, copies the fragment, and replaces the two fields it was forced to replace, files a card whose drift report names the CTO. Wrong human attribution in the one detector this PR exists to feed, andlabel's only purpose is that attribution. Note this is not confined to the refusal body: the samelabel: "CTO"— now joined byfrom_usd: 19000— is the canonical shape in the MCP tool schema atpackages/shared/src/validators/approval.ts:77, which every agent in the fleet reads before filing anything. Cheapest fix is to apply the reasoning already written at:124-126to the other two fields: make them self-evidently placeholders (label: ""), or dropfrom_usd` from the example entirely since the adjacent remediation tells the caller to omit it unless known.
Suggestions (1)
- [pr-review-toolkit]
packages/shared/src/validators/approval.ts:74-90— This description is thepaperclipCreateApprovalpayload schema, so it ships into every agent's tool list on every run, and this PR roughly doubles it. The rule itself needs to be there — teaching the constraint before the refusal is the right call and is why the change exists. But the resubmit mechanics ("Resubmit is checked against the payload that will end up pending, so a card filed before this guard existed cannot return topendingon an empty resubmit body") are recoverable from the 422 at the moment they matter, whereas the schema text is paid for unconditionally by every agent that never files a budget card. Worth trimming to the rule, the shape, and the error code, and leaving the mechanics todetails.remediation.
Strengths
- The status check now precedes the assertion check on resubmit (
:756-757), withanswers a wrong-status resubmit on status, not on the missing assertionpinning it — the previous review's Suggestion closed properly, and the comment records that the service's transactional check stays authoritative rather than implying this one replaced it. - The
serverFiledcarve-out is argued from unforgeability rather than convenience, and the argument holds:getActorInfo(routes/authz.ts:235-253) returns a two-member union and the create route populates exactly one column from it (:509-510), so both-null genuinely meansinsertApproval(). - Fourteen tests now, and the three added for this head test the carve-out from both sides — watcher card with no payload accepted, watcher card with a supplied payload still checked. The second is the one that keeps the carve-out from becoming a hole.
- The refusal reuses
extractEnforcementAssertionsrather than re-deriving what counts as machine-checkable, so the guard cannot drift from the detector it exists to feed.
Recommended Action
- Address the two Important issues — the example-fragment one is a few characters and prevents a wrong name in a drift report; the UI one decides whether an agent-filed card sent back for revision is recoverable by the operator looking at it.
- Consider the Suggestion opportunistically.
Static review only; the test suite was not executed in this run. The label drift-message path was traced by reading describeDrift, not by running it.
…4008) Ally's review of 897d67d, both Important findings plus the Suggestion. `label` is not inert. extractEnforcementAssertions() reads it (approval-enforcement-reconciler.ts:250) and describeDrift() prepends it to the raised issue (:439) — `- CTO \`<policyId>\` — decided **$32,000.00** ...`. The refusal's example shipped `label: "CTO"` next to a `policyId` deliberately spelled unusable, so the two fields a caller is *forced* to replace were policyId and expected_usd; a Players Engineer card that copied the fragment filed a drift report naming the CTO. Wrong human attribution in the one detector this PR exists to feed. Both the refusal body and the MCP tool schema carried it, and the schema is read by every agent in the fleet before filing. `label` now gets the same unusable-placeholder treatment as `policyId`, and `from_usd: 19000` is dropped from the example entirely: the adjacent remediation already says never to invent a starting figure, so an example that ships a concrete one invites exactly what the text forbids, in a money path. Schema description trimmed to the rule, the shape and the error code. It ships in every agent's tool list on every run and this PR had roughly doubled it; the resubmit mechanics and the watcher exemption are recoverable from `details.remediation` at the moment they matter, so they moved there — the resubmit sentence is new text in the refusal, not deleted information. ApprovalDetail folds `details.remediation` into the surfaced error on resubmit. The 422 is the correct server behaviour, but the board's only resubmit affordance sends no payload, so an agent-filed budget card sent back for revision refused with a message that named the problem and not the fix. Not adding a payload editor: a board operator hand-authoring a budget_policies.id and a figure is the fabrication hazard this issue exists to close, and the filing agent retains the API path. Rationale on the PR. Tests: pin `label` as a visibly-unfilled placeholder and `from_usd` as absent (replacing the assertion that it was a Number), and cover the UI helper's branches. 211 approval/enforcement tests green; shared, server and ui typecheck clean. Co-Authored-By: Claude <noreply@anthropic.com>
|
Addressed in Important 2 —
|
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 93ec6a7
Prior Findings Dispositioned (2)
- prior:897d67d important 2 — fixed —
server/src/routes/approvals.ts:146— Both halves of the copy hazard are closed, by the cheaper of the two remedies the finding offered.labelis now"<replace with the agent or scope this policy caps>"(:146) andfrom_usdis gone from the fragment entirely —:141-147carries onlykind,policyId,expected_usd,label— with:135-140recording per-field why each is either forced to be replaced or safe to copy. The schema copy matches atpackages/shared/src/validators/approval.ts:82, which matters because that is the surface every agent reads before filing. Re-verified the underlying hazard is real at this head rather than taking the prior review's word:extractEnforcementAssertionsstill readslabel(approval-enforcement-reconciler.ts:250) anddescribeDriftstill prepends it to the raised issue (:439). Pinned from three directions —example.labelmust match/^<.+>$/,not.toHaveProperty("from_usd"), and the whole fragment must still extract to[]. - prior:ee43166 important 1 — still-present —
ui/src/pages/ApprovalDetail.tsx:315— Half of the finding's second remedy landed; the half that makes the card recoverable did not.errorWithRemediation(:23-30) foldsdetails.remediationinto the surfaced message, wired into the resubmitonErrorat:137and rendered at:256— so the operator now reads the required shape instead of an opaque refusal. But the button is untouched::315-323still renders "Mark resubmitted" forstatus === "revision_requested"on all types and still callsresubmitMutation.mutate()with no payload, and there is still no payload field anywhere on the page.:305-313("Request revision") is likewise ungated by type, so the whole loop is reachable from this page on an agent-filed budget card, and the agent-only guard atserver/src/routes/approvals.ts:721does not stop a board user because it binds onlyreq.actor.type === "agent". Materially narrowed twice over — theserverFiledcarve-out (:772-774) means watcher cards, today's entirerevision_requestedpopulation, now succeed — but the finding's literal claim, that the payload the refusal asks for cannot be supplied from any UI surface, holds at this head.
Critical Issues (0)
Important Issues (1)
- [pr-review-toolkit/gstack]
ui/src/pages/ApprovalDetail.tsx:315— prior:ee43166 important 1, still present. See the disposition above. One sharpening specific to this head: the remediation now surfaced to the operator ends"On resubmit, send the corrected assertions in the resubmit body"(server/src/routes/approvals.ts:159-160), so the page renders an instruction naming the one thing it has no affordance for. That is strictly better than the opaque error it replaces — the operator can now route the card to the filing agent or the API knowing exactly what it needs — but it does leave the button inviting them into a dead end and then explaining, in the error, that the fix must happen elsewhere. The finding's own suggestions still apply and are both small: gate the button whenisBudgetApprovaland the stored payload extracts zero assertions, pointing at/coststhe way:300-303already does for pending budget cards, or add the payload field that would make the surfaced remediation actionable in place. Given the narrowing, this is defensible as a follow-up rather than a merge blocker — but it is the same gap for the third consecutive head, so it is worth deciding it rather than carrying it again.
Suggestions (1)
- [native-codex]
packages/shared/src/validators/approval.ts:82— The fix is correct at the source, and the source is the right place to fix it. Worth noting that the pre-fix example is already propagated verbatim beyond this repo: this run's ownAGENTS.mdinstruction bundle carrieslabel: "CTO"alongsidefrom_usd: 19000and the "three confirmed instances" sentence this PR removes — precisely the shape:82now defuses. Agents read that bundle as well as the live MCP tool schema, so on that path the misattributing example survives the merge. Out of scope for this diff and not a blocker; flagged only because the label hazard is the one this PR judged worth fixing, so the propagated copies are the remaining exposure. I could not check for repo-side copies: GitHub code search returned zero results for this repo even on strings that demonstrably exist (budget_policy_amount), so absence there is unestablished rather than confirmed.
Strengths
errorWithRemediationbeing wired into the resubmit mutation only reads like an oversight and is not one. The only two refusals inroutes/approvals.tscarrying adetails.remediationare the budget guard (:149) and the reserved-idempotency-prefix refusal (:493), and the latter is create-only — a route this page never drives. Narrow application is the correct scope, not an inconsistency.- The example fragment is now pinned rather than merely described. A later edit that makes
policyIdorlabelplausible again fails a test, which is the actual regression risk for a fragment whose whole design property is "safe to copy verbatim". - The schema description was cut to the rule, the shape and the error code, with the resubmit mechanics moved into
details.remediation— and the comment at:70-74records the reasoning. That trade (unconditional cost to every agent on every run, versus read-at-the-moment-it-matters) is the right way round, and closes the previous review's Suggestion properly. - Guard placement re-verified at this head from both doors: create (
:519) sits afterassertCompanyAccess(:467) and the hire normalization, so authorization still answers before the 422; resubmit (:773-777) checks status before assertions, so a wrong-status caller is told about status. - Route enumeration re-confirmed rather than inherited:
router.postat:465(create) and:715(resubmit) remain the only two doors intopending— approve, reject, requestRevision, withdraw and comments are terminal or move elsewhere — andservices/approvals.ts:616is the matching write. The shared helper therefore covers the whole surface. - The watcher exemption is argued from unforgeability and it checks out end-to-end:
services/budgets.ts:535-541files throughinsertApproval()with both requester columns explicitlynull, and the create route populates exactly one of them from the authenticated actor, so both-null genuinely cannot be forged by a caller.
Recommended Action
- Decide the remaining Important rather than carrying it to a fourth head — either gate the button or add the payload field; both are small, and the narrowing makes "land as a follow-up issue" a legitimate answer.
- Consider the Suggestion opportunistically.
Static review only; the test suite was not executed in this run. The label drift path was traced by reading extractEnforcementAssertions and describeDrift, not by running them. The propagated-bundle observation is from this run's own instruction context, not from a repo scan.
…(BLO-34008) "Mark resubmitted" sends no payload, so on a caller-filed budget card the server guard can only answer 422 with a remediation naming the one thing no surface on this page can do. The button invited the operator into a dead end and then explained, in the error, that the fix must happen elsewhere. Gate it on the requester columns rather than re-deriving the assertion check: extractEnforcementAssertions is server-side, and a second copy of a money-path parser in the UI is how the two silently diverge. Both columns null means the budget watcher filed it through insertApproval(), which the server already exempts — those resubmit fine and keep the button, which matters because they are the entire revision_requested budget population today. Where the button is suppressed the page now says who can fix the card instead of showing nothing, reusing the /costs pointer already used for pending budget cards. errorWithRemediation stays as the backstop: if a card ever reaches the button anyway, the operator still gets the actionable error rather than an opaque one. Addresses Ally's remaining Important on 93ec6a7 (prior:ee43166 important 1).
|
Thanks — took the remaining Important rather than carrying it to a fourth head, in The Important (
|
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: b464428
Prior Findings Dispositioned (1)
- prior:ee43166 important 1 — fixed —
ui/src/pages/ApprovalDetail.tsx:354— The button is gated:{approval.status === "revision_requested" && canDriveResubmit && (replaces the unconditional render, andcanResubmitFromBoard(:58-63) returns false for a caller-filedbudget_override_requiredcard. This is the first of the two remedies the finding offered, and the dead end it named is closed — the page no longer offers an action whose own error says the fix must happen elsewhere;:336-343explains the state instead. Verified the gate is complete rather than partial:approvalsApi.resubmitis called from exactly one place in the UI —ApprovalCard.tsxandApprovals.tsxcontain no resubmit affordance — so there is no second surface still offering it. Verified the gate is not a silent no-op, which was the live risk in gating on fields the client may not hold:Approvalcarries both requester columns asstring | null(packages/shared/src/types/approval.ts:8-9),svc.getByIdis a baredb.select()over the row (server/src/services/approvals.ts:282-287), andredactApprovalPayloadrebuilds via{ ...approval }touching onlypayload(server/src/routes/approvals.ts:44-58), so both columns reach the page populated. And the half that would have regressed today's population holds: the page sends no payload, sonormalizedPayload === undefinedand theserverFiledcarve-out atserver/src/routes/approvals.ts:772-774still skips the guard — watcher cards, the entire currentrevision_requestedbudget population, keep a working button, whichcanResubmitFromBoardmirrors by returning true for both-null.
Critical Issues (0)
Important Issues (1)
- [pr-review-toolkit/native-codex]
ui/src/pages/ApprovalDetail.tsx:338— The suppression message states the one thing that is false in the case it was widened to cover.canResubmitFromBoardhides the button when either requester column is populated (:62), and the added test pins the user-filed half deliberately ("suppresses it on a user-filed budget card too"), so abudget_override_requiredcard carryingrequestedByUserIdrenders "Only the agent that filed this card can resubmit it." No agent can: the server's identity guard isreq.actor.type === "agent" && req.actor.agentId !== existing.requestedByAgentId(server/src/routes/approvals.ts:721), and withrequestedByAgentIdnull that comparison is true for every agent, so all of them get 403. The only identity that passes that guard is a board user — the person reading the sentence telling them they cannot. Worse in the sub-case where the stored payload does carry assertions:serverFiledis false so the guard runs,budgetAssertionRefusalpasses, and the resubmit would have succeeded — the page hides a button that works and explains a refusal that would not have happened. Where the payload carries no assertion the gate itself is still right (the page sends none, so it would 422); only the attribution is wrong. Latent rather than live — the prior head established today'srevision_requestedbudget population is watcher cards only, and I did not find a UI surface that files a budget card as a user, thoughserver/src/routes/approvals.ts:526-527derivesrequestedByUserIdfrom any user actor on the create route, so the state is reachable by API. Cheapest fix is a reword that names the mechanism rather than the requester — "this card must be resubmitted with correctedenforcement_assertionsin the resubmit body, which this page cannot send" — which is true for both halves of the gate. Flagged at this weight because accurate guidance in the money path is this PR's own thesis, and the same reasoning upheld prior:897d67d important 2 one head ago on a misattribution that also had no current victim.
Suggestions (1)
- [pr-review-toolkit]
ui/src/pages/ApprovalDetail.remediation.test.ts:57-85— The predicate is tested four ways; the wiring is not tested at all. Deleting&& canDriveResubmitfromui/src/pages/ApprovalDetail.tsx:354— the entire fix — leaves all four new tests green, because they callcanResubmitFromBoarddirectly and never render the page. That is the actual regression risk for a change whose whole content is one JSX condition: the helper is pure and unlikely to rot, the call site is what a later refactor drops. One render assertion (revision_requested budget card with an agent requester → no "Mark resubmitted" button; watcher card → button present) would pin it. Noting the cost honestly: this file is deliberately a.test.tsunit test with no render harness, so this is a real addition rather than a free one.
Strengths
- The gate is argued from the constraint that actually binds rather than from the symptom. The doc comment at
:35-57rejects re-deriving the assertion check client-side becauseextractEnforcementAssertionsis server-side and "a second copy of a money-path parser in the UI is how the two silently diverge" — correct, and it is the same reasoning that made the server guard reuse the extractor instead of re-implementing it. - The over-inclusion is stated as a known ceiling rather than discovered later: "Wider than the refusal by exactly one case: a caller-filed budget card that does carry assertions would resubmit successfully. Deliberate." A reviewer disagreeing now argues with a recorded decision instead of guessing at an oversight.
- The test that matters most is the one asserting the button is kept on a watcher card, with the comment recording why ("these are the entire
revision_requestedbudget population today, so gating them would remove the only budget resubmit that currently works"). Gating too widely was the live way to fix the finding and break the working path; that test is what stops it. canResubmitFromBoardtakesPick<Approval, ...>rather than the whole record, so the test can build a three-field fixture and the signature states exactly which columns the decision depends on.- The replacement paragraph points at
/coststhe way:331-335already does for pending budget cards, so the suppressed state matches the established convention on the page rather than inventing a new one.
Recommended Action
- Reword
:338so the sentence is true for both halves of the gate it explains — a few characters, and it is the same defect class this PR just fixed atpackages/shared/src/validators/approval.ts:82. - Consider the render assertion opportunistically; the predicate tests are good, they just do not cover the line that constitutes the fix.
Static review only; the test suite was not executed in this run. The 403-for-every-agent path and the serverFiled carve-out were traced by reading server/src/routes/approvals.ts at this head, not by running them. The "only UI resubmit surface" claim is from fetching each approval-related file under ui/src/ and grepping it directly — GitHub code search returns zero results for this repo even on strings that demonstrably exist, so it was not relied on.
… (BLO-34008)
`canResubmitFromBoard` suppresses the button when *either* requester column is
populated, but the suppression text named only one of them: "Only the agent that
filed this card can resubmit it." On the user-filed half that sentence is false
in both directions. The server's identity guard is
req.actor.type === "agent" && req.actor.agentId !== existing.requestedByAgentId
so with `requestedByAgentId` null that comparison is true for *every* agent and
all of them get 403. The only identity that passes is a board user — the person
reading the sentence telling them they cannot. It also claimed a refusal that
would not always happen: where the stored payload does carry assertions,
`serverFiled` is false, `budgetAssertionRefusal` passes, and the resubmit would
have succeeded. That case is the gate's recorded over-inclusion, so the text
must not assert a 422 it cannot promise.
Reworded to the one thing true across every case behind the gate: this page
sends no payload, so it can only resubmit unchanged, and a caller-filed budget
card goes back through the API it was filed from with its
`enforcement_assertions` reviewed. No identity is attributed.
Also pins the fix itself. `ApprovalDetail.remediation.test.ts` covers
`canResubmitFromBoard` as a predicate four ways, and deleting `&& canDriveResubmit`
from the call site left all four green — the helper is pure and unlikely to rot,
the call site is what a later refactor drops. `ApprovalDetail.render.test.tsx`
renders the page: caller-filed card -> no "Mark resubmitted" button and no
"Only the agent" text; watcher card -> button kept, which is the half that must
not regress since watcher cards are the entire `revision_requested` budget
population today. Verified by mutation: removing the condition fails the new
test while all 9 predicate tests stay green.
|
Both findings taken, in important 1 — Took the reword, and widened it past the suggested phrasing on one point. "…must be resubmitted with corrected suggestion — the wiring is untested — taken, and it was worse than "untested". Reproduced your claim as a mutation before writing anything: deleting Added Verified it pins the line rather than restating the predicate: with the condition removed the new test fails ( |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 8ac2486
Prior Findings Dispositioned (1)
- prior:b464428 important 1 — fixed —
ui/src/pages/ApprovalDetail.tsx:338— The false sentence is gone.:338-343now reads "This page can only resubmit a card unchanged, and a budget card a caller filed has to go back with itsenforcement_assertionsreviewed — so resubmit it through the API it was filed from", replacing "Only the agent that filed this card can resubmit it." That is the reword the finding recommended — name the mechanism, not the requester — and it is true on both halves of the gatecanResubmitFromBoardapplies. For an agent-filed card the filing agent is the API caller that passesserver/src/routes/approvals.ts:721. For a user-filed card — the half where the old text was false, because that guard comparesreq.actor.agentId !== existing.requestedByAgentIdand so 403s every agent whenrequestedByAgentIdis null, leaving a board user as the only identity that passes — the sentence no longer attributes the capability to an agent, and "resubmit it through the API it was filed from" is what that board user does. Re-verified the guard is unchanged at this head (:721), so the claim the finding rested on still holds and the text now matches it. It also stays true in the sub-case the finding called worse (stored payload does carry assertions, sobudgetAssertionRefusalpasses and an unchanged resubmit would have succeeded): the new wording is a statement about what this page can send and what the card needs, not a prediction that the server would refuse, so it no longer over-claims where the gate is deliberately wider than the refusal (:55-57). Pinned by a negative assertion on the exact removed phrase atui/src/pages/ApprovalDetail.render.test.tsx:137.
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [pr-review-toolkit]
ui/src/pages/ApprovalDetail.render.test.tsx:55— The file setsIS_REACT_ACT_ENVIRONMENT = truebut never imports or callsact, driving updates withflushSyncplus raw promise flushing instead. That flag's only effect is to make React warn when an update lands outsideact(), and the four react-query resolutions this page issues (ApprovalDetail.tsx:76-97) all land outside one — so the file opts into the warnings without the mechanism that silences them. The sibling component test in the same directory sets the same flag and does useact(CompanyAccess.test.tsx:73-76). Nothing breaks — these areconsole.errorwarnings, andwaitForAssertionre-checks until the tree settles — so this is log noise and convention drift rather than a correctness issue. Either wrap the render and flush inactthe way the sibling does, or drop the flag.
Strengths
- The render test pins the line that constitutes the fix, which is exactly what the previous Suggestion asked for, and it is not vacuous: delete
&& canDriveResubmitfromApprovalDetail.tsx:355and the button's condition reduces toapproval.status === "revision_requested", which the agent-filed fixture satisfies, soexpect(resubmitButton(container)).toBeNull()fails. The four predicate tests stay green under that deletion — the gap the file was added to close. - The reword is checked by asserting the removed phrase is absent rather than asserting the new copy verbatim, so it survives future wording edits while still failing if the misattribution returns. Testing the defect rather than the fix is the right way round for a copy change.
- The second test ("keeps the button on a watcher-filed budget card, which the server exempts") now guards the currently-working path at render level rather than only through the predicate, so a later gate that over-includes watcher cards — the live way to fix this finding and break today's entire
revision_requestedbudget population — fails here too. canResubmitFromBoard's doc comment (:35-57) still records both the deliberate over-inclusion and the reason for not re-deriving the assertion check client-side, and the new wording is now consistent with that recorded scope instead of describing a server refusal that would not fire.- The mocks are scoped to the four APIs the page actually queries (
approvalsApi.get/listComments/listIssues,agentsApi.list), so the fixture states the page's real data dependencies rather than stubbing the module surface wholesale.
Recommended Action
- No blockers — the last Important is closed and I found no new one at this head. Consider the
actSuggestion opportunistically. - Confirm CI before merge: at this head
Build, all eightGeneral testsshards,Typecheck + Release Registryande2earequeuedorin_progress(run35092390643), so the new render test has not been executed anywhere I can observe. It is the first jsdom render test on this page, andui/vitest.config.tssetsenvironment: "node"globally with the per-file@vitest-environment jsdomdocblock as the override — the same mechanismCompanyAccess.test.tsxalready relies on, so this is a "watch it go green once", not a suspected misconfiguration. - The PR is
BEHINDmaster with no conflict reported; it will need an update before merge.
Static review only; the test suite was not executed in this run. The 403-for-every-agent path and the serverFiled carve-out were traced by reading server/src/routes/approvals.ts at this head, not by running them. The "delete the gate and the test fails" claim was derived by reading the JSX condition at :355 against the fixture, not by running the mutation.
…Sync (BLO-34008) The file set IS_REACT_ACT_ENVIRONMENT = true but never imported act, driving updates with flushSync plus raw promise flushing. That flag's only effect is to make React warn when an update lands outside act(), and the page's four react-query resolutions all land outside one — so the file opted into the warnings without the mechanism that silences them. Log noise, not a correctness bug. Adopt the convention the other ~180 ui test files use, and that the sibling CompanyAccess.test.tsx in this directory already follows: import act from react, wrap render/flush/unmount in it, drop flushSync. The mutation sensitivity the file exists for is unchanged — deleting `&& canDriveResubmit` from ApprovalDetail.tsx:355 still fails "suppresses the button on a caller-filed budget card".
…rget on budget cards)
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 9bed650
Prior Findings Dispositioned (2)
- prior:897d67d important 1 — fixed —
ui/src/pages/ApprovalDetail.tsx:355— The dead-end affordance is gone. The button's condition is nowapproval.status === "revision_requested" && canDriveResubmit, andcanResubmitFromBoard(:58-63) returns false for a caller-filedbudget_override_requiredcard, so the page no longer offers an action whose own 422 names something no surface here can do.:336-343explains the state instead. Verified at this head rather than assumed:canDriveResubmitis bound at:197, the gate reads the requester columns which both arrive populated (svc.getByIdis a bare row select,server/src/routes/approvals.ts:44-58rebuilds onlypayload), and the watcher carve-out still holds — the page sends no payload, sonormalizedPayload === undefinedandserverFiledskips the guard atserver/src/routes/approvals.ts:773-775, keeping the button for today's entirerevision_requestedbudget population. - prior:93ec6a7 important 1 — fixed —
ui/src/pages/ApprovalDetail.tsx:336— Same defect, and the sharpening this finding added is answered directly. It objected that the surfaced remediation ends by naming the resubmit body — an affordance the page lacks — so the operator was invited into a dead end and then told the fix was elsewhere. At this head that invitation is withdrawn for exactly the cards it was wrong for: the suppression paragraph renders in the button's place and states what the page can do ("This page can only resubmit a card unchanged … so resubmit it through the API it was filed from"), pointing at/coststhe way:331-335already does for pending budget cards.errorWithRemediation(:27-33) still foldsdetails.remediationinto the error for the cards that do keep the button.
Both IDs restate prior:ee43166 important 1, which was retired as fixed at b464428. The ledger is keyed per attesting head, so retiring the original left these two live under their own identities — which is why gate/ally-comment-findings is red at this head naming 93ec6a7. The code fix landed three heads ago; only the bookkeeping was outstanding.
Critical Issues (0)
Important Issues (0)
Suggestions (1)
- [pr-review-toolkit]
ui/src/pages/ApprovalDetail.render.test.tsx:119-122—afterEachclears mocks and callscontainer.remove(), but unmounting is done per-test at:142-144and:156-158.container.remove()detaches the DOM node; it does not unmount the React root. So on a failing test —waitForAssertionthrowing after 50 attempts — the per-test unmount never runs, and a mounted root plus itsQueryClientsurvive into the next test attached to a detached container, where subsequent updates tend to surface as unrelatedconsole.errornoise on top of the real failure. Moving the unmount intoafterEach(track the root in the outerlet, the waycontaineralready is) makes teardown unconditional and removes the duplicated block. Minor and failure-path-only — the passing path is already correct.
Strengths
- The
actmigration is complete rather than partial, which is what makes it worth doing:flush(:56-61), the render (:95-101) and both unmounts are wrapped, so the file no longer opts intoIS_REACT_ACT_ENVIRONMENTwarnings without the mechanism that silences them. That was the previous head's Suggestion and this is the version of the fix that actually discharges it — wrapping only the render would have left the four react-query resolutions landing outsideact. import { act } from "react"is the correct source for React 19 (ui/package.jsonpins^19.2.7), not thereact-dom/test-utilspath that still appears in older guidance.renderDetailbecameasyncand every call site was updated toawaitit. An un-awaitedactreturns a floating thenable and silently reintroduces the exact interleaving the change exists to remove, so this is the detail that decides whether the migration is real.- The wiring assertion still bites after the change: deleting
&& canDriveResubmitfrom:355reduces the button's condition toapproval.status === "revision_requested", which the agent-filed fixture satisfies, soexpect(resubmitButton(container)).toBeNull()fails. The four predicate tests inApprovalDetail.remediation.test.tsstay green under that deletion — the gap this file was added to close. - The master merge (118 commits) touches none of this PR's six files, so the guard, the validator description and the page are byte-identical to the tree reviewed at
8ac2486. Re-verified rather than assumed, since a merge is the usual way a reviewed surface changes underneath an attestation. - On the server guard,
normalizedPayloadis derived by a truthiness ternary (server/src/routes/approvals.ts:726-734), so it is object-or-undefinedand nevernull. That is what makesnormalizedPayload === undefinedandnormalizedPayload ?? existing.payload(:774-777) agree on which payload is being checked — an explicitpayload: nullcannot split them.
Recommended Action
- No blockers. Zero Critical, zero Important, and both carried findings are retired above against evidence at this head.
- Consider the
afterEachunmount opportunistically; it only changes behaviour when a test is already failing. - CI is mid-flight at this head, not failing: the
PRworkflow (35094221355) isqueuedwithBuild, all eightGeneral testsshards andCanary Dry Runqueued,Typecheck + Release Registryande2ein_progress;policy,Helm chart,reviewand the vendored-adapter job are green. Nothing is red except the two Ally gates this review resolves. Confirm the shards go green — the render test'sactrewrite has not executed anywhere I can observe.
Static review only; no test or build was executed in this run. The act/React-19 claim is from the pinned dependency range, not from running the suite. The "deleting the gate fails the test" claim was derived by reading the JSX condition at :355 against the fixture, not by running the mutation. The merge-overlap claim is from the compare file list between the previously reviewed head and this one.
Suggestion at
|
Thinking Path
Linked Issues or Issue Description
Related PRs searched (
enforcement_assertions,budget_override_required,BLO-34008,BLO-24631,BLO-32796): no duplicate. #1846 is the only adjacent open PR — it editsapproval-enforcement-reconciler.ts; this PR only imports two exports from it, so they do not overlap.What Changed
server/src/routes/approvals.ts— refuse abudget_override_requiredcreate whose payload yields zero assertions fromextractEnforcementAssertions, with HTTP 422, codebudget_approval_missing_enforcement_assertion, and adetails.examplepayload that can be copied straight back. Placed on the single caller-supplied boundary: HTTP and MCPpaperclipCreateApprovalboth POST here.packages/shared/src/validators/approval.ts— the payload description told callers assertions were advisory. It now states they are REQUIRED for this type and names the error code, so an agent learns the rule before hitting the refusal.server/src/__tests__/approval-budget-assertion-required.test.ts— 7 tests (new file).Two scope decisions argued rather than implemented
insertApproval()(services/budgets.ts) and never reach the guarded route.buildApprovalPayloadknowspolicyIdand the cap that was crossed, never the figure to raise it to — that only exists once the board writes one at/costs. The tempting fix,expected = policy.amount, is worse than silence: a card approved with nothing applied would then classify asappliedand report nothing, i.e. the 0-of-8 class laundered into a pass. Pinned by a test.resubmitrequires statusrevision_requested(approvals.ts:579) and nullsdecidedAt, while the reconciler population isstatus = approved AND decidedAt <= cutoff(approval-enforcement-reconciler.ts:680). A card cannot reach a mutated payload without leaving that population.from_usdis guided, not enforced — enforcing it needs #1846'sreadPriorAmountCentsto land first, otherwise this route duplicates money-parsing that would immediately drift from the extractor.Verification
Negative control. Guard reverted, tests unchanged: 4 of 7 fail. The 3 that stay green in both states are the over-reach guards — legacy
exact_changesstill accepted, other approval types untouched, watcher payload declares nothing — so they are correct to be insensitive.Tests cover: prose-only card refused and never filed; refusal → corrected payload → accepted in one retry; legacy
exact_changes(card6f45844e) still accepted; declared-but-unparseable assertion refused (the dangerous near-miss — reads as compliance, skipped by the extractor exactly like prose); no figure accepted fromdecisionNote/summary/recommendedAction; other types unaffected.No UI change, so no screenshots.
Risks
budget_override_requiredcard the old prose-only way now gets a 422 instead of a card. That is the point, but it lands on a path used when a cap is about to stop an agent, so a refusal that costs a round of guesswork would be its own outage — hence the copyabledetails.exampleand the schema-description update, and a test asserting the refusal→retry→accept sequence.approval-payload-title-guard.test.tsstructurally pins that everydb.insert(approvals)call site goes through one of the two entry points, which is what makes that exemption reliable rather than incidental.304ea443stays a historical instance; it was applied by hand on 2026-08-10 and its drift rows are all closed.label: "CTO"/expected_usdexample is deliberately paired withpolicyId: "<uuid>", which failsUUID_PATTERN. So an agent that copies the fragment wholesale yields zero parseable assertions and is refused by this same guard, with the corrected shape handed back indetails.example— it cannot mint an assertion against someone else's policy. That is what makes the misattribution hazard narrow rather than urgent (BLO-34254, cancelled 2026-09-17: the fragment lives only in the generated tool schema, not in any checked-inAGENTS.md, so merging this PR retires it fleet-wide with no bundle edit).Model Used
claude-opus-5), 1M context, extended thinking, with tool use and code execution — running as Claude Code in the Paperclip agent harness.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template🤖 Generated with Claude Code