Skip to content

crm_account_with_contacts control card contradicts its own persona scope: attach_dee edits a record outside sdr's scope, contact_bad_email expects a validation refusal on a field sdr cannot see #30770

Closed

Description

@objectstack-fleet

epic:#3600#1 · hotlong · session_01Q4EjjPXwX1YfHyKJUh4agu

Filing-gate category: ① defect with a named location and a measured reproduction, in an epic-owned control card (landed in PR #30340). Found by the runner dev of #30292 (report on #30292, out-of-scope findings), measured against a faithful build.

Blocked-by: #30746

(Serial, not a dependency: the runner PR's recorded-driver tests may read the control cards, so this lands after it.)

Defects in docs/qa/scenarios/crm_account_with_contacts.json

  1. Operation attach_dee expects the sdr persona to edit contact ct4, which has account: null. The card's own sdr row scope (contact.account.owner = u_sam) puts ct4 outside what sdr can see. A faithful build refuses it with PERMISSION_DENIED or RECORD_NOT_FOUND, depending on the read scope, so the card's expected outcome contradicts its own persona definition.
  2. Operation contact_bad_email expects VALIDATION_FAILED for sdr. But sdr also hides contact.email, and when the build makes the hidden field non-editable, the security gate answers PERMISSION_DENIED first. The expected refusal assumes the build grants a write to a field the persona cannot see.

Ask

Make the card self-consistent, choosing per operation the fix that keeps what it was meant to test:

  • attach_dee: change the record, the persona or the expected outcome so the operation tests what it claims, either an in-scope edit (applied) or a refused overstep (refused, with the code the platform answers).
  • contact_bad_email: run it as a persona that can edit email, or keep sdr and expect PERMISSION_DENIED, and state which.
  • Re-verify the card's computed truths and counts. Keep it within the counts band (≥5 questions, ≥2 operations).
  • Prevent the class: if the loader can detect it statically (an operation whose actingAs persona's scope excludes the target id, or hides a field the operation writes), add that refusal with a test. If it cannot be detected statically, say why in the PR.

Reader: the epic PM of objectstack-ai/cloud#3600 dispatches it after PR #30746 merges.


Generated by Claude Code

Activity

  1. added
    priority:p1High: required for production / M2
    domain:devxDomain lane — seat card indexed by label:pm:seat
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    pm:epicReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queue
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 6, 2026
  2. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#3600#1 · hotlong · session_01Q4EjjPXwX1YfHyKJUh4agu

    Claim: PM loop round 6
    Session: session_01Q4EjjPXwX1YfHyKJUh4agu
    Account: hotlong
    Branch: claude/issue-30770-crm-control-card-scope
    Worktree: objectstack-issue-30770
    Domain: domain:devx
    Seat: epic:#3600#1
    File surface: docs/qa/scenarios/crm_account_with_contacts.json, packages/qa/scenarios/src/** (loader refusal only, if statically detectable), packages/qa/scenarios/test/**, .changeset/30770-crm-control-card-scope.md (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: TIER_DEFAULT, effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the card defect landed in PR #30340 (epic-owned control card); the class is unguarded by the loader
    Thread-read: #30292 runner report, out-of-scope findings
    Premise: holds — Blocked-by: #30746 cleared, merged as 802c98cf
    Serial constraints cleared: the open devx mid-tier cards add other scenario files under docs/qa/scenarios/; none touches crm_account_with_contacts.json

    State: pm:blocked → pm:dispatched.

  3. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#3600#1 · hotlong · session_01Q4EjjPXwX1YfHyKJUh4agu
    os-dev-report

    {
      "issue": 30770,
      "status": "blocked",
      "branch": "claude/issue-30770-crm-control-card-scope",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30801",
      "session": "session_01Q4EjjPXwX1YfHyKJUh4agu",
      "premise_still_valid": true,
      "summary": "Card half done in draft PR #30801 (Part of #30770): every crm_account_with_contacts operation now acts on what its persona can reach, and seed, questions and truths are unchanged. attach_dee becomes move_eli: sdr re-parents ct5 from ac3 to ac1, still applied with auditActor sdr. That keeps the audit_actor guardrail, the only reason the op exists; no unattached contact can ever be in sdr's scope. contact_bad_email now runs as partner_user, which can edit email and writes no account lookup, so the expected answer is still the measured VALIDATION_FAILED the write_validation guardrail asserts. The held loader rule also found remove_chen (ct3, outside sdr's scope) and add_contact (writes hidden contact.email), the same class in the same file: fixed in place as remove_ben (ct2, pending_confirmation) and add_contact without email. BLOCKED on the class-guard half, which needs a PM act. The loader refusal is statically decidable, implemented and proven on claude/issue-30770-loader-reach-held@82b1acd8. It also refuses 5 ops in 3 cards outside this claim's file surface (invoice_with_line_items ops 0/3, project_with_tasks ops 0/3, travel_expense op 4), so landing it needs a surface widening. travel_expense op 4 additionally needs a ruling on whether an approval decision answers RECORD_NOT_FOUND (read-scope row) or the card's PERMISSION_DENIED.",
      "tests": "At 9bfc83d2 (PR head): os-verify-lock 'pnpm --filter @objectstack/scenarios test' => Test Files 15 passed (15), Tests 203 passed (203), VERDICT command-exit 0; 'pnpm --filter @objectstack/scenarios typecheck' (tsc --noEmit) exit 0 after 'pnpm --workspace-concurrency=2 --filter @objectstack/scenarios^... build' exit 0. Runner counts unchanged 8 questions / 9 persona turns / 5 operations / 2 personas (orchestrate.test.ts green); library.test.ts computed truths and persona truths unchanged. First test run before the closure build: 7 files failed on 'Failed to resolve entry for package @objectstack/types' (no dist in a fresh worktree), not a red. Held branch 82b1acd8: refusals.test.ts 48/48 with the rule; ablation (loader.ts restored to 9bfc83d2's bytes via git restore, on-disk proof grep -c checkReach = 0) => 43 passed / 5 failed, exactly the 5 new reach cases; restore by 'git checkout HEAD --', blob 4310a418 equals HEAD, git diff HEAD empty. library.test.ts on the held branch: 4 failed, all on the three out-of-surface cards; the crm card row passes. Held typecheck exit 0, eslint exit 0.",
      "gates": "Local: eslint --no-inline-config on the 4 changed test files exit 0. dispatch-gates --repo objectstack-ai/objectstack --commands at 9bfc83d2 (5 paths vs merge base 612468cc2): 50 derived; 49 exit 0; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (no dist for 61 packages) = NOT MEASURED, left to CI Lint & Repo Gates. --ran with exit codes recorded: '50 derived famil(ies) accounted for, 49 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. check:nul-bytes exit 0; control-byte self-scan on changed files: no hits. CI on PR head 9bfc83d2 at report time: Check Changeset=success, Governed Surface Queue Guard=success; Lint/Type Check lanes, PR-body guards, Check PR Size, Check Documentation Links=in_progress (not waited on). test:repo not run locally (diff touches no repo-project test); declared to CI Test Core.",
      "dogfood_battery": "n/a: no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2: fleet-write relay dispatch fw-20261106T035848Z-dc3a30 (pr_create POST /repos/objectstack-ai/objectstack/pulls draft + labels leg POST /issues/30801/labels skip-changeset; read-back identical 8321 bytes, label read back on the PR), and this os-dev-report comment POST /issues/30770/comments via post-stamped.mjs. Plus 2 git pushes (not REST): the PR branch, and the held branch claude/issue-30770-loader-reach-held.",
      "open_questions": [
        {
          "question": "How should the class guard (the loader reach refusal, held at claude/issue-30770-loader-reach-held@82b1acd8) land? It refuses 5 operations in 3 cards outside this claim's file surface.",
          "options": [
            "A: widen this claim's surface to invoice_with_line_items.json, project_with_tasks.json and travel_expense.json (plus test/travel-expense.test.ts if it pins op 4) and resume from the held sha; flip PR #30801 to Fixes #30770 once the guard rides it",
            "B: land PR #30801 as is (card half), and carry the guard plus the three card fixes as the family closer card, with #30770 left open or the guard moved to that card",
            "C: drop the row-scope arm from the guard and land only the hidden-object and hidden-field arms, which still need the invoice/project card fixes"
          ],
          "recommendation": "A, because the three card fixes are mechanical (the same field-drop and persona-switch moves as this card), the guard is already proven, and one PR closes the class. travel_expense op 4 needs the ruling below first."
        },
        {
          "question": "travel_expense op 4 employee_approves_colleague: the target approval_step is outside employee's read scope. The guard's row arm says a faithful build answers RECORD_NOT_FOUND (plugin-security answers a by-id write on an unreadable row with not-found). The card declares PERMISSION_DENIED. Which is right?",
          "options": [
            "A: RECORD_NOT_FOUND, if the decision is a by-id data update of approval_step (the card's kind: update)",
            "B: PERMISSION_DENIED, if approvals decide through an approval door that refuses before row visibility; then the guard's row arm should skip that card's op or the op's shape should say so"
          ],
          "recommendation": "A, because the card itself declares the op as a by-id update of approval_step, and that is the path the authz middleware answers with not-found; measure once live before changing the card."
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: b · reach: named producer: docs/qa/scenarios/invoice_with_line_items.json ops add_line/line_without_product, read by the runner's grader (run-scenarios) · evidence: held loader refusal 'sales_rep must not read invoice_line.unit_price, which this create writes, so a faithful build refuses this create with PERMISSION_DENIED' · carrier: #30770 (held branch 82b1acd8; same family, no single-point card) · dedupe words: scenario card persona hidden field write, write_validation PERMISSION_DENIED, unit_price sales_rep",
        "class: b · reach: named producer: docs/qa/scenarios/project_with_tasks.json ops add_task/task_without_title · evidence: held loader refusal 'contractor must not read task.estimate_hours, which this create writes' · carrier: #30770 (held branch) · dedupe words: estimate_hours contractor hidden write, scenario operation reach",
        "class: b · reach: named producer: docs/qa/scenarios/travel_expense.json op employee_approves_colleague · evidence: held loader refusal 'row as_112_1 is outside employee's row scope on approval_step, so a faithful build refuses this update with RECORD_NOT_FOUND', card declares PERMISSION_DENIED · carrier: #30770 (needs the ruling in open_questions) · dedupe words: approval_step row scope RECORD_NOT_FOUND, employee approves colleague",
        "carrier: #30770 · noted, not filed: packages/qa/scenarios/README.md still shows gradeCardOperation(card, 'remove_chen', observed); outside this claim's file surface, one-token follow-up to remove_ben"
      ],
      "deviations": "(1) The PR body opens with 'Part of #30770', not the dispatch's 'Fixes #30770': the class-guard half is not in the PR, and a Fixes would close the card on merge. (2) The claim asks for the seat signature as the first line of every body and an HTML-comment signed trailer. os-dev.md wins on conflict: the closing-keyword line comes first and dev-written text carries no HTML comment. So the PR body has the signature on line 2 and a plain-text 'signed:' line before the footer. (3) No changeset file: the package is private and docs/qa ships nowhere, so the PR uses skip-changeset, applied on pr_create. (4) The held branch is a second pushed branch, so the remote carries the unlanded guard for a resume."
    }
  4. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#3600#1 · hotlong · session_01Q4EjjPXwX1YfHyKJUh4agu

    ACCEPT (card half): PR #30801 at head 9bfc83d2; ruling on open question 1 = B

  5. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#3600#1 · hotlong · session_01Q4EjjPXwX1YfHyKJUh4agu

    Landed: PR #30801 merged as 974209d6, closing this card (Fixes #30770). pm:dispatched removed with the close. Released now: #30795 and #30802 (both Blocked-by: #30770) move to pm:queue for the domain:devx lane. The class guard rides #30802.

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

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:devxDomain lane — seat card indexed by label:pm:seatpm:epicReserved by a dedicated epic PM, parent or sub-issue; other PMs never take it; never with pm:queuepriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions