codebee 0.1.96 → 0.1.98

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.
@@ -73,18 +73,27 @@ _CLI_EXTS = (".exe", ".cmd", ".bat")
73
73
 
74
74
 
75
75
  def _fallback_probe_cli(name):
76
- """which() 落空时按常见安装位兜底找 CLI(Windows only),返回全路径或 None。
76
+ """which() 落空时按常见安装位兜底找 CLI,返回全路径或 None。
77
77
 
78
78
  服务进程的 PATH 是启动那一刻的快照:CLI 在服务起跑之后才安装/迁移(如
79
79
  2026-09-30 claude 转 winget 原生安装)时,快照里没有新目录,which 解析
80
80
  不到、裸名直传 Popen 直接 FileNotFoundError,直到重启服务才自愈。这里
81
81
  只探测磁盘已知落点,不改 PATH、不缓存(装没装是低频事件,探几次不亏)。
82
- """
83
- if os.name != "nt":
84
- return None
82
+ Mac 同病:curl 脚本装的 uv/aider 落 ~/.local/bin,GUI 启动的服务常吃不到
83
+ 这条 PATH;brew 在 Apple Silicon 落 /opt/homebrew/bin。"""
85
84
  name = str(name)
86
85
  if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name):
87
86
  return None # 带斜杠/参数形态的不是裸命令名,交给原路径逻辑
87
+ if os.name != "nt":
88
+ for d in (os.path.join(os.path.expanduser("~"), ".local", "bin"),
89
+ "/opt/homebrew/bin", "/usr/local/bin"):
90
+ p = os.path.join(d, name)
91
+ try:
92
+ if os.path.isfile(p) and os.access(p, os.X_OK):
93
+ return p
94
+ except OSError:
95
+ pass
96
+ return None
88
97
  local = os.environ.get("LOCALAPPDATA") or ""
89
98
  home = os.path.expanduser("~")
90
99
  appdata = os.environ.get("APPDATA") or os.path.join(home, "AppData", "Roaming")
@@ -1370,17 +1379,18 @@ def _attempt_protocol(att):
1370
1379
 
1371
1380
 
1372
1381
  def _codex_sandbox(readonly):
1373
- """codex 沙箱档位。写步默认 danger-full-access(2026-09-17 用户拍板「默认给全部
1374
- 权限」):workspace-write 禁网+禁盘外写,模型偶发还会误判沙箱受限、谎报
1375
- 「进程执行被策略拦截」直接躺平(mo-so BUG#27596 实测,沙箱探针证明命令本可跑)。
1376
- 只读步(评审/规划)仍 read-only——评审者可写会污染 git diff 裁决。
1377
- env TUTTI_CODEX_SANDBOX 可钉死某档(workspace-write / read-only / danger-full-access)。"""
1382
+ """Return the narrowest supported Codex sandbox mode.
1383
+
1384
+ A full-access override defeats task-level filesystem policy and checkpoints;
1385
+ ignore the former ``danger-full-access`` value even if it remains in the
1386
+ user's environment. Read-only review steps must not be able to mutate files.
1387
+ """
1378
1388
  if readonly:
1379
1389
  return "read-only"
1380
1390
  env = os.environ.get("TUTTI_CODEX_SANDBOX", "").strip().lower()
1381
- if env in ("workspace-write", "read-only", "danger-full-access"):
1391
+ if env in ("workspace-write", "read-only"):
1382
1392
  return env
1383
- return "danger-full-access"
1393
+ return "workspace-write"
1384
1394
 
1385
1395
 
1386
1396
  def _prompt_to_file(prompt, workdir):
@@ -14,8 +14,9 @@ _FILE = paths.DATA_DIR / "settings.json"
14
14
  # default_workdir 为空表示未自定义,用 builtin_workdir() 回落;
15
15
  # telemetry_errors:匿名错误回传开关(默认开;关掉后版本 ping/错误上传/诊断包遥测部分全部停发,
16
16
  # 「导出诊断包」是用户手动操作不受此开关限制)
17
- # publish_daily_cap / publish_fail_streak:自动发布护栏——每任务每平台每日
18
- # 成功发章上限、平台连续失败几次后暂停自动发布(publish/auto.py 读取)
17
+ # publish_daily_cap / publish_fail_streak / publish_fail_window_h:自动发布护栏——
18
+ # 每任务每平台每日成功发章上限、平台连续失败几次后暂停自动发布、连败只统计
19
+ # 最近多少小时内的失败(跨天旧失败不进「连续」口径)(publish/auto.py 读取)
19
20
  # pet_enabled / pet_mode:桌面蜜蜂(app/pet.py)开关与显示模式(默认关,设置页
20
21
  # 可开)。关闭后看护线程不再拉起、在岗蜜蜂轮询到 false 自行退出;mode=always
21
22
  # 不再拉起、在岗蜜蜂轮询到 false 自行退出;mode=always 常驻,tasks_only 仅任务
@@ -28,7 +29,8 @@ _FILE = paths.DATA_DIR / "settings.json"
28
29
  DEFAULTS = {"max_concurrent_jobs": 12, "default_workdir": "", "hooks_token": "",
29
30
  "hooks_signing_secret": "",
30
31
  "telemetry_errors": True, "publish_daily_cap": 10,
31
- "publish_fail_streak": 3, "notify_webhook": "", "notify_base_url": "",
32
+ "publish_fail_streak": 3, "publish_fail_window_h": 12,
33
+ "notify_webhook": "", "notify_base_url": "",
32
34
  "notify_bark_server": "", "notify_bark_key": "",
33
35
  "notify_ntfy_topic": "", "notify_serverchan_key": "",
34
36
  "notify_telegram_token": "", "notify_telegram_chat_id": "",
@@ -142,14 +144,20 @@ def save(patch):
142
144
  cur["telemetry_errors"] = bool(patch.get("telemetry_errors"))
143
145
  if "publish_daily_cap" in patch:
144
146
  try:
145
- cur["publish_daily_cap"] = max(1, min(50, int(patch.get("publish_daily_cap"))))
147
+ v = int(patch.get("publish_daily_cap"))
146
148
  except (TypeError, ValueError):
147
- return cur, "publish_daily_cap 必须是 1-50 的整数"
149
+ return cur, "publish_daily_cap 必须是 0-50 的整数(0=不限制)"
150
+ cur["publish_daily_cap"] = max(0, min(50, v))
148
151
  if "publish_fail_streak" in patch:
149
152
  try:
150
153
  cur["publish_fail_streak"] = max(1, min(10, int(patch.get("publish_fail_streak"))))
151
154
  except (TypeError, ValueError):
152
155
  return cur, "publish_fail_streak 必须是 1-10 的整数"
156
+ if "publish_fail_window_h" in patch:
157
+ try:
158
+ cur["publish_fail_window_h"] = max(1, min(168, float(patch.get("publish_fail_window_h"))))
159
+ except (TypeError, ValueError):
160
+ return cur, "publish_fail_window_h 必须是 1-168 的数字"
153
161
  if "notify_webhook" in patch:
154
162
  cur["notify_webhook"] = str(patch.get("notify_webhook") or "").strip()[:300]
155
163
  if "notify_base_url" in patch:
@@ -223,8 +223,8 @@ def register_default_namespaces():
223
223
  register_namespace(
224
224
  "orchestrator",
225
225
  fields=[
226
- FieldDef("compaction.enabled", "bool", False,
227
- "上下文压缩总开关(灰度:与 TUTTI_COMPACTION 任一开启即生效)"),
226
+ FieldDef("compaction.enabled", "bool", True,
227
+ "上下文压缩总开关(默认开启;可在设置页或用 TUTTI_COMPACTION=0 关闭)"),
228
228
  FieldDef("compaction.pressure_threshold", "float", 0.8,
229
229
  "触发压缩的压力比阈值",
230
230
  clamp=(0.1, 0.99)),
@@ -259,6 +259,17 @@ def register_default_namespaces():
259
259
  ],
260
260
  validate=lambda v: None,
261
261
  )
262
+
263
+ # Revision 1 is the pre-release schema default (disabled). Values written
264
+ # by mutate() advance the namespace to revision 2+, so preserve an explicit
265
+ # user opt-out while migrating untouched installations to the new default.
266
+ with _LOCK:
267
+ orchestrator = _VALUES.get("orchestrator", {})
268
+ if (_REVISIONS.get("orchestrator") == 1 and
269
+ _get_path(orchestrator, "compaction.enabled") is False):
270
+ _set_path(orchestrator, "compaction.enabled", True)
271
+ _REVISIONS["orchestrator"] = 2
272
+ _persist()
262
273
  if "cascade" not in _NAMESPACES:
263
274
  register_namespace(
264
275
  "cascade",
@@ -268,4 +279,4 @@ def register_default_namespaces():
268
279
  "FrugalGPT 式级联:easy 任务按 tier 升序重排模型链(需供应商声明 tier)"),
269
280
  ],
