Skip to content

v0.14.1

Choose a tag to compare

@xr843 xr843 released this 24 Oct 14:32
8330cd8

Security fixes from the 2026-10-21 review, and the most-misquoted lines given their
originals.
No dependency had a known vulnerability (cargo audit, pip-audit on all
three requirements files). The review found three issues and the follow-up a fourth, on
Windows; each was reproduced before it was fixed. Three are in the desktop app and the
FoJin retrieval fence, below; the fourth is in CI.

The retrieval fence's random number was probed on real models before release, at about
$0.30. The injected text sat behind a forged end marker made of -, the shape the line
filter keeps. Ten runs per arm:

  • No model obeyed it in either arm (claude-sonnet-5-5, claude-haiku-5-5; old fence and
    prompt vs new).
  • With the new prompt, Haiku told the user the retrieval data looked tampered with in 7 of
    10 runs, against 0 of 10 with the old one, and one run cited the number mismatch.
    Sonnet flagged it every time in both arms.
  • The probe is too small to show the number blocks an attack the old fence let through. It
    shows the rule is read and does not hurt answers.

Security — CI: the publish token and the hook wrapper (#341)

  • npm-publish.yml granted id-token: write to the whole workflow, so the gate job,
    which installs floor-pinned requirements and runs the full suite, could also mint the
    OIDC token npm's trusted publishing exchanges. Only the publish job holds it now, and
    scripts/tests/test_workflow_permissions.py fails any workflow that grants it at the top.
  • hooks/run-hook.cmd ran with enabledelayedexpansion, which strips every ! from an
    expanded path. A plugin under a Windows user named like bang!x failed its SessionStart
    hook with exit 127 (measured on Windows 11). The wrapper no longer needs delayed
    expansion, and a test runs it from a path holding ! & ^ % ( ) and spaces.

Performance — update --all is ten times faster (#346)

resolveSkill() rebuilt the whole catalog on every lookup, so all 21 SKILL.md files and
the personas' meta.json were re-read and re-parsed each time, and cmdInstall looks
skills up inside a loop over names × personas. A CPU profile put 70% of update --all's
2.8s in those reads. The catalog is now built once per process: 0.27s. The two
named-pipe tests, which give update --all 3 seconds, had been failing on a loaded
machine for this reason.

Content (#340)

  • master-tsongkhapa: the two 《广论》 lines the fd3d832 run misquoted most (卷1 四殊胜,
    卷17 「唯无我慧乃能永断三有根本」) are verbatim 原典 blocks with CBETA B10n0067 page
    and line.
  • master-yinguang: three 《文钞》 letters checked word for word against the pinned 殆知阁
    corpus.
  • compare-masters, master-debate, master-curriculum: cross_critique positions are this
    repository's synthesis, not a master's remark about another.

Dependencies

  • anthropic 1.8.0 → 1.17.0 and openai 3.19.2 → 3.35.1 for the paid eval (#301). Both
    verification scripts pass, and the keyless smoke output is unchanged apart from the
    version line.
  • actions/setup-node 7.1.0 (#306), download-artifact 8.0.2 (#307), upload-artifact 7.0.2
    (#308). The new SHAs were checked against the upstream tags.
  • eframe 0.37 (#310) is not in this release. CI is green, but nobody has looked at the
    window yet.

Security — the Windows desktop exe no longer runs a python.exe dropped beside it (2026-10-24)

The desktop app spawns python, node and npm.cmd by bare name. On Windows, Rust's
Command searches the directory of the running executable before PATH, and the released
exe is a single file people run from Downloads. Reproduced with the v0.14.0 release on
Windows: with a python.exe next to the exe (a copy of the desktop exe itself),
--baseline reported invalid arguments: ["…\scripts\test-fidelity.py", "--master", …]
as the dry-run's stderr, although a real Python was on PATH. Without the decoy the same
run printed ok demo. A decoy in the working directory or the repo root did not run.

  • On Windows a bare interpreter name is now looked up on PATH by the app and spawned by
    absolute path. Only absolute PATH entries are searched, and only .exe is appended,
    as std does: PATHEXT would let a python.bat outrank python.exe.
  • Not found on PATH is an error naming the program and the override to set
    (NODE, MASTER_SKILL_PYTHON, MASTER_SKILL_NPM). Falling back to the bare name
    would put the executable's directory back in the search.
  • Unix is unchanged. execvp never searched the executable's directory.

Security — the desktop app no longer runs a planted script under a safe-looking root (2026-10-21)

The desktop app finds its repo root by walking up from the working directory, then runs
python3 <root>/scripts/test-fidelity.py, node <root>/bin/cli.mjs and npm test from it.
The trust check only looked at the root's own mode bits. A reviewer reproduced the gap: root
0755, scripts/ 0777, a planted scripts/test-fidelity.py, and --baseline ran it, announced
as "discovered from the working directory".

  • On Unix the root, scripts/, bin/, scripts/test-fidelity.py, bin/cli.mjs and
    package.json must now all be owned by the effective uid and not group- or
    world-writable. A symlinked scripts/ or bin/ is judged by its target.
  • A refused root is named on the startup line together with the path that failed and why,
    for example …/scripts is group- or world-writable (mode 0777).
  • Windows is unchanged: no check, as before. ACLs are not inspected, and the gap is
    documented in desktop/src/cli.rs. MASTER_SKILL_REPO_ROOT pins the root on any platform.

Security — a near-copy of the FoJin data fence's end marker broke out of it (2026-10-21)

rag_query.py and master_builder.py wrap FoJin text in boundary markers and strip
forged markers from the body, but only exact copies. ===== FOJIN 检索数据结束 =====
(one extra space) went through, so the agent saw two end markers and whatever followed
the fake one read as text outside the data. FoJin's data is partly enriched from
editable sources (Wikidata), so the body is attacker-reachable.

  • Both fences now drop any body line that reads as a marker once it is folded the way a
    reader sees it: NFKD, invisibles and combining marks removed, case ignored. That covers
    irregular spaces, tabs, U+3000, NBSP, a different number of =, full-width = / <>
    / letters, zero-width characters and 檢索數據. A line that only mentions FoJin's retrieval
    data in prose has no = run or angle brackets and is kept. The shared helper is
    fojin_bridge.drop_forged_boundary_lines.
  • rag_query.py's real start and end lines carry a per-call random number, and
    prompts/rag_instructions.md says only the end line with the start line's number closes
    the data. A body line cannot know the number. The build-time <<<FOJIN_DATA>>> fence in
    master_builder.py keeps its literal marker, which the analysis prompts document.
  • tests/test_boundary_forgery.py: 21 forged variants for emit, 10 for _fence, and
    prose that must survive. 27 of them failed before the change.