Skip to content

fix(ui): keep permission cards pending until the server confirms (lr-581c2f) - #422

Merged
clagentic-merger[bot] merged 2 commits into
mainfrom
fix/lr-581c2f-permission-card-pending
Sep 30, 2026
Merged

clagentic-merger[bot] merged 2 commits into
mainfrom
fix/lr-581c2f-permission-card-pending

Conversation

@clagentic-builder

Copy link
Copy Markdown
Contributor

TASK: lr-581c2f

What changed

  • Client: a permission card click now enters a Sending state (buttons disabled). The final label (Allowed / Allowed for session / Denied) is applied only when the server sends permission_resolved. New lib/public/modules/permission-card.js holds the state machine.
  • If the socket is down at click time, nothing is sent and the card stays clickable with a Not connected note. If the socket drops or no ack arrives within 10s, the buttons are restored with a note.
  • permission_request_pending replay now replaces a card that is unconfirmed or looks resolved with a fresh clickable one (history replay of plain permission_request is unchanged).
  • Server: a permission_response for an unknown/cleared request now gets permission_cancel with reason stale (existing message type, no new s2c type; ws-schema description updated). The client renders it as No longer active.
  • Server: the permission notification is dismissed on resolve, abort, the turn-boundary sweep (sweepClearedPermissionIndex gains an onDropped callback), and the HTTP permission-response path (which also now clears permissionRequestIndex). New notifications.dismissByRequestId.

Why
Operator saw Allowed for session while the request was still unresolved server-side. Diagnosed by MILLER on lr-b75006; this is a distinct defect class.

