@tyhld/conductor 0.12.74 → 0.12.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/headless.js +108 -1
- package/dist/view.js +55 -12
- package/package.json +1 -1
- package/scripts/hooks/policy.py +38 -6
- package/scripts/hooks/test_policy.py +76 -0
package/dist/headless.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { execFileSync, spawn } from 'node:child_process';
|
|
18
18
|
import { createInterface } from 'node:readline';
|
|
19
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import os from 'node:os';
|
|
22
22
|
import { CENTRAL_BUSY_HOLD_MAX_MS, RELAY_DELIVER_MAX, RELAY_FETCH_TIMEOUT_MS, RELAY_HEARTBEAT_INTERVAL_MS, RELAY_RESTORE_ORPHANS_ENABLED, buildAnchor, buildSendText, canDeliver, completeWithReport, createRelayState, deliveryReport, fetchCommand, fetchCommandStatus, fetchControl, fetchOrphans, isTerminalCommandStatus, kindOf, markDelivered, noteCentralDown, noteCentralUp, noteReadiness, patchStatus, reconnectDelayMs, selectOrphanToRestore, sendHeartbeat, } from './relay.js';
|
|
@@ -546,6 +546,107 @@ export function isAuthError(job) {
|
|
|
546
546
|
export function eventsLogPath(commandId, attempt) {
|
|
547
547
|
return path.join(RUNS_DIR, commandId, `attempt-${attempt}.events.jsonl`);
|
|
548
548
|
}
|
|
549
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
550
|
+
// 実行ログの後片付け(便ごとに増え続けるので、古い便の分を消す)
|
|
551
|
+
//
|
|
552
|
+
// 【なぜ要るか】1便ごとに runs/<便ID>/ が1つ増え、中に生の記録(attempt-N.jsonl・
|
|
553
|
+
// 実物は1行が数千文字)と進捗(attempt-N.events.jsonl)と stderr が入る。
|
|
554
|
+
// 消す仕組みが無いと、動いているPCで際限なく増える(前便 fbaba4b1 の残件)。
|
|
555
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
556
|
+
/**
|
|
557
|
+
* 実行ログを残す期間(ms)。これより古い便のフォルダは消す。
|
|
558
|
+
* ★線引きはこの2つの定数だけ(1か所)。
|
|
559
|
+
*
|
|
560
|
+
* 【なぜ14日か】この記録を読むのは「あの便は何をしたのか」を後から確かめるとき。
|
|
561
|
+
* 受入判定(CC1)は当日〜数日、えふさんの確認と作り直しを挟んでも2週あれば足りる。
|
|
562
|
+
* それを過ぎた便は報告(reports/<指示ID>.md)と devlog に残っているので、
|
|
563
|
+
* 生の記録そのものは要らなくなる。
|
|
564
|
+
*/
|
|
565
|
+
export const RUNS_RETENTION_MS = 14 * 24 * 60 * 60_000;
|
|
566
|
+
/**
|
|
567
|
+
* 残す便のフォルダの最大数(新しい順)。期間内でも、これを超えた分は古い方から消す。
|
|
568
|
+
*
|
|
569
|
+
* 【なぜ50件か】1便の生の記録は実測で数MB〜十数MB。50件なら最悪でも数百MBに収まり、
|
|
570
|
+
* かつ「直近の数日ぶんの便」は必ず残る(1日に50便を超える現場は今のところ無い)。
|
|
571
|
+
* ★期間だけだと短期間に大量の便が来たPCで膨らみ、上限だけだと静かなPCで古い物が
|
|
572
|
+
* いつまでも残る。両方を掛けて初めて上限が決まる。
|
|
573
|
+
*/
|
|
574
|
+
export const RUNS_RETENTION_MAX_DIRS = 50;
|
|
575
|
+
/** 便IDらしい名前だけを消す対象にする(★runs/ の中の見知らぬ物を消さない)。 */
|
|
576
|
+
const RUN_DIR_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
577
|
+
/** そのフォルダの中で、いちばん新しい更新時刻(ms)を返す。読めなければ 0。 */
|
|
578
|
+
function newestMtimeMs(dir) {
|
|
579
|
+
let newest = 0;
|
|
580
|
+
try {
|
|
581
|
+
newest = statSync(dir).mtimeMs;
|
|
582
|
+
for (const name of readdirSync(dir)) {
|
|
583
|
+
try {
|
|
584
|
+
newest = Math.max(newest, statSync(path.join(dir, name)).mtimeMs);
|
|
585
|
+
}
|
|
586
|
+
catch { /* 読めない物は飛ばす */ }
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch { /* フォルダが読めなければ 0 のまま */ }
|
|
590
|
+
return newest;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* 古い便の実行ログを消す。消したフォルダ名を返す。
|
|
594
|
+
*
|
|
595
|
+
* ★消さない物(Fail Safe):
|
|
596
|
+
* - keep に挙げた便(いま作業中の便・直前に終わった便・これから書く便)
|
|
597
|
+
* - 期間内(RUNS_RETENTION_MS)の便
|
|
598
|
+
* - 新しい順に RUNS_RETENTION_MAX_DIRS 件まで
|
|
599
|
+
* - 便IDらしくない名前・フォルダでない物
|
|
600
|
+
* ★失敗しても作業は止めない(後片付けは本業ではない)。
|
|
601
|
+
*/
|
|
602
|
+
export function cleanupRunLogs(opts = {}) {
|
|
603
|
+
const root = opts.root ?? RUNS_DIR;
|
|
604
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
605
|
+
const keep = new Set(opts.keep ?? []);
|
|
606
|
+
const removed = [];
|
|
607
|
+
let names;
|
|
608
|
+
try {
|
|
609
|
+
names = readdirSync(root);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
return removed; // まだ1便も走っていない
|
|
613
|
+
}
|
|
614
|
+
const dirs = [];
|
|
615
|
+
for (const name of names) {
|
|
616
|
+
if (!RUN_DIR_NAME_RE.test(name))
|
|
617
|
+
continue;
|
|
618
|
+
const p = path.join(root, name);
|
|
619
|
+
try {
|
|
620
|
+
if (!statSync(p).isDirectory())
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
dirs.push({ name, at: newestMtimeMs(p) });
|
|
627
|
+
}
|
|
628
|
+
dirs.sort((a, b) => b.at - a.at); // 新しい順
|
|
629
|
+
for (let i = 0; i < dirs.length; i++) {
|
|
630
|
+
const d = dirs[i];
|
|
631
|
+
if (keep.has(d.name))
|
|
632
|
+
continue;
|
|
633
|
+
const tooOld = nowMs - d.at > RUNS_RETENTION_MS;
|
|
634
|
+
const overflow = i >= RUNS_RETENTION_MAX_DIRS;
|
|
635
|
+
if (!tooOld && !overflow)
|
|
636
|
+
continue;
|
|
637
|
+
try {
|
|
638
|
+
rmSync(path.join(root, d.name), { recursive: true, force: true });
|
|
639
|
+
removed.push(d.name);
|
|
640
|
+
}
|
|
641
|
+
catch (err) {
|
|
642
|
+
log(`WARN 古い実行ログを消せません (${d.name}): ${err instanceof Error ? err.message : String(err)}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (removed.length > 0) {
|
|
646
|
+
log(`古い実行ログを ${removed.length} 件片付けました(${RUNS_RETENTION_MS / 86_400_000}日・最大${RUNS_RETENTION_MAX_DIRS}件を残す)`);
|
|
647
|
+
}
|
|
648
|
+
return removed;
|
|
649
|
+
}
|
|
549
650
|
/** 実行ログのディレクトリを作り、stdout / stderr / 進捗 の WriteStream を返す。 */
|
|
550
651
|
function openRunLogs(commandId, attempt) {
|
|
551
652
|
try {
|
|
@@ -649,6 +750,12 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
|
|
|
649
750
|
return null;
|
|
650
751
|
}
|
|
651
752
|
const logs = openRunLogs(cmd.id, attempt);
|
|
753
|
+
// ★後片付けはフォルダが増える瞬間にだけ行う(便が1つ増えたら古い便を1つ片付ける)。
|
|
754
|
+
// これから書く便・いま手元に記録がある便・直前に終わった便は、名指しで残す。
|
|
755
|
+
cleanupRunLogs({
|
|
756
|
+
keep: [cmd.id, loadActiveJob(cfg.site)?.commandId, loadLastJob(cfg.site)?.commandId]
|
|
757
|
+
.filter((id) => typeof id === 'string' && id.length > 0),
|
|
758
|
+
});
|
|
652
759
|
const job = {
|
|
653
760
|
commandId: cmd.id,
|
|
654
761
|
process: proc,
|
package/dist/view.js
CHANGED
|
@@ -26,11 +26,25 @@ import { RUNS_DIR, eventsLogPath, isProcessAlive, loadActiveJob, loadLastJob, ma
|
|
|
26
26
|
// 定数
|
|
27
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* 「最近」の線引き(ms)。★線引きはこの1か所だけ。
|
|
30
|
+
*
|
|
31
|
+
* 【使い所は2つだけ(便 062431f2 の直し)】
|
|
32
|
+
* ① 見守り役の生死が確かめられないときに限り、「作業記録が最近あるか」の判定に使う。
|
|
33
|
+
* ② 🟠 のまま「記録が N分 出ていません」と1行添えるかの線引き。
|
|
34
|
+
*
|
|
35
|
+
* 【★これを「止まった(🟡)」の判定には使わない(えふさん確定 2026-10-02 の直し)】
|
|
36
|
+
* 長い試験(建造くんは4,000件超・TYHLD-platform は8分級)や長い考えごとの間は、
|
|
37
|
+
* 実プロセスが動いていても記録は何分も出ない。その間に「職人は止まった/終了しました」と
|
|
38
|
+
* 出すのは、動いている物を「止まった」と言う嘘になる。
|
|
39
|
+
* 確定の文言は「最近のheartbeat/作業記録」=**どちらかが最近あればよい**。
|
|
40
|
+
*
|
|
41
|
+
* 【なぜ5分か】①は見守り役の生死が分からない機体(systemd も launchd も無い)でだけ働く
|
|
42
|
+
* 予備の判定で、そこでは「記録が出ている」しか手がかりが無い。②の注記は、読む人が
|
|
43
|
+
* 「止まったのでは」と不安になる前に理由を添えるのが目的なので、ふつうの1操作
|
|
44
|
+
* (ファイルを読む・編集する=数十秒)より十分長く、長い試験(8分級)より短い所に置く。
|
|
45
|
+
* ★この値は表示の言葉を変えるだけで、状態(🟠/🟡)を変えない。
|
|
32
46
|
*/
|
|
33
|
-
export const
|
|
47
|
+
export const QUIET_ACTIVITY_MS = 5 * 60_000;
|
|
34
48
|
/** 通常表示に出す「流れ」の行数の上限。 */
|
|
35
49
|
export const FLOW_LINES_MAX = 20;
|
|
36
50
|
/** 詳細表示に出す生ログの行数の上限。 */
|
|
@@ -58,11 +72,20 @@ export const STATUS_NOTE = {
|
|
|
58
72
|
/**
|
|
59
73
|
* 6つの状態のどれかを決める純粋関数(★「作業中」の条件を厳密にする所)。
|
|
60
74
|
*
|
|
75
|
+
* 【えふさん確定(2026-10-02・1文字も変えない)】
|
|
76
|
+
* 🟠 作業中 と表示してよいのは「Claude Codeの実プロセスが動いている +
|
|
77
|
+
* 最近のheartbeat/作業記録がある」場合だけ。
|
|
78
|
+
* =実プロセスは必須。もう一方は **heartbeat か 作業記録 のどちらかが最近あればよい**。
|
|
79
|
+
*
|
|
61
80
|
* ★順番に意味がある。
|
|
62
81
|
* 1. 見守り役が止まっていれば、何も管制へ届かない=💤。
|
|
63
82
|
* 2. えふさんの手番は、動いているかどうかより先に出す(人を待たせないため)。
|
|
64
|
-
* 3.
|
|
65
|
-
*
|
|
83
|
+
* 3. 実行中の仕事があるとき:
|
|
84
|
+
* a. 実プロセスが消えた・終了した・生死を確かめられない → 🟡 未確認。
|
|
85
|
+
* b. 実プロセスが生きていて、見守り役も生きている → 🟠(★記録が止まっていても 🟠)。
|
|
86
|
+
* 長い試験や長い考えごとの最中に「止まった」と言わないため(便 062431f2 の直し)。
|
|
87
|
+
* c. 実プロセスが生きていて、見守り役の生死が確かめられない(null)→
|
|
88
|
+
* 手がかりは記録だけ。最近の記録があれば 🟠、無ければ 🟡。
|
|
66
89
|
* 4. 実行中の仕事が無ければ、直前の結果が在れば ✅、無ければ ⏳。
|
|
67
90
|
*/
|
|
68
91
|
export function decideSiteStatus(input) {
|
|
@@ -73,16 +96,26 @@ export function decideSiteStatus(input) {
|
|
|
73
96
|
if (input.hasActiveJob) {
|
|
74
97
|
if (!input.processAlive)
|
|
75
98
|
return 'unconfirmed';
|
|
76
|
-
if (input.
|
|
77
|
-
return '
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return 'working';
|
|
99
|
+
if (input.relayRunning === true)
|
|
100
|
+
return 'working';
|
|
101
|
+
const activityFresh = input.sinceActivityMs !== null && input.sinceActivityMs <= QUIET_ACTIVITY_MS;
|
|
102
|
+
return activityFresh ? 'working' : 'unconfirmed';
|
|
81
103
|
}
|
|
82
104
|
if (input.hasLastJob)
|
|
83
105
|
return 'done';
|
|
84
106
|
return 'queued';
|
|
85
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* 🟠 のまま添える注記を返す(記録が長く止まっているときだけ1行)。
|
|
110
|
+
* ★状態は変えない。「止まったのでは」と読まれる前に、理由の候補を添えるだけ。
|
|
111
|
+
*/
|
|
112
|
+
export function quietActivityNote(sinceActivityMs) {
|
|
113
|
+
if (sinceActivityMs === null)
|
|
114
|
+
return null;
|
|
115
|
+
if (sinceActivityMs <= QUIET_ACTIVITY_MS)
|
|
116
|
+
return null;
|
|
117
|
+
return `記録が ${friendlyDuration(sinceActivityMs)} 出ていません(長い試験の最中かもしれません)`;
|
|
118
|
+
}
|
|
86
119
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
87
120
|
// 書式
|
|
88
121
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -347,9 +380,19 @@ export function renderNormalView(v) {
|
|
|
347
380
|
else {
|
|
348
381
|
out.push('実作業:—');
|
|
349
382
|
}
|
|
350
|
-
|
|
383
|
+
// ★「いつ」だけでなく「どれだけ前か」も出す(止まっている時間が一目で分かる)。
|
|
384
|
+
const sinceActivityMs = v.lastActivityAt === null ? null : v.nowMs - v.lastActivityAt;
|
|
385
|
+
out.push(v.lastActivityAt !== null
|
|
386
|
+
? `最後に動いた:${friendlyTime(v.lastActivityAt, v.nowMs)}(${friendlyDuration(sinceActivityMs ?? 0)}前)`
|
|
387
|
+
: `最後に動いた:${unknown}`);
|
|
351
388
|
const current = v.flow.length > 0 ? v.flow[v.flow.length - 1].text : '';
|
|
352
389
|
out.push(`現在:${current.length > 0 ? current : unknown}`);
|
|
390
|
+
// ★作業中のまま記録が長く止まっているときは、理由の候補を1行添える(状態は変えない)。
|
|
391
|
+
if (v.status === 'working') {
|
|
392
|
+
const note = quietActivityNote(sinceActivityMs);
|
|
393
|
+
if (note !== null)
|
|
394
|
+
out.push(`※${note}`);
|
|
395
|
+
}
|
|
353
396
|
out.push(RULE);
|
|
354
397
|
if (v.flow.length > 0) {
|
|
355
398
|
for (const l of v.flow)
|
package/package.json
CHANGED
package/scripts/hooks/policy.py
CHANGED
|
@@ -2065,12 +2065,23 @@ def _prod_db_change_result(reason):
|
|
|
2065
2065
|
# 危険でない作業フォルダの絶対パス先頭成分(/tmp/xxx は限定削除として通す)。
|
|
2066
2066
|
_SAFE_ABS_TOP = frozenset({'tmp'})
|
|
2067
2067
|
|
|
2068
|
+
# rm で保護するディレクトリ名(workDir の中であっても削除禁止)。
|
|
2069
|
+
# .git … リポジトリの履歴、.claude … エージェント設定、.tyhld … 番人・道具の設定。
|
|
2070
|
+
_RM_PROTECTED_DIRS = frozenset({'.git', '.claude', '.tyhld'})
|
|
2071
|
+
|
|
2072
|
+
|
|
2073
|
+
def _rm_hits_protected(rel_comps: list) -> bool:
|
|
2074
|
+
"""相対パスの成分リストが保護対象を含むか(.git・.claude・.tyhld のいずれかに当たるか)。"""
|
|
2075
|
+
return bool(_RM_PROTECTED_DIRS.intersection(rel_comps))
|
|
2076
|
+
|
|
2068
2077
|
|
|
2069
2078
|
def rm_path_risk(path: str, work_dir: str = None) -> str:
|
|
2070
2079
|
"""rm の対象パスの危険度: 'dangerous' / 'safe' / 'unknown'。
|
|
2071
2080
|
|
|
2072
2081
|
work_dir が渡されたとき、絶対パスが work_dir の中を指すなら 'safe'(相対パスと同じ扱い)。
|
|
2073
|
-
ただし work_dir
|
|
2082
|
+
ただし work_dir 自体・保護対象(.git/.claude/.tyhld)を含むパス・
|
|
2083
|
+
.. やシンボリックリンクで外に出るパスは 'dangerous'。
|
|
2084
|
+
相対パスでも保護対象を含むなら 'dangerous'。
|
|
2074
2085
|
work_dir が None のとき(非 conductor 環境)は従来どおり。
|
|
2075
2086
|
"""
|
|
2076
2087
|
p = path.strip().strip('"').strip("'")
|
|
@@ -2092,10 +2103,8 @@ def rm_path_risk(path: str, work_dir: str = None) -> str:
|
|
|
2092
2103
|
if not comps:
|
|
2093
2104
|
return 'dangerous'
|
|
2094
2105
|
# ★workDir 内の絶対パスは相対パスと同じ扱い(便 24c9b6b6)。
|
|
2095
|
-
# workDir
|
|
2106
|
+
# workDir 自体・保護対象(.git/.claude/.tyhld)・.. やシンボリックリンクで外に出るパスは dangerous。
|
|
2096
2107
|
# workDir を知れないときは従来どおり止める(Fail Closed)。
|
|
2097
|
-
# ★秘密(.env 等)と設定(.claude 等)は別の検査(_secret_danger / _guard_write_scan)が
|
|
2098
|
-
# 引き続き止めるので、ここでは .git だけを追加で守る。
|
|
2099
2108
|
if work_dir:
|
|
2100
2109
|
try:
|
|
2101
2110
|
resolved = os.path.realpath(p)
|
|
@@ -2104,14 +2113,37 @@ def rm_path_risk(path: str, work_dir: str = None) -> str:
|
|
|
2104
2113
|
pass # workDir 自体 → fall through to 'dangerous'
|
|
2105
2114
|
elif resolved.startswith(work_resolved + os.sep):
|
|
2106
2115
|
rel_comps = resolved[len(work_resolved) + 1:].split(os.sep)
|
|
2107
|
-
if
|
|
2116
|
+
if not _rm_hits_protected(rel_comps):
|
|
2108
2117
|
return 'safe'
|
|
2109
|
-
#
|
|
2118
|
+
# 保護対象を含む → fall through to 'dangerous'
|
|
2110
2119
|
except (OSError, ValueError):
|
|
2111
2120
|
pass # 解決できない → fall through(Fail Closed)
|
|
2112
2121
|
if comps[0] in _SAFE_ABS_TOP and len(comps) >= 2:
|
|
2113
2122
|
return 'safe'
|
|
2114
2123
|
return 'dangerous'
|
|
2124
|
+
# 相対パス: 保護対象を含むなら dangerous(書き方に依らず最終的に消される先で判定)。
|
|
2125
|
+
# work_dir があれば解決して判定、なければ成分名で判定(Fail Closed)。
|
|
2126
|
+
rel_comps = [c for c in p.replace('\\', '/').split('/') if c and c != '.']
|
|
2127
|
+
if '..' in rel_comps:
|
|
2128
|
+
# .. を含む相対パスは解決しないと行き先が分からない
|
|
2129
|
+
if work_dir:
|
|
2130
|
+
try:
|
|
2131
|
+
resolved = os.path.realpath(os.path.join(work_dir, p))
|
|
2132
|
+
work_resolved = os.path.realpath(work_dir)
|
|
2133
|
+
if resolved == work_resolved or not resolved.startswith(work_resolved + os.sep):
|
|
2134
|
+
return 'dangerous'
|
|
2135
|
+
inner = resolved[len(work_resolved) + 1:].split(os.sep)
|
|
2136
|
+
if _rm_hits_protected(inner):
|
|
2137
|
+
return 'dangerous'
|
|
2138
|
+
except (OSError, ValueError):
|
|
2139
|
+
# 解決できない+保護対象の名前を含む → dangerous(Fail Closed)
|
|
2140
|
+
if _RM_PROTECTED_DIRS.intersection(rel_comps):
|
|
2141
|
+
return 'dangerous'
|
|
2142
|
+
else:
|
|
2143
|
+
if _RM_PROTECTED_DIRS.intersection(rel_comps):
|
|
2144
|
+
return 'dangerous'
|
|
2145
|
+
elif _rm_hits_protected(rel_comps):
|
|
2146
|
+
return 'dangerous'
|
|
2115
2147
|
return 'safe'
|
|
2116
2148
|
|
|
2117
2149
|
|
|
@@ -1825,6 +1825,82 @@ finally:
|
|
|
1825
1825
|
else:
|
|
1826
1826
|
os.environ['CONDUCTOR_WORK_DIR'] = _wd_env
|
|
1827
1827
|
|
|
1828
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1829
|
+
# workDir 内の rm: .claude/.tyhld/.git を絶対パス・相対パス両方で保護(便 13954515)
|
|
1830
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1831
|
+
print('\n=== workDir 内の rm: 環境設定の保護(絶対パス) ===')
|
|
1832
|
+
check('workDir 内の .claude/hooks (絶対パス)',
|
|
1833
|
+
policy.rm_path_risk(f'{_WD}/.claude/hooks', _WD), 'dangerous')
|
|
1834
|
+
check('workDir 内の .claude/任意ファイル (絶対パス)',
|
|
1835
|
+
policy.rm_path_risk(f'{_WD}/.claude/any-file.json', _WD), 'dangerous')
|
|
1836
|
+
check('workDir 内の .tyhld (絶対パス)',
|
|
1837
|
+
policy.rm_path_risk(f'{_WD}/.tyhld', _WD), 'dangerous')
|
|
1838
|
+
check('workDir 内の .tyhld/hooks/p.py (絶対パス)',
|
|
1839
|
+
policy.rm_path_risk(f'{_WD}/.tyhld/hooks/p.py', _WD), 'dangerous')
|
|
1840
|
+
check('workDir 内の作業ファイルは引き続き safe (絶対パス)',
|
|
1841
|
+
policy.rm_path_risk(f'{_WD}/src/main.ts', _WD), 'safe')
|
|
1842
|
+
|
|
1843
|
+
print('\n=== workDir 内の rm: 環境設定の保護(相対パス) ===')
|
|
1844
|
+
check('rm -rf .git (相対パス)',
|
|
1845
|
+
policy.rm_path_risk('.git', _WD), 'dangerous')
|
|
1846
|
+
check('rm -rf ./.git (相対パス)',
|
|
1847
|
+
policy.rm_path_risk('./.git', _WD), 'dangerous')
|
|
1848
|
+
check('rm -rf .git/config (相対パス)',
|
|
1849
|
+
policy.rm_path_risk('.git/config', _WD), 'dangerous')
|
|
1850
|
+
check('rm -rf .claude/hooks (相対パス)',
|
|
1851
|
+
policy.rm_path_risk('.claude/hooks', _WD), 'dangerous')
|
|
1852
|
+
check('rm -rf .claude/settings.json (相対パス)',
|
|
1853
|
+
policy.rm_path_risk('.claude/settings.json', _WD), 'dangerous')
|
|
1854
|
+
check('rm -rf .tyhld (相対パス)',
|
|
1855
|
+
policy.rm_path_risk('.tyhld', _WD), 'dangerous')
|
|
1856
|
+
check('rm -rf .tyhld/hooks (相対パス)',
|
|
1857
|
+
policy.rm_path_risk('.tyhld/hooks', _WD), 'dangerous')
|
|
1858
|
+
# .. で保護対象を指す
|
|
1859
|
+
check('sub/../.git (相対パス)',
|
|
1860
|
+
policy.rm_path_risk('sub/../.git', _WD), 'dangerous')
|
|
1861
|
+
check('sub/../.claude/hooks (相対パス)',
|
|
1862
|
+
policy.rm_path_risk('sub/../.claude/hooks', _WD), 'dangerous')
|
|
1863
|
+
check('sub/../.tyhld (相対パス)',
|
|
1864
|
+
policy.rm_path_risk('sub/../.tyhld', _WD), 'dangerous')
|
|
1865
|
+
# 普通の相対パスは引き続き safe
|
|
1866
|
+
check('rm -rf node_modules (相対パス) → safe',
|
|
1867
|
+
policy.rm_path_risk('node_modules', _WD), 'safe')
|
|
1868
|
+
check('rm -rf dist (相対パス) → safe',
|
|
1869
|
+
policy.rm_path_risk('dist', _WD), 'safe')
|
|
1870
|
+
check('rm foo.txt (相対パス) → safe',
|
|
1871
|
+
policy.rm_path_risk('foo.txt', _WD), 'safe')
|
|
1872
|
+
check('rm -rf .next (相対パス) → safe',
|
|
1873
|
+
policy.rm_path_risk('.next', _WD), 'safe')
|
|
1874
|
+
|
|
1875
|
+
print('\n=== workDir 内の rm: decide_bash 経由で環境設定の保護 ===')
|
|
1876
|
+
_wd_env2 = os.environ.get('CONDUCTOR_WORK_DIR')
|
|
1877
|
+
try:
|
|
1878
|
+
os.environ['CONDUCTOR_WORK_DIR'] = _WD
|
|
1879
|
+
# 絶対パス
|
|
1880
|
+
check('decide_bash: rm -rf .claude/hooks (絶対パス) → deny',
|
|
1881
|
+
policy.decide_bash(f'rm -rf {_WD}/.claude/hooks')['decision'], policy.DENY)
|
|
1882
|
+
check('decide_bash: rm -rf .tyhld (絶対パス) → deny',
|
|
1883
|
+
policy.decide_bash(f'rm -rf {_WD}/.tyhld')['decision'], policy.DENY)
|
|
1884
|
+
# 相対パス
|
|
1885
|
+
check('decide_bash: rm -rf .git (相対パス) → deny',
|
|
1886
|
+
policy.decide_bash('rm -rf .git')['decision'], policy.DENY)
|
|
1887
|
+
check('decide_bash: rm -rf .claude/hooks (相対パス) → deny',
|
|
1888
|
+
policy.decide_bash('rm -rf .claude/hooks')['decision'], policy.DENY)
|
|
1889
|
+
check('decide_bash: rm -rf .tyhld (相対パス) → deny',
|
|
1890
|
+
policy.decide_bash('rm -rf .tyhld')['decision'], policy.DENY)
|
|
1891
|
+
# 普通の削除は引き続き allow
|
|
1892
|
+
check('decide_bash: rm -rf node_modules (相対パス) → allow',
|
|
1893
|
+
policy.decide_bash('rm -rf node_modules')['decision'], policy.ALLOW)
|
|
1894
|
+
check('decide_bash: rm -rf dist (相対パス) → allow',
|
|
1895
|
+
policy.decide_bash('rm -rf dist')['decision'], policy.ALLOW)
|
|
1896
|
+
check('decide_bash: rm foo.txt → allow',
|
|
1897
|
+
policy.decide_bash('rm foo.txt')['decision'], policy.ALLOW)
|
|
1898
|
+
finally:
|
|
1899
|
+
if _wd_env2 is None:
|
|
1900
|
+
os.environ.pop('CONDUCTOR_WORK_DIR', None)
|
|
1901
|
+
else:
|
|
1902
|
+
os.environ['CONDUCTOR_WORK_DIR'] = _wd_env2
|
|
1903
|
+
|
|
1828
1904
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
1829
1905
|
print('\n' + '=' * 72)
|
|
1830
1906
|
if failures:
|