Skip to content

fix: review follow-ups from #4 — proxy credential strip, Vary append, ledger hardening#5

Merged
OriNachum merged 1 commit into
mainfrom
fix/review-followups
Jul 11, 2026
Merged

fix: review follow-ups from #4 — proxy credential strip, Vary append, ledger hardening#5
OriNachum merged 1 commit into
mainfrom
fix/review-followups

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

What

The four real findings from #4's review threads, fixed with regression tests (user-approved follow-up-PR lane):

  • Proxy credential strip (thread 3562990747): the /learn zone-mount proxy no longer forwards Cookie/Authorization to the Pages origin — learner sessions stay with the API.
  • Vary append (3562990748): withCors appends Origin to an existing Vary instead of overwriting it.
  • Ledger permissions (3562990752): ledger.jsonl is created 0600, same as auth.json.
  • Ledger corruption tolerance (3562990757): a malformed ledger line is skipped (stable under the append-only sync-cursor arithmetic — documented) instead of crashing progress/next/sync.

Worker tests 37 → 39; Python suite 353 → 355. Version 0.5.0 → 0.5.1.

The other three threads (agentfront runtime dep, parallel App registry, json defaults) are deliberate/defused — pushback replies on the threads with evidence.

  • learn-cli (Claude)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jkvh4uxSi6AsBJYPjiurze

…0600 + corruption tolerance

Addresses the four real findings from PR #4's review threads
(3562990747, 3562990748, 3562990752, 3562990757).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jkvh4uxSi6AsBJYPjiurze
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix proxy credential leakage, preserve Vary, and harden profile ledger parsing/perms

🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Strip Cookie/Authorization when proxying /learn to Pages to prevent session leakage.
• Append Origin to existing Vary header in CORS wrapper to preserve upstream caching semantics.
• Create profile ledger as 0600 and skip malformed lines to avoid progress/sync crashes.
Diagram

