@tyhld/conductor 0.12.75 → 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/package.json +1 -1
- package/scripts/hooks/policy.py +38 -6
- package/scripts/hooks/test_policy.py +76 -0
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:
|