Skip to content

fix(safety): O103 pre-block dangling symlinks, curl/wget output, git core.hooksPath - #115

Merged
QiaolongLi1201 merged 2 commits into
mainfrom
fix/o103-protected-path-gaps
Oct 12, 2026
Merged

QiaolongLi1201 merged 2 commits into
mainfrom
fix/o103-protected-path-gaps

Conversation

@QiaolongLi1201

Copy link
Copy Markdown
Collaborator

Summary (O103 — P2 security, #93 follow-up)

Closes three protected-path gaps that #93 left as non-blocking: dangling symlinks, download writes (curl/wget), and git config core.hooksPath. Baseline measured on tip 6407f90e. Only stricter; no off switch. Written by Moss Developer — needs QA security review.

Baseline (tip 6407f90e → this PR)

Probe: findProtectedPathHits (pre) + O101 L3 snapshot around real bash exec.

Gap Tip class After
A dangling symlink (shell / write_file / chain / relative / ../) post-only or missed pre-blocked
B curl -o/--output/-O/--output-dir, wget -O/-P, attached flags post-only pre-blocked
B8 curl … > .env (redirect) already pre-blocked unchanged
C git config [scope] core.hooksPath, git -c core.hooksPath=… post-only / missed (global) pre-blocked
C4 direct edit .git/config already pre-blocked unchanged
D $VAR / $(…) download target post-only / uncertain pre-blocked (uncertain + protected mention)
N ordinary downloads / git config user.* / --get / --unset allowed allowed (no false prompt)

Full matrix: /workspace/moss-notes/o103-baseline.md.

What changed

  1. classifyProtectedPath — walk symlink chain via lstat+readlink (relative targets, loops capped) so a dangling link → ~/.ssh/new_key classifies before the target exists.
  2. detectShellWriteTargets — recognize curl/wget output flags (-o/--output/-O/--output-dir, -O/--output-document/-P); $/\`` in the target → uncertain` (policy confirms when a protected path is named).
  3. findShellProtectedWrites — treat git config … core.hooksPath <dir> and git -c core.hooksPath=… as protected git writes; reads/--unset/other keys still pass.

UX: reuses O94 confirm / -p deny (Blocked: / 已拦截: + category); never L3's 「已被修改」 on the pre-run path.

Tests

  • test/o103-protected-path-gaps.spec.mjs — 53 cases (blocked + allowed + -p/interactive). Revert-each-fix turns red (A:9 / B:14 / C:8 fails).
  • Related: o94, o97, o101, o107, o108, read-scope, shell-write-detect — green.
  • npm run check green. Full npm test (Node 22) reported in PR comments / follow-up.

Files

  • src/safety/protected-paths.ts
  • src/safety/shell-write-detect.ts
  • test/o103-protected-path-gaps.spec.mjs

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Root cause: approval gate denied the tool, but the completion tracker never
recorded a protected-path block, so resolveRunOutcome treated the run as
completed (exit 0) whenever the model claimed success.

- Track protected-path blocks on tool_end; drive incomplete + 被拦 / Blocked by
  from that record (not claim detection). L3 "modified" still wins if the file
  is written another way; user-approved writes stay completed.
- Drop spurious `$F` (git) mention when the download target resolved; unresolved
  `$OUT`/`$(…)` get one honest confirmation line. Pre-block git --config-env=
  core.hooksPath. Optional hooksPath wording.
@QiaolongLi1201
QiaolongLi1201 merged commit 99f4256 into main Oct 12, 2026
8 of 12 checks passed
@QiaolongLi1201
QiaolongLi1201 deleted the fix/o103-protected-path-gaps branch October 12, 2026 10:29
QiaolongLi1201 added a commit that referenced this pull request Oct 12, 2026
* feat(sdk): expose embedded host policies and loop observation

* fix: preserve durable task acceptance and platform verification

* test: keep git and redaction safety fixtures executable across platforms

* fix: honor PATH order for Windows MCP npx startup

* fix: resolve Windows MCP PATH entries against child cwd

* test: force real filter execution in git safety fixtures

* fix(windows): preserve host command quoting and directory permission errors

* fix(task): preserve acceptance authority and durable completion

* fix(task): serialize replay and drain native acceptance commits

* fix(task): clear proven undispatched acceptance prepares

* test: verify requested config directory mode on Windows

* fix: recover undispatched task prepares and reject late suite evidence

* test: run first-run journeys through real Windows ConPTY

* test: distinguish literal Windows paths from localized copy

* test: use a native absolute source checkout fixture path

* fix: preserve native task completion and Windows Git boundaries

* test: observe real Windows config and trust mode requests

* test: verify Windows acceptance child PATH inheritance

* fix(runtime): join task refresh IO and surface terminal storage errors

* test(windows): exercise selected host shell and real cmd fallback

* test(runtime): observe real workspace presence at the drain barrier

* test: isolate registry caches and verify native background shutdown

* test(background): declare the native parent command directly

* test(device): use native canonical folder trust keys

* fix(windows): preserve quoted hook and goal verification commands

* fix(sdk): scope host device tools and preserve real goal verification failures

* test(windows): preserve real egress replacement through PowerShell mv alias

* test(windows): observe real background stops and preserve Git hook hardening

* fix(sdk): expose bounded canonical tool completion output

* fix(merge): deduplicate goal verification shell import

* fix(safety): avoid quadratic URL credential scans

* fix(edit): include full file in partial-read rejection so next edit succeeds (O97)

The read-scope check is unchanged: an edit after an offset/limit read is
still rejected. The rejection now carries the full current file (same
100k-char cap and tool-output budget as read_file) and records it as a
full read, so models stop thrashing and bypassing via node -e. Files over
the cap get explicit guidance and stay locked.

* fix(cli): report PATH scan facts and test Windows termination

* test(exec): reject exceptions as native termination evidence

* fix(edit): partial-read refusal carries full file; block shell writes to refused files (O97)

Read-scope check unchanged. A partial-window refusal now attaches the full
file exactly as read_file presents it (withLineNumbers + read_file
presentToolOutput redaction) and records the read via recordFileState, so
the next edit matches. Credential files get no content/size/first line;
files over 100k chars or the caller's output budget get guidance only and
stay locked. Edit tools get read's output budget so ~20k-char files arrive
whole.

New src/safety/shell-write-detect.ts (detectShellWriteTargets,
canonicalPath, findProtectedWrite) for reuse by O94. Loop guard: after a
read-scope refusal without attachment or edit thrash, exec/bash writes to
that path (realpath-compared) are blocked with 已拦截, as are uncertain
commands (inline code, scripts) that name the file. Unlocks on full
read_file or successful edit; per-run state.

* fix(exec): bind Windows background lifetime to an owned Job

* fix(O94): protected paths need one confirmation even in full mode

Writing/deleting/moving .git, .ssh, shell rc, .env, or Moss config always
asks once in interactive full mode and is denied in -p. Shell write
targets come from O97 detectShellWriteTargets; chmod/chown/chgrp are an
O94-only supplement. Default full mode is unchanged for normal paths.
Reads and git's own .git writes are not gated.

* docs(release-policy): record v0.26.1 install smoke and publish path

latest=0.26.1 live on npm; temp-prefix install renders v0.26.1 (4cf10a9).
Also records the TTY-coupled cli-permission-defaults failure mode seen when
running directory-level npm publish from an interactive terminal.

* fix(cli): fail closed missing Windows hooks and preserve OSC read context

* docs(O94): protected paths are best-effort interception, not a sandbox; script writes tracked as O101

* docs(plan): sync roadmap to upstream main 2a7d187 and npm 0.26.1 reality

Roadmap v1.1: fork's 11 draft PRs all concluded; upstream campaign PRs
#76-#92 merged (O13/O43/O57-58/O66-67/O70/O83/O85-88/O90/O97); npm first
release @rdk-moss/agent@0.26.1 live (rolling policy supersedes P4 gate,
remaining obligations folded into P2/P3); in-flight O94 (#93), O98 (#94),
SDK draft #68. AGENTS.md package name fixed to @rdk-moss/agent.

* fix(O94): -p/--print deny protected paths even when stdin is a TTY

QA BLOCK on #93: approval only took the headless deny path when
!stdin.isTTY, so moss -p on a real terminal fell through to
defaultAskUser and hung. Detect print mode from the printMode option
(wired from parsedArgs.print) or argv -p/--print, and deny immediately.

Also: rm maps to delete/「删除」 in the protected-path prompt; startup
full-mode banner no longer says "no approval prompts" (protected paths
still ask once). Exit code after deny stays with O98.

Tests: TTY+printMode and TTY+argv -p regressions with 2s hang timeout;
rm label; locale banner assertions updated.

* fix(o98): no false success at end of --print runs

- new src/cli/run-completion.ts: track checks actually run + file writes,
  detect unverified success claims, assess completion, format 未完成 line
- oneshot text mode: incomplete runs exit non-zero (ExitCode.INCOMPLETE=9 or
  existing error code) and end stdout with 未完成:…;卡在:…
- test/run-completion-o98.spec.mjs

* fix(o98): export stable RunOutcome for O99

- add src/cli/run-outcome.ts: resolveRunOutcome + formatOutcomeLine
  status completed | incomplete | unverified (false check claims)
- oneshot -p path uses run-outcome for exitCode + last line
- tracker stays in run-completion; O99 imports from run-outcome
- tests cover unverified vs incomplete line prefixes

* fix(o98): VERIFY re-check, 完成 line, 未验证, Stop non-zero

Rebase already on 58c9e3d. Close QA/UX gaps on #94:

- formatOutcomeLine always emits a conclusion (完成 / 未完成 / 未验证 /
  验证失败 / 被拦); drop 未核实 wording
- status: completed | incomplete | verify_failed | blocked | unverified
  (soft unverified = exit 0; VERIFY_CMD / Stop = non-zero)
- VERIFY_CMD always recorded on tracker; continue once then re-verify;
  timeout/spawn not swallowed
- headless Stop hook blocked → non-zero + 被拦 line
- TUI shows outcome under the done stamp
- tests cover A–E plus completed / blocked / TUI line

* feat(cli): O99 show real progress during long runs

Emit real progress at least every 60s on TUI and --print (stderr only):
tool start/end summaries, long-tool heartbeats via tool_progress, and
thinking-wait lines. Map existing tool_execution_progress heartbeats;
no empty working… slogans; unfinished runs get a clear last line on the
progress path (full false-success exit-code gate depends on O98/#94).
Display only — no safety/scheduling change.

* test(O99): expect real tool progress in TUI live spinner

Live spinner now shows the in-flight tool summary instead of Working….
Update tui-run-state and tui-shell assertions; narrow panes may clip
token/queue tails after the tool line.

* fix(o99): wire progress footer to O98 run-outcome

Consume resolveRunOutcome + formatOutcomeLine on the progress path
(no second outcome vocabulary). Unfinished stopReasons → Incomplete/
未完成 via O98; honest complete → non-empty 完成/Completed. Exit codes
remain owned by oneshot's O98 resolveRunOutcome (now on main).

* fix(loop-guard): keep real casing in shell-write block paths (red macos CI)

findProtectedWrite returned the case-folded lock key, so on darwin/win32
the O97 guard relativised a lower-cased path against the real-case
workspace and displayed garbage like `../../t/moss-o97-lock-x/src/big.ts`
instead of `src/big.ts` (macos Test matrix red since #92; exec-write-lock
spec is the regression test). Matching still folds on darwin/win32; the
returned path is now the case-preserving canonical target, the thrash
display map is re-keyed the same way, and the guard base dir is
canonicalised for symlink/case symmetry. Windows failures in
exec-git-config are a pre-existing legacy issue, untouched here.

* fix(o96): do not count lint/test non-zero exits as broken exec

Normal completed exec/run_tests non-zero exits (lint/test fail) no longer
increment byToolFailure or trigger STOP calling it. Timeouts, start
failures, kills, and unusable empty output still count. Same-command
identical-output streaks use a separate counter with a change-code/command
hint (zh+en). Does not touch src/safety or raise failure limits.

* fix(o96): count real exec timeouts as byToolFailure with 超时 copy

Production exec ProcessError timeouts return "timed out after Nms" without
an Error: prefix, so is_error stayed false and loop-guard treated them as
success — falling into the identical-input generic message without 超时.

- isStringToolFailureResult: recognize timed out after / Command killed by
- recordToolLoopOutcome: classify VERIFY_SHELL faults even when isError=false
- e2e: production-shaped + real execTool timeout through executeOneToolCall

* fix(o96): anchor shell fault classification to first line; drop isError=false text scan

Child stdout containing 'timed out after' / 'killed by SIG' / ENOENT /
'command not found' was misclassified as a tool fault. Only trust is_error
from execute-tool-call's anchored isStringToolFailureResult; exit_code: N
header => normal_nonzero regardless of stdout. Adds 4 negative tests (QA).

* fix(config): O93 support agent.reasoningBudget in config set/unset/show/env

* test(safety): O97a ln/touch/rm/shred protected-path cases + O97b .moss/config.json no-leak

* fix(o96): exit 126/127 = start failure with sanitized command name; mark exec timeout/kill structurally

- classifyShellToolFailure: first-line exit_code 126/127 (and Command failed
  (exit 126|127)) => start_failure (未能启动). Stdout never classifies.
- Block copy names the missing command (parsed from shell 'not found' output,
  display-only, [A-Za-z0-9._/-]{1,64}) with next step 装依赖或改命令; otherwise
  generic 未能启动.
- Remove 'timed out after' / 'Command killed by' text regexes from
  isStringToolFailureResult; exec's ProcessError timeout/signal branch calls
  ctx.markResultError() so is_error comes from process facts, not stdout.

* test(ui): O93 allow reasoningBudget literal tokens in zh surface scan

* fix(config): O93 complete UX/QA gates for agent.reasoningBudget

Help (en/zh) explains meaning and cost/latency; text config show lists
value+source; untrusted projects drop reasoningBudget with the usual
skipped-settings tip; empty set lists off|adaptive|high; invalid
MOSS_REASONING_BUDGET warns once (stderr + configWarnings) then falls back.

* fix(config): O93 untrusted project may only lower reasoningBudget

Untrusted folders keep a project agent.reasoningBudget when it is <= the
user effective value (unset user => adaptive); a raise is dropped into
ignoredProjectRouting so config show, -p, and TUI startup share the same
untrusted-folder notice. Help clarifies off = do not escalate (not disable
provider thinking). QA cases + revert-red covered in
test/config-reasoning-budget-o93.spec.mjs.

* feat(O101): detect indirect writes to protected paths (inline/script/post-run)

Three layers on top of O94/O97:
1) Inline python -c / node -e / bash -c with protected-path literals
2) Pre-run scan of workspace scripts (skip npm test/pytest/node --test)
3) Post-run metadata snapshot (mtime/size/hash only) → O98 incomplete

Best-effort interception, not a sandbox. No auto-restore; notices say
「已被修改」not「已拦截」. Delayed background writes are a known limit.

* fix(safety): O105 redact Moss config on direct read_file execute

Defense in depth: readFileTool.execute now runs presentToolOutput for
isMossCredentialPath targets so apiKey never returns plaintext when
bypassing the agent post-hook. Project .moss/devices.json is treated as
a credential path alongside config.json. Directed tests cover execute
redaction, agent-path regression, and non-credential fixtures.

* fix(O101): tip adapt — notice on all exec exits, stronger A/D asserts

- Attach layer-3 notice on signal-kill and non-zero exit paths too
  (command already ran; detect after any exit).
- A1–A3 assert O101 inline source metadata (revert-red for O101 path;
  -p deny itself still covered by O97 mentions).
- D4 asserts filterExpectedGitMetadataChanges empties git porcelain.

* fix(safety): O107 follow credential realpath/symlink (O105 follow-up)

isMossCredentialPath only path.resolve'd, so a workspace symlink into
.moss/config.json, devices.json, ~/.moss/*, ~/.config/moss/*, or
.apikey-key bypassed the O105 readFileTool.execute redaction gate.

Follow realpath (and readlink when dangling) like protected-paths /
canonicalPath; tighten isRawApiKeyFile the same way. Ordinary file
symlinks stay ungated. No kill-switch; O105 asserts unchanged.

Tests: symlink→config/devices redact; notes symlink plaintext;
dangling→config fail-closed; revert-red without follow.

* fix(o108): wire O101 layer-3 protected-path snapshot to exec_background exit

Run the same takeProtectedPathSnapshot/diff helpers on background process
close/error (full exit), not when the tool returns a job id while still
running. Append 「已被修改」 notice to the buffer; fold hits into the run
outcome tracker via noteBackgroundProtectedPathMods so late exits still
mark incomplete / non-zero. Docs: exec_background covered; &/nohup and
post-Moss-exit writes remain known limits.

* fix(hooks): reject non-string Moss hook command at config load (O89) (#104)

Validate hooks.*.command is a string in loadConfigFile so a bad object/array/
number/boolean/null never reaches formatHookFailure as [object Object].
Error copy (en/zh) includes the config path and event index; add focused tests.

Co-authored-by: Moss Developer 2 <dev2@local>

* fix(o109): soft Unverified exits 9 (ExitCode.INCOMPLETE) (#105)

When -p ends with「未验证」/ Unverified (claimed checks passed but none
ran this session), process exit is now 9 instead of 0 so scripts can tell
failure/unverified from success. Last-line copy unchanged; Completed and
pure Q&A still exit 0; verify_failed / incomplete / blocked stay non-0.

* docs(readme): note Unverified shares exit 9 with incomplete (O109 follow-up) (#106)

* fix(config): register MOSS_TOOL_LOOP_SAME_COMMAND_LIMIT in config env (O112) (#108)

moss config env is the authoritative MOSS_* reference. O96 added
MOSS_TOOL_LOOP_SAME_COMMAND_LIMIT in tool-loop-guard but never listed it,
so cli-config-env failed on main and the fail-fast suite stopped there.

Also register MOSS_LOCALE (read since O101 for protected-path notice locale);
without it the same coverage test stays red after the O112 key alone.

No loop-guard or test-allowlist changes.

* fix(run-completion): EN/ZH claim parity (O110) (#107)

* fix(run-completion): align ZH claim/request synonyms with EN (O110)

* fix(run-completion): ignore ZH Q&A/conditional frames for claim parity (O110)

Keep EN/ZH synonym alignment (修好↔Fixed, 检查全绿↔All checks green), but
do not treat question/hypothetical soft verbs (能不能修好 / 改好之后) as
expecting file writes, and do not treat explanatory/conditional 通过/全绿
(才算/表示/等…后; means / wait until … then merge) as success claims.

Adds EN/ZH paired cases C2–C7 (exit 0). Existing EN outcomes and O98
assertions unchanged; exit 9 semantics unchanged.

* fix(run-outcome): omit stuck unknown; fill concrete class (O111) (#109)

* fix(run-outcome): omit stuck unknown; fill concrete class (O111)

Incomplete / blocked / verify_failed last lines no longer fall back to
「卡在:未知」 / `stuck at: unknown`. When stuckAt is known (failed tool,
Stop hook, VERIFY_CMD, turn/budget class), append it; otherwise omit the
stuck half and keep only the main reason. Exit codes and O98/O109/O110
semantics unchanged.

* fix(run-outcome): omit empty reason; combine budget class with last step (O111)

formatOutcomeLine no longer falls back to a placeholder reason; an empty
reason is omitted like the stuck half. When an unfinished class (turn /
budget / …) and a tool step are both known, stuck reads e.g.
'run budget (read_file: src/a.ts)' / '运行预算(read_file: src/a.ts)' so
the -p stdout line and progress stderr line agree. Exit codes unchanged.

* fix(ui): O91 Chinese display names for Moss-format hook events (#110)

Add a single copy table (hook-event-names.ts) so zh UI stderr/blocked/
help/TUI copy names hooks in Chinese (e.g. 停止前钩子) instead of bare
English event ids. Table values omit the 「钩子」 suffix; sentence
templates append it. Config keys, MOSS_HOOK_EVENT, and en UI stay
English. Directed tests cover the 8-spec table + formatHookFailure;
hooks-fail-closed zh expectations updated. No LITERAL_TOKENS expansion;
src/safety untouched.

* fix(O100): end Node gate output with upgrade + reinstall next steps (#112)

* fix(run-outcome): 被拦 wording + progress tracker (O113) (#111)

* fix(run-outcome): label safety block as 被拦; pass progress tracker (O113)

Safety/approval denials on incomplete runs now use 「被拦:…」 / `Blocked by: …`
with the tool/command instead of looking like a normal 「卡在:」. Non-block cases
(tool fail, zero writes, budget) stay 「卡在」 or omit — never mislabeled.

Progress stderr (output.ts done) passes the progress completionTracker into
formatProgressOutcomeLine so budget-exhausted lines match stdout, e.g.
「卡在:运行预算(read_file: …)」.

Exit codes and src/safety unchanged; O111 omit-unknown preserved.

* test: track O91 Stop zh display name (停止前钩子) in O111/O113 specs

* fix(provider): O114 retry mid-stream stalls; inter-event idle default truly 60s (#113)

* fix(provider): O114 retry mid-stream stalls; inter-event idle default truly 60s

- pi-ai-watchdog: inter-event idle no longer Math.min()'d with the 45s
  first-event deadline; default 60s, env overrides independent. Error
  reports the timeout of the phase that fired.
- classifier: PiAiFirstEventTimeoutError phase=inter -> retryable timeout,
  retried within existing llm-call budget (maxLLMRetries default 2 = up to
  2 retries). Exhausted -> LlmRetriesExhaustedError, PROVIDER_UPSTREAM_ERROR.
- phase=first untouched.
- test: o114-stream-stall-retry.spec.mjs; pi-ai-watchdog.spec pins inter env.

* fix(o114): stall retries actually fire on MossAgent path; honest exhausted outcome

- llm-provider-stream-adapter: keep original error (non-enumerable) on the
  error event; stream helper rethrows a phase=inter stall as-is so the
  retry policy sees it (previously flattened to non-retryable MossError).
- agent-error-outcome: exhausted mid-stream stall -> recoverable:false
  (scoped; other wrapped errors unchanged).
- watchdog: inter message starts with 上游流停滞 and carries an English
  'Upstream stream stalled' sentence.
- -p / progress footer: exhausted stall says 上游流停滞 / upstream stream
  stalled instead of generic 'error during execution'.
- tests: explicit default env clear; recoverable=false; process-level
  json + -p zh/en exit 6 spec.

* fix(run-outcome): zh budget-exceeded tail without English sentence (O116) (#114)

* fix(safety): O103 pre-block dangling symlinks, curl/wget output, git core.hooksPath (#115)

* fix(safety): O103 pre-block dangling symlinks, curl/wget output, git core.hooksPath on protected paths

* fix(safety): O103 blocked protected writes end Incomplete/被拦 exit 9

Root cause: approval gate denied the tool, but the completion tracker never
recorded a protected-path block, so resolveRunOutcome treated the run as
completed (exit 0) whenever the model claimed success.

- Track protected-path blocks on tool_end; drive incomplete + 被拦 / Blocked by
  from that record (not claim detection). L3 "modified" still wins if the file
  is written another way; user-approved writes stay completed.
- Drop spurious `$F` (git) mention when the download target resolved; unresolved
  `$OUT`/`$(…)` get one honest confirmation line. Pre-block git --config-env=
  core.hooksPath. Optional hooksPath wording.

* fix(o106): treat Windows exit 9009 as start_failure like 127 (#116)

On win32, cmd.exe "command not found" returns 9009. Classify it the same
as 126/127 (launch failure) for loop-guard byToolFailure; non-win32 9009
stays normal_nonzero. Platform is injectable for tests. Missing command
name reuses the O96 safe-charset path.

* fix(run-outcome): O121 every safety refusal → Incomplete exit 9 'Blocked by: <what>'; loop-guard stops → stuck at, not blocked

- run-completion: classifySafetyBlock() records dangerous command, protected
  path, O97 partial-read edit, hook block, -p approval deny, workspace policy
  and sandbox refusals into tracker.safetyBlocks (gate: loop outcome
  denied/blocked or the exec tools' own refusal prefix). Partial-read block is
  cleared when the same path is later edited successfully.
- Loop-guard short-circuits (PM): tracker.loopGuardStops → Incomplete exit 9,
  stuck at 'exec failed to start (command `X` not found)', never Blocked by.
  Tool line copy 已拦截/Blocked → 已停止重试/Stopped retrying.
- run-outcome: one main cause + (+N more)/(另有 N 处); names what was refused.
- JSON/stream-json: result is_error, error = same conclusion line,
  recoverable:false, exit 9.
- test/o121-safety-block-outcome.spec.mjs: process-level -p cases zh+en.

* fix(run-completion): O121 unrecognized pre-blocks (Unknown tool …) are failures, not safety blocks

* test(O121): win32 9009 loop-guard stop → stuck at exec 未能启动, not Blocked

* docs(readme): split the bilingual README into zh main + README.en.md

One 900-line file mixing both languages read as clutter. README.md is now
the Chinese document; the English mirror lives in README.en.md with its
own badges, promoted headings, and file-level language switchers.
Section parity: 15 sections + 6 subsections on both sides; split replayed
on top of the latest main so O109/O94/O101 entries carry over.

* fix(O121): json/stream-json loop-guard stop → exit 9, error_during_execution, recoverable:false, error = 卡在 line (UX BLOCK #118)

* fix(O121): json/stream-json fail from resolveRunOutcome (exitCode≠0) for every unfinished outcome; tests for loop-guard, budget, stream stall (QA #118)

* test: de-flake git-spawn fixture; win32 portability for exec-git-config spec

* test: correct win32 hooks skip reason (POSIX-only assertions; win32 covered by O76)

* test: report win32 hooks partial skip via t.skip instead of silent pass

* test runner: under CI run all files and summarize failures; local stays fail-fast

* test(readme): assert zh README.md and en README.en.md separately after split; ship README.en.md

ad58a7d split the README into Chinese README.md + English README.en.md and
turned ubuntu Test red. English sentences are now asserted in README.en.md,
Chinese ones in README.md, shared commands in both (no assertion dropped).
Ship README.en.md in the npm package and use absolute GitHub URLs for the
language links so they work on npmjs.com.

* moss-self: O65 assert background pid parses before exit check

* fix(safety): O129 realpath workspace/HOME in script scan and protected anchors (macOS o101 red)

* test(O129): -p process-level deny for symlinked/outside-workspace scripts; name outside script in -p notice

* fix(O128,O122): partial-read edit gate holds on retry; read_file never answered from replay cache

- O97 attached body no longer stamps a full read (tool-helpers partialReadRecovery); loop-guard locks on every refusal
- every partial-read refusal ends with the same next step (en+zh)
- read_file metadata.noReplay: loop replay cache skipped so a full re-read registers (O122)
- process test with 48k-char file + small-file QA repro; O97 unit tests tightened

* fix(O128): QA review — shell lock resolves $PWD/$(pwd)/`pwd`/$VAR targets; credential + noReplay-scope + exec-bypass tests

- shell-write-detect: expand $PWD/${PWD}/$HOME before resolving; run-time-built targets match a locked path by literal suffix (only adds blocks)
- findLoopReplay exported: noReplay only on read_file, other read-only builtins still replay (test)
- process tests: sed -i, cat > ./x, ../dir/x, tee, python, symlink alias, $PWD, $D, $(pwd), backtick all blocked after partial-read refusal
- credential files (.moss/config.json, .env, .apikey-key, *.credentials): no body on refusal or any resend

* fix(O128): refuse whole-file write_file over a file whose full read was truncated (PM ruling); edits unchanged

* feat(O128): gate-type blocks (partial-read, truncated write) are resolvable by realpath via prescribed tools; exec never resolves

* fix(runtime): avoid redundant terminal and task probes

* fix(O128): unresolved resolvable gate blocks use 「还没改」 last line, never 「被拦」

* test(tui): preserve rejecting fixture without lint suppression

* test: preserve native Windows fixture execution and cleanup

* fix(safety): O117 L3 .git/config allow-list (fail-closed), watch config.worktree, name keys in last line

* fix(safety): O117 allow remote/branch upstream leaves and https/ssh/scp remote.*.url by value (PM 口径)

* fix(safety): O117 check branch.*.remote by value (name, '.', or safe URL)

* fix(safety): O123 L3 path allow-list for ordinary git commit/checkout/merge (HEAD/refs judged by content)

* fix(safety): O127 L3 snapshot follows linked-worktree .git file to gitdir/commondir

* fix(safety): O127 pre-check no longer treats a bare cd operand (workspace root) as an ancestor-delete target

* fix(safety): O127 cd exemption only when target realpath is the workspace root (cd / and cd ~ still prompt)

* fix(safety): O127 cancel workspace-root cd exemption after a second dir change

QA B1: exemption checked only that the first cd operand was the workspace
root, so `cd <ws> && cd && node x`, `cd <ws> && cd $HOME && node x`, and
similar follow-up cd/pushd/popd/chdir (any operand) stayed silent. Count
directory-changing builtins; allow the exemption only when there is at most
one. Add P4 regression tests that go red when this guard is reverted.

* fix: reject dangling runtime gitignore entries before creation

* test: recognize exact public endpoint in locale oracle

* O130: block pwsh/cmd writes into Moss config dir via env vars (P1 security) (#129)

* fix(safety): O130 gate Windows/PowerShell writes into Moss config dirs ($env:/%VAR%/Set-Content/process.env)

* fix(safety): O130 gate PowerShell/env-form write targets into Moss config dir; block unresolvable write targets

- new src/safety/pwsh-write-targets.ts: write targets of Set-Content/Add-Content/
  Out-File/New-Item/Copy-Item/Move-Item/Tee-Object/Clear-Content (+ sc/ac/ni/cpi/mi/
  copy/move/tee/cp/mv), [IO.File]::Write*/Append*, and >, >>, 2>, *>, &>
- $env:X / ${env:X} / %X% / $X expanded from the current env (case-insensitive);
  unknown vars, vars assigned earlier on the same line, command substitution,
  relative targets after an unresolvable cd -> blocked as unresolved
- only write targets are examined: $ in args / reads never prompts
- inline interpreter env-concat detection scoped to node -e / python -c etc.
- UX copy for unresolved variable targets (reason + next step, zh/en)
- o94: set USERPROFILE alongside HOME (Windows os.homedir), no assertion change

* fix(safety): O130 close QA BLOCK gaps — abbrev flags, paren targets, writers, one-level nest

- Value-flag abbreviations match by PowerShell unique-prefix (-En/-Va/-Enc/-Fi)
  so they cannot steal the path positional slot
- Parenthesized expression write targets (Join-Path, string concat) always block
  as unresolvable
- Add writers: Set-Item, Export-Clixml/Csv, Invoke-WebRequest -OutFile, curl -o/-O,
  xcopy/robocopy, New-Object IO.StreamWriter
- One-level nested scan for &{}/.{} / pwsh -c / iex (same rules inside);
  -EncodedCommand always blocks; deeper → O148 + O143 L3
- Temporarily rename o130-*.spec.mjs → aa-o130-*.spec.mjs so Windows CI runs them
  before exec-git-config (pre-existing red) aborts the suite; proper fix O149 (P2)

* style: prettier on O130 pwsh-write-targets after main merge

* fix(O130): block EncodedCommand unique prefixes (-EncodedC/-ec/-e)

hasEncodedCommand only matched -enc or the full -EncodedCommand name, so
mid-prefixes (-EncodedC, -EncodedCom) and documented aliases (-ec, -en, -e)
passed. Match by unique-prefix of EncodedCommand (VALUE_FLAGS style) plus
-ec; tests cover mid-prefixes and go red when the old regex is restored.

* moss-self: O74 device-bench sweeps stale temp dirs on startup (#126)

* moss-self: O74 sweep stale device-bench provider config dirs at startup

* moss-self: O74 sweep only dirs whose owner pid is dead (review rework 1)

* test: await event-triggered task artifact refresh

* ci: make Test blocking again; add Test summary gate job (CI gate part 2) (#120)

* ci: make Test blocking again and add Test summary gate job

* ci: set top-level permissions to contents: read

* ci: Test summary blocks on ubuntu/macOS only; Windows reported, TODO(2026-10-15) make blocking (O134)

* ci: Windows Test blocks Test summary automatically after 2026-10-15 (UTC+8); manual override WINDOWS_BLOCKING=auto|true|false (O134)

* fix(goal-verify): keep non-zero ProcessError a rejection so FAIL/one-continuation branch runs

Merging codex/studio-public-sdk introduced a .catch() that normalized a
plain non-zero VERIFY_CMD exit into a resolved value, recording the run
as passed (exit 0, no continuation turn) — breaking run-completion-o98
and the goal-loop case in dotenv-safety-env. Restore the main semantics.

* test(goal-verify): expect honest non-zero exit and the re-verify pass after continuation

With the rejection semantics restored, a failing VERIFY_CMD now exits
ExitCode.INCOMPLETE (9) instead of 0, and the continuation turn
re-verifies (marker records exit 7 twice) — matching the documented
'the continuation always re-verifies' contract and O98 no-false-success.

---------

Co-authored-by: QiaolongLi1201 <qiaolong.li@d-robotics.cc>
Co-authored-by: Moss Grok <moss-grok@local>
Co-authored-by: Moss Developer 2 <dev2@local>
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