Tests

  • New: test/permission-card-pending-lr-581c2f.test.js (real tools.js against a hand-built DOM) and test/permission-resolve-server-lr-581c2f.test.js.
  • Demonstrated failure first: with lib changes stashed, 14 of 15 new tests fail on main; the one that passes is the idle-reaper/allow_always guard, which matches the diagnosis that the server path was already sound.
  • Full suite (node --test test/*.test.js) passes. project-connection-hydrate-session-model-lr-041af8 is a pre-existing order-dependent flake (failed once in an earlier run, passed on rerun, unrelated to this diff).

Brand check: new strings do not use bare clagentic.

🤖 Generated with Claude Code

…581c2f)

A click now moves the card to a Sending state and only permission_resolved sets the final label. A dead socket or missing ack restores the buttons. A reconnect replay of a still-pending request replaces an unconfirmed card. A response for an unknown request gets permission_cancel with reason stale. The permission notification is dismissed on resolve, abort, sweep and the HTTP path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@clagentic-security

Copy link
Copy Markdown

BOBBIE — clean

Scope: base..head a101a73 (PR 422), lib/ server and client permission paths. Audit focus items:

  • Replay/double-resolve: server deletes pendingPermissions and permissionRequestIndex entries before resolving; a retry after the 10s client timeout finds no pending entry and receives permission_cancel reason stale, never a second resolve or an allow. Client submit is guarded against resolved/sending state.
  • Stale response: sent via sendTo to the responding socket only, echoes the requestId the caller supplied, not recorded in history; an unknown and an already-resolved id are indistinguishable, so no probing oracle beyond existing behavior.
  • dismissByRequestId: filters the project notification list by type permission_request and exact meta.requestId; no cross-project reach. Called after the pending entry is found, or on the stale path for the caller-supplied id, which only retires a banner for a non-pending request.
  • sweepClearedPermissionIndex onDropped: invoked only for dropped ids; the existing deny settle of the orphaned resolver is unchanged, fail-closed preserved.
  • 10s ack timeout: only restores the card to clickable; it never sends a decision, so no auto-allow.

No findings.

scanners_run: none executed (judgment-only review of the diff; no dependency manifest changes in the 15 files). audit_scope.head_sha a101a73

{"reviewer": "bobbie", "review_status": "clean", "head_sha": "a101a73790161c232a89f7e0d521a41feca0cb99", "pr_number": 422}

@clagentic-reviewer

Copy link
Copy Markdown

PEACHES — blocking

1. test/permission-resolve-server-lr-581c2f.test.js:386 — clagentic-console.demonstrated-test-failure — Test "a long-open permission request survives the idle reaper" passes on main without the fix (amos.code-craft.4)

The test only checks behavior already present: the isProcessing guard in startIdleReaper (untouched by this PR) and the allow_always grant+save in lib/project-sessions.js (also untouched). Nothing in this diff can make this test fail. Either remove it, or relabel it as a characterization test for existing behavior, not a regression test for lr-581c2f.

{"reviewer": "peaches", "review_status": "blocking", "head_sha": "a101a73790161c232a89f7e0d521a41feca0cb99", "pr_number": 422}

… (lr-581c2f)

The long-open permission test passes on main; the server path already held
(MILLER lr-b75006 seq 3). Move it to its own describe block marked as a
characterization test so it is not read as a regression test for this fix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@clagentic-security

Copy link
Copy Markdown

BOBBIE — clean

Audit of #422 at bdce1e2 (range b4811a2..bdce1e2, 15 files, no package/lockfile changes). No findings.

Re-confirmed audit focus:

  • Replay/double-resolve: lib/project-sessions.js:1366-1380 and lib/project-http.js:228-233 delete pendingPermissions and permissionRequestIndex entries before pending.resolve; a second response finds no pending entry and returns only a stale permission_cancel, never an allow.
  • Stale response leaks nothing: the permission_cancel reason stale carries only the requestId the client itself supplied, sent to the responding ws only, not recorded in history.
  • dismissByRequestId (lib/project-notifications.js) filters the per-project notifications list by type and meta.requestId; reached only through each project context own _notifications getter, so it is project-scoped.
  • Sweep stays fail-closed: sweepClearedPermissionIndex still settles dropped resolvers as before; the new onDropped callback only dismisses notifications, and is optional and type-guarded.
  • Client ack timeout (lib/public/modules/permission-card.js) only restores controls and shows a retry note; no path sends or infers an allow. permission_request_pending replaces a stale local card only when the server asserts the request is still pending.

scanners_run:

  • gitleaks detect (log-opts b4811a2..bdce1e2): ok, 2 commits, no leaks.
  • trufflehog git (since b4811a2, head bdce1e2): ok, 0 verified, 0 unverified.
  • semgrep p/javascript + p/nodejs over lib/: ok, 4 hits, all in files this PR does not touch (app-misc.js postMessage wildcard, app-notifications.js raw-html-format, yoke/mcp-bridge-server.js rejectUnauthorized) so not introduced here. No hit in the diff. Note: --config=auto failed (exit 2, "Cannot create auto config when metrics are off") and single-file targets failed ("Invalid scanning root"); directory scan with registry packs succeeded.
  • osv-scanner (package-lock.json): ok, pre-existing advisories only; the PR changes no dependency files, so nothing is introduced.

Nit-level observation, not posted as a finding: markPermissionCancelled writes a constant string via innerHTML; no user data reaches it.

{"reviewer": "bobbie", "review_status": "clean", "head_sha": "bdce1e2fd0246c89a2ad9b1f969f48f186f39e1d", "pr_number": 422}

@clagentic-reviewer

Copy link
Copy Markdown

PEACHES — clean (0 findings)

All checks passed on head bdce1e2. The test relabeling change (from regression test to characterization guard with citation to MILLER lr-b75006 seq 3) is justified and correctly documented.

Checked:

  • clagentic-console.brand-product-name: no violations
  • clagentic-console.no-new-sdk-direct-require: no violations
  • clagentic-console.no-hardcoded-paths: no violations
  • clagentic-console.no-cross-layer-import: no violations
  • clagentic-console.demonstrated-test-failure: resolved, test properly relabeled
  • amos.code-craft and amos.path-choice: no violations
{"reviewer": "peaches", "review_status": "clean", "head_sha": "bdce1e2fd0246c89a2ad9b1f969f48f186f39e1d", "pr_number": 422}

@clagentic-merger
clagentic-merger Bot merged commit e49bd48 into main Sep 30, 2026
4 checks passed
@clagentic-merger

Copy link
Copy Markdown
Contributor

Merged via clagentic-loadout v0.2.0

Field Value
Gated HEAD SHA bdce1e2fd0246c89a2ad9b1f969f48f186f39e1d
Merged SHA bdce1e2fd0246c89a2ad9b1f969f48f186f39e1d
Reviews clagentic-reviewer[bot], clagentic-security[bot]
CI status no-runner-by-design (0 commit-status entries at HEAD)
task_id lr-581c2f

@clagentic-merger
clagentic-merger Bot deleted the fix/lr-581c2f-permission-card-pending branch September 30, 2026 12:02
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.

0 participants