Repository navigation
fix(safety): O103 pre-block dangling symlinks, curl/wget output, git core.hooksPath - #115
Merged
Merged
Conversation
…core.hooksPath on protected paths
|
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.
This was referenced Oct 12, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary (O103 — P2 security, #93 follow-up)
Closes three protected-path gaps that #93 left as non-blocking: dangling symlinks, download writes (
curl/wget), andgit config core.hooksPath. Baseline measured on tip6407f90e. 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.../)curl -o/--output/-O/--output-dir,wget -O/-P, attached flagscurl … > .env(redirect)git config [scope] core.hooksPath,git -c core.hooksPath=….git/config$VAR/$(…)download targetgit config user.*/--get/--unsetFull matrix:
/workspace/moss-notes/o103-baseline.md.What changed
classifyProtectedPath— walk symlink chain vialstat+readlink(relative targets, loops capped) so a danglinglink → ~/.ssh/new_keyclassifies before the target exists.detectShellWriteTargets— recognizecurl/wgetoutput flags (-o/--output/-O/--output-dir,-O/--output-document/-P);$/\`` in the target →uncertain` (policy confirms when a protected path is named).findShellProtectedWrites— treatgit config … core.hooksPath <dir>andgit -c core.hooksPath=…as protected git writes; reads/--unset/other keys still pass.UX: reuses O94 confirm /
-pdeny (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).npm run checkgreen. Fullnpm test(Node 22) reported in PR comments / follow-up.Files
src/safety/protected-paths.tssrc/safety/shell-write-detect.tstest/o103-protected-path-gaps.spec.mjs