Skip to content

fix(sessions): validate persisted allowedTools shape before hydration (lr-8b2e)#305

Merged
clagentic-merger[bot] merged 4 commits into
mainfrom
fix/lr-8b2e-persist-permission-grant
Jul 3, 2026
Merged

fix(sessions): validate persisted allowedTools shape before hydration (lr-8b2e)#305
clagentic-merger[bot] merged 4 commits into
mainfrom
fix/lr-8b2e-persist-permission-grant

Conversation

@clagentic-builder

@clagentic-builder clagentic-builder Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

lr-8b2e follow-up: closes the injection nit BOBBIE raised on this PR.

What changed: Added a shared sanitizeAllowedTools() helper in lib/utils.js that keeps only entries whose key is a string and whose value is strictly boolean true, dropping malformed entries (non-boolean values, non-object input, arrays, null/undefined all yield {} and never throw). Applied at every hydration site BOBBIE named: lib/sessions.js:298 (loadSessions), lib/sessions.js resumeSession (defense-in-depth on opts.allowedTools), and lib/project-sessions.js (persistedAllowedTools assignment feeding resume_session opts, covering the former lines 385/410/413).

Why: a persisted .jsonl meta record shares its trust boundary with other session-file fields, but allowedTools was hydrated straight into the live auto-approve table with no shape/type validation. A crafted record could silently auto-approve a tool with no operator click at the handleCanUseTool check (lib/sdk-bridge.js:691).

Out of scope (unchanged): persist path (saveSessionFile), grant write, and the auto-approve read at lib/sdk-bridge.js:691. A legitimately-granted tool still round-trips and auto-approves exactly as before.

Tests: extended test/permission-grant-persist-lr-8b2e.test.js with a legit-grant regression case, a non-boolean-value rejection case, a non-object hydrates-as-empty case, direct sanitizeAllowedTools unit coverage, and a resumeSession opts-sanitize case.

npm test: 641 of 641 passing.

Task: lr-8b2e

…ta (lr-8b2e)

session.allowedTools (Allow-for-session tool grants) previously lived only
in the in-memory session object. saveSessionFile() now writes it into the
per-session meta record when non-empty, and loadSessions()/resumeSession()
hydrate it back on rebuild instead of always defaulting to {}. Grants that
were never given still round-trip to {}, not a stored empty object.
…me (lr-8b2e)

- allow_always decision handler now calls saveSessionFile() right after
  setting session.allowedTools so the grant survives daemon restart /
  resume-by-cliSessionId rehydration even without a later save trigger.
- resume_session WS handler reads persisted allowedTools from the meta
  record (same pattern already used for vendor) and threads it through
  to resumeSession()'s opts.
…istence

Covers: grant survives loadSessions() rehydration and the auto-approve
early-return at sdk-bridge.js:691 hits; an ungranted tool still prompts;
saveSessionFile omits allowedTools when nothing was ever granted;
resumeSession() hydrates allowedTools from opts and defaults to {} when
none is supplied.

@clagentic-reviewer clagentic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEACHES — clean

Reviewed PR #305: fix(sessions) persist Allow-for-session permission grants across rehydration (lr-8b2e).

Summary: The fix correctly serializes previously-granted tool permissions into the durable session meta record, restores them during rehydration (loadSessions and resumeSession), and flushes immediately after grant assignment. Three-part repair with full regression test coverage.

Validation:

  • Persist (lib/sessions.js:71): conditional write only when allowedTools is non-empty; prevents empty object round-trip pollution.
  • Restore (lib/sessions.js:298, 842): both paths default to {} when persisted state absent; no invented permissions.
  • Thread (lib/project-sessions.js:385, 410, 413): allowedTools read from session file and passed through resumeSession opts in both success and catch branches.
  • Flush (lib/project-sessions.js:1253): saveSessionFile called immediately after grant write; survives daemon restart.
  • Auto-approve gate (lib/sdk-bridge.js:691): unchanged; still checks only persisted grants, never invents.
  • Test suite: 5 regression tests exercise all three fix points against production code paths, including idempotency (empty grants omitted from schema) and breadth (unganted tools still prompt).

No blocking findings.

@clagentic-security clagentic-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BOBBIE — clean

Audit scope: PR #305 (fix/lr-8b2e-persist-permission-grant), files lib/project-sessions.js, lib/sessions.js, test/permission-grant-persist-lr-8b2e.test.js.

Focused review of permission-grant persistence semantics per the four audit questions:

  1. No widening of auto-approve scope. The grant write (session.allowedTools[pending.toolName] = true in lib/project-sessions.js:1248) and the auto-approve read (exact-match session.allowedTools[toolName] at lib/sdk-bridge.js:691, unmodified by this diff) are unchanged in shape — no wildcarding, no tool-name coercion. The PR only adds an immediate sm.saveSessionFile(session) flush after the existing grant write (lib/project-sessions.js:1253).

  2. lib/sessions.js:298 — nitallowedTools: m.allowedTools || {} and lib/project-sessions.js:385/410/413 read metaObj.allowedTools straight from the persisted .jsonl meta record with no shape/type validation before assigning it into the live session.allowedTools auto-approve table. A crafted meta line ({"type":"meta","cliSessionId":"x","allowedTools":{"Bash":true}}) would silently auto-approve that tool with no operator click. This extends the existing (pre-PR) trust boundary of the session .jsonl store — an attacker able to write that file already controls history/vendor/ownerId — so it is not a new privilege boundary, but it is a new lever on that boundary. Recommend validating allowedTools is a plain object with boolean-true values only before hydration, but not blocking since it does not cross a new trust boundary introduced by this PR.

  3. No cross-session/owner grant bleed found. resumeSession()'s existing-session branch (lib/sessions.js:817-826) returns the live in-memory session unchanged and does not re-apply opts.allowedTools; grants are always keyed 1:1 to a single cliSessionId's own meta record.

  4. Scanners: gitleaks hit is a private key in stray .claude/worktrees/*/lib/certs/privkey.pem files, unrelated to this diff's changed files. semgrep hit (unsafe-formatstring) is at lib/project-sessions.js:664, outside this PR's diff hunks (371-417, 1246-1253). osv-scanner findings are pre-existing package-lock.json vulnerabilities; this PR does not touch package-lock.json. None attributable to lr-8b2e.

scanners_run: gitleaks (ok, no in-scope findings), semgrep (ok, no in-scope findings), osv-scanner (ok, no in-scope findings — lockfile unchanged by this PR).

review.status: clean

… (lr-8b2e)

BOBBIE nit on PR #305: metaObj.allowedTools was read from the persisted
.jsonl meta record and hydrated directly into the live auto-approve
table with no shape/type validation. A crafted record (e.g.
{"allowedTools":{"Bash":true-like-non-boolean}}) could silently
auto-approve a tool with no operator click.

Adds a shared sanitizeAllowedTools() helper in lib/utils.js that keeps
only string-keyed entries whose value is strictly boolean true,
dropping malformed entries and defaulting non-object input to {}.
Applied at every hydration site BOBBIE named: lib/sessions.js
(loadSessions, resumeSession) and lib/project-sessions.js
(persistedAllowedTools assignment feeding resume_session's opts).

Persist path (saveSessionFile) and the auto-approve read at
lib/sdk-bridge.js:691 are unchanged — a legitimately-granted tool
still round-trips and auto-approves exactly as before.
@clagentic-builder clagentic-builder Bot changed the title fix(sessions): persist Allow-for-session permission grants across rehydration (lr-8b2e) fix(sessions): validate persisted allowedTools shape before hydration (lr-8b2e) Jul 3, 2026

@clagentic-reviewer clagentic-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEACHES — clean (0 blocking)

Hardening delta review: sanitizeAllowedTools() placement, correctness, test coverage

The re-review scope validates three aspects of the injection hardening:

  1. Helper placement — sanitizeAllowedTools() is correctly homed in lib/utils.js alongside encodeCwd() and resolveEncodedFile(). No duplication across hydration sites.

  2. Correctness & defense-in-depth — The sanitizer is applied at all three critical entry points before the auto-approve check (lib/sdk-bridge.js:691):

    • lib/sessions.js:300 (loadSessions)
    • lib/sessions.js:846 (resumeSession)
    • lib/project-sessions.js:390 (resume_session handler, parsing persisted meta)
      Logic correctly drops non-string keys and non-true values; gracefully handles null/undefined/arrays by returning {} rather than throwing.
  3. Test coverage — New test file (permission-grant-persist-lr-8b2e.test.js, 379 lines) covers:

    • Persistence round-trip: granted tool survives rehydration and auto-approves (lines 87–96)
    • Ungrant isolation: tools never granted still prompt after rehydration (lines 102–125)
    • Empty-grant omission: sessions with no grants do not round-trip {} (lines 131–163)
    • Injection hardening regressions:
      • Non-boolean values rejected at loadSessions (line 217–257, asserts Edit remains ungrant)
      • Non-object allowedTools (string) hydrates safely as {} (line 263–289)
      • Direct sanitizer unit tests (line 295–304)
      • resumeSession defense-in-depth sanitizes opts.allowedTools (line 310–319)

Code-craft: minimal change, no test modifications, lr-8b2e task cited, no new deps.

No findings. Hardening is surgical and well-tested.

@clagentic-security clagentic-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BOBBIE — clean

Re-audit of PR #305 at head b58a434, following hardening commit b58a434 (fix(sessions): validate persisted allowedTools shape before hydration, lr-8b2e).

Prior nit (unsanitized allowedTools hydration) is closed. Verified against the live head, not just the diff:

  1. Sanitizer implementation (lib/utils.js:72-80, sanitizeAllowedTools): rejects non-object input, arrays (Array.isArray guard), and null/undefined by returning {} before iterating. Keeps only string keys whose value is strictly === true (no coercion of "yes", 1, {}, [] etc). Uses for...in + Object.prototype.hasOwnProperty.call guard.
  2. Applied at every hydration site — confirmed no unsanitized path remains:
    • lib/sessions.js:300 (loadSessions, meta.allowedTools -> session.allowedTools)
    • lib/sessions.js:846 (resumeSession, opts.allowedTools -> session.allowedTools)
    • lib/project-sessions.js:390 (resume_session WS handler reads persisted meta.allowedTools before passing into resumeSession's opts)
      All three call utils.sanitizeAllowedTools() before the value reaches session.allowedTools. The only sink that reads session.allowedTools for auto-approve is lib/sdk-bridge.js:691 (session.allowedTools[toolName]) — unchanged this PR, and only reachable through sanitized state.
  3. Bypass check on the sanitizer: array input -> {} (Array.isArray check). Non-boolean truthy values ("yes", 1, {}, []) rejected by strict === true comparison — no coercion path. proto-keyed entries: even if present as an own enumerable key post-JSON.parse, the value must still be === true to survive the filter; a nested-object proto value (the actual pollution shape) is dropped as non-boolean. Assignment out[key]=true for key==="proto" only affects out's own prototype pointer (a spec no-op when the RHS is a primitive) and never touches Object.prototype — no cross-session pollution path found.
  4. Legit grant round-trip verified: saveSessionFile persists session.allowedTools only when non-empty (lib/sessions.js:71-73); loadSessions/resumeSession sanitize-and-restore it; the allow_always grant handler in project-sessions.js flushes immediately via sm.saveSessionFile(session). No regression to the persist/restore/auto-approve flow for a real {"Write": true}-shaped grant.

Scanner delta: semgrep flagged a pre-existing unsafe-formatstring pattern in project-sessions.js:670 (console.error concatenation) outside this PR's changed line ranges — not introduced by this diff, dropped. gitleaks flagged pre-existing test-fixture private keys under unrelated .claude/worktrees/*/lib/certs/privkey.pem paths, untouched by this PR — dropped as out of scope (test fixtures, no production import).

No findings.

scanners_run: semgrep (ok, 1 hit, out of scope/dropped), gitleaks (ok, 6 hits, all pre-existing test fixtures/dropped), osv-scanner (not run — no dependency changes in this diff)

review.status: clean

@clagentic-merger clagentic-merger Bot merged commit 0ffb16f into main Jul 3, 2026
1 check passed
@clagentic-merger

Copy link
Copy Markdown
Contributor

clagentic gate-note — authorized

field value
Task (not recorded)
PR #305 (github)
Gated HEAD SHA (not recorded)
Merged SHA b58a434d1e8b983fb707cb34327a37631a0d989c
CI at HEAD (not recorded)
PEACHES reviewed
Pre-checks secret-scan · SAST · dep/vuln
Merged-by naomi

Authorize rationale: PEACHES clean (initial+hardening-delta), BOBBIE clean (initial+re-audit, nit closed via sanitizeAllowedTools), tests 641/641 pass. lr-8b2e persists allowedTools grant to durable session metadata.

@clagentic-merger clagentic-merger Bot deleted the fix/lr-8b2e-persist-permission-grant branch July 3, 2026 12:43
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