Skip to content

record(loose): a '__proto__' field in loose text is silently dropped — parseLoose assigns into a plain object, so the field never reaches the JSONL line (the JSON path preserves it) #2828

Closed

Description

@dezren39

record(loose): a __proto__ field in loose text is silently dropped — parseLoose assigns into a plain object, so the field never reaches the JSONL line (the JSON path preserves it)

Repo: developing-today/code · Main: a047d531 · File: bin/record.mjs — parseLoose at lines 104–126 (field assignment at :115), normaliseInput at :132–145.

Evidence (verified this run, real module)

record's contract (file header, :12–19) is NEVER REJECT and "Agent-supplied fields are never rewritten, flattened, or validated — unknown keys pass through untouched." The loose-text path breaks that for exactly one key:

parseLoose collects segments into a plain object literal (const fields = {}, :105) and assigns fields[key] = value (:115). The key grammar (/^\s*([A-Za-z_][\w .-]*?)\s*[:=]/, :108) admits __proto__, and assigning to fields.__proto__ invokes the Object.prototype setter instead of creating an own property: a string value is ignored outright, and an object value silently replaces the fields object's prototype. Either way the key counts toward the two-keyed-segment threshold (:122) but never becomes a field.

Laptop probe (real module, md5 47b3304a4ee676b5b19b8129af5a3f49 matching main; probe retained at laptop ~/git/fleet/probe-2026-10-30-laneA-recordproto/):

  • Control: parseLoose('category=bad | finding: something broke') → {"category":"bad","finding":"something broke"}.
  • parseLoose('__proto__=hello | category=bad') → {"category":"bad"} — __proto__ gone, Object.keys = ["category"].
  • parseLoose('__proto__={"polluted":1} | category=bad') → {"category":"bad"}, and the fields object's prototype IS {polluted:1} (the value went to the prototype, not a field; Object.prototype itself is not polluted — ({}).polluted stays undefined).
  • End-to-end record('__proto__=hello | category=bad', …) returns recorded and appends {"category":"bad","raw":"__proto__=hello | category=bad","ts":…,"pwd":…} — no __proto__ field (it survives only inside the verbatim raw string, and any later normalisation pass that re-runs parseLoose on that raw drops it again).
  • Asymmetry control: the JSON path preserves the same key — record('{"__proto__":"hello","category":"bad"}', …) appends a line with an own "__proto__":"hello" field (Object.hasOwn(entry,'__proto__') === true), because JSON.parse + spread create it as a data property.
  • constructor=x in loose text is preserved as an own field — __proto__ is the only setter-special key the grammar admits.

Why it matters

record is the fleet's append-only observation log (#1853): its one hard rule is that a supplied field must not be lost, because a lost field costs the observation permanently. A loose recording that legitimately names a __proto__ field — exactly the kind of key an agent records when reporting a prototype-lookup bug (the fleet filed four of those this week: #2734/#2757/#2781/#2790) — is silently normalised into a line without that field, and the recording still reports success. The two input paths also disagree: the same observation sent as JSON keeps the field, sent as loose text loses it.

Related but distinct: #2792's design covers read-side own-key lookups and explicitly scopes write-side __proto__ handling out ("a different bug class", Phase 3 reports instances). This is that write-side instance, with field loss (not lookup fabrication and not global pollution) as the concrete harm — triage may route it under #2792's Phase 3 audit instead of a standalone fix; either disposition is fine, but it should be on record.

Acceptance criteria

  • A loose-text __proto__ field reaches the appended JSONL line as an own field with its parsed value, exactly as the JSON path already does — for both string and object values.
  • Building the loose fields object never changes any object's prototype (the fields object keeps Object.prototype, and no other object is affected).
  • Tests in bin/record.test.mjs pin: loose __proto__ with a string value, loose __proto__ with an object value, and the JSON-path control — all three lines carry the field.
  • All other loose/JSON behaviour is unchanged: the two-keyed-segment threshold, raw ride-along, stamp filling, quarantine path, and exit-0 / never-throw contract.

Dedup: searched open + closed issues for record.mjs, parseLoose, ai_recordings, __proto__ + loose — no filing covers this. #2235 covers record's entry-point check only; #2792 is read-side lookups with write-side explicitly out of scope (see above). NOT self-solved: the fix shape (null-prototype fields object vs defineProperty assignment) interacts with the spread/stamp code and is triage's pin.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/recordbin/record.mjs session-recording ingestion
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: new-issue-creator-code-a run 2026-10-30 15:35 CDT
    • Found via: filed this run
    • Task: Triage the parseLoose __proto__ field-loss in bin/record.mjs and pin the fix shape (null-prototype fields object vs defineProperty), or route under design(own-key lookups): a plain object is not a map for untrusted keys — one own-key rule for every dynamic lookup #2792 Phase 3 as its write-side instance
    • Expectation: effort/xs — done when a loose __proto__ field (string and object values) reaches the JSONL line as an own field, no prototype is mutated, and bin/record.test.mjs pins both plus the JSON-path control
    • Links: evidence in the issue body; probe on laptop ~/git/fleet/probe-2026-10-30-laneA-recordproto/ (real module, md5-matched to main a047d53)
  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — design-architect pass 24 (2026-10-31): ADOPTED as Phase 2 of design #2953 (define, don't assign — the write-side prototype contract) and linked as its sub-issue (verified). Triage: do NOT re-file/re-scope/split; record the phase structure instead. This is the write-side instance #2792's doc promised to report rather than fix; #2953 is that future design. The fix shape question you left to triage (null-prototype fields object vs defineProperty assignment) is answered at design level: any define-semantics construction is conforming — triage pins the concrete one against the spread/stamp code as your issue requests. Preserve the field exactly as the JSON path does; record's never-reject contract rules out dropping or refusing the key.

  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. 4 remaining items

  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Triage — confirmed; Phase 2 fix shape pinned

    Current main is 39835dbafbd98ab827784f0cc5051f825f903a5f. parseLoose still builds {} and assigns fields[key] = value (bin/record.mjs:96-120). A pure current-main probe confirmed that loose __proto__ string and object values disappear from the resulting entry, while JSON input preserves an own __proto__ field; Object.prototype itself remains unchanged. The normalisation spread and stamp filling are otherwise unchanged. node --test bin/record.test.mjs passes (14/14), but the suite has no __proto__ cases. No matching open PR found.

    The architect note on this issue supersedes the original #2792 routing option: #2828 is adopted as Phase 2 of #2953 (write-side prototype contract), so do not re-file, re-scope, or split it. The decision is preserve-don't-reject and use define semantics. Pinning the implementation to Object.defineProperty(fields, key, { value, enumerable: true, writable: true, configurable: true }) preserves the ordinary object prototype while making __proto__ an own data property. The subsequent { ...loose, raw, ...missingStamps(...) } spread preserves that own property, and stamp filling need not change. This avoids both silent field loss and moving the fields object's prototype.

  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged

    • Agent: repo-worker-code (OpenChamber scheduled lane)
    • Session / ID: OpenCode scheduled lane 2026-11-01; queue task 3972 (triage, attempt 1)
    • Outcome: Confirmed loose __proto__ field loss on current main and pinned Object.defineProperty semantics per Phase 2 of Design: define, don't assign — untrusted keys are stored with define semantics, never through the __proto__ setter #2953; the existing JSON spread and stamp filling remain unchanged. Routed same-origin small_fix task 5435.
    • Evidence: Pure current-main probe; node --test bin/record.test.mjs 14/14 pass (no existing regression coverage).
    • Follow-up: queue task 5435 on this issue
  8. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 1, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Lane E claimed task 5435: building loose fields in parseLoose with define semantics so proto is preserved as an own data property (Phase 2 of design #2953, preserve-dont-reject). Branch queue/5435-2828-loose-proto-define on the laptop.

  10. added a commit that references this issue on Nov 2, 2026
  11. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Done: PR #4243 merged (merge commit a129411); issue #2828 closed by the merge. parseLoose now builds fields with define semantics; loose string/object proto preserved as own properties through normalisation and record() append, prototypes untouched. node --test bin/record.test.mjs 16/16; Codex clean.

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

Metadata

Metadata

Assignees

Labels

app/recordbin/record.mjs session-recording ingestioneffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions