@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.75",
3
+ "version": "0.12.77",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 自体・.git を含むパス・.. やシンボリックリンクで外に出るパスは 'dangerous'。
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 自体・.git・.. やシンボリックリンクで外に出るパスは引き続き dangerous。
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 '.git' not in rel_comps:
2116
+ if not _rm_hits_protected(rel_comps):
2108
2117
  return 'safe'
2109
- # .git を含む → fall through to 'dangerous'
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: