@tyhld/conductor 0.12.71 → 0.12.72

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 CHANGED
@@ -427,6 +427,7 @@ export function spawnClaudeProcess(cfg, cmd, sessionId, attempt, isResume) {
427
427
  delete env.ANTHROPIC_API_KEY;
428
428
  env.CONDUCTOR_SITE = cfg.site;
429
429
  env.CONDUCTOR_MACHINE = cfg.machine;
430
+ env.CONDUCTOR_WORK_DIR = cfg.workDir;
430
431
  // ★テスト用: CONDUCTOR_CLAUDE_CONFIG_DIR を設定すると、claude の設定フォルダを差し替える。
431
432
  // 本物のログインを壊さずに認証エラーを再現できる(空のフォルダを指す → not logged in)。
432
433
  // 本番では設定しない(claude は既定の ~/.claude を使う)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.71",
3
+ "version": "0.12.72",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2066,8 +2066,13 @@ def _prod_db_change_result(reason):
2066
2066
  _SAFE_ABS_TOP = frozenset({'tmp'})
2067
2067
 
2068
2068
 
2069
- def rm_path_risk(path: str) -> str:
2070
- """rm の対象パスの危険度: 'dangerous' / 'safe' / 'unknown'。"""
2069
+ def rm_path_risk(path: str, work_dir: str = None) -> str:
2070
+ """rm の対象パスの危険度: 'dangerous' / 'safe' / 'unknown'。
2071
+
2072
+ work_dir が渡されたとき、絶対パスが work_dir の中を指すなら 'safe'(相対パスと同じ扱い)。
2073
+ ただし work_dir 自体・.git を含むパス・.. やシンボリックリンクで外に出るパスは 'dangerous'。
2074
+ work_dir が None のとき(非 conductor 環境)は従来どおり。
2075
+ """
2071
2076
  p = path.strip().strip('"').strip("'")
2072
2077
  if p == '':
2073
2078
  return 'unknown'
@@ -2086,14 +2091,34 @@ def rm_path_risk(path: str) -> str:
2086
2091
  comps = [c for c in p.split('/') if c]
2087
2092
  if not comps:
2088
2093
  return 'dangerous'
2094
+ # ★workDir 内の絶対パスは相対パスと同じ扱い(便 24c9b6b6)。
2095
+ # workDir 自体・.git・.. やシンボリックリンクで外に出るパスは引き続き dangerous。
2096
+ # workDir を知れないときは従来どおり止める(Fail Closed)。
2097
+ # ★秘密(.env 等)と設定(.claude 等)は別の検査(_secret_danger / _guard_write_scan)が
2098
+ # 引き続き止めるので、ここでは .git だけを追加で守る。
2099
+ if work_dir:
2100
+ try:
2101
+ resolved = os.path.realpath(p)
2102
+ work_resolved = os.path.realpath(work_dir)
2103
+ if resolved == work_resolved:
2104
+ pass # workDir 自体 → fall through to 'dangerous'
2105
+ elif resolved.startswith(work_resolved + os.sep):
2106
+ rel_comps = resolved[len(work_resolved) + 1:].split(os.sep)
2107
+ if '.git' not in rel_comps:
2108
+ return 'safe'
2109
+ # .git を含む → fall through to 'dangerous'
2110
+ except (OSError, ValueError):
2111
+ pass # 解決できない → fall through(Fail Closed)
2089
2112
  if comps[0] in _SAFE_ABS_TOP and len(comps) >= 2:
2090
2113
  return 'safe'
2091
2114
  return 'dangerous'
2092
2115
  return 'safe'
2093
2116
 
2094
2117
 
2095
- def _rm_danger(command: str):
2118
+ def _rm_danger(command: str, work_dir: str = None):
2096
2119
  """再帰的 rm の対象パスを全部見て、危険なものがあれば理由を返す。"""
2120
+ if work_dir is None:
2121
+ work_dir = os.environ.get('CONDUCTOR_WORK_DIR') or None
2097
2122
  for m in re.finditer(r'\brm\b', command):
2098
2123
  seg = _SPLIT_RE.split(command[m.end():], maxsplit=1)[0]
2099
2124
  toks = seg.split()
@@ -2101,7 +2126,7 @@ def _rm_danger(command: str):
2101
2126
  recursive = any(('r' in f.lstrip('-').lower()) or f in ('--recursive',) for f in flags)
2102
2127
  paths = [t for t in toks if not t.startswith('-')]
2103
2128
  for p in paths:
2104
- risk = rm_path_risk(p)
2129
+ risk = rm_path_risk(p, work_dir=work_dir)
2105
2130
  if risk == 'dangerous':
2106
2131
  return f'rm on dangerous path ({p})'
2107
2132
  if risk == 'unknown' and recursive:
@@ -1742,6 +1742,89 @@ try:
1742
1742
  finally:
1743
1743
  shutil.rmtree(_secret_dir, ignore_errors=True)
1744
1744
 
1745
+ # ─────────────────────────────────────────────────────────────────────────────
1746
+ # workDir 内の rm 絶対パス判定(便 24c9b6b6)
1747
+ # ─────────────────────────────────────────────────────────────────────────────
1748
+ print('\n=== workDir 内の rm 絶対パス: 作業ファイルの絶対パスは safe ===')
1749
+ _WD = '/home/goto/projects/goto-renshu'
1750
+ check('workDir 内の作業ファイル (絶対パス)',
1751
+ policy.rm_path_risk('/home/goto/projects/goto-renshu/goto-renshu-check.txt', _WD), 'safe')
1752
+ check('workDir 内の深いパス',
1753
+ policy.rm_path_risk('/home/goto/projects/goto-renshu/sub/dir/file.txt', _WD), 'safe')
1754
+ # git リポジトリでない作業フォルダでも通る
1755
+ _WD2 = '/home/user/projects/work'
1756
+ check('workDir 内の作業ファイル (別のユーザー)',
1757
+ policy.rm_path_risk('/home/user/projects/work/output.log', _WD2), 'safe')
1758
+
1759
+ print('\n=== workDir 内の rm 絶対パス: 引き続き止まるもの ===')
1760
+ # workDir 自体
1761
+ check('workDir 自体の削除',
1762
+ policy.rm_path_risk('/home/goto/projects/goto-renshu', _WD), 'dangerous')
1763
+ check('workDir/ (末尾にスラッシュ)',
1764
+ policy.rm_path_risk('/home/goto/projects/goto-renshu/', _WD), 'dangerous')
1765
+ # workDir の外
1766
+ check('workDir の外 (親)',
1767
+ policy.rm_path_risk('/home/goto/projects/other-repo/file.txt', _WD), 'dangerous')
1768
+ check('workDir の外 (/etc)',
1769
+ policy.rm_path_risk('/etc/passwd', _WD), 'dangerous')
1770
+ # .. で外に出るパス
1771
+ check('.. で workDir の外',
1772
+ policy.rm_path_risk('/home/goto/projects/goto-renshu/../other/file', _WD), 'dangerous')
1773
+ # .git
1774
+ check('workDir 内の .git',
1775
+ policy.rm_path_risk('/home/goto/projects/goto-renshu/.git', _WD), 'dangerous')
1776
+ check('workDir 内の .git/ 配下',
1777
+ policy.rm_path_risk('/home/goto/projects/goto-renshu/.git/config', _WD), 'dangerous')
1778
+ check('workDir 内の sub/.git',
1779
+ policy.rm_path_risk('/home/goto/projects/goto-renshu/sub/.git/HEAD', _WD), 'dangerous')
1780
+ # / ~ /home はそもそも dangerous(workDir に関係なく)
1781
+ check('/ (ルート)',
1782
+ policy.rm_path_risk('/', _WD), 'dangerous')
1783
+ check('~ (ホーム)',
1784
+ policy.rm_path_risk('~', _WD), 'dangerous')
1785
+ check('/home',
1786
+ policy.rm_path_risk('/home', _WD), 'dangerous')
1787
+
1788
+ # workDir を知れないとき → 従来どおり dangerous
1789
+ check('workDir なし → dangerous',
1790
+ policy.rm_path_risk('/home/goto/projects/goto-renshu/file.txt'), 'dangerous')
1791
+ check('workDir なし → dangerous (None)',
1792
+ policy.rm_path_risk('/home/goto/projects/goto-renshu/file.txt', None), 'dangerous')
1793
+
1794
+ print('\n=== workDir 内の rm: decide_bash 経由で秘密・設定は引き続き止まる ===')
1795
+ # ★ .env 等は _secret_danger が止める(rm_path_risk の変更とは別の検査)
1796
+ _wd_env = os.environ.get('CONDUCTOR_WORK_DIR')
1797
+ try:
1798
+ os.environ['CONDUCTOR_WORK_DIR'] = _WD
1799
+ check('rm .env (workDir 内・絶対パス) → deny',
1800
+ policy.decide_bash(f'rm {_WD}/.env')['decision'], policy.DENY)
1801
+ check('rm .dev.vars (workDir 内) → deny',
1802
+ policy.decide_bash(f'rm {_WD}/.dev.vars')['decision'], policy.DENY)
1803
+ check('rm .npmrc (workDir 内) → deny',
1804
+ policy.decide_bash(f'rm {_WD}/.npmrc')['decision'], policy.DENY)
1805
+ check('rm id_rsa (workDir 内) → deny',
1806
+ policy.decide_bash(f'rm {_WD}/id_rsa')['decision'], policy.DENY)
1807
+ check('rm server.key (workDir 内) → deny',
1808
+ policy.decide_bash(f'rm {_WD}/server.key')['decision'], policy.DENY)
1809
+ check('rm server.pem (workDir 内) → deny',
1810
+ policy.decide_bash(f'rm {_WD}/server.pem')['decision'], policy.DENY)
1811
+ # .git は rm_path_risk 自体が止める
1812
+ check('rm -rf .git (workDir 内) → deny',
1813
+ policy.decide_bash(f'rm -rf {_WD}/.git')['decision'], policy.DENY)
1814
+ # .claude/settings.json は _guard_write_scan が止める
1815
+ check('rm .claude/settings.json (workDir 内) → deny',
1816
+ policy.decide_bash(f'rm {_WD}/.claude/settings.json')['decision'], policy.DENY)
1817
+ # 作業ファイルは通る
1818
+ check('rm 作業ファイル (workDir 内・絶対パス) → allow',
1819
+ policy.decide_bash(f'rm {_WD}/goto-renshu-check.txt')['decision'], policy.ALLOW)
1820
+ check('rm 作業ファイル (workDir 内・相対パス) → allow',
1821
+ policy.decide_bash('rm goto-renshu-check.txt')['decision'], policy.ALLOW)
1822
+ finally:
1823
+ if _wd_env is None:
1824
+ os.environ.pop('CONDUCTOR_WORK_DIR', None)
1825
+ else:
1826
+ os.environ['CONDUCTOR_WORK_DIR'] = _wd_env
1827
+
1745
1828
  # ─────────────────────────────────────────────────────────────────────────────
1746
1829
  print('\n' + '=' * 72)
1747
1830
  if failures: