From c6da18c708682a08bd313d0552c9c2c8aff3149e Mon Sep 17 00:00:00 2001 From: dancinlife Date: Fri, 24 Jul 2026 17:20:18 +0900 Subject: [PATCH] =?UTF-8?q?fix(sign):=20detectSelfMint=20false-positive=20?= =?UTF-8?q?=E2=80=94=20redirect=20token=20after=20'sidecar=20sign'=20misre?= =?UTF-8?q?ad=20as=20mint=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A shell redirect token after `sidecar sign` was read as a mint key, so a routine `sidecar sign 2>/dev/null` / `sidecar sign >out` (bare list + redirect) was wrongly BLOCKED as SIGN-SELF-MINT (caught live on a `sidecar sign 2>&1` diagnostic). Root: segments() splits on unquoted `&`, so `2>&1` left rest[1]=`2>`, and any rest[1] not in the read-verb allowlist was treated as a mint key. Fix: a mint key must match /^[A-Za-z][\w-]*/ (a real word like rent/commons) — redirects/operators/empty fall through to ALLOW (bare list). Real mints (`sidecar sign rent`, `sudo sidecar sign rent`) still block; `sidecar sign check rent 2>&1` still allows (rest[1]=check). Follow-up to #432. Co-Authored-By: Claude Opus 4.8 (1M context) --- ARCHITECTURE.json | 2 +- CHANGELOG.jsonl | 2 +- modules/sign.ts | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ARCHITECTURE.json b/ARCHITECTURE.json index df2f2de..bace1bf 100644 --- a/ARCHITECTURE.json +++ b/ARCHITECTURE.json @@ -82,7 +82,7 @@ "id": "RENT_HUMAN_CONSENT_GATE", "state": "pos-conv", "value": "a GPU-pod rent (`hexa cloud rent|up`) is a real-money action gated on a fresh HUMAN consent token (`!sidecar sign rent`) minted only via the TUI bang — the agent's own `sidecar sign` mint and any write into ~/.sidecar/signs/ are code-blocked (SIGN-SELF-MINT/SIGN-FORGE); one-shot, consumed on admission, TTL/one-shot are code constants (no config opt-out). Gate rests on 'the agent cannot emit a `!` bang', not on the text parser being airtight", - "threshold": "renting a pod costs money but nothing forced a human in the loop — the agent could `hexa cloud rent` unilaterally; ports the retired hexa `_sign_guard.hexa` consent gate (old `! sidecar sign commons` pattern). Indirect rents (bash -c/eval/ssh/pool wrapper) are blocked 'run it directly'; structural closure (hexa itself requiring the token) is a documented phase-2", + "threshold": "renting a pod costs money but nothing forced a human in the loop — the agent could `hexa cloud rent` unilaterally; ports the retired hexa `_sign_guard.hexa` consent gate (old `! sidecar sign commons` pattern). Indirect rents (bash -c/eval/ssh/pool wrapper) are blocked 'run it directly'; structural closure (hexa itself requiring the token) is a documented phase-2. FALSE-POSITIVE fix: detectSelfMint read the token after `sidecar sign` as a mint key even when it was a shell redirect (`sidecar sign 2>/dev/null` → rest[1]=`2>` since segments() splits on unquoted `&`), false-blocking a bare list+redirect — a mint key must now match /^[A-Za-z][\\w-]*/ so operators/empty fall through to ALLOW", "source": "modules/sign.ts" }, { diff --git a/CHANGELOG.jsonl b/CHANGELOG.jsonl index d651526..22ad9a9 100644 --- a/CHANGELOG.jsonl +++ b/CHANGELOG.jsonl @@ -1,3 +1,4 @@ +{"ts":"2026-07-24","title":"fix(sign): detectSelfMint false-positive — redirect token after 'sidecar sign' misread as mint key","body":"Fix a false-positive in the sign self-mint guard (detectSelfMint): a shell redirect token after 'sidecar sign' was misread as a mint key, so a routine 'sidecar sign 2>/dev/null' / 'sidecar sign >out' (bare list with a redirect) was wrongly BLOCKED as SIGN-SELF-MINT (caught live on a diagnostic 'sidecar sign 2>&1'). Root: segments() splits on unquoted & so '2>&1' left rest[1]='2>', and any rest[1] not in the read-verb allowlist was treated as a mint key. Fix: a mint key must match /^[A-Za-z][\\w-]*/ (a real word like rent/commons), so redirects/operators/empty fall through to ALLOW (bare list). Real mints ('sidecar sign rent', 'sudo sidecar sign rent') still block; 'sidecar sign check rent 2>&1' still allows (rest[1]=check). Follow-up to #432."} {"ts":"2026-07-24","title":"feat(sign): GPU-pod-rent consent gate — !sidecar sign rent (human-only one-shot token)","body":"New sidecar sign command + GPU-pod-rent consent gate: renting a pod (hexa cloud rent/up) is a real-money action, so pre bash now BLOCKS it (CLOUD-RENT-UNSIGNED) until the HUMAN mints a one-shot consent token via the TUI bang '!sidecar sign rent' — ported from the retired hexa _sign_guard.hexa (old '! sidecar sign commons' pattern). modules/sign.ts: host-global plain-epoch token store ~/.sidecar/signs/.sign, TTL rent=600s, one-shot (consumed atomically on admission via rename-then-unlink; pre-consume stops parallel double-spend); CLI verbs list/mint/check/clear; helpers signFresh/consumeSign/detectSelfMint/signForgeBashTarget/isSignsPath. cloud-guard.ts: detectCloudRent (direct head-match gated; indirect bash-c/eval/ssh/pool wrapper blocked); fixed pre-existing segment-local weakness — isHexaBuiltin exempted the WHOLE command string so 'hexa cloud pods; raw-create' leaked; now sanctioned per-SEGMENT. pre.ts: SIGN-SELF-MINT + SIGN-FORGE(bash) in preBash, rent gate LAST (never burns the one-shot token), SIGN-FORGE(write) in preWrite. TTL/one-shot are CODE constants (no config opt-out). No new hook wiring: both surfaces already route bash and write. Docs: commands/sign.md, templates/sign.md. Skipped as over-eng (sol): repo-scope hash, JSON/nonce token, MCP pre-tool deny — phase-2. Smoke: unsigned/self-mint/forge block; mint->rent allow+consume->re-block; echo/grep allow; indirect block; seg-local block; tsc+lint+ci green."} {"ts":"2026-07-19","title":"commons: add no-bare-jargon slug","body":"User-facing prose must gloss every jargon/acronym/hyphenated/backtick term at first use (plain-word swap or term(=plain meaning)); elevates the easy-style advisory into an always-on governance hard rule. Also trimmed the lab ARCHITECTURE node under the 300-char cell cap."} {"ts":"2026-07-18","title":"fix(lab): guard child.stdin null (TS18047) — origin/main tsc RED 복구","body":"child.stdin 은 spawn 타입상 Writable|null 인데 널가드 없이 .write/.end 호출 → strict tsc TS18047(2곳: fable·sol 백엔드). origin/main 이 이 상태로 머지돼 sidecar 전체 ci(npx tsc --noEmit)가 RED. optional chaining(child.stdin?.write/?.end)으로 가드 — stdin 이 null 이면 spawn 실패라 error 핸들러가 이미 settle(127) 처리하므로 안전. tsc exit 0 복구."} @@ -27,4 +28,3 @@ {"ts":"2026-07-08","title":"feat(frontier): single north-star objective command — 지정/진행/교체/해제 on a dedicated frontier git ref","body":"신규 sidecar frontier 명령 — 세션/repo 의 단일 north-star 목표(최전선) 추적기. ing(다건 진행보드)과 달리 SINGLE-SLOT. 저장은 ing ref 패턴 미러: 전용 refs/heads/frontier 에 FRONTIER.jsonl(1줄) plumbing(hash-object→mktree→commit-tree→update-ref)+best-effort push origin frontier → branch-switch/reset 무관·공유·protected-main-safe. verb: set(지정·기존 있으면 clobber 거부→swap 유도) · go(진행·push-now 지시 출력+선택 인자를 진행노트 append) · swap(교체·기존 은퇴 id 증가→CHANGELOG+신규 지정, 없으면 set) · clear(해제) · show · inject. 한글 별칭(지정/진행/교체/해제)+designate/advance/replace/release 허용. 셸 특수문자 텍스트는 --stdin. inject 는 SessionStart+per-turn 로 최전선 표면화(미지정=0B 침묵 · git-context/worktree 선례 · advisory only, Stop 강제게이트 없음). 배선 wire-to-prod 양쪽: setup.ts(UserPromptSubmit/SessionStart/PreCompact/PostCompact)+hooks/hooks.json(UserPromptSubmit/SessionStart)+pi/sidecar.ts PER_TURN + pi/CLAUDE.md 매핑 lockstep. 등록 cli/index.ts import+case+HELP · commands/frontier.md 델리게이터 · toolkit write(82→83) · ARCHITECTURE module-frontier 노드. QA 14케이스 ALL PASS(show empty·go-unset warn·set·clobber refuse·go+note·swap id증가·inject set/unset(0B)·한글별칭 지정/진행/해제·usage) · help 파싱 스모크 green."} {"ts":"2026-07-08","title":"feat(worktree+ing): ING↔stranded task linkage (#7) + worktree stop-check last-Stop nudge (#5)","body":"stranded-work 가시성 마무리 2건(Fable 랭킹 #5·#7). (#7 ING↔stranded 링크) ing Item 에 branch? 필드 추가 + ing add/next 가 현재 branch(BOARD_ROOT 기준·≠main/master/HEAD)를 스탬프 → stranded 워크트리/브랜치를 그 태스크로 역링크. 신규 export findIngForBranch(branch,items): Tier1=branch 정확 스탬프 매치(최신), Tier2=무스탬프 구엔트리용 보수적 fallback(전체 branch명 리터럴이 text 에 포함 AND 이름이 distinctive(len≥8||\"/\") AND 유일매치일 때만), 모호/무매치→null(잘못된 매치가 no-match보다 해로우므로 FP-averse). worktree inject + sidecar end 6/6b 의 각 stranded 라인에 ` — 🔄 ING#: ` 인라인(readItems 1콜·stranded 있을 때만·clean=0B). (#5 Stop-time 넛지) 신규 `sidecar worktree stop-check`(warn-only·never block): prompt-scan(다음 프롬프트)·worktree inject(다음 세션)·shipStopCheck(미커밋만)이 모두 못 보는 유일 창=세션 마지막 Stop 에 worktree 에 committed-but-unpushed 남기고 터미널 닫는 경우를 warm-context 에서 상기. stop_hook_active anti-wedge + 0-linked fast-exit(classify per-wt git콜 회피) + git-common-dir keyed dedup(branch@tip: 같은 tip 침묵·새 커밋 재경고·해소된 키 prune) 로 긴 세션서 매 Stop 잔소리 방지. 배선 setup.ts Stop[] + hooks/hooks.json Stop 양쪽(setup-ts-1) + install-hooks --global. Stop 훅은 CC 전용이라 CC만 배선(warn-only → wire-to-prod 위반 아님). help/toolkit/ARCHITECTURE(surface-stop·detail-worktree·module-ing) lockstep. QA: findIngForBranch 5케이스(Tier1/Tier2/short-blocked/no-match/empty) PASS · ing add worktree 내 branch 스탬프 확인 · worktree inject 가 `🔄 ING#N` 링크 표시 · stop-check WARN→dedup 침묵→anti-wedge 침묵→new tip 재경고 전부 출력검증 · ci/help/architecture lint green."} {"ts":"2026-07-08","title":"feat(load+end): active stranded-work gauges — wt N(M⚠)🔴 in load line (TTL-cached) + end classify() upgrade + stranded-branch check","body":"stranded-work 가시성 후속 2건(Fable 랭킹 #4·#6). (#4) load 라인 wt 게이지가 stranded 워크트리를 능동 표시: `wt N(M⚠)` + M>0면 무조건 🔴(개수 3/10 임계 무시) — 매 턴 항상 보이는 표면이라 방치작업이 즉시 눈에 띔. classify()는 워크트리당 ~4 git콜이라 매 UserPromptSubmit sync 핫패스엔 과함 → readOpenPRs 와 동일한 git-common-dir TTL캐시(60s) sync 헬퍼 readStrandedWt(포slain 재사용·stranded=비-main 워크트리 dirty|unpushed 정의 동일), cold 턴만 git콜 지불. (#6) `sidecar end` 세션종료 점검을 classify() 로 업그레이드: 방치(⚠ dirty/unpushed)와 정리대기(○ clean merged)를 구분(기존 raw count는 구분 불가), stranded 워크트리는 warn++ 하고 항목별 resume/폐기 명령 출력 + 신규 6b 항목으로 워크트리없는 stranded 로컬브랜치(strandedBranches 재사용 — 소유자의 'stale 브랜치' 케이스)도 flag. 둘 다 기존 워크트리 감지(classify/strandedWorktrees/strandedBranches) 재사용, 발명 0. help 무변경(내부 게이지·기존 verb). ARCHITECTURE detail-3(wt 게이지 stranded 🔴) lockstep. QA: end 가 라이브 stranded 브랜치(ing) flag·load 가 임시 stranded 워크트리에 `wt 1(1⚠)🔴`·clean시 기존 표시 유지·ci green."} -{"ts":"2026-07-08","title":"feat(worktree+prompt-scan): surface stranded worktrees/branches at session-start & first-action (stderr→emitInject bugfix + worktree inject verb)","body":"세션이 stale 브랜치/워크트리에 방치되는 걸 세션 시작·첫 행동 시점에 잡는다. Fable investigate-mode 조사로 근본원인 확정: 신호는 이미 계산되는데 stderr로 나가 에이전트 context에 안 닿음(배선 결함, detection 결함 아님). (A) prompt-scan.ts 버그픽스: strandedWorktrees() 블록을 stderr→emitInject(stdout additionalContext, Claude Code 프로토콜)로 라우팅 — 매 UserPromptSubmit(첫 행동+지속) 노출, clean이면 0바이트(self-limiting). 헤더 주석은 \"additionalContext로 주입\"이라 주장했는데 실제 stderr였던 문서-코드 불일치를 정정. keyword/hint는 per-turn bloat 회피 위해 stderr 유지. (B) 신규 `sidecar worktree inject` verb(SessionStart+PreCompact+PostCompact WARN·gc 직후): strandedWorktrees()+신규 strandedBranches()(워크트리없는 stranded 로컬브랜치 — classify() 사각지대, 라이브 `ing` 브랜치가 증거)+refs/reaped tip 카운트를 이전세션 이월 방치작업으로 항목별 resume/보존/폐기 명령과 함께 surface, clean이면 0바이트. config worktree.branchAllow 로 의도적 장수브랜치 예외. WARN(SessionStart는 block 불가·FP-averse·persistence가 teeth). 배선: setup.ts SessionStart/Pre/PostCompact[] + hooks/hooks.json SessionStart 양쪽(setup-ts-1) + install-hooks --global + grep 검증. classify()는 isAgent뿐 아니라 모든 linked worktree 열거라 WORKTREE_GONE_ONLY_PILEUP 의 isAgent-only 사각지대 미반복(단 cross-repo는 여전히 repo-local·별개과제). help/toolkit/ARCHITECTURE(detail-worktree·surface-precompact) lockstep. QA: prompt-scan stranded-wt→stdout inject·worktree inject 라이브 ing 브랜치+임시 stranded wt 둘 다 emit·clean=0바이트·ci/help/architecture lint green."} diff --git a/modules/sign.ts b/modules/sign.ts index 3bde128..d8806d4 100644 --- a/modules/sign.ts +++ b/modules/sign.ts @@ -104,8 +104,13 @@ export function detectSelfMint(rawCmd: string): string | null { rest = rest.slice(1); } const base = head.slice(head.lastIndexOf("/") + 1); // ./bin/sidecar, abs paths → sidecar - if (base === "sidecar" && rest[0] === "sign" && !MINT_ALLOWED.has(rest[1] ?? "")) { - return `sidecar sign ${rest[1]}`; + // A mint key is a real WORD token (`rent`, `commons`), not a shell redirect/operator: + // `sidecar sign 2>&1` / `sidecar sign >out` / bare `sidecar sign` are LISTs, not mints, + // so rest[1] like `2>`/`>out`/undefined must NOT be read as a key (that false-blocked a + // routine `sidecar sign 2>/dev/null`). Only a `[A-Za-z]…` key that isn't a read verb mints. + const key = rest[1] ?? ""; + if (base === "sidecar" && rest[0] === "sign" && /^[A-Za-z][\w-]*$/.test(key) && !MINT_ALLOWED.has(key)) { + return `sidecar sign ${key}`; } // shell -c / eval indirection: the inner mint is quoted (masked from the head scan) // → scan the raw segment for a non-read `sidecar sign …`.