feat(ack): add grok workers and allow --always-approve
Grok is a first-class worker CLI. Launcher argv includes --always-approve so unattended tool calls are not blocked; sandbox stays required.
This commit is contained in:
@@ -303,6 +303,7 @@ class AckDeliveryValidationTests(unittest.TestCase):
|
||||
{
|
||||
"enabled": False,
|
||||
"defaultProfile": None,
|
||||
"intents": {"testEnvironment": None, "release": None},
|
||||
"entrypoints": {},
|
||||
"artifacts": {},
|
||||
"destinations": {},
|
||||
@@ -313,6 +314,42 @@ class AckDeliveryValidationTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(validate_delivery.validate_builtin(contract), [])
|
||||
|
||||
def test_intents_must_point_at_matching_stop_points(self) -> None:
|
||||
contract = valid_contract()
|
||||
contract["intents"] = {
|
||||
"testEnvironment": "review",
|
||||
"release": None,
|
||||
}
|
||||
|
||||
errors = validate_delivery.validate_builtin(contract)
|
||||
self.assertTrue(
|
||||
any("intents.testEnvironment" in item and "validation_ready" in item for item in errors)
|
||||
)
|
||||
|
||||
contract["intents"]["testEnvironment"] = "local-validation"
|
||||
contract["profiles"]["local-validation"] = {
|
||||
"stopAt": "validation_ready",
|
||||
"steps": [
|
||||
{"id": "build-local", "action": "build", "artifact": "service-deb"},
|
||||
{
|
||||
"id": "deploy-local",
|
||||
"action": "deploy",
|
||||
"artifact": "service-deb",
|
||||
"environment": "test-server",
|
||||
},
|
||||
{
|
||||
"id": "health-local",
|
||||
"action": "health-check",
|
||||
"environment": "test-server",
|
||||
},
|
||||
],
|
||||
}
|
||||
self.assertEqual(validate_delivery.validate_builtin(contract), [])
|
||||
|
||||
contract["intents"]["release"] = "missing-release"
|
||||
errors = validate_delivery.validate_builtin(contract)
|
||||
self.assertTrue(any("未定义 profile 'missing-release'" in item for item in errors))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -324,6 +324,9 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
||||
"OPENAI_API_KEY": "test-token",
|
||||
"AZURE_OPENAI_API_KEY": "azure-token",
|
||||
"CURSOR_API_KEY": "cursor-token",
|
||||
"XAI_API_KEY": "xai-token",
|
||||
"GROK_HOME": "/tmp/evil-grok",
|
||||
"GROK_SANDBOX": "off",
|
||||
"ANTHROPIC_API_KEY": "anthropic-token",
|
||||
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/tmp/dbus",
|
||||
"DISPLAY": ":99",
|
||||
@@ -336,17 +339,24 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
||||
control = launch_worker.control_environment()
|
||||
codex = launch_worker.worker_environment("codex")
|
||||
cursor = launch_worker.worker_environment("cursor-agent")
|
||||
grok = launch_worker.worker_environment("grok")
|
||||
|
||||
for environment in (control, codex, cursor):
|
||||
for environment in (control, codex, cursor, grok):
|
||||
self.assertNotIn(hostile_path, environment["PATH"].split(os.pathsep))
|
||||
self.assertEqual(environment["LANG"], "C.UTF-8")
|
||||
self.assertNotIn("OPENAI_API_KEY", control)
|
||||
self.assertNotIn("CURSOR_API_KEY", control)
|
||||
self.assertNotIn("XAI_API_KEY", control)
|
||||
self.assertEqual(codex["OPENAI_API_KEY"], "test-token")
|
||||
self.assertEqual(codex["AZURE_OPENAI_API_KEY"], "azure-token")
|
||||
self.assertNotIn("CURSOR_API_KEY", codex)
|
||||
self.assertNotIn("XAI_API_KEY", codex)
|
||||
self.assertEqual(cursor["CURSOR_API_KEY"], "cursor-token")
|
||||
self.assertNotIn("OPENAI_API_KEY", cursor)
|
||||
self.assertNotIn("XAI_API_KEY", cursor)
|
||||
self.assertEqual(grok["XAI_API_KEY"], "xai-token")
|
||||
self.assertNotIn("OPENAI_API_KEY", grok)
|
||||
self.assertNotIn("CURSOR_API_KEY", grok)
|
||||
for forbidden in (
|
||||
"ANTHROPIC_API_KEY",
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
@@ -356,11 +366,13 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
||||
"NODE_OPTIONS",
|
||||
"PYTHONPATH",
|
||||
"CODEX_HOME",
|
||||
"GROK_HOME",
|
||||
"GROK_SANDBOX",
|
||||
"GIT_SSH_COMMAND",
|
||||
"WAYLAND_DISPLAY",
|
||||
"XDG_RUNTIME_DIR",
|
||||
):
|
||||
for environment in (control, codex, cursor):
|
||||
for environment in (control, codex, cursor, grok):
|
||||
self.assertNotIn(forbidden, environment)
|
||||
|
||||
def test_executable_resolution_ignores_hostile_path(self) -> None:
|
||||
@@ -373,6 +385,51 @@ class EnvironmentAndExecutableTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(resolved, Path("/usr/bin/git"))
|
||||
|
||||
def test_grok_vendor_layout_is_trusted_and_outside_artifacts_are_not(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
home = Path(temporary) / "home"
|
||||
downloads = home / ".grok" / "downloads"
|
||||
downloads.mkdir(parents=True)
|
||||
artifact = downloads / "grok-linux-x86_64"
|
||||
artifact.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
artifact.chmod(0o775)
|
||||
bindir = home / ".local" / "bin"
|
||||
bindir.mkdir(parents=True)
|
||||
(bindir / "grok").symlink_to(artifact)
|
||||
|
||||
hostile_dir = Path(temporary) / "tmp"
|
||||
hostile_dir.mkdir()
|
||||
hostile = hostile_dir / "grok-linux-x86_64"
|
||||
hostile.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
hostile.chmod(0o775)
|
||||
hostile_bin = Path(temporary) / "hostile-bin"
|
||||
hostile_bin.mkdir()
|
||||
(hostile_bin / "grok").symlink_to(hostile)
|
||||
|
||||
with mock.patch.object(
|
||||
launch_worker,
|
||||
"account_identity",
|
||||
return_value=(home.resolve(strict=True), "ace"),
|
||||
), mock.patch.object(
|
||||
launch_worker,
|
||||
"trusted_path_entries",
|
||||
return_value=[bindir.resolve(strict=True)],
|
||||
):
|
||||
resolved = launch_worker.resolve_executable("grok")
|
||||
self.assertEqual(resolved, artifact.resolve(strict=True))
|
||||
|
||||
with mock.patch.object(
|
||||
launch_worker,
|
||||
"account_identity",
|
||||
return_value=(home.resolve(strict=True), "ace"),
|
||||
), mock.patch.object(
|
||||
launch_worker,
|
||||
"trusted_path_entries",
|
||||
return_value=[hostile_bin.resolve(strict=True)],
|
||||
):
|
||||
with self.assertRaises(launch_worker.LaunchError):
|
||||
launch_worker.resolve_executable("grok")
|
||||
|
||||
|
||||
class PlanTests(unittest.TestCase):
|
||||
def test_authoritative_board_is_derived_from_project_root_without_repo_path(self) -> None:
|
||||
|
||||
@@ -27,6 +27,9 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
"references/kickoff.md",
|
||||
"不要修改项目的 `AGENTS.md`",
|
||||
"当前会话担任 Coordinator",
|
||||
"intents.testEnvironment",
|
||||
"运行测试环境",
|
||||
"运行版本发布",
|
||||
):
|
||||
self.assertIn(expected, content)
|
||||
|
||||
@@ -128,7 +131,7 @@ class AckSkillContentTests(unittest.TestCase):
|
||||
):
|
||||
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
|
||||
version = (ack_dir / "VERSION").read_text(encoding="utf-8").strip()
|
||||
self.assertEqual(version, "0.15.0")
|
||||
self.assertEqual(version, "0.17.1")
|
||||
self.assertIn(
|
||||
f'ackVersion: "{version}"',
|
||||
(ack_dir / "examples" / "tasks.example.yaml").read_text(encoding="utf-8"),
|
||||
|
||||
@@ -438,6 +438,47 @@ class AckTaskValidationTests(unittest.TestCase):
|
||||
|
||||
self.assert_board_accepted_in_all_modes(board)
|
||||
|
||||
def test_intent_delivery_run_allows_empty_task_ids(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["project"]["deliveryFile"] = "docs/ack/delivery.yaml"
|
||||
board["deliveryRuns"] = [
|
||||
{
|
||||
"id": "DR-test-env-1",
|
||||
"profile": "test-local",
|
||||
"intent": "testEnvironment",
|
||||
"taskIds": [],
|
||||
"status": "validation_ready",
|
||||
"sourceRevision": "a" * 40,
|
||||
"configRevision": "b" * 40,
|
||||
"pullRequest": None,
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "garden-bin",
|
||||
"type": "file",
|
||||
"reference": "garden",
|
||||
"digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
],
|
||||
"deployments": [
|
||||
{
|
||||
"environment": "local-write",
|
||||
"result": "succeeded",
|
||||
"evidence": "GET /login returned 200",
|
||||
}
|
||||
],
|
||||
"evidence": ["http://write.localhost:8080/ ready"],
|
||||
"updatedAt": "2026-08-23T00:57:00+08:00",
|
||||
}
|
||||
]
|
||||
|
||||
self.assert_board_accepted_in_all_modes(board)
|
||||
|
||||
del board["deliveryRuns"][0]["intent"]
|
||||
self.assert_board_rejected_in_all_modes(
|
||||
board,
|
||||
"taskIds: 必须是非空任务 ID 列表",
|
||||
)
|
||||
|
||||
def test_delivery_runs_and_delivery_file_must_appear_together(self) -> None:
|
||||
board = valid_manual_routing_board()
|
||||
board["deliveryRuns"] = []
|
||||
|
||||
@@ -30,6 +30,13 @@ def valid_orchestration() -> dict:
|
||||
"developer": {"standard": ["cursor-auto"]},
|
||||
"test": {"standard": ["cursor-auto"]},
|
||||
},
|
||||
"grok": {
|
||||
"developer": {
|
||||
"standard": ["grok-4.5"],
|
||||
"strong": ["grok-4.6"],
|
||||
},
|
||||
"test": {"standard": ["grok-4.5"]},
|
||||
},
|
||||
},
|
||||
"profiles": {
|
||||
"codex-dev-standard": {
|
||||
@@ -64,6 +71,30 @@ def valid_orchestration() -> dict:
|
||||
"reasoningEffort": None,
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
"grok-dev-standard": {
|
||||
"role": "developer",
|
||||
"cli": "grok",
|
||||
"tier": "standard",
|
||||
"model": "grok-4.5",
|
||||
"reasoningEffort": "medium",
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
"grok-dev-strong": {
|
||||
"role": "developer",
|
||||
"cli": "grok",
|
||||
"tier": "strong",
|
||||
"model": "grok-4.6",
|
||||
"reasoningEffort": "high",
|
||||
"permissionMode": "workspace-write",
|
||||
},
|
||||
"grok-test-readonly": {
|
||||
"role": "test",
|
||||
"cli": "grok",
|
||||
"tier": "standard",
|
||||
"model": "grok-4.5",
|
||||
"reasoningEffort": "low",
|
||||
"permissionMode": "read-only",
|
||||
},
|
||||
},
|
||||
"defaults": {
|
||||
"developer": "codex-dev-standard",
|
||||
@@ -278,12 +309,16 @@ class ProfileValidationTests(unittest.TestCase):
|
||||
codex["profiles"]["codex-dev-standard"]["reasoningEffort"] = None
|
||||
cursor = valid_orchestration()
|
||||
cursor["profiles"]["cursor-test-standard"]["reasoningEffort"] = "low"
|
||||
grok = valid_orchestration()
|
||||
grok["profiles"]["grok-dev-standard"]["reasoningEffort"] = None
|
||||
|
||||
codex_errors = worker_profiles.validate_orchestration(codex)
|
||||
cursor_errors = worker_profiles.validate_orchestration(cursor)
|
||||
grok_errors = worker_profiles.validate_orchestration(grok)
|
||||
|
||||
self.assertTrue(any("Codex requires" in error for error in codex_errors))
|
||||
self.assertTrue(any("Cursor requires null" in error for error in cursor_errors))
|
||||
self.assertTrue(any("Grok requires" in error for error in grok_errors))
|
||||
|
||||
def test_test_cannot_use_strong_tier(self) -> None:
|
||||
routing = valid_orchestration()
|
||||
@@ -444,6 +479,86 @@ class ArgvRendererTests(unittest.TestCase):
|
||||
self.assertNotIn("--yolo", argv)
|
||||
self.assertNotIn("--force", argv)
|
||||
|
||||
def test_grok_workspace_write_exact_safe_argv(self) -> None:
|
||||
profile = valid_orchestration()["profiles"]["grok-dev-standard"]
|
||||
|
||||
argv = worker_profiles.render_worker_argv(
|
||||
profile,
|
||||
"/usr/local/bin/grok",
|
||||
"/repo/demo",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
argv,
|
||||
[
|
||||
"/usr/local/bin/grok",
|
||||
"--model",
|
||||
"grok-4.5",
|
||||
"--reasoning-effort",
|
||||
"medium",
|
||||
"--permission-mode",
|
||||
"acceptEdits",
|
||||
"--always-approve",
|
||||
"--sandbox",
|
||||
"workspace",
|
||||
"--cwd",
|
||||
"/repo/demo",
|
||||
],
|
||||
)
|
||||
for forbidden in (
|
||||
"--yolo",
|
||||
"bypassPermissions",
|
||||
"auto",
|
||||
"dontAsk",
|
||||
"off",
|
||||
):
|
||||
self.assertNotIn(forbidden, argv)
|
||||
|
||||
def test_grok_read_only_uses_plan_and_read_only_sandbox(self) -> None:
|
||||
profile = valid_orchestration()["profiles"]["grok-test-readonly"]
|
||||
|
||||
argv = worker_profiles.render_worker_argv(
|
||||
profile,
|
||||
"/usr/local/bin/grok",
|
||||
"/repo/demo",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
argv,
|
||||
[
|
||||
"/usr/local/bin/grok",
|
||||
"--model",
|
||||
"grok-4.5",
|
||||
"--reasoning-effort",
|
||||
"low",
|
||||
"--permission-mode",
|
||||
"plan",
|
||||
"--always-approve",
|
||||
"--sandbox",
|
||||
"read-only",
|
||||
"--cwd",
|
||||
"/repo/demo",
|
||||
],
|
||||
)
|
||||
self.assertNotIn("--yolo", argv)
|
||||
self.assertNotIn("bypassPermissions", argv)
|
||||
|
||||
def test_grok_accepts_vendor_artifact_basename(self) -> None:
|
||||
profile = valid_orchestration()["profiles"]["grok-dev-standard"]
|
||||
artifact = "/home/ace/.grok/downloads/grok-linux-x86_64"
|
||||
|
||||
argv = worker_profiles.render_worker_argv(profile, artifact, "/repo/demo")
|
||||
|
||||
self.assertEqual(argv[0], artifact)
|
||||
self.assertTrue(
|
||||
worker_profiles.executable_basename_matches_cli(artifact, "grok")
|
||||
)
|
||||
self.assertFalse(
|
||||
worker_profiles.executable_basename_matches_cli(
|
||||
artifact, "cursor-agent"
|
||||
)
|
||||
)
|
||||
|
||||
def test_renderer_rejects_wrong_executable_or_unsafe_worktree(self) -> None:
|
||||
profile = valid_orchestration()["profiles"]["codex-dev-standard"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user