Skip to content

fix: harden agent report-access program (post-merge review of #258)#262

Merged
important-new merged 2 commits into
InspectorHub:mainfrom
important-new:fix/agent-report-followups
Jul 22, 2026
Merged

fix: harden agent report-access program (post-merge review of #258)#262
important-new merged 2 commits into
InspectorHub:mainfrom
important-new:fix/agent-report-followups

Conversation

@important-new

Copy link
Copy Markdown
Contributor

Follow-up hardening from a systematic post-merge review of the agent report-access program (#258 — people/role profiles + role-aware sending + agent unified link). Four independent reviewers swept the merged diff (data foundation, role-aware sending, agent auth, downstream refactor); this PR fixes the verified findings.

Security / correctness

  • SMS consent (TCPA): the consent gate keyed on the rule's recipientKind, so a recipientKind='all' rule texted the client with no recorded consent. Now keyed on the per-recipient log.recipientRoleKey, covering both role and all.
  • Primary-client TOCTOU: addPerson did SELECT-then-INSERT with no constraint, so two concurrent adds of different client contacts both landed. Now an atomic INSERT … WHERE NOT EXISTS (D1 serializes writes); the friendly 409 is preserved.
  • Tenant /login: validateCredentials selected by email only, so a global agent (role='agent', tenant_id IS NULL) could authenticate through the tenant front door and shadow a member sharing the email. Now excludes global-agent rows.
  • Report-link → agent session: POST /api/agent/magic-login/request returned the single-use login URL to the caller, so a forwarded/leaked durable report link could be replayed into a full agent session. The link is now emailed to the agent's own inbox; the endpoint returns { sent: true } (anti-oracle). UI shows a "check your email" confirmation.
  • resolveRecipients: an unguarded listPeople broke the documented never-throws contract — a transient DB error aborted the whole fan-out with no retry. Now guarded.

Robustness / data

  • Analytics D1 bind cap: the dashboard agent-name lookup bound the tenant's entire (unbounded) inspection set, throwing past ~95 inspections. Scoped to the rendered rows and chunked under the 100-bind cap.
  • removePerson is scoped to the URL inspection id (was tenant+id only).
  • Dual-identity portal: a find-my-report magic link for an email that is both a client and a registered agent routed to the agent dashboard, hiding the client report. Now prefers a client session when the email holds a client-kind grant; agents still route to the dashboard.
  • recipientKind='all' shows an editor warning (it includes any listing agent on the inspection).
  • Misc: reactivate→clean 409, cross-tenant template-id rejection, idempotent role-profile seed, fail-closed capabilitiesForKind default.

Tests

Regression tests for the SMS-all bypass, the login exclusion, and the dual-identity redeem (both directions); existing SMS/magic-login/component tests updated to the new contracts. Full gate green: type-check, test:unit, test:web, test:workers, db:check, lint.

🤖 Generated with Claude Code

important-new and others added 2 commits July 22, 2026 09:55
…orHub#258)

Follow-up fixes from a review of the merged people/role-profiles + role-aware
sending + agent unified-link program (InspectorHub#258):

- SMS consent gate now keys on the per-recipient role, so a recipientKind=all
  rule can no longer text the client without recorded consent (TCPA)
- primary-client add is an atomic insert-if-no-client (closes a TOCTOU race)
- tenant /login excludes global-agent rows (no member lockout on a shared email)
- resolveRecipients guards listPeople so a transient DB error can't silently
  drop every report delivery
- report-token sign-in is emailed to the agent's own inbox instead of returned
  to the caller (closes a report-link -> full agent-session takeover)
- automation editor warns when recipients = "everyone on the inspection"
- analytics agent-name lookup is scoped to rendered rows and chunked under the
  D1 bind-parameter cap
- removePerson is scoped to the URL inspection id
- find-my-report redeem prefers a client session when the email holds a
  client-kind grant (agents still route to the dashboard)
- misc: reactivate 409 mapping, cross-tenant template-id rejection, idempotent
  role-profile seed, fail-closed capability default

Adds regression tests for the SMS-all, login-exclusion, and dual-identity paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YiCgWAmqyzpfs3pp1kjg3
…mailed link

The magic-login/request response changed from { loginUrl } to { sent: true }
(the single-use link is emailed to the agent now, not returned), which drifted
the committed OpenAPI snapshot and broke the e2e that asserted the old
return-and-navigate flow.

- regenerate server/lib/mcp/openapi-snapshot.json
- rewrite Scenario 1 + Scenario 4 to the emailed-link flow: request -> { sent }
  -> read the single-use link from the E2E email sink -> redeem -> /agent-dashboard
  (Scenario 4 waits for a code that differs from an earlier scenario's email)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YiCgWAmqyzpfs3pp1kjg3
@important-new
important-new merged commit e1ff2fc into InspectorHub:main Jul 22, 2026
5 checks passed
@important-new
important-new deleted the fix/agent-report-followups branch July 22, 2026 02:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant