周末 Agent demo 常在第三次重试时失控——改 README、配置文件和测试同时发生。用小型 permit 文件和磁盘 outbox 队列替代直接写树,保持探索阶段廉价且提交行为显式。
周末代理演示常常在模型还在探索阶段时就能修改工作树,从而导致演示崩溃。一个小小的许可证文件加上一份磁盘上的发件箱,可以让探索成本低廉、提交行为明确。演示不是更长的对话。它是一次被刷新出去的写操作,且测试可以拒绝它。
本文是一份周末构建日志,不是平台评测。首先砍掉了范围。工作路径是一张许可证台。其他一切都是有意跳过的。
那个浪费周六的失败
边做边学的代理项目通常不会在第一个 prompt 就挂掉。它们往往在第三次重试时崩溃——那时候一个"修一下演示"的回合会把 README.md、配置文件和测试用例一起改掉。 transcript 看起来仍然很高效。但目录树不是。
只读回合成本低廉。写操作才是稀缺资源。周末套件应该这样对待它们。
最近关于"氛围编程"与工程之间关系的公开讨论很嘈杂。但在笔记本上的实际分歧更小:如果一个工具调用可以修改磁盘,它就需要一张许可证。如果不能,它就可以循环到时间耗尽为止。
为一个周末切割的范围
这个构建保留了三个任务,删掉了其余的。
从仓库加载一个写路径的静态白名单。
捕获写尝试,将其追加到 outbox.jsonl 而不是直接碰目录树。
通过一条测试可以运行两次的命令刷新一条已批准的记录。
周六跳过的内容:多用户认证、云端队列、模型路由、token 仪表盘,以及任何关于哪个模型"最好"的声明。这些会让演示膨胀。它们无法证明许可证的有效性。
产物是一个 Python 模块、一张许可证文件、一个测试用例,加上一条简短的 shell 路径。这就是整个交付物。
白名单故意做得很无聊。无趣的文件才能撑过一个周末。
{
"version": 1,
"demo_id": "weekend-outbox-001",
"write_roots": ["demo_out/"],
"max_flush": 1,
"forbidden_globs": [
".git/**",
"**/*secret*",
"**/.env",
"permits.json"
]
}
write_roots 是刷新操作唯一可以落盘的地方。forbidden_globs 是硬否决,即使模型据理力争也没用。max_flush 是周六的停止信号:提交一个文件,然后就停。代理可以提出很多写操作。许可证台只保留一条。
下面的模块是一个完整、可运行的草图。它不调用网络模型。它只管控写操作。把它标记为本地 fixture,而不是生产级代理运行时。
#!/usr/bin/env python3
"""permit_desk.py — catch writes, flush one permitted path."""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parent
PERMIT_PATH = ROOT / "permits.json"
OUTBOX_PATH = ROOT / "outbox.jsonl"
FLUSHED_PATH = ROOT / "flushed.json"
@dataclass(frozen=True)
class Permit:
version: int
demo_id: str
write_roots: tuple[str, ...]
max_flush: int
forbidden_globs: tuple[str, ...]
def load_permit(path: Path = PERMIT_PATH) -> Permit:
raw = json.loads(path.read_text(encoding="utf-8"))
return Permit(
version=int(raw["version"]),
demo_id=str(raw["demo_id"]),
write_roots=tuple(raw["write_roots"]),
max_flush=int(raw["max_flush"]),
forbidden_globs=tuple(raw["forbidden_globs"]),
)
def _is_forbidden(rel: str, permit: Permit) -> bool:
text = rel.replace("\\", "/")
for glob in permit.forbidden_globs:
pattern = re.escape(glob).replace("\\*\\*", ".*").replace("\\*", "[^/]*")
if re.fullmatch(pattern, text):
return True
return False
def _under_root(rel: str, permit: Permit) -> bool:
text = rel.replace("\\", "/")
return any(text.startswith(root) for root in permit.write_roots)
def propose_write(relpath: str, content: str, permit: Permit | None = None) -> dict[str, Any]:
permit = permit or load_permit()
rel = relpath.replace("\\", "/")
record = {
"ts": datetime.now(timezone.utc).isoformat(),
"demo_id": permit.demo_id,
"relpath": rel,
"bytes": len(content.encode("utf-8")),
"content": content,
"status": "queued",
}
if _is_forbidden(rel, permit) or not _under_root(rel, permit):
record["status"] = "rejected"
record["reason"] = "path not permitted"
OUTBOX_PATH.parent.mkdir(parents=True, exist_ok=True)
with OUTBOX_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
return record
def flush_one(permit: Permit | None = None) -> dict[str, Any]:
permit = permit or load_permit()
if FLUSHED_PATH.exists():
raise SystemExit("demo already flushed; refuse second write")
if not OUTBOX_PATH.exists():
raise SystemExit("outbox empty")
queued = []
for line in OUTBOX_PATH.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
item = json.loads(line)
if item.get("status") == "queued":
queued.append(item)
if not queued:
raise SystemExit("no queued writes")
if permit.max_flush != 1:
raise SystemExit("this weekend kit flushes exactly one record")
chosen = queued[0]
target = ROOT / chosen["relpath"]
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(chosen["content"], encoding="utf-8")
chosen["status"] = "flushed"
FLUSHED_PATH.write_text(json.dumps(chosen, indent=2), encoding="utf-8")
return chosen
重要的行为是负向的。被拒绝的路径仍然会落入发件箱。它永远不会进入 demo_out/。周六的操作员读日志,而不是模型的自述。
一个在演示之前就失败的测试
这个套件在测试拒绝代理很想执行的一个写操作之前都不算真实。
# test_permit_desk.py
from pathlib import Path
import json
import permit_desk as desk
def test_rejects_git_and_env(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
(tmp_path / "permits.json").write_text(json.dumps({
"version": 1,
"demo_id": "t",
"write_roots": ["demo_out/"],
"max_flush": 1,
"forbidden_globs": [".git/**", "**/.env"],
}))
monkeypatch.setattr(desk, "ROOT", tmp_path)
monkeypatch.setattr(desk, "PERMIT_PATH", tmp_path / "permits.json")
monkeypatch.setattr(desk, "OUTBOX_PATH", tmp_path / "outbox.jsonl")
monkeypatch.setattr(desk, "FLUSHED_PATH", tmp_path / "flushed.json")
bad = desk.propose_write(".env", "TOKEN=nope")
assert bad["status"] == "rejected"
assert not Path(".env").exists()
good = desk.propose_write("demo_out/hello.txt", "ok\n")
assert good["status"] == "queued"
flushed = desk.flush_one()
assert flushed["relpath"] == "demo_out/hello.txt"
assert (tmp_path / "demo_out/hello.txt").read_text() == "ok\n"
python3 -m pip install pytest
python3 -m pytest -q test_permit_desk.py
演示失败用例和通过用例一样重要。如果 .env 可以被写入,周末就已经结束了。
证明一条路径的命令
一条简短的 shell 路径是公开的产物。聊天日志不是。
python3 - <<'PY'
from permit_desk import propose_write, flush_one
print(propose_write("README.md", "# hijack\n"))
print(propose_write("demo_out/receipt.txt", "demo ok\n"))
print(flush_one())
PY
cat outbox.jsonl
cat flushed.json
cat demo_out/receipt.txt
test ! -f README.md
outbox.jsonl 的预期形态:
{"relpath": "README.md", "status": "rejected", "reason": "path not permitted"}
{"relpath": "demo_out/receipt.txt", "status": "queued"}
刷新之后,README.md 仍然不存在。demo_out/receipt.txt 存在。第二次调用 flush_one() 会非零退出。那第二次失败也是演示的一部分。它记录了跳过:收到收据后不再有额外的写操作。
周六的决策表
这张表才是产品。模型可以叙述。但表不能。
一个免费远程主机适合放在哪里
许可证文件必须随仓库一起分发。本地笔记本和远程工作空间应该加载相同的 permits.json。如果主机变了而白名单没变,说明代理换了一台机器但没有换合约。
声明:本文是 MonkeyCode 产品推广的一部分。MonkeyCode 的免费模型访问和免费服务器选项可以托管相同的布局,这样许可证台就不必绑定在某台笔记本上。这里没有附加任何模型名称、配额或硬件声明,因为运行这个 fixture 根本不需要它们。即使进程跑在远端,许可证台仍然会拒绝 .env。
已经有机器的读者可以忽略主机部分,保留文件即可。这个工作流不依赖某个厂商持续可用。
这个周末跳过了什么
把跳过的项目写下来,这样它们才不会悄悄以"小重构"的名义溜回来。
没有重试预算。步数统计是另一套套件的事。
没有消费计量器。Token 成本是另一套套件的事。
没有黄金 stdout 日志。收据是磁盘上的一个文件,而不是被捕获的流。
生成的补丁没有合并门控。刷新不是一次 pull request。
没有尝试对模型排序。白名单不在乎是谁提出了这个写操作。
这些跳过是周六的特性。如果一个演示还要同时度量成本、步数和模型质量,那它就不再是演示了。它是一个平台。
glob 匹配器是一段简短的 regex 替代品。不是完整的 gitignore 引擎。符号链接、大小写不敏感的文件系统,以及通过子进程执行但从不调用 propose_write 的写操作都会绕过它。许可证台是一个荣誉系统加一个测试,不是内核沙箱。
max_flush 是一个文件标志,不是分布式锁。两个 shell 可以竞争。这对周末边做边学的项目是可接受的。对共享的生产卷则不可接受。
发件箱在 JSONL 中存储完整的文件内容。大二进制文件不适合放这里。文本收据适合。
谁不应该用这种方法
有真正沙箱、强制代码审查或已有策略引擎的团队不需要 JSON 白名单。处理敏感信息、支付或医疗数据的人不应该把发件箱文件当作隔离手段。想做模型排序 bake-off 的操作员不会在这个日志里找到想要的东西。
这个套件面向的是这样一个单一维护者:他曾经让一个代理在仓库里游荡过一次,对 diff 不满意。它恢复了一条无聊的规则:读操作可以循环,写操作需要许可证,周末在一条路径被刷新之后结束。