graph TD
  A["Client"] --> B["learn-api worker"] --> C["proxyToPages()"] --> D["Pages origin"]
  B --> E["withCors()"] --> F["CORS response"]
  G["learn-cli"] --> H["profile ledger.jsonl"]
  subgraph Legend
    direction LR
    _u["Caller"] ~~~ _svc["Service"] ~~~ _store[("Local file")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Create ledger with atomic open + mode (no post-chmod)
  • ➕ Avoids a small window where the file could exist with broader permissions if the process crashes between create and chmod
  • ➕ More robust on systems with strict umask/permission expectations
  • ➖ More code complexity (os.open flags, fdopen) and platform nuances
  • ➖ Current approach is typically sufficient for local single-user stores

Recommendation: The PR’s approach is the right tradeoff: it directly addresses the review findings with minimal behavioral change and adds regression tests. If you want to further harden the ledger privacy guarantees, consider switching to an atomic create-with-mode path (os.open + fdopen) to eliminate the post-create chmod window, but it’s not strictly necessary given the local, single-user context.

Files changed (6) +105 / -6

Bug fix (2) +34 / -5
_ledger.pyHarden ledger privacy and parsing tolerance +26/-3

Harden ledger privacy and parsing tolerance

• Creates the ledger file with mode 0600 after initial creation to match other private local state. Updates ledger reading to skip malformed JSON lines and non-object values, preventing crashes in progress/next/sync flows and documents the rationale for cursor stability.

learn/profile/_ledger.py

index.jsStrip credentials in /learn proxy and preserve upstream Vary +8/-2

Strip credentials in /learn proxy and preserve upstream Vary

• Updates the /learn zone-mount proxy to delete Cookie and Authorization headers before forwarding to the Pages origin. Changes withCors to append Origin to an existing Vary header rather than overwriting it.

workers/learn-api/src/index.js

Tests (2) +61 / -0
test_profile_store.pyAdd ledger permission and corruption regression tests +17/-0

Add ledger permission and corruption regression tests

• Adds tests asserting the ledger file is created with 0600 permissions and that malformed/non-dict ledger lines are skipped while keeping ledger_count consistent.

tests/test_profile_store.py

worker.test.jsAdd worker tests for header stripping and Vary append behavior +44/-0

Add worker tests for header stripping and Vary append behavior

• Adds regression tests verifying the proxy to PAGES_ORIGIN does not forward Cookie/Authorization while preserving other headers. Adds a test ensuring withCors keeps an upstream Vary value and adds Origin.

workers/learn-api/test/worker.test.js

Documentation (1) +9 / -0
CHANGELOG.mdDocument 0.5.1 security and robustness fixes +9/-0

Document 0.5.1 security and robustness fixes

• Adds a 0.5.1 release entry describing the proxy credential stripping, Vary handling fix, and ledger permission/corruption-tolerance changes.

CHANGELOG.md

Other (1) +1 / -1
pyproject.tomlBump project version to 0.5.1 +1/-1

Bump project version to 0.5.1

• Updates the package version from 0.5.0 to 0.5.1 to reflect the shipped fixes.

pyproject.toml

@sonarqubecloud

Copy link
Copy Markdown

@OriNachum OriNachum merged commit 741027a into main Jul 11, 2026
8 checks passed
@OriNachum OriNachum deleted the fix/review-followups branch July 11, 2026 03:06
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 9 rules

Grey Divider


Action required

1. Ledger chmod can crash 🐞 Bug ☼ Reliability
Description
append_ledger() calls os.chmod() without handling OSError, so the first ledger write can raise
and break learn record/sync on filesystems that reject chmod (unlike auth.json, which is
best-effort).
Code

learn/profile/_ledger.py[R34-40]

+    path = ledger_path()
+    created = not path.exists()
+    with path.open("a", encoding="utf-8") as handle:
        handle.write(json.dumps(row, ensure_ascii=False))
        handle.write("\n")
+    if created:
+        os.chmod(path, _LEDGER_MODE)
Evidence
The ledger write path performs an unguarded chmod on creation, while the existing auth credential
file explicitly treats chmod failures as expected and non-fatal.

learn/profile/_ledger.py[31-41]
learn/profile/_auth.py[76-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`learn.profile._ledger.append_ledger()` calls `os.chmod()` unguarded. On platforms/filesystems that reject chmod, this can raise `OSError` and fail the command even though the ledger write itself succeeded.

### Issue Context
`learn.profile._auth.save_auth()` already treats chmod as best-effort (it catches `OSError` and continues), with a comment noting some environments reject chmod.

### Fix Focus Areas
- learn/profile/_ledger.py[31-41]
- learn/profile/_auth.py[76-85]

### Suggested fix
- Wrap the ledger chmod in `try: ... except OSError: pass` (mirroring `save_auth`).
- (Optional) If you want the test to remain strict while still being portable, conditionally skip/relax `test_ledger_file_is_created_private` when chmod is unsupported (platform/filesystem-specific).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Ledger mode not enforced 🐞 Bug ⛨ Security
Description
append_ledger() only applies 0600 when the ledger file is newly created, so upgrading to this
version will not harden an existing permissive ledger.jsonl and learner data may remain readable
by other users.
Code

learn/profile/_ledger.py[R34-40]

+    path = ledger_path()
+    created = not path.exists()
+    with path.open("a", encoding="utf-8") as handle:
        handle.write(json.dumps(row, ensure_ascii=False))
        handle.write("\n")
+    if created:
+        os.chmod(path, _LEDGER_MODE)
Evidence
The implementation sets created based on existence and only chmods in that case, despite stating
the ledger is private learner data and should be 0600.

learn/profile/_ledger.py[12-16]
learn/profile/_ledger.py[31-41]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new `0600` hardening is only applied when the file does not exist prior to append. If an existing `ledger.jsonl` was created with broader permissions (older versions, manual copy, different umask), this change will not correct it.

### Issue Context
The module docstring states the ledger is learner data and should be private (0600), matching the intent for `auth.json`.

### Fix Focus Areas
- learn/profile/_ledger.py[12-16]
- learn/profile/_ledger.py[31-41]

### Suggested fix
- After writing (or before writing), check `path.is_file()` and ensure the mode is `0600`; if not, attempt to `chmod` it (best-effort, catching `OSError`).
- Alternatively, run `chmod` unconditionally after every append (still best-effort), which also covers upgrades/migrations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Sync misses repaired row 🐞 Bug ≡ Correctness
Description
Because read_ledger() now drops malformed/non-dict lines, SyncState.cursor (a count over the
filtered list) no longer maps to a stable file position; if a previously malformed line is manually
repaired into a valid dict later, push_pending() can treat it as already pushed and never upload
it.
Code

learn/profile/_ledger.py[R58-63]

+            try:
+                parsed = json.loads(stripped)
+            except json.JSONDecodeError:
+                continue
+            if isinstance(parsed, dict):
+                rows.append(parsed)
Evidence
read_ledger() now removes malformed/non-object lines from the returned list, while sync slices and
advances solely by list index (cursor), so inserting a newly-valid row earlier in the list after
the cursor was saved will prevent it from being included in rows[cursor:].

learn/profile/_ledger.py[43-64]
learn/profile/_sync.py[58-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`read_ledger()` now filters out invalid lines. Sync uses `rows = read_ledger()` and slices by `SyncState.cursor`, which means the cursor is tied to the *filtered* list index, not a physical file line number. If an invalid line is later edited into a valid dict, it can be inserted before the cursor index and skipped forever by sync.

### Issue Context
The ledger docstring mentions manual edits as a source of corruption; manual edits are also the most likely way a corrupted line could be repaired.

### Fix Focus Areas
- learn/profile/_ledger.py[43-64]
- learn/profile/_sync.py[58-66]
- learn/profile/_syncstate.py[1-10]

### Suggested fix options
- Make the sync cursor track physical positions (e.g., file line count or byte offset) rather than `len(read_ledger())`.
- Or, explicitly document/enforce that malformed lines are permanently ignored for sync unless the user resets `sync.json` (and optionally surface a warning when corruption is detected).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread learn/profile/_ledger.py
Comment on lines +34 to +40
path = ledger_path()
created = not path.exists()
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(row, ensure_ascii=False))
handle.write("\n")
if created:
os.chmod(path, _LEDGER_MODE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Ledger chmod can crash 🐞 Bug ☼ Reliability

append_ledger() calls os.chmod() without handling OSError, so the first ledger write can raise
and break learn record/sync on filesystems that reject chmod (unlike auth.json, which is
best-effort).
Agent Prompt
### Issue description
`learn.profile._ledger.append_ledger()` calls `os.chmod()` unguarded. On platforms/filesystems that reject chmod, this can raise `OSError` and fail the command even though the ledger write itself succeeded.

### Issue Context
`learn.profile._auth.save_auth()` already treats chmod as best-effort (it catches `OSError` and continues), with a comment noting some environments reject chmod.

### Fix Focus Areas
- learn/profile/_ledger.py[31-41]
- learn/profile/_auth.py[76-85]

### Suggested fix
- Wrap the ledger chmod in `try: ... except OSError: pass` (mirroring `save_auth`).
- (Optional) If you want the test to remain strict while still being portable, conditionally skip/relax `test_ledger_file_is_created_private` when chmod is unsupported (platform/filesystem-specific).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread learn/profile/_ledger.py
Comment on lines +34 to +40
path = ledger_path()
created = not path.exists()
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(row, ensure_ascii=False))
handle.write("\n")
if created:
os.chmod(path, _LEDGER_MODE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Ledger mode not enforced 🐞 Bug ⛨ Security

append_ledger() only applies 0600 when the ledger file is newly created, so upgrading to this
version will not harden an existing permissive ledger.jsonl and learner data may remain readable
by other users.
Agent Prompt
### Issue description
The new `0600` hardening is only applied when the file does not exist prior to append. If an existing `ledger.jsonl` was created with broader permissions (older versions, manual copy, different umask), this change will not correct it.

### Issue Context
The module docstring states the ledger is learner data and should be private (0600), matching the intent for `auth.json`.

### Fix Focus Areas
- learn/profile/_ledger.py[12-16]
- learn/profile/_ledger.py[31-41]

### Suggested fix
- After writing (or before writing), check `path.is_file()` and ensure the mode is `0600`; if not, attempt to `chmod` it (best-effort, catching `OSError`).
- Alternatively, run `chmod` unconditionally after every append (still best-effort), which also covers upgrades/migrations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread learn/profile/_ledger.py
Comment on lines +58 to +63
try:
parsed = json.loads(stripped)
except json.JSONDecodeError:
continue
if isinstance(parsed, dict):
rows.append(parsed)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

3. Sync misses repaired row 🐞 Bug ≡ Correctness

Because read_ledger() now drops malformed/non-dict lines, SyncState.cursor (a count over the
filtered list) no longer maps to a stable file position; if a previously malformed line is manually
repaired into a valid dict later, push_pending() can treat it as already pushed and never upload
it.
Agent Prompt
### Issue description
`read_ledger()` now filters out invalid lines. Sync uses `rows = read_ledger()` and slices by `SyncState.cursor`, which means the cursor is tied to the *filtered* list index, not a physical file line number. If an invalid line is later edited into a valid dict, it can be inserted before the cursor index and skipped forever by sync.

### Issue Context
The ledger docstring mentions manual edits as a source of corruption; manual edits are also the most likely way a corrupted line could be repaired.

### Fix Focus Areas
- learn/profile/_ledger.py[43-64]
- learn/profile/_sync.py[58-66]
- learn/profile/_syncstate.py[1-10]

### Suggested fix options
- Make the sync cursor track physical positions (e.g., file line count or byte offset) rather than `len(read_ledger())`.
- Or, explicitly document/enforce that malformed lines are permanently ignored for sync unless the user resets `sync.json` (and optionally surface a warning when corruption is detected).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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