Repository navigation
fix(quotes,hooks): refuse an acceptance an approval holds; draft a rep's contract; the capability gate and activity bubbles cross the caller's grants (runAs: 'system') - #2035
Merged
Conversation
…e contract draft, the capability gate and the activity bubbles Accepting a quote closes its deal as won, and a rep's acceptance on a deal held by an approval was admitted while its close-won met the lock inside an async hook that swallowed it. quote_workflow now refuses that acceptance at the moment it is made (RECORD_LOCKED / 409), naming the approval that holds the deal, and exactly when the rep's own close-won would be refused. The contract draft moves out of quote_on_accepted into its own elevated hook, quote_accepted_contract_draft (runAs: 'system'), so a sales rep's acceptance gets its contract; the close-won stays the caller's write. opportunity_account_capability reads the account's verdict elevated and refuses with "this account", naming none; task_activity_bubble and event_activity_bubble write the recency clocks elevated. Every elevated read is pinned to the triggering record's organization. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
…nostics The split moved the two bare cascade throws out of the `failures.push` sink the source hygiene gate recognises, and both interpolated a record id. They now name the quote the way every screen does: the contract draft by its `quote_number - name` label, the close-won by its quote number. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #2014
Clause-②: no
Second round of #2014, on the maintainer's ruling recorded in comment 6076828219 (batch hotcrm-R74b): F1 → C, F11 → B, plus the
task_activity_bubble/event_activity_bubblepair. F9 is not in this PR: it waits on objectstack-ai/objectstack#22437 (AGENTS.md §2), andlead_auto_assign,case_auto_assignandlead_duplicate_checkare untouched. This card stays open for F9.What was measured before the fix (17.7.0, through the engine's write door)
Every reading below is a person's write through
@objectstack/verify'shooks.run(the REST data ingress's path) on branch base08cfa209.F1, a sales rep accepts a quote on a deal under approval. The rep opens a 150,000 deal, so
opportunity_approval_on_createopens a request whose approval node locks the deal (lockRecord: true). Presented → accepted is admitted. The close-won write is then refusedRECORD_LOCKED("Opportunity 'Big 2' is locked while an approval is in progress …"), andonError: 'log'swallows it. The contract insert is refusedPERMISSION_DENIED(a rep holdscrm_contract.allowCreate: false). Result: 0 contracts, the deal staysproposal/pending, and the quote reads Accepted, which freezes it.On an unlocked deal, the rep's acceptance closes the deal
closed_wonand still drafts 0 contracts (the samePERMISSION_DENIED).What holds the deal. A rep's own direct close-won, measured per state:
approval_statuspending)RECORD_LOCKED(platform lock)approval_statusapproved/rejected(a recall also leavesrejected)status_change_approval_statuspending/rejected(gate armed)RECORD_LOCKED(opportunity_lifecycle); with a request open the platform lock holds toostatus_change_approval_statusapprovedqualification_approval_statuspending/rejected(立项 armed)RECORD_LOCKED(opportunity_lifecycle). Its request does not lock the record (lockRecord: false); rep edits other than the stage land.qualification_approval_statusapprovedisSystem)A rep cannot read
sys_approval_request(PERMISSION_DENIED). So the gate reads the deal's own mirror columns as the caller, not the request table.F11. An outsider rep cannot read a Settlement Only account (0 rows). Their new deal on it is STORED: under the caller's record access
findOneanswersnull, and the gate stands down. The owner's identical deal is refused, and the refusal names the account (ACC-9001 - Tender Agency).Activity bubble. A service agent completes a task on their own case. The account write is
PERMISSION_DENIED, swallowed, andlast_activity_datestays empty; the admin's identical completion stamps it.event_activity_bubblewas measured the same way (round 1 did not): an agent's held call on their own case leaves the account empty while the admin's stamps it. A rep's completed task on their own deal, whose account and contact a manager owns, stamps neither.The fix, and why each part is the minimal one
quote_workflow,beforeUpdate, user writes only). It throwsRECORD_LOCKED/ 409, an existing code; no code is minted. Example message: "Quote QTE-0007 - Q-3 cannot be accepted while its opportunity is held by an approval (Approval Status is Pending): accepting it would close that deal as won. Accept it once the approval is granted."approval_statuspending, or either REQ-0006 verdictpending/rejected, on a deal that is not already closed.quote_on_acceptedinto its own hook,quote_accepted_contract_draft(runAs: 'system'), with its rule-9 reason beside it. The rep's acceptance gets its draft, and the rep still cannot create a contract by hand.quote_on_acceptedand is not elevated. Elevating it would let a system write close a locked deal.withRunAs('system')carries through, andtest/hook-org-inheritance.test.tspins that on a walled deployment.opportunity_account_capabilitydeclaresrunAs: 'system'and reads onlycommercial_capability. Its refusal says "this account" and names no number, name or id. The read is pinned to the new deal'sorganization_idwhen it carries one.task_activity_bubbleandevent_activity_bubbledeclarerunAs: 'system'. Every walk-up read is pinned to the triggering record'sorganization_id.wherekey (the engine refuses it), so each write target is first confirmed inside the organization by one pinned read per object.src/revenueauthored total is 18,990 against a ceiling of 19,000, paid by compactingquote.hook.tscode (comments are not counted). No ceiling moved. Thesrc/salesheadline (~57k / ~28k / ~101k) is unchanged.The tests that pin each business fact
test/hooks-runtime-sales.test.tsRECORD_LOCKED/409, the message names the hold and the quote's title and never an id. The quote stays presented, the deal staysproposal/pending, 0 contracts, and no contract insert or opportunity update reaches the engine.created_by= the rep). The rep's own contract insert is stillPERMISSION_DENIED.test/opportunity-account-capability-gate.test.ts: the outsider is refused, and nothing it read under elevation (number, name, title, id) is echoed. The raw-id-in-prose class survives in five more places after #1208 — 15 of 31 tasks in a demo org still name a record by its primary key #1243's naming case is re-pinned to "this account". The message has one locale; the three docs locales are updated.test/hooks-runtime-service.test.ts,test/activity-recency.test.ts: an agent's completed task and held call on their own case stamp the case account (updated_by= the agent).test/case-first-response.test.ts: a writer who cannot read the case still stamps its first response. The two best-effort cases are now staged faults on the real engine, since the caller's grants no longer refuse anything.Ablation. One-off, through
scripts/ablation-replace.mjs: the anchor hit x1 → x0, every restore was proven by blob == HEAD with an emptygit diff HEAD, and no permanent file was kept.runAs→inheriton the draftrunAs→inheriton the gaterunAs→inheriton the task bubblerunAs→inheriton the event bubbleThe organization pins were proven on a two-organization boot with a tenant-less (system) trigger. With the pins, a task in org A naming B's account stamps only A, and a held call in A naming B's case stamps neither B's account nor B's case. Removing each pin turns its case red (B stamped).
File surface beyond the claim, and why
src/revenue/objects/contract.object.tsandsrc/sales/objects/_picklists.ts(comments only): they name the hook that performs the payment-terms carry-over, which is nowquote_accepted_contract_draft.test/quote-accepted-payment-terms.test.tspins that name.content/docs/sales/accounts*.mdx: the dispatch names the Settlement Only refusal docs, and those pages said the refusal names the account.test/contract-write-depth.test.ts,test/record-id-not-in-prose.test.ts,test/quote-contact-required-when.test.ts.test/case-first-response.test.ts: it pinned the caller-context behaviour this fix removes.docs/feature-inventory.md: QUO-005 and QUO-006 name the split hooks.Acceptance notes
crm_opportunity. An install adding another locking approval there is not mirrored (narrower, not wider).scripts/check-source-hygiene.mjs's comment saysquote.hook.tskeeps twofailures.pushdiagnostics. It keeps none now: both cascade throws name the quote by its label or number. Not edited here (a gate script).test/hooks-runtime-service.test.tsis at 82% of the 100KB file cap (hygiene advisory).Gates
pnpm verifyona83103b5:OS_VERIFY_LOCK_SLOT=hotcrm-2014 bash os-verify-lock.sh -c 'pnpm verify'→VERDICT command-exit 0(held 477s).✓ Validation passed;tsc --noEmitclean;lint --strict18 suggestions, no error; i18n gate 0 issues;✓ source hygiene clean;✓ source token ratchet clean;✓ Build complete.Test Files 172 passed (172),Tests 3589 passed | 1 skipped (3590).73c712a3was red on source hygiene: two record ids interpolated into the cascade diagnostics. Fixed ina83103b5.test:coveragethresholds, docs-apptypes:check, e2e, link-check.Generated by Claude Code