270
281
  validate=lambda v: None,
271
- )
282
+ )
package/app/core/store.py CHANGED
@@ -180,15 +180,13 @@ def create_task(payload):
180
180
  "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
181
181
  "status": "created",
182
182
  }
183
- # Keep the execution policy with the task so runners and integrations
184
- # share one bounded, auditable contract. Legacy tasks remain permissive.
183
+ # Every new task must persist one verified execution policy. If policy
184
+ # normalization fails, do not silently replace it with a broader default.
185
185
  try:
186
186
  from . import policy
187
187
  task["sandbox"] = policy.normalize_sandbox(payload.get("sandbox"), wd)
188
- except Exception:
189
- task["sandbox"] = {"allowed_roots": [str(wd)], "env_allowlist": [],
190
- "network": True, "timeout_s": 3600,
191
- "max_output_bytes": 10 * 1024 * 1024}
188
+ except Exception as exc:
189
+ raise ValueError("沙箱策略无法验证,任务未创建") from exc
192
190
  thinking = str(payload.get("thinking") or "standard").strip().lower()
193
191
  task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
194
192
  # 流程修订出处(借鉴 WorkDSH ADR-0010):创建时钉住当时流程定义的指纹。
@@ -1747,6 +1745,23 @@ def retry_task(task_id):
1747
1745
  "chapter_scores": scores,
1748
1746
  }
1749
1747
  break
1748
+ else:
1749
+ # 断点续跑(代码等非连载任务,A+B):上一遍失败/取消/超时 run 的
1750
+ # 实现会话(inherit_session,跨 run 续 CLI 会话)与断点简报
1751
+ # (handoff,走到哪/改了什么/还剩什么)继承给新 run——重试从断点
1752
+ # 继续而不是从零烧 token。done 的 run 不继承:主动重跑语义就是重来。
1753
+ prev_runs = sorted((r for r in _RUNS.values()
1754
+ if r.get("task_id") == task_id and r["id"] != run["id"]),
1755
+ key=lambda r: r["id"], reverse=True)
1756
+ for prev in prev_runs:
1757
+ if prev.get("status") not in ("failed", "cancelled", "timeout"):
1758
+ continue
1759
+ if prev.get("impl_session") and "inherit_session" not in run:
1760
+ run["inherit_session"] = dict(prev["impl_session"])
1761
+ if prev.get("handoff") and "handoff" not in run:
1762
+ run["handoff"] = dict(prev["handoff"])
1763
+ if "inherit_session" in run or "handoff" in run:
1764
+ break
1750
1765
  task["status"] = "queued"
1751
1766
  _save_json(paths.TASKS_DIR / (task_id + ".json"), task)
1752
1767
  _save_json(paths.RUNS_DIR / run["id"] / "run.json", run)
