Skip to content

Fill in a published study's original wording on this computer (link + hash rehydration) - #94

Merged
assassin808 merged 8 commits into
mainfrom
feat/rehydrate
Nov 10, 2026
Merged

assassin808 merged 8 commits into
mainfrom
feat/rehydrate

Conversation

@assassin808

@assassin808 assassin808 commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

A published protocol.json (hsenv 0.8.1 release projection) holds placeholders where wording from a non-redistributable source goes. This PR restores that wording on the researcher's machine with the bundled hsenv.rehydrate. A study runs only once every screen is filled in and checked.

Design

  • Helper pack/runtime/lib/hs_original_text.py, run by main from the app's own copy and isolated like the checks, with network access only for downloads:
    • downloads link-tier sources:
      • http(s) only, never a local or private address, redirects included; a host that does not resolve here is never fetched
      • at most 90 s per download and 150 s per check; each socket read waits at most the time left and takes what has arrived, so a slow server stops at the deadline
      • cached; a failure is retried after 10 minutes
      • a web page returned for a non-HTML source counts as a failed download (a sign-in or paywall page)
    • tries the researcher's files in sources/private/, paper.pdf and sources/
    • checks each item by its own text hash
    • caches the restored protocol per protocol.json version in <userData>/original-text/, never in the project
  • Readiness (main/original-text.ts, new text lane in package-readiness.ts):
    • runs when the study opens, after a file is dropped in, and in readiness checks; a result is kept at most 10 minutes
    • missing wording blocks a protocol run, and the study still previews
    • writes a status for the agent to .humanstudy/original-text.json (counts, titles, needs; no wording)
    • the held list (distribution) is pinned in app storage the first time the study is seen. An edited or removed list blocks the run, and the message says to put back protocol.json as received.
    • this build has no import path for a published protocol.json: the library forks only program.json, package/, artifacts/ and sources/. When one lands, it must call repinHeld after writing a new published version.
  • Engine checks:
    • no app-side rule decides which texts are excused. Each held source gets a source text of placeholders, so the engine's own verbatim rule finds the held placeholders and still blocks any other text cited verbatim from that source (regression test for the review's probe q3).
    • after the wording is restored, hsenv.checks runs again on the restored copy in app storage, and any finding blocks
      • the result is cached by the restored copy plus the sourcetext/ contents, and only for completed runs
      • the message is plain en/zh, with no check names and no blame on the publisher
  • Run:
    • the restored copy travels with the trusted run code as restored/protocol.json
    • app_run reads it only from beside its own code and plays it only if every held text hashes to its text_sha256, no placeholder is left and nothing else differs
    • it refuses any protocol with placeholder runs and no restored copy
    • records keep no screens; the run-records test checks every file the run leaves in the project
  • Private copies:
    • a drop into a published study goes to sources/private/, and an app note in the prompt (not in the researcher's bubble) tells the agent the file was received to fill in the wording and must not be opened
    • the run skill and AGENTS.md forbid opening files in sources/private/ or the app's original-text/ storage
    • research capture has one boundary for private material, redactCapture:
      • every research-v1 event (agent adapter, selection, program.json and artifact versions) and every telemetry metadata object passes it before being stored, put in a blob or sent
      • an event that names a private path, or quotes a run of 40 letters or digits from indexed private text, keeps only its structural fields
      • paths count also when built in pieces, written with ./ segments or URL-encoded; globs and searches under sources/ count in published or private-folder projects
      • the agent adapter also blanks the end event of a tool call that read a private path
      • the skeleton is letters only (CJK included; index format v2, older files are rebuilt), and each text is also checked with line enumerators removed (1., Q1:, (a), Item 3:, bullets), so re-numbered items and a 1-5 column still match. Against an 8-item questionnaire index, 0 of 8 ordinary texts were flagged.
      • an event target that names a withheld file (sources/private/…) is not logged
      • every string value is checked raw, never the event's JSON: also with escape sequences replaced by spaces (strings holding JSON or a heredoc) and with a read tool's line-number prefixes removed, so quotes across lines, tabs or escapes are caught
      • every blob passes the same check: putBlob refuses private text, fit checks before anything goes inline or to a blob, and the workspace context is redacted before it is fitted
    • project files go through mayCollectFile:
      • a published study's run outputs (artifacts/simulations/, runs/) and any file quoting private text are never collected
      • in published or private-folder projects, files that can't be read as text and texts over 300k characters are not collected either; ordinary projects keep collecting their PDFs and figures
    • the per-project index stores hashes only, never text:
      • only what verifiably ships is subtracted: the strings of the protocol.json as received (kept with the pin at first sight) and files whose sha256 a hosted source of that copy records
      • nothing written on this computer counts as public (sources/ outside private/, sources/researcher/, effects.json, the live protocol.json)
      • each file is cached by its sha256 and extracted once
      • the index is written before any download; a drop indexes the copy before importFiles returns, and the chip shows "Adding …"
      • the app merges the sorted per-project files once per session and after each drop, and prunes the index of a folder that no longer exists
    • the agent note matches exact path tokens of the [Attached: …] line
  • UI (no new buttons):
    • the Simulation tab words each need in en and zh, mentioning the publisher only for link sources
    • "held list changed" and "checks failed with the wording filled in" have their own heading and button text
    • the preview shows "[text not available]"
  • PDF: pypdfium2 is already in the runtime lock (via pdfplumber). Without it, PDF sources say so and the other formats still work.

Follow-up for hsenv 0.8.2 / sandbox skills/run (vendored, not edited here)

  • run_study.load_protocol (or hsenv.engine.Protocol.load) should refuse a protocol with a non-empty distribution.held or runs of U+2591, so placeholders cannot reach AI participants by any route. The app already refuses them in app_run.
  • checks.check_verbatim should report the paths it counts as unchecked, so the app can excuse exactly the held paths instead of passing a placeholder source text.

Tests

  • invented fixture (CC0); the Python suite covers: link source over a local HTTP server, user files, hash mismatch, sign-in page, changed file online, slow server cut off, dead link then the researcher's copy, no PDF reader, other extractor version, held picture, redirects to local addresses, unresolvable host
  • node tests:
    • real-engine q3 regression; held-list pin; checks on the restored copy (findings never quote it)
    • published-run pack test: forged, placeholder, stripped-list and environment-variable copies are refused, and no file the run left holds wording
    • preview; telemetry test showing that a dropped private file's text never reaches capture
  • npm run typecheck, the full node suite (320 pass, 2 skipped), pack/runtime/tests (574 OK) and scripts/tests: green, with HUMANSTUDY_TEST_PYTHON set to the app runtime's Python

Follow-ups

  • Accepted limit of the beta (telemetry is opt-in research data): a quote whose items, each shorter than 40 letters, are reordered, or separated by inserted HTML tags or Likert labels, is not recognised by content. The path rule still covers reading the file itself.
  • The index covers the first million letters of each private copy or download; a quote from text past that point is not recognised.
  • "Export complete conversation" and "Export my research data" are explicit local saves and are not redacted.

Open questions

  • Model calls send the restored wording to the provider through the hosted relay. Is that acceptable under "never uploaded"?
  • Held pictures and rendered regions are not supported yet: such studies stay blocked.
  • Any pypdfium2 upgrade invalidates published PDF locators.
  • An agent that reads a private file anyway: capture is redacted, but the text still reaches the model.

🤖 Generated with Claude Code

assassin808 and others added 8 commits November 10, 2026 03:55
… hash)

