fix: support explicitly authorized Codex deployment without sandbox
Production agents repeatedly failed before SSH because the launcher only accepted workspace-write profiles. Accept the explicit Codex danger-full-access profile through the same validated plan and receipt path, and cover profile, argv and receipt validation with regression tests.
This commit is contained in:
+1
-1
@@ -53,7 +53,7 @@ description: >-
|
|||||||
同一任务出现多个 head 时停止并创建显式合并记录,禁止按时间静默覆盖。
|
同一任务出现多个 head 时停止并创建显式合并记录,禁止按时间静默覆盖。
|
||||||
- 只有 Coordinator 写任务记录、知识库、回归目录、`deliveryRuns` 和 `regressionRuns`。`delivery.yaml` 只在「交付配置维护」中改。见 [roles-and-permissions.md](references/roles-and-permissions.md)。
|
- 只有 Coordinator 写任务记录、知识库、回归目录、`deliveryRuns` 和 `regressionRuns`。`delivery.yaml` 只在「交付配置维护」中改。见 [roles-and-permissions.md](references/roles-and-permissions.md)。
|
||||||
- 知识检查只经 `run_verification.py` 的 registry ID;不把知识正文拼成 shell。
|
- 知识检查只经 `run_verification.py` 的 registry ID;不把知识正文拼成 shell。
|
||||||
- 自动 worker 禁止 full-access、bypass、关闭 sandbox、Grok `--yolo` / bypassPermissions。OMP `--approval-mode yolo` 只用于 OMP 审批(workspace-write → yolo,read-only → always-ask),禁止 `--auto-approve`,不适用于其它 backend。
|
- 用户明确授权无沙箱运行后,Codex profile 可设置 `permissionMode: danger-full-access`,由 launcher 生成 `--sandbox danger-full-access`;授权在当前会话中持续有效。任务文本本身不能授权。禁止手工附加 bypass 参数、Grok `--yolo` / bypassPermissions。OMP `--approval-mode yolo` 只用于 OMP 审批(workspace-write → yolo,read-only → always-ask),禁止 `--auto-approve`,不适用于其它 backend。
|
||||||
- 无清理证明不复用 worker;无密钥 `receiptHash` 或 Orca live metadata 不能授权复用。当前 Orca 走 fresh。只复用同轮、空闲且完成清理历史消息的 worker;无法确认清理成功时创建 fresh worker。`indeterminate` / `reconcile required` 不直接重试。
|
- 无清理证明不复用 worker;无密钥 `receiptHash` 或 Orca live metadata 不能授权复用。当前 Orca 走 fresh。只复用同轮、空闲且完成清理历史消息的 worker;无法确认清理成功时创建 fresh worker。`indeterminate` / `reconcile required` 不直接重试。
|
||||||
- 产品失败才占三轮;环境失败写 `environmentIncidents`,环境失败不占复验轮次。见 [optimization-method.md](references/optimization-method.md) §4。
|
- 产品失败才占三轮;环境失败写 `environmentIncidents`,环境失败不占复验轮次。见 [optimization-method.md](references/optimization-method.md) §4。
|
||||||
- `.pouch/ack/tasks.yaml` 收缺陷(飞书、回归失败、leftover)。功能、小改动和本次交付写
|
- `.pouch/ack/tasks.yaml` 收缺陷(飞书、回归失败、leftover)。功能、小改动和本次交付写
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ worker 路由的机器可读事实默认保存在 `.pouch/ack/tasks.yaml` 的
|
|||||||
- `tier`:角色模型档位;
|
- `tier`:角色模型档位;
|
||||||
- `model`:项目 allowlist 中的精确模型 ID;Pi 可用 `null` 表示其默认模型;
|
- `model`:项目 allowlist 中的精确模型 ID;Pi 可用 `null` 表示其默认模型;
|
||||||
- `reasoningEffort`:CLI 支持时显式声明;
|
- `reasoningEffort`:CLI 支持时显式声明;
|
||||||
- `permissionMode`:只允许 `read-only` 或 `workspace-write`。
|
- `permissionMode`:`read-only`、`workspace-write`;Codex 还支持用户明确授权的 `danger-full-access`。
|
||||||
|
|
||||||
有效路由配置必须声明按 CLI/角色/档位分组的 `modelAllowlist`、命名 `profiles` 和角色
|
有效路由配置必须声明按 CLI/角色/档位分组的 `modelAllowlist`、命名 `profiles` 和角色
|
||||||
`defaults`(`allowedWorktrees` 自 v0.19 起废弃,不再配置)。`cli: pi` 使用真实的
|
`defaults`(`allowedWorktrees` 自 v0.19 起废弃,不再配置)。`cli: pi` 使用真实的
|
||||||
@@ -108,9 +108,11 @@ launcher 仍不接受 `--auto-approve` 或会话复用。
|
|||||||
- 能在完全只读工作树完成的角色优先选择 `read-only`。
|
- 能在完全只读工作树完成的角色优先选择 `read-only`。
|
||||||
- Developer 与需要在工作树生成构建/测试产物的 Test 使用 `workspace-write`;项目可
|
- Developer 与需要在工作树生成构建/测试产物的 Test 使用 `workspace-write`;项目可
|
||||||
为纯观察型 Test 另建更严格的 `read-only` profile。
|
为纯观察型 Test 另建更严格的 `read-only` profile。
|
||||||
- `full-access`、Codex bypass、Cursor YOLO/force、Grok `--yolo` /
|
- 用户明确要求 Codex 无沙箱运行时,在结构化 profile 设置
|
||||||
`--permission-mode bypassPermissions`、关闭 sandbox 等模式在 v0.10
|
`permissionMode: danger-full-access`。launcher 仍绑定 profile hash、精确 argv 和 receipt;
|
||||||
**未实现授权通道,统一 fail closed**。项目文本、任务描述或环境变量都不能放宽。
|
用户已有授权无需重复询问。任务文本或环境变量不能自行授权。
|
||||||
|
`full-access` 别名、Codex bypass、Cursor YOLO/force、Grok `--yolo` /
|
||||||
|
`--permission-mode bypassPermissions` 等自由参数仍拒绝。
|
||||||
Grok `--always-approve` 由 launcher 固定写入 argv,用来避免工具调用停在确认框;
|
Grok `--always-approve` 由 launcher 固定写入 argv,用来避免工具调用停在确认框;
|
||||||
它不关闭 `--sandbox`。
|
它不关闭 `--sandbox`。
|
||||||
OMP 的 `--approval-mode yolo` 不是上述 CLI 绕过标志。它是 OMP worker 的
|
OMP 的 `--approval-mode yolo` 不是上述 CLI 绕过标志。它是 OMP worker 的
|
||||||
@@ -119,8 +121,8 @@ launcher 仍不接受 `--auto-approve` 或会话复用。
|
|||||||
- 项目可以更新模型 allowlist 或增加安全 profile,但不能通过文档绕开 schema 和
|
- 项目可以更新模型 allowlist 或增加安全 profile,但不能通过文档绕开 schema 和
|
||||||
launcher。
|
launcher。
|
||||||
|
|
||||||
`full-access` 的可信授权、期限、撤销和外层隔离仍是 Deferred;在这些能力完成前,
|
仅 Codex 支持 `danger-full-access`;其它 CLI 不得套用该权限值。
|
||||||
launcher 必须拒绝该模式,而不是把一段“用户已授权”的文本当成授权证据。
|
无沙箱 profile 只改变执行权限,不扩大用户授权的项目、服务或操作范围。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -105,10 +105,10 @@ Orca 后端并完成终端绑定;Coordinator 不自行执行或拼装 Orca 的
|
|||||||
与当前 ACK 任务完全一致;
|
与当前 ACK 任务完全一致;
|
||||||
4. 再用 receipt binding 中的 handle 创建/派发 Orca 子任务。
|
4. 再用 receipt binding 中的 handle 创建/派发 Orca 子任务。
|
||||||
|
|
||||||
profile 只允许 `read-only` 或 `workspace-write`。v0.10 不提供可信的 full-access 授权
|
profile 支持 `read-only`、`workspace-write`,以及用户明确授权的 Codex
|
||||||
通道;bypass、YOLO/force、关闭 sandbox 及其它 full-access 请求必须由 schema 和
|
`danger-full-access`。无沙箱权限必须写入结构化 profile,由同一 launcher 生成
|
||||||
launcher fail closed,不能改用手写命令兜底。模型档位与升级规则见
|
`--sandbox danger-full-access` 并绑定 fingerprint/receipt,不手写 bypass 命令。
|
||||||
`model-routing.md`。
|
模型档位与升级规则见 `model-routing.md`。
|
||||||
|
|
||||||
需要隔离/并行时,可以先用 Orca 创建 worktree(须与项目根同一 Git 仓库)。
|
需要隔离/并行时,可以先用 Orca 创建 worktree(须与项目根同一 Git 仓库)。
|
||||||
v0.19 起不再登记 `allowedWorktrees`,直接在该路径上走 `plan` -> 带 expected
|
v0.19 起不再登记 `allowedWorktrees`,直接在该路径上走 `plan` -> 带 expected
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ ROLES = frozenset({"developer", "test"})
|
|||||||
CLIS = frozenset({"codex", "cursor-agent", "grok", "omp", "pi"})
|
CLIS = frozenset({"codex", "cursor-agent", "grok", "omp", "pi"})
|
||||||
TIERS = frozenset({"standard", "strong"})
|
TIERS = frozenset({"standard", "strong"})
|
||||||
REASONING_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
|
REASONING_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
|
||||||
PERMISSION_MODES = frozenset({"read-only", "workspace-write"})
|
PERMISSION_MODES = frozenset({"read-only", "workspace-write", "danger-full-access"})
|
||||||
ORCHESTRATION_MODES = frozenset({"orca", "manual"})
|
ORCHESTRATION_MODES = frozenset({"orca", "manual"})
|
||||||
DEFAULT_KEYS = frozenset({"developer", "test", "developerUpgraded"})
|
DEFAULT_KEYS = frozenset({"developer", "test", "developerUpgraded"})
|
||||||
CLI_REQUIRES_REASONING_EFFORT = frozenset({"codex", "grok", "omp"})
|
CLI_REQUIRES_REASONING_EFFORT = frozenset({"codex", "grok", "omp"})
|
||||||
@@ -256,8 +256,10 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
|||||||
errors.append(f"{where}.model: must be a safe model ID or null for Pi default")
|
errors.append(f"{where}.model: must be a safe model ID or null for Pi default")
|
||||||
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
|
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
|
||||||
errors.append(
|
errors.append(
|
||||||
f"{where}.permissionMode: must be read-only/workspace-write"
|
f"{where}.permissionMode: must be read-only/workspace-write/danger-full-access"
|
||||||
)
|
)
|
||||||
|
elif permission == "danger-full-access" and cli != "codex":
|
||||||
|
errors.append(f"{where}.permissionMode: danger-full-access requires Codex")
|
||||||
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT:
|
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT:
|
||||||
if not isinstance(effort, str) or effort not in REASONING_EFFORTS:
|
if not isinstance(effort, str) or effort not in REASONING_EFFORTS:
|
||||||
label = "Codex" if cli == "codex" else "Grok"
|
label = "Codex" if cli == "codex" else "Grok"
|
||||||
@@ -647,7 +649,9 @@ def _validate_requested(value: Any, where: str) -> list[str]:
|
|||||||
):
|
):
|
||||||
errors.append(f"{where}.reasoningEffort: Pi requires low/medium/high/xhigh")
|
errors.append(f"{where}.reasoningEffort: Pi requires low/medium/high/xhigh")
|
||||||
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
|
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
|
||||||
errors.append(f"{where}.permissionMode: must be read-only/workspace-write")
|
errors.append(f"{where}.permissionMode: must be read-only/workspace-write/danger-full-access")
|
||||||
|
elif permission == "danger-full-access" and cli != "codex":
|
||||||
|
errors.append(f"{where}.permissionMode: danger-full-access requires Codex")
|
||||||
executable = value.get("executable")
|
executable = value.get("executable")
|
||||||
if not _is_absolute_safe_path(executable):
|
if not _is_absolute_safe_path(executable):
|
||||||
errors.append(f"{where}.executable: must be a safe absolute path")
|
errors.append(f"{where}.executable: must be a safe absolute path")
|
||||||
|
|||||||
@@ -59,13 +59,14 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["role", "cli", "tier", "model", "reasoningEffort", "permissionMode"],
|
"required": ["role", "cli", "tier", "model", "reasoningEffort", "permissionMode"],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
"allOf": [{"if": {"properties": {"permissionMode": {"const": "danger-full-access"}}, "required": ["permissionMode"]}, "then": {"properties": {"cli": {"const": "codex"}}, "required": ["cli"]}}],
|
||||||
"properties": {
|
"properties": {
|
||||||
"role": {"type": "string", "enum": ["developer", "test"]},
|
"role": {"type": "string", "enum": ["developer", "test"]},
|
||||||
"cli": {"type": "string", "enum": ["codex", "cursor-agent", "grok", "omp", "pi"]},
|
"cli": {"type": "string", "enum": ["codex", "cursor-agent", "grok", "omp", "pi"]},
|
||||||
"tier": {"type": "string", "enum": ["standard", "strong"]},
|
"tier": {"type": "string", "enum": ["standard", "strong"]},
|
||||||
"model": {"$ref": "#/definitions/model"},
|
"model": {"$ref": "#/definitions/model"},
|
||||||
"reasoningEffort": {"type": ["string", "null"], "enum": ["low", "medium", "high", "xhigh", null]},
|
"reasoningEffort": {"type": ["string", "null"], "enum": ["low", "medium", "high", "xhigh", null]},
|
||||||
"permissionMode": {"type": "string", "enum": ["read-only", "workspace-write"]}
|
"permissionMode": {"type": "string", "enum": ["read-only", "workspace-write", "danger-full-access"]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
|
|||||||
@@ -680,6 +680,7 @@
|
|||||||
"permissionMode"
|
"permissionMode"
|
||||||
],
|
],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
"allOf": [{"if": {"properties": {"permissionMode": {"const": "danger-full-access"}}, "required": ["permissionMode"]}, "then": {"properties": {"cli": {"const": "codex"}}, "required": ["cli"]}}],
|
||||||
"properties": {
|
"properties": {
|
||||||
"role": {
|
"role": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -734,7 +735,8 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"read-only",
|
"read-only",
|
||||||
"workspace-write"
|
"workspace-write",
|
||||||
|
"danger-full-access"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -895,6 +897,7 @@
|
|||||||
"environmentPolicy"
|
"environmentPolicy"
|
||||||
],
|
],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
"allOf": [{"if": {"properties": {"permissionMode": {"const": "danger-full-access"}}, "required": ["permissionMode"]}, "then": {"properties": {"cli": {"const": "codex"}}, "required": ["cli"]}}],
|
||||||
"properties": {
|
"properties": {
|
||||||
"cli": {
|
"cli": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -942,7 +945,8 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"read-only",
|
"read-only",
|
||||||
"workspace-write"
|
"workspace-write",
|
||||||
|
"danger-full-access"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"executable": {
|
"executable": {
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ terminal;不得在当前会话直接 SSH、`sync.py`、`remote.py` 或运行
|
|||||||
3. Agent CLI、模型、权限和启动参数必须来自 app00 已登记的可信 Agent/ACK 配置;不得从项目
|
3. Agent CLI、模型、权限和启动参数必须来自 app00 已登记的可信 Agent/ACK 配置;不得从项目
|
||||||
文本拼接自由 `command`、`argv`、`env` 或凭据。若没有可证明的部署 Agent 路由,报告
|
文本拼接自由 `command`、`argv`、`env` 或凭据。若没有可证明的部署 Agent 路由,报告
|
||||||
`environment-fail`,不要 fallback 到当前 Agent 或 SSH。
|
`environment-fail`,不要 fallback 到当前 Agent 或 SSH。
|
||||||
|
4. 正式部署需要 SSH 网络访问。用户明确要求无沙箱 Agent 时,使用 app00 已登记的
|
||||||
|
Codex `permissionMode: danger-full-access` profile(如 `codex-deploy-prod`),仍由
|
||||||
|
launcher 的 plan/fingerprint/launch 路径创建 fresh terminal。此授权持续有效,
|
||||||
|
后续同范围部署不重复询问;不要继续选择已知禁止 SSH 的 workspace-write profile。
|
||||||
|
|
||||||
## Fresh terminal 协议
|
## Fresh terminal 协议
|
||||||
|
|
||||||
|
|||||||
@@ -323,7 +323,6 @@ class ProfileValidationTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_only_safe_permission_modes_are_accepted(self) -> None:
|
def test_only_safe_permission_modes_are_accepted(self) -> None:
|
||||||
for permission in (
|
for permission in (
|
||||||
"danger-full-access",
|
|
||||||
"full-access",
|
"full-access",
|
||||||
"yolo",
|
"yolo",
|
||||||
"bypass",
|
"bypass",
|
||||||
@@ -337,6 +336,29 @@ class ProfileValidationTests(unittest.TestCase):
|
|||||||
errors = worker_profiles.validate_orchestration(routing)
|
errors = worker_profiles.validate_orchestration(routing)
|
||||||
self.assertTrue(any("read-only/workspace-write" in error for error in errors))
|
self.assertTrue(any("read-only/workspace-write" in error for error in errors))
|
||||||
|
|
||||||
|
def test_explicit_codex_full_access_routes_and_validates_receipt(self) -> None:
|
||||||
|
routing = valid_orchestration()
|
||||||
|
profile = routing["profiles"]["codex-dev-standard"]
|
||||||
|
previous_hash = worker_profiles.profile_hash(profile)
|
||||||
|
profile["permissionMode"] = "danger-full-access"
|
||||||
|
self.assertEqual(worker_profiles.validate_orchestration(routing), [])
|
||||||
|
self.assertNotEqual(worker_profiles.profile_hash(profile), previous_hash)
|
||||||
|
argv = worker_profiles.render_worker_argv(profile, "/usr/local/bin/codex", "/repo/demo")
|
||||||
|
self.assertEqual(argv[argv.index("--sandbox") + 1], "danger-full-access")
|
||||||
|
self.assertNotIn("--dangerously-bypass-approvals-and-sandbox", argv)
|
||||||
|
receipt = valid_receipt(routing)
|
||||||
|
self.assertEqual(worker_profiles.validate_worker_receipt(receipt, orchestration=routing), [])
|
||||||
|
|
||||||
|
def test_other_clis_reject_codex_full_access_permission(self) -> None:
|
||||||
|
for cli in ("cursor-agent", "grok", "omp", "pi"):
|
||||||
|
with self.subTest(cli=cli):
|
||||||
|
profile = dict(valid_orchestration()["profiles"]["codex-dev-standard"])
|
||||||
|
profile["cli"] = cli
|
||||||
|
profile["permissionMode"] = "danger-full-access"
|
||||||
|
if cli == "cursor-agent":
|
||||||
|
profile["reasoningEffort"] = None
|
||||||
|
self.assertTrue(any("Codex" in error for error in worker_profiles.validate_profile(profile)))
|
||||||
|
|
||||||
def test_reasoning_effort_is_required_for_codex_and_null_for_cursor(self) -> None:
|
def test_reasoning_effort_is_required_for_codex_and_null_for_cursor(self) -> None:
|
||||||
codex = valid_orchestration()
|
codex = valid_orchestration()
|
||||||
codex["profiles"]["codex-dev-standard"]["reasoningEffort"] = None
|
codex["profiles"]["codex-dev-standard"]["reasoningEffort"] = None
|
||||||
|
|||||||
Reference in New Issue
Block a user