@@ -0,0 +1,107 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Bounded, run-scoped storage for large tool results omitted from model context."""
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import re
8
+ import stat
9
+ from pathlib import Path
10
+
11
+ from . import paths
12
+
13
+ _DIR = paths.DATA_DIR / "tool_outputs"
14
+ _REF_RE = re.compile(r"^[a-f0-9]{32}$")
15
+ MAX_RESULT_BYTES = 4 * 1024 * 1024
16
+ MAX_RUN_BYTES = 32 * 1024 * 1024
17
+ MAX_READ_CHARS = 20000
18
+
19
+
20
+ def _run_dir(run_id):
21
+ return _DIR / hashlib.sha256(str(run_id or "").encode("utf-8")).hexdigest()
22
+
23
+
24
+ def save(run_id, call_id, content):
25
+ """Store one output and return an opaque reference, or empty string on limits/errors."""
26
+ if not str(run_id or "").strip():
27
+ return ""
28
+ raw = str(content or "").encode("utf-8", errors="replace")
29
+ if len(raw) > MAX_RESULT_BYTES:
30
+ return ""
31
+ run_dir = _run_dir(run_id)
32
+ ref = hashlib.sha256((str(call_id or "") + "\0").encode("utf-8") + raw).hexdigest()[:32]
33
+ try:
34
+ _DIR.mkdir(parents=True, exist_ok=True)
35
+ run_dir.mkdir(mode=0o700, exist_ok=True)
36
+ if run_dir.is_symlink() or (hasattr(run_dir, "is_junction") and run_dir.is_junction()):
37
+ return ""
38
+ target = run_dir / (ref + ".txt")
39
+ if target.exists():
40
+ return ref if target.is_file() and not target.is_symlink() else ""
41
+ total = sum(p.stat().st_size for p in run_dir.glob("*.txt") if p.is_file())
42
+ if total + len(raw) > MAX_RUN_BYTES:
43
+ return ""
44
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
45
+ flags |= getattr(os, "O_NOFOLLOW", 0)
46
+ fd = os.open(str(target), flags, 0o600)
47
+ try:
48
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
49
+ return ""
50
+ with os.fdopen(fd, "wb", closefd=False) as handle:
51
+ handle.write(raw)
52
+ handle.flush()
53
+ finally:
54
+ os.close(fd)
55
+ return ref
56
+ except (OSError, ValueError):
57
+ return ""
58
+
59
+
60
+ def read(run_id, ref, *, offset=0, limit=MAX_READ_CHARS):
61
+ """Read a bounded slice, scoped to a single run and an opaque content reference."""
62
+ if not str(run_id or "").strip() or not _REF_RE.fullmatch(str(ref or "")):
63
+ return {"ok": False, "error": "工具结果引用无效"}
64
+ run_dir = _run_dir(run_id)
65
+ if run_dir.is_symlink() or (hasattr(run_dir, "is_junction") and run_dir.is_junction()):
66
+ return {"ok": False, "error": "工具结果存储路径不安全"}
67
+ target = run_dir / (str(ref) + ".txt")
68
+ if target.is_symlink() or (hasattr(target, "is_junction") and target.is_junction()):
69
+ return {"ok": False, "error": "工具结果存储路径不安全"}
70
+ try:
71
+ offset = max(0, int(offset or 0))
72
+ limit = max(1, min(MAX_READ_CHARS, int(limit or MAX_READ_CHARS)))
73
+ with target.open("rb") as handle:
74
+ handle.seek(offset)
75
+ raw = handle.read(limit * 4 + 4)
76
+ text = raw.decode("utf-8", errors="replace")[:limit]
77
+ end = handle.tell()
78
+ handle.seek(0, os.SEEK_END)
79
+ size = handle.tell()
80
+ return {"ok": True, "text": text, "offset": offset,
81
+ "next_offset": end if end < size else None, "size_bytes": size}
82
+ except (OSError, ValueError, TypeError):
83
+ return {"ok": False, "error": "找不到此运行中的工具结果引用"}
84
+
85
+
86
+ def matches_compacted_output(run_id, call_id, ref, head, tail):
87
+ """Verify a compacted preview belongs to this call and stored full output."""
88
+ if not str(run_id or "").strip() or not _REF_RE.fullmatch(str(ref or "")):
89
+ return False
90
+ run_dir = _run_dir(run_id)
91
+ if run_dir.is_symlink() or (hasattr(run_dir, "is_junction") and run_dir.is_junction()):
92
+ return False
93
+ target = run_dir / (str(ref) + ".txt")
94
+ if target.is_symlink() or (hasattr(target, "is_junction") and target.is_junction()):
95
+ return False
96
+ try:
97
+ if target.stat().st_size > MAX_RESULT_BYTES:
98
+ return False
99
+ raw = target.read_bytes()
100
+ expected_ref = hashlib.sha256(
101
+ (str(call_id or "") + "\0").encode("utf-8") + raw).hexdigest()[:32]
102
+ if expected_ref != ref:
103
+ return False
104
+ return (raw.startswith(str(head or "").encode("utf-8", errors="replace"))
105
+ and raw.endswith(str(tail or "").encode("utf-8", errors="replace")))
106
+ except (OSError, ValueError):
107
+ return False
@@ -358,7 +358,14 @@ def _summarize(cfg, group, window, watch):
358
358
  m["ts"], m["sender"] or "?", m["text"].replace("\n", " ")) for m in window)
359
359
  prompt = _PROMPT % (group, len(window), body)
360
360
  # 工作目录用专用空目录而非监控文件夹:见 _scratch_dir 的说明
361
- r = builtin_agent.run(bi, prompt, workdir=str(_scratch_dir()), timeout=240)
361
+ scratch = str(_scratch_dir())
362
+ from . import builtin_agent as _builtin_agent
363
+ disabled_tools = [str(tool.get("name") or "") for tool in _builtin_agent.TOOLS_SPEC]
364
+ disabled_tools.extend(["create_task", "read_tool_output"])
365
+ r = builtin_agent.run(
366
+ bi, prompt, workdir=scratch, timeout=240,
367
+ sandbox={"allowed_roots": [scratch], "network": False,
368
+ "disabled_tools": disabled_tools})
362
369
  if not r.get("ok"):
363
370
  raise RuntimeError(r.get("error") or "模型调用失败")
364
371
  text = (r.get("text") or "").strip()
package/app/main.py CHANGED
@@ -286,6 +286,13 @@ class Handler(BaseHTTPRequestHandler):
286
286
  if path == "/api/board":
287
287
  # 任务驾驶舱大屏的轻量聚合(KB 级;别让它去拉 /api/state 全量)
288
288
  return self._json(200, board.payload())
289
+ if path == "/api/board/run":
290
+ # 大屏下钻抽屉的步骤链:点开才拉,不进 /api/board 的 8 秒缓存
291
+ rid = (parse_qs(urlparse(self.path).query).get("run_id") or [""])[0]
292
+ detail = board.run_detail(rid)
293
+ if not detail:
294
+ return self._json(404, {"error": "not found"})
295
+ return self._json(200, detail)
289
296
  if path == "/api/analytics":
290
297
  from core import analytics
291
298
  return self._json(200, analytics.summary(store))
@@ -341,6 +348,16 @@ class Handler(BaseHTTPRequestHandler):
341
348
  if path == "/api/knowledge":
342
349
  from core import knowledge
343
350
  return self._json(200, knowledge.view())
351
+ m = re.match(r"^/api/tasks/([^/]+)/memory$", path)
352
+ if m:
353
+ task = store.get_task(m.group(1))
354
+ if not task:
355
+ return self._json(404, {"error": "not found"})
356
+ from core import project_memory
357
+ try:
358
+ return self._json(200, {"entries": project_memory.list_entries(task.get("workdir"))})
359
+ except (OSError, ValueError) as exc:
360
+ return self._json(409, {"error": str(exc)})
344
361
  if path == "/api/agents/presence":
345
362
  from core import presence
346
363
  return self._json(200, {"agents": presence.list_agents()})
@@ -592,7 +609,11 @@ class Handler(BaseHTTPRequestHandler):
592
609
  if not store.get_run(m.group(1)):
593
610
  return self._json(404, {"error": "not found"})
594
611
  from core import checkpoints
595
- return self._json(200, {"checkpoint": checkpoints.replay_preview(m.group(1))})
612
+ run = store.get_run(m.group(1)) or {}
613
+ task = store.get_task(run.get("task_id")) or {}
614
+ return self._json(200, {
615
+ "checkpoint": checkpoints.replay_preview(m.group(1)),
616
+ "files": checkpoints.snapshot_preview(m.group(1), task.get("workdir") or "")})
596
617
  m = re.match(r"^/api/runs/([^/]+)/preview$", path)
597
618
  if m:
598
619
  # 网页成品预览:入口 HTML + 同目录代码文件(前端渲染「预览」页签)
@@ -886,6 +907,22 @@ class Handler(BaseHTTPRequestHandler):
886
907
  return self._json(200, {"ok": ok, "output": out})
887
908
  if path == "/api/tasks/clarify":
888
909
  return self._api_task_clarify()
910
+ m = re.match(r"^/api/tasks/([^/]+)/memory$", path)
911
+ if m:
912
+ task = store.get_task(m.group(1))
913
+ if not task:
914
+ return self._json(404, {"error": "not found"})
915
+ body = self._body() or {}
916
+ from core import project_memory
917
+ try:
918
+ row = project_memory.decide(task.get("workdir"), body.get("id"),
919
+ body.get("action"),
920
+ expected_version=body.get("expected_version"))
921
+ return self._json(200, {"entry": row})
922
+ except KeyError as exc:
923
+ return self._json(404, {"error": str(exc)})
924
+ except (OSError, TypeError, ValueError) as exc:
925
+ return self._json(400, {"error": str(exc)})
889
926
  if path == "/api/agents/presence":
890
927
  from core import presence
891
928
  body = self._body() or {}
@@ -1247,6 +1284,35 @@ class Handler(BaseHTTPRequestHandler):
1247
1284
  m = re.match(r"^/api/(tasks|runs)/([^/]+)/reveal$", path)
1248
1285
  if m:
1249
1286
  return self._api_reveal(m.group(1), m.group(2))
1287
+ m = re.match(r"^/api/runs/([^/]+)/checkpoints/restore$", path)
1288
+ if m:
1289
+ if not bool((self._body() or {}).get("confirm")):
1290
+ return self._json(400, {"error": "文件恢复会覆盖检查点记录的文件,请确认 confirm=true 后重试"})
1291
+ run = store.get_run(m.group(1))
1292
+ if not run:
1293
+ return self._json(404, {"error": "not found"})
1294
+ if run.get("status") not in ("done", "failed", "cancelled", "timeout"):
1295
+ return self._json(409, {"error": "任务仍在运行或排队;请先停止并等待终态后再恢复文件"})
1296
+ task = store.get_task(run.get("task_id")) or {}
1297
+ if not task.get("workdir"):
1298
+ return self._json(409, {"error": "任务工作目录不可用,拒绝恢复"})
1299
+ from core import checkpoints
1300
+ # Serialize the final safety check and file restore with run creation
1301
+ # and status transitions. Otherwise a new run can start on this task
1302
+ # after the terminal check and race with restoration in the same tree.
1303
+ with store.LOCK:
1304
+ current = store.get_run(m.group(1)) or {}
1305
+ task = store.get_task(current.get("task_id")) or {}
1306
+ latest = store.latest_run_by_task().get(current.get("task_id")) or {}
1307
+ terminal = ("done", "failed", "cancelled", "timeout")
1308
+ if current.get("status") not in terminal:
1309
+ return self._json(409, {"error": "任务状态已变化;请等待终态后再恢复文件"})
1310
+ task_active = task.get("status") in ("queued", "running")
1311
+ newer_run_active = latest.get("status") in ("queued", "running")
1312
+ if task_active or newer_run_active:
1313
+ return self._json(409, {"error": "该任务已有运行中的新任务;请等待其结束后再恢复文件"})
1314
+ return self._json(200, checkpoints.restore_files(
1315
+ m.group(1), task.get("workdir") or ""))
1250
1316
  m = re.match(r"^/api/runs/([^/]+)/messages$", path)
1251
1317
  if m:
1252
1318
  return self._api_add_message(m.group(1))
@@ -3057,8 +3123,16 @@ class Handler(BaseHTTPRequestHandler):
3057
3123
  '[{"q": "问题", "options": ["选项1", "选项2"]}]' % (ttype, goal[:200]))
3058
3124
  try:
3059
3125
  # 澄清是可选增强,不能占住创建请求;主流程有自己的执行超时。
3126
+ # 此调用只负责生成问题,不得继承完整执行工具权限或访问网络。
3127
+ from core import policy
3128
+ clarify_sandbox = policy.normalize_sandbox({
3129
+ "network": False,
3130
+ "disabled_tools": ["run_command", "write_file", "edit_file",
3131
+ "append_file", "fs_manage", "create_task",
3132
+ "read_tool_output"],
3133
+ }, os.getcwd() if hasattr(os, "getcwd") else ".")
3060
3134
  res = builtin_agent.run(bi, prompt, os.getcwd() if hasattr(os, "getcwd") else ".",
3061
- timeout=8)
3135
+ timeout=8, sandbox=clarify_sandbox)
3062
3136
  import json as _json
3063
3137
  arr = None
3064
3138
  text = (res.get("text") or "").strip()