A published protocol.json (hsenv 0.8.1 release projection) holds placeholders where
wording from a non-redistributable source goes. The app now restores it locally with
hsenv.rehydrate and runs the study only once every screen is filled in.

- pack/runtime/lib/hs_original_text.py: downloads link-tier sources (http(s) only,
  no local addresses, cached, failures retried after 10 min), tries the files the
  researcher dropped in (sources/private/, paper.pdf, sources/), checks every item by
  its text hash, caches the restored protocol per study version in app storage, and
  prints a status without any wording. PDF via the runtime's pypdfium2; without it,
  PDF sources say so and the other formats still work.
- main/original-text.ts + package-readiness lane `text`: runs it from the app's own
  copy on open, on a dropped file and in readiness; missing wording blocks a protocol
  run; status for the agent in .humanstudy/original-text.json. A file dropped into a
  published study goes to sources/private/ and never becomes paper.pdf.
- Engine checks: verbatim.unchecked no longer blocks when every unchecked source is a
  held one; table_spans_unserved is now listed.
- Run: the restored protocol travels with the trusted run code; app_run plays it only
  when it is exactly protocol.json with the held texts filled in, records the approved
  hash and keeps no screens; a published study without it is refused.
- Preview shows "[text not available]"; Simulation tab words each need in en and zh;
  run skill "Original wording" section for the agent.
- Tests: invented fixture (link source over a local HTTP server, user files, hash
  mismatch, dead link, no PDF reader, newer extractor), node + pack tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion, safer downloads

- app_run reads the restored copy from beside its own code, never from the environment,
  and accepts it only when every held text hashes to its text_sha256, no placeholder is
  left and nothing else differs (an agent-written copy can no longer put placeholders or
  other wording on screen).
- verbatim.unchecked is relaxed only when it names held sources and counts no more texts
  than the held entries that really are placeholders.
- Downloads: redirects to local/private addresses refused (is_global); a download that
  restored nothing (a sign-in page) is dropped and retried after ten minutes; candidate
  hashing uses the size cap; a file replaces a download only when it restores more.
- Held pictures are not supported yet: the study stays unresolved with need "picture"
  instead of being marked complete.
- Original-wording answers are kept at most ten minutes; "different PDF reader version"
  wording; plural fix; the run skill keeps the researcher's copy in sources/private/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in, restored checks

- Engine checks: no app-side relaxation any more. Each held source (without its own
  sourcetext) gets a source text of placeholders, so the engine's own verbatim rule finds
  the held placeholders and still blocks any other verbatim text from that source (q3
  is a regression test). The relaxed count rule is gone.
- Once restored, hsenv.checks runs on the restored copy in app storage (held texts
  checked against themselves); any finding blocks, reported by check name only.
- The held list is pinned in app storage on first sight; an edited or removed list
  (or leftover placeholders without one) blocks the run.
- Research capture redacts tool input/output and raw parts that name sources/private/
  or the original-text cache; a drop into a published study tells the agent not to open
  the file; skill and AGENTS.md forbid opening those files.
- app_run refuses any protocol with placeholder runs and no restored copy (run_study is
  vendored: the upstream guard is a follow-up).
- Downloads: 90 s per download / 150 s per check with a failure marker, a web page for
  a non-HTML source is a failed download, an unresolvable host is not public.
- Strings: "original materials (the paper, questionnaire or booklet)", publisher only
  for link sources, singular/plural, plain format names; en and zh.
- Tests: the run-records test scans every file the run left; PDF cases skip when
  pypdfium2 differs from the lock; fixture without the typed-number range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on, honest recovery

- Downloads: every socket read waits at most the time left and read1 takes what has
  arrived, so a trickling server stops at the deadline (test asserts elapsed time with
  a server that keeps trickling).
- Research capture: besides a sturdier path rule (pieces, globs, searches under sources/,
  ./ segments, URL-encoding; .humanstudy/original-text.json no longer redacted), any tool,
  text, reasoning or raw part that quotes a 40-character run of a private copy, a
  download or the restored wording is dropped. The helper writes hashes of every such run
  (never text) to app storage; the app loads them per session and after each drop.
- The agent note for a dropped private copy goes into the prompt only, not the bubble.
- Held-list pin: the message says to put back protocol.json as received (no import path
  for published protocols exists yet); repinHeld for a future import path.
- Restored-copy check: cached by the copy and the sourcetext contents, completed reports
  only; plain en/zh message without check names or blame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…leaner index

- Research capture never collects a published study's run outputs (artifacts/simulations/,
  runs/), nor any project file that quotes a 40-character run of private text, nor (while
  anything is indexed) a file it cannot read as text (mayCollectFile, used by file
  snapshots and source collection).
- Index: per project, minus every run that also occurs in the project's public files
  (protocol.json's shipped strings, effects.json, paper.pdf, sources/ outside private/);
  sorted 8-byte binary hashes in a typed array; each file's runs cached by sha256 so it
  is extracted once; written before any download, and a drop indexes the copy before
  importFiles returns.
- A part over 300k characters is redacted whole while anything is indexed; glob and
  search path rules only where sources/private/ exists.
- Own heading and button for "held list changed" / "checks failed"; the agent note
  matches dropped paths as whole strings (commas and ] in names).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… public

- Research capture: redactCapture is the single boundary for private material. Every
  research-v1 event goes through captureResearch (the only caller of the collector:
  agent adapter, selection, program.json and artifact versions), and telemetry metadata
  through the same check before any blob. The agent adapter also blanks the end event
  of a tool call whose input named a private path (ra.mjs replay is a test).
- Index: subtract only what verifiably ships, the protocol.json as received (stored with
  the pin at first sight) and files whose sha256 a hosted source of it records; never
  live sources/, sources/researcher/, effects.json or the live protocol.json (launder.py
  keeps all 37 runs, now a test).
- Ordinary projects (not published, no sources/private/) keep collecting PDFs, figures and
  long texts; the unreadable/300k rules apply only in published or private-folder projects.
- Drop chip shows "Adding <name>…" while the copy is indexed; the index merges sorted files
  and prunes those of folders that no longer exist; exact path tokens for the agent note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- redactCapture / redactPrivatePart check every string value raw (never the event's
  JSON), also with escape sequences turned into spaces and read-tool line numbers
  removed: quotes across lines, tabs, CRLF, heredocs in JSON inputs are caught (rc.mjs
  cases A-E are tests).
- putBlob refuses private text itself; fit checks before anything goes inline or to a
  blob; the workspace context is redacted before it is fitted; the transcript's path rules
  are scoped by published status too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t logged

- The redaction skeleton is letters only (any script, CJK included) in the app and the
  helper; every text is also checked with line enumerators removed (1., Q1:, (a), Item 3:,
  bullets). Index format v2: older .bin files are dropped and rebuilt. probe2/probe3
  numbered, Q-prefixed, table and changed-numbering cases are tests; 0 of 8 ordinary
  texts flagged against a questionnaire index.
- Reordered items or inserted HTML/Likert labels between short items are documented as
  an accepted limit of the beta.
- An event target naming a withheld file in sources/private/ is not logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@assassin808
assassin808 merged commit 0be55d9 into main Nov 10, 2026
1 of 2 checks passed
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