Codex 权限 profiles 实验:只让报告目录可写
把需要读的输入、唯一可写的产物和应拒绝的目录逐项列出来。
1. 先把“只生成报告”落到目录上
一项只需要生成报告的任务,往往会同时读源码、输入数据和另一个目录里的规范。直接给整个工作区写权限,任务当然更容易跑通,但输入文件和验收材料也一起变成了可修改对象。这个练习从一张路径清单开始:哪些文件必须读,产物放在哪里,哪些目录应当连内容都看不到。
目标是一份小型目录报告:读取 app/input/catalog.json 与 shared/spec.txt,把结果写到 app/reports。app/private 和 outside 只放人工生成的标记文本,用来检查拒读边界。没有安装依赖、调用模型、访问真实秘密或上传文件的步骤。
本文提供三份完整候选:audit 用于只读核对,report 增加一个精确报告目录的写入,report-net 再增加一个明确的公共域名。audit 只做只读核对;report 已足够生成报告。第三份只为说明网络权限怎样核查,可以停在纸面评审。
2. 本稿依据哪版规则
资料核对日期为 2026 年 10 月 7 日。权限 profiles 在官方权限文档中仍标为 beta。代码核对固定在 openai/codex 提交 1fbe15c962cc3d8eabec36d67987c83cfc4eeec9,包括 配置结构与继承、权限编译、配置选择和 配置 Schema。这个提交是源码基线,不是本文安装并验证过的 CLI 发行版本。
正文中的 Python 检查在本地离线执行;三份生成后的 TOML 还通过该固定提交的 Schema 校验。没有启动真实 Codex 会话,也没有验证操作系统沙箱、代理、账户权限或模型输出。首次真实试验时,应把实际 CLI 版本填进观察记录;不能拿源码提交号代替运行版本。
权限文档与固定源码在混用旧配置时存在差异:文档提示已加载的 sandbox_mode 会选择旧机制;这里核对的源码会按配置层处理选择,SessionFlags 中的 default_permissions 也有专门分支。因此本练习不依赖混用时谁覆盖谁。候选中完全不出现 sandbox_mode 或 sandbox_workspace_write,启动时也不追加 --sandbox。出现旧设置时,先让配置维护者核对并整理,保留现有受管要求。
3. 先区分四件容易混在一起的事
命名权限 profile 由 default_permissions 选择,定义在 permissions 下。普通配置 profile 是另一种配置组织方式;--profile 不能仅凭同名就被理解成“选择权限 profile”。本文只交付独立 TOML 候选,不生成旧式 profiles 表,也不提供自动覆盖用户配置的安装脚本。
approval_policy 仍是独立设置,本例显式使用 on-request。沙箱决定命令原本可以做什么;审批负责判断额外请求。遇到拒绝时,本练习记录结果并结束该项,不批准提升权限,也不改规则后继续把这次拒绝计为通过。组织要求、规则和平台能力可能使一个看似允许的动作仍被阻止。
文档同时提醒,受管 allowed_permission_profiles 会强制采用权限 profiles,并有客户端版本要求。团队部署还要单独核对 requirements.toml。本例没有生成或修改受管配置,不声称能放宽组织策略。源码中的显式 sandbox_mode 与 default_permissions 覆盖值也有冲突检查,不能把旧参数和新候选拼接成一条通用启动命令。
4. 生成一次性、没有秘密的练习目录
需要 Python 3.11 或更新版本。下面的命令只运行随文附带的原创标准库程序,创建新的临时目录并检查候选,不会调用 Codex。请从配套包根目录执行。每次生成都会得到新的随机路径,不复用已有工作目录。
离线准备
python3 make_fixture.py
# 将上一行输出记作 LAB;例如 /tmp/permission-lab-某个随机后缀
# 手动把实际路径代入以下参数,不要照抄示意后缀。
python3 check_candidate.py /实际的/LAB audit
python3 check_candidate.py /实际的/LAB report
python3 check_candidate.py /实际的/LAB report-net
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -v
临时目录的完整绝对路径不能含 * ? [ ]。例如父目录叫 lab[demo] 时,TOML 里的引号只解决字符串语法,不会把权限路径变成字面量。生成器在创建 fixture 前检查已解析的父目录;遇到这些字符就报错,不生成候选,也不自动改名或转到别处。请另行选择不含这些字符的临时父目录,再重新准备。
生成后的 app 是未来会话的唯一预定运行工作区;shared 由候选额外加入根集合。不要把 LAB 整体作为工作区,也不要顺手加上 control 或用户主目录。control 保存候选和空观察记录,由宿主阅读。模板中的 __APP_REPORTS__、__SHARED_ROOT__、__OUTSIDE_ROOT__ 会替换为本次临时目录里的绝对路径;它们不是 Codex 内建变量。
真实运行还可能带入环境原有的工作区根。workspace_roots 是补充集合,不是清空并替换运行时根集合。确认有效根只有本练习需要的范围后,才继续。目录里不放符号链接,也不把外部仓库或真实个人文件复制进来。
目录结构
LAB/
app/
src/label.txt
input/catalog.json
reports/README.txt
private/demo.txt
shared/spec.txt
outside/marker.txt
control/
audit.toml
report.toml
report-net.toml
observation.json
5. 三份完整候选:每次只选择一份
先阅读模板,再阅读生成后的绝对路径版本。候选不是可以直接复制进现有配置的补丁。生成的 audit.toml 等文件名只用于审阅;仅执行 --profile audit 不会自动加载它们。配置维护者应按所用 CLI 的配置方式在独立试验环境中加载选定候选,保留必要组织控制,并在会话开始时检查实际选择、配置来源和工作区根。如果无法确认这些信息,就停在静态检查阶段。
这里从空白自定义配置写起,是为了只给 minimal 运行时读取、工作区读取和一个精确报告目录的写入。与直接继承可写工作区相比,本例不需要再逐个收回输入、共享规范和控制目录的写权限。minimal 会包含工具运行所需的例外路径,其实际展开随平台而定,所以不能把此候选宣传为“只能读文中六个文件”。
configs/audit.toml
# Inert candidate. Never copy over a live config without reviewing all layers.
# Source snapshot: openai/codex 1fbe15c962cc3d8eabec36d67987c83cfc4eeec9
# Replace the path tokens with generated absolute paths before use.
approval_policy = "on-request"
default_permissions = "audit"
[permissions.audit]
description = "Synthetic catalog lab: audit candidate"
[permissions.audit.workspace_roots]
"__SHARED_ROOT__" = true
[permissions.audit.filesystem]
":minimal" = "read"
"__OUTSIDE_ROOT__" = "deny"
[permissions.audit.filesystem.":workspace_roots"]
"." = "read"
"private" = "deny"
[permissions.audit.network]
enabled = false
configs/report.toml
# Inert candidate. Never copy over a live config without reviewing all layers.
# Source snapshot: openai/codex 1fbe15c962cc3d8eabec36d67987c83cfc4eeec9
# Replace the path tokens with generated absolute paths before use.
approval_policy = "on-request"
default_permissions = "report"
[permissions.report]
description = "Synthetic catalog lab: report candidate"
[permissions.report.workspace_roots]
"__SHARED_ROOT__" = true
[permissions.report.filesystem]
":minimal" = "read"
"__OUTSIDE_ROOT__" = "deny"
"__APP_REPORTS__" = "write"
[permissions.report.filesystem.":workspace_roots"]
"." = "read"
"private" = "deny"
[permissions.report.network]
enabled = false
configs/report-net.toml
# Inert candidate. Never copy over a live config without reviewing all layers.
# Source snapshot: openai/codex 1fbe15c962cc3d8eabec36d67987c83cfc4eeec9
# Replace the path tokens with generated absolute paths before use.
approval_policy = "on-request"
default_permissions = "report-net"
[features]
network_proxy = true
[permissions.report-net]
description = "Synthetic catalog lab: report-net candidate"
[permissions.report-net.workspace_roots]
"__SHARED_ROOT__" = true
[permissions.report-net.filesystem]
":minimal" = "read"
"__OUTSIDE_ROOT__" = "deny"
"__APP_REPORTS__" = "write"
[permissions.report-net.filesystem.":workspace_roots"]
"." = "read"
"private" = "deny"
[permissions.report-net.network]
enabled = true
[permissions.report-net.network.domains]
"example.com" = "allow"
三个候选都把每个有效根的 private 子目录设为 deny。outside 使用精确绝对目录的 deny;report 和 report-net 只对 app/reports 的精确绝对目录设为 write,因此 shared 即使出现同名 reports 目录,也不会自动变成可写。
read 允许读文件和列目录,write 也包含读取,并可在操作系统允许时创建、修改、改名或删除。deny 同时拒绝读写。规则按路径具体程度处理,同路径冲突再比较访问等级;不要将“deny 永远压过任何更具体规则”当成通用结论。本练习没有更具体的重新开放规则,也不用 glob;生成器和检查器会拒绝含 glob 元字符的完整路径,避免把目录名字误当成模式。若将来改用 glob,还要重新研究平台的匹配和启动前展开边界。
没有任何写规则指向脚本、输入、共享规范或配置目录。虽然报告目录可写,本轮任务仍只允许报告产物;文件系统允许写入某目录,并不等于用户批准在其中放可执行脚本。
6. 先写预期,再给每项留下实际观察
“允许”表示这份候选规则允许进入下一步,前提是候选已选中、平台能执行该限制,且没有更高优先级限制或其他审批拦截。它不保证命令一定成功。“拒绝”也必须确认发生在预期层:Python 不存在、文件不存在或启动失败,不能冒充路径拒绝。
| 编号 | 操作 | audit | report | report-net | 核对依据 |
|---|---|---|---|---|---|
| R1 | 读取 app/input/catalog.json | 允许 | 允许 | 允许 | 输入目录 read |
| R2 | 读取 shared/spec.txt | 允许 | 允许 | 允许 | 额外根 + 工作区 read |
| W1 | 新建 app/reports/result.json | 拒绝 | 允许 | 允许 | 仅 report 类候选有精确 write |
| W2 | 新建 app/input/denied-probe.txt | 拒绝 | 拒绝 | 拒绝 | 只读输入不应创建文件 |
| W3 | 新建 shared/denied-probe.txt | 拒绝 | 拒绝 | 拒绝 | 额外根没有获得 write |
| D1 | 读取 app/private/demo.txt | 拒绝 | 拒绝 | 拒绝 | private 子目录 deny |
| D2 | 读取 outside/marker.txt | 拒绝 | 拒绝 | 拒绝 | 精确目录 deny |
| N1 | 无请求体访问 example.com | 拒绝 | 拒绝 | 域名允许* | 网络开启且代理已生效 |
| N2 | 访问未列入的公开主机 | 拒绝 | 拒绝 | 域名拒绝* | 未在 allow 表内;仅纸面核对 |
| N3 | 浏览器或连接器请求 | 另行控制 | 另行控制 | 另行控制 | 不属于本地命令域名表 |
* 网络一栏只描述域名政策。DNS、TLS、代理启动和远端响应仍需分别观察。N2 只评审域名表,不需要为了证明拒绝而选择并请求陌生真实网站。所有实际观察保持空白。生成器创建的是 control/observation.json,其中 cases 初始为空;本文不依赖另一个 CSV 文件。
- 先由宿主检查合成文件存在,记录哈希或内容长度,确认没有符号链接;记录实际 CLI 版本、系统、沙箱能力、所选候选和所有有效根。
- 在明确的独立试验会话中先选 audit。一次只请求一个 R 或 D 项,用实际绝对路径,保留原始工具命令、退出码和必要错误片段。只读项不要求把合成标记内容抄入报告。
- 对 W 项只尝试创建表中的新文件,不覆盖已有文件、不删除目录。一次操作完成或被拒绝后,由宿主检查该精确路径是否出现,再记录实际结果。
- 切到 report 前创建新的 fixture,重新核对候选与根集合,再按同一顺序检查。不要沿用前一次的 result.json;文件已存在导致的错误与沙箱拒绝无关。
- 网络候选可以只做静态审阅。如另行安排真实 N1,先获得联网许可并核实代理已生效,只做无请求体的公共页面读取,不发送目录内容、标记、参数或凭据,不跟随到新域名。
- 一旦预期拒绝的动作成功,停止本轮,将候选、配置来源和路径记录交给配置维护者。不要再读真实敏感目录“看看是否也能访问”。
本练习没有提供自动探测全部边界的运行脚本,避免一条命令把未核准的运行和网络动作串在一起。观察中区分 allowed、denied、blocked_before_probe 和 not_run,并另记判断依据。不要直接把模型的一句“已拒绝”当作操作系统证据。
需要开始记录时,将下面数组复制到生成文件的 cases 字段,保留其余顶层字段。每份文件只记录一个候选和一次新 fixture;candidate 填 audit、report 或 report-net,expected 按上表填写,它不是实测结果。未执行项保持 observed="not_run",命令、退出码和证据保持 null。实际执行后才填 allowed、denied 或 blocked_before_probe,并记录具体依据;host_check 写宿主对精确路径是否出现、输入是否保持原样的核查。N2、N3 在本实验中只作纸面评审,仍保持 not_run。
[
{
"id": "R1",
"operation": "读取 app/input/catalog.json",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
},
{
"id": "R2",
"operation": "读取 shared/spec.txt",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
},
{
"id": "W1",
"operation": "新建 app/reports/result.json",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
},
{
"id": "W2",
"operation": "新建 app/input/denied-probe.txt",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
},
{
"id": "W3",
"operation": "新建 shared/denied-probe.txt",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
},
{
"id": "D1",
"operation": "读取 app/private/demo.txt",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
},
{
"id": "D2",
"operation": "读取 outside/marker.txt",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
},
{
"id": "N1",
"operation": "无请求体访问 example.com",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
},
{
"id": "N2",
"operation": "只在纸面核对未列入的公开主机",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
},
{
"id": "N3",
"operation": "浏览器或连接器请求的独立控制",
"candidate": null,
"expected": null,
"observed": "not_run",
"command": null,
"exit_code": null,
"evidence_excerpt_redacted": null,
"host_check": null
}
]只有完成了对应真实观察,才更新顶层 actual_cli_version、os_and_sandbox、effective_workspace_roots 与审阅标记。一次文件探针不能证明网络代理有效;没有代理观察证据时 proxy_active_evidence 仍为 null。离线检查通过时 real_sandbox_run 继续保持 false。
7. 网络开关与代理必须一起核对
report-net 的网络声明是有意分成两处的:network.enabled 允许本地命令联网;features.network_proxy 负责启用执行域名规则的代理。仅写 example.com 的 allow 条目不能证明直连会被过滤。若最终配置把代理关掉,这份域名表就失去预期的筛选作用,应停止联网阶段。
example.com 是说明域名表的单一公共示例,不是报告任务所需的服务,也不是对该站点的可信背书。改成团队服务时,需要重新核对收件方、请求内容和访问目的。域名允许表不会限制向已允许的地址发送什么数据,也不能替代保密和动作授权。
这里的代理不统一接管网页搜索、连接器、MCP、浏览器、Computer Use 或模型认证流量。不要因为沙箱里的网络被禁用,就据此宣布这些工具全部离线。需要限制其他能力时,应查对应功能的配置与受管要求。
8. 本地检查到底证明了什么
check_candidate.py 只检查这篇文章定义的窄配方:选中的名字、精确路径、只读与拒绝项、代理和单一域名,同时严格区分布尔值与整数。它故意拒绝其他配置,即使那些配置在 Codex 中可能合法。检查成功的含义是“候选与本文约定相符”,没有模拟路径匹配、继承、配置合并或平台沙箱。
随文 18 个 unittest 用例覆盖三份正常候选、合成 fixture、空观察记录,以及混入旧配置、选择错名、添加根、放开输入写入、移除拒读、父路径穿越、关闭代理、扩大域名等变化;还包含非 BMP Unicode 临时路径、整数冒充布尔值、glob 父目录在创建前被拒绝,以及三份候选都拒绝 glob 根路径的子用例。修改后可以重跑这些检查,但它们没有执行真实被拒绝的文件访问,也没有发送请求。
路径检查为何不能只看 TOML 或 Schema:离线回归把 fixture 放进名为 lab[demo] 的合成父目录,旧检查器和固定 Schema 都会放过三份候选。固定源码的 路径语法检查却把 * ? [ ] 识别为 glob 字符;权限编译会把含此类字符的 deny 路径按模式处理,非尾部 /** 的 write glob 也不能当作精确写路径。现在生成器先拒绝这样的父目录,检查器也独立拒绝含这些字符的根路径。新增回归对四种字符逐一检查,避免合法 TOML 造成“精确路径已经核对”的假象。这是教学配方与固定源码的静态一致性修正,不是沙箱执行结果。
另一次作者侧静态核对将生成后的 TOML 转为对象,对照上述固定官方 Schema 做 Draft 7 验证,三份均通过。Schema 仅约束结构与字段类型;它不等于 Codex 的配置解析器,更不验证路径存在、选中层、代理或操作系统执行。官方 Schema 没有放进发布包,配套离线程序只用 Python 标准库。
实际平台无法实施分离读写规则时,可能在命令执行前拒绝启动。此时记录 blocked_before_probe 并调查平台支持,保留原有限制,不改成宽泛权限来获得一个绿色结果。
9. 配套程序与测试完整原文
make_fixture.py
"""Create synthetic files and inert config candidates; never launch Codex."""
import json
import tempfile
from pathlib import Path
TEMPLATE_ROOT = Path(__file__).resolve().parent / 'configs'
def create_fixture(parent=None):
base = Path(tempfile.gettempdir() if parent is None else parent).resolve()
if any(char in str(base) for char in '*?[]'):
raise ValueError('lab parent must not contain glob characters: * ? [ ]')
root = Path(tempfile.mkdtemp(prefix='permission-lab-', dir=base)).resolve()
files = {
'app/src/label.txt': 'A small catalog report exercise.\n',
'app/input/catalog.json': '[{"name":"blue mug","count":2}]\n',
'app/reports/README.txt': 'Only synthetic report outputs belong here.\n',
'app/private/demo.txt': 'SYNTHETIC PRIVATE MARKER; not a credential.\n',
'shared/spec.txt': 'Report the number of catalog entries.\n',
'outside/marker.txt': 'SYNTHETIC OUTSIDE MARKER.\n',
}
for name, value in files.items():
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value, encoding='utf-8')
(root / 'control').mkdir()
for source in sorted(TEMPLATE_ROOT.glob('*.toml')):
# JSON strings are valid TOML basic strings for these generated paths.
raw = source.read_text(encoding='utf-8')
for token, target in [('"__APP_REPORTS__"', root / 'app/reports'),
('"__SHARED_ROOT__"', root / 'shared'),
('"__OUTSIDE_ROOT__"', root / 'outside')]:
raw = raw.replace(token, json.dumps(str(target), ensure_ascii=False))
(root / 'control' / source.name).write_text(raw, encoding='utf-8')
(root / 'control/observation.json').write_text(json.dumps({
'actual_cli_version': None, 'os_and_sandbox': None,
'effective_workspace_roots': [], 'effective_config_reviewed': False,
'proxy_active_evidence': None, 'cases': [],
'real_sandbox_run': False
}, indent=2) + '\n', encoding='utf-8')
return root
if __name__ == '__main__':
print(create_fixture())
check_candidate.py
"""Check this article's narrow candidate recipe. Not a Codex parser or sandbox."""
import argparse
import json
import tomllib
from pathlib import Path
def check(data, root, kind):
if any(char in str(root) for char in '*?[]'):
raise ValueError('lab root must not contain glob characters: * ? [ ]')
if kind not in {'audit', 'report', 'report-net'}:
raise ValueError('unknown candidate kind')
expected_top = {'approval_policy', 'default_permissions', 'permissions'}
if kind == 'report-net':
expected_top.add('features')
if set(data) != expected_top:
raise ValueError('unexpected or mixed top-level settings')
if data['approval_policy'] != 'on-request':
raise ValueError('keep the documented approval policy')
if data['default_permissions'] != kind or set(data['permissions']) != {kind}:
raise ValueError('profile selection mismatch')
profile = data['permissions'][kind]
if set(profile) != {'description', 'workspace_roots', 'filesystem', 'network'}:
raise ValueError('unexpected profile fields')
if not isinstance(profile['description'], str) or not profile['description'].strip():
raise ValueError('missing description')
roots = profile['workspace_roots']
if not isinstance(roots, dict) or any(type(v) is not bool for v in roots.values()):
raise ValueError('workspace root flags must be booleans')
if profile['workspace_roots'] != {str(root / 'shared'): True}:
raise ValueError('extra or mismatched workspace roots')
expected_fs = {':minimal': 'read', str(root / 'outside'): 'deny', ':workspace_roots': {
'.': 'read', 'private': 'deny'}}
if kind != 'audit':
expected_fs[str(root / 'app/reports')] = 'write'
if profile['filesystem'] != expected_fs:
raise ValueError('filesystem recipe changed; review every change')
network = profile['network']
if not isinstance(network, dict) or type(network.get('enabled')) is not bool:
raise ValueError('network enabled flag must be a boolean')
expected_net = {'enabled': False}
if kind == 'report-net':
expected_net = {'enabled': True, 'domains': {'example.com': 'allow'}}
features = data['features']
if not isinstance(features, dict) or type(features.get('network_proxy')) is not bool:
raise ValueError('network proxy flag must be a boolean')
if data['features'] != {'network_proxy': True}:
raise ValueError('network proxy must be explicitly enabled')
if profile['network'] != expected_net:
raise ValueError('network recipe changed')
return {'status': 'candidate_recipe_matches', 'kind': kind,
'sandbox_tested': False, 'config_layers_checked': False,
'proxy_tested': False}
def main():
parser = argparse.ArgumentParser()
parser.add_argument('root', type=Path)
parser.add_argument('kind', choices=['audit', 'report', 'report-net'])
args = parser.parse_args()
root = args.root.resolve(strict=True)
with (root / 'control' / (args.kind + '.toml')).open('rb') as handle:
data = tomllib.load(handle)
print(json.dumps(check(data, root, args.kind), indent=2))
if __name__ == '__main__':
main()
tests/test_candidates.py
import copy
import tempfile
import tomllib
import unittest
from pathlib import Path
from unittest.mock import patch
from check_candidate import check
from make_fixture import create_fixture
class CandidateTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix='candidate-test-')
self.root = create_fixture(self.tmp.name)
def tearDown(self):
self.tmp.cleanup()
def data(self, kind='report'):
return tomllib.loads((self.root / 'control' / (kind+'.toml')).read_text())
def rejects(self, mutate, kind='report'):
data = copy.deepcopy(self.data(kind))
mutate(data)
with self.assertRaises(ValueError):
check(data, self.root, kind)
def test_three_candidates(self):
for kind in ['audit', 'report', 'report-net']:
with self.subTest(kind=kind):
self.assertFalse(check(self.data(kind), self.root, kind)['sandbox_tested'])
emoji_parent = Path(self.tmp.name) / '路径-🌱'
emoji_parent.mkdir()
emoji_root = create_fixture(emoji_parent)
for kind in ['audit', 'report', 'report-net']:
with self.subTest(kind=kind, path='non-BMP'):
data = tomllib.loads((emoji_root/'control'/(kind+'.toml')).read_text())
self.assertFalse(check(data, emoji_root, kind)['sandbox_tested'])
def test_glob_parent_rejected_before_creation(self):
for char in '*?[]':
parent = Path(self.tmp.name) / ('parent-' + char)
with self.subTest(char=char):
with patch('make_fixture.tempfile.mkdtemp') as create:
with self.assertRaises(ValueError):
create_fixture(parent)
create.assert_not_called()
self.assertFalse(parent.exists())
with patch('make_fixture.tempfile.gettempdir',
return_value=str(Path(self.tmp.name) / 'default[lab]')):
with patch('make_fixture.tempfile.mkdtemp') as create:
with self.assertRaises(ValueError):
create_fixture()
create.assert_not_called()
def test_glob_root_recipe_rejected(self):
for char in '*?[]':
root = Path(self.tmp.name) / ('lab-' + char)
for kind in ['audit', 'report', 'report-net']:
with self.subTest(char=char, kind=kind):
data = self.data(kind)
profile = data['permissions'][kind]
profile['workspace_roots'] = {str(root / 'shared'): True}
fs = profile['filesystem']
fs[str(root / 'outside')] = fs.pop(str(self.root / 'outside'))
if kind != 'audit':
fs[str(root / 'app/reports')] = fs.pop(str(self.root / 'app/reports'))
with self.assertRaises(ValueError):
check(data, root, kind)
def test_fixture_is_synthetic(self):
self.assertIn('SYNTHETIC', (self.root/'app/private/demo.txt').read_text())
def test_observation_not_filled(self):
self.assertIn('"real_sandbox_run": false', (self.root/'control/observation.json').read_text())
def test_legacy_sandbox_rejected(self):
self.rejects(lambda d: d.update(sandbox_mode='workspace-write'))
def test_legacy_workspace_rejected(self):
self.rejects(lambda d: d.update(sandbox_workspace_write={}))
def test_other_config_profiles_rejected(self):
self.rejects(lambda d: d.update(profiles={'x': {}}))
def test_wrong_selection_rejected(self):
self.rejects(lambda d: d.update(default_permissions='audit'))
def test_extra_root_rejected(self):
self.rejects(lambda d: d['permissions']['report']['workspace_roots'].update({'/extra':True}))
self.rejects(lambda d: d['permissions']['report']['workspace_roots'].update({str(self.root/'shared'):1}))
def test_input_write_rejected(self):
self.rejects(lambda d: d['permissions']['report']['filesystem'][':workspace_roots'].update({'input':'write'}))
def test_workspace_write_rejected(self):
self.rejects(lambda d: d['permissions']['report']['filesystem'][':workspace_roots'].update({'.':'write'}))
def test_private_read_rejected(self):
self.rejects(lambda d: d['permissions']['report']['filesystem'][':workspace_roots'].update({'private':'read'}))
def test_parent_traversal_rejected(self):
self.rejects(lambda d: d['permissions']['report']['filesystem'][':workspace_roots'].update({'../outside':'read'}))
def test_proxy_off_rejected(self):
self.rejects(lambda d: d['features'].update(network_proxy=False), 'report-net')
self.rejects(lambda d: d['features'].update(network_proxy=1), 'report-net')
self.rejects(lambda d: d['permissions']['report']['network'].update(enabled=0))
self.rejects(lambda d: d['permissions']['report-net']['network'].update(enabled=1), 'report-net')
def test_broad_domain_rejected(self):
self.rejects(lambda d: d['permissions']['report-net']['network']['domains'].update({'*':'allow'}),'report-net')
def test_unknown_kind_rejected(self):
with self.assertRaises(ValueError):
check(self.data(), self.root, 'other')
def test_never_claims_layers_checked(self):
self.assertFalse(check(self.data(), self.root, 'report')['config_layers_checked'])
if __name__ == '__main__':
unittest.main()
下列六份合成文件也单独放在配套包 fixture 目录。生成器输出的内容与它们相同。
fixture/app/input/catalog.json
[{"name":"blue mug","count":2}]
fixture/app/private/demo.txt
SYNTHETIC PRIVATE MARKER; not a credential.
fixture/app/reports/README.txt
Only synthetic report outputs belong here.
fixture/app/src/label.txt
A small catalog report exercise.
fixture/outside/marker.txt
SYNTHETIC OUTSIDE MARKER.
fixture/shared/spec.txt
Report the number of catalog entries.
10. 交接时留下哪些证据
交接一份权限配置时,至少保留候选原文、生成的绝对路径、实际 CLI 版本、配置来源、有效根集合、组织要求是否参与、代理状态、逐项命令和结果。正文中的矩阵可作为起点,空白处留给第一次真实观察,不能提前填上“通过”。
读者完成这个练习后,应当能指出:报告任务到底为什么需要 shared;为什么输入不能写;哪个规则保护 private;为什么只有 app/reports 获得写入;域名过滤依赖什么;一次拒绝停在了哪一层。说清这些细节,再把相同方法用于经过授权的真实项目。
本文提供三份配置模板、fixture 生成器、检查程序、18 项测试、观察表和三张原创 SVG。已完成离线程序测试与固定 Schema 校验,尚未验证真实 Codex 会话、操作系统沙箱及网络代理。插图为原创概念图,没有声称是真实运行截图。
11. 来源与复核边界
- 官方 Permissions:beta 范围、路径及网络语义
- 固定源码:配置字段、继承合并与域名声明
- 固定源码:从声明编译本地权限
- 固定源码:resolve_permission_config_syntax 与覆盖参数冲突
- 固定 Schema:本稿静态类型核查依据
- openai/codex Apache-2.0 许可
代码只作只读核对,没有摘抄其实现或把官方源码再分发进配套包。配置候选、目录案例、检查程序、测试与插图均为本文原创。日期之后的文档、客户端和管理策略可能变化;升级时重新核对,不把本稿当成跨版本不变的产品契约。