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:
@@ -39,8 +39,10 @@ if str(SCRIPT_DIR) not in sys.path:
|
||||
|
||||
from validate_tasks import load_document, validate_builtin # noqa: E402
|
||||
from worker_profiles import ( # noqa: E402
|
||||
GROK_EXECUTABLE_NAME_RE,
|
||||
LAUNCH_PROTOCOL_VERSION,
|
||||
canonical_sha256,
|
||||
executable_basename_matches_cli,
|
||||
profile_hash,
|
||||
render_worker_argv,
|
||||
validate_routing_document,
|
||||
@@ -81,6 +83,12 @@ WORKER_ENVIRONMENT_NAMES = CONTROL_ENVIRONMENT_NAMES | {
|
||||
WORKER_CREDENTIAL_NAMES = {
|
||||
"codex": frozenset({"AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"}),
|
||||
"cursor-agent": frozenset({"CURSOR_API_KEY"}),
|
||||
"grok": frozenset({"XAI_API_KEY"}),
|
||||
}
|
||||
CLI_TITLE_LABELS = {
|
||||
"codex": "CODEX",
|
||||
"cursor-agent": "CURSOR",
|
||||
"grok": "GROK",
|
||||
}
|
||||
INHERITED_ENVIRONMENT_PREFIXES = (
|
||||
"LC_",
|
||||
@@ -263,8 +271,41 @@ def load_authoritative_board(project_root_value: str) -> tuple[Path, dict]:
|
||||
return project_root, board
|
||||
|
||||
|
||||
def _is_under_real_grok_home(resolved: Path) -> bool:
|
||||
"""Return True when ``resolved`` is under a non-symlink ``$HOME/.grok``."""
|
||||
|
||||
home, _ = account_identity()
|
||||
grok_root = home / ".grok"
|
||||
try:
|
||||
root_metadata = os.lstat(grok_root)
|
||||
if stat.S_ISLNK(root_metadata.st_mode) or not stat.S_ISDIR(root_metadata.st_mode):
|
||||
return False
|
||||
grok_root.resolve(strict=True)
|
||||
resolved.relative_to(grok_root.resolve(strict=True))
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _is_trusted_grok_executable(resolved: Path, metadata: os.stat_result) -> bool:
|
||||
"""Accept Grok's vendor artifact names under ``~/.grok``, or a 0755 ``grok``."""
|
||||
|
||||
if GROK_EXECUTABLE_NAME_RE.fullmatch(resolved.name) is None:
|
||||
return False
|
||||
if not executable_basename_matches_cli(str(resolved), "grok"):
|
||||
return False
|
||||
mode = stat.S_IMODE(metadata.st_mode)
|
||||
if mode & 0o002:
|
||||
return False
|
||||
if resolved.name == "grok" and not (mode & 0o020):
|
||||
return True
|
||||
if not _is_under_real_grok_home(resolved):
|
||||
return False
|
||||
return metadata.st_uid == os.getuid() and metadata.st_gid == os.getgid()
|
||||
|
||||
|
||||
def resolve_executable(name: str) -> Path:
|
||||
supported = {"codex", "cursor-agent", "git", "orca"}
|
||||
supported = {"codex", "cursor-agent", "grok", "git", "orca"}
|
||||
if name not in supported:
|
||||
raise LaunchError(f"不支持的可执行文件: {name}")
|
||||
search_paths = trusted_path_entries()
|
||||
@@ -289,6 +330,10 @@ def resolve_executable(name: str) -> Path:
|
||||
continue
|
||||
if metadata.st_uid not in {0, os.getuid()}:
|
||||
continue
|
||||
if name == "grok":
|
||||
if _is_trusted_grok_executable(resolved, metadata):
|
||||
return resolved
|
||||
continue
|
||||
if stat.S_IMODE(metadata.st_mode) & 0o022:
|
||||
continue
|
||||
if resolved.name != name:
|
||||
@@ -697,7 +742,10 @@ def build_plan(
|
||||
"slot": slot,
|
||||
}
|
||||
)
|
||||
cli_label = "CODEX" if profile["cli"] == "codex" else "CURSOR"
|
||||
try:
|
||||
cli_label = CLI_TITLE_LABELS[str(profile["cli"])]
|
||||
except KeyError as exc:
|
||||
raise LaunchError(f"不支持的 worker CLI: {profile['cli']}") from exc
|
||||
role_label = "DEV" if role == "developer" else "TEST"
|
||||
digest_short = launch_fingerprint.split(":", 1)[-1][:10]
|
||||
title = (
|
||||
|
||||
@@ -43,6 +43,7 @@ TOP_LEVEL_FIELDS = {
|
||||
"project",
|
||||
"enabled",
|
||||
"defaultProfile",
|
||||
"intents",
|
||||
"entrypoints",
|
||||
"artifacts",
|
||||
"destinations",
|
||||
@@ -99,6 +100,11 @@ CHANNELS = {"preview", "staging", "stable"}
|
||||
ENVIRONMENT_TYPES = {"ssh-host", "docker-compose", "kubernetes", "custom"}
|
||||
CLASSIFICATIONS = {"development", "staging", "production"}
|
||||
STOP_POINTS = {"verified", "validation_ready", "review_ready", "released"}
|
||||
INTENT_FIELDS = {"testEnvironment", "release"}
|
||||
INTENT_STOP_AT = {
|
||||
"testEnvironment": "validation_ready",
|
||||
"release": "released",
|
||||
}
|
||||
ACTIONS = {
|
||||
"verify",
|
||||
"pull-request",
|
||||
@@ -675,6 +681,38 @@ def _validate_profiles(
|
||||
errors.append(f"{where}: defaultProfile 不能部署 production 环境")
|
||||
|
||||
|
||||
def _validate_intents(
|
||||
values: Any,
|
||||
profiles: dict[str, Any],
|
||||
errors: list[str],
|
||||
) -> None:
|
||||
if values is None:
|
||||
return
|
||||
if not _mapping(values):
|
||||
errors.append("intents: 必须是对象")
|
||||
return
|
||||
_reject_unknown(values, INTENT_FIELDS, "intents", errors)
|
||||
for field in sorted(INTENT_FIELDS):
|
||||
if field not in values:
|
||||
errors.append(f"intents.{field}: 必填")
|
||||
continue
|
||||
profile_id = values[field]
|
||||
if profile_id is None:
|
||||
continue
|
||||
if not isinstance(profile_id, str) or ID_RE.fullmatch(profile_id) is None:
|
||||
errors.append(f"intents.{field}: 必须是 null 或小写连字符 profile ID")
|
||||
continue
|
||||
profile = profiles.get(profile_id)
|
||||
if profile is None:
|
||||
errors.append(f"intents.{field}: 未定义 profile {profile_id!r}")
|
||||
continue
|
||||
expected_stop = INTENT_STOP_AT[field]
|
||||
if _mapping(profile) and profile.get("stopAt") != expected_stop:
|
||||
errors.append(
|
||||
f"intents.{field}: profile {profile_id!r} 必须 stopAt {expected_stop}"
|
||||
)
|
||||
|
||||
|
||||
def validate_builtin(data: dict[str, Any], project_root: Path | None = None) -> list[str]:
|
||||
errors: list[str] = []
|
||||
_reject_unknown(data, TOP_LEVEL_FIELDS, "<root>", errors)
|
||||
@@ -720,6 +758,7 @@ def validate_builtin(data: dict[str, Any], project_root: Path | None = None) ->
|
||||
environments=environments,
|
||||
errors=errors,
|
||||
)
|
||||
_validate_intents(data.get("intents"), profiles, errors)
|
||||
|
||||
if enabled:
|
||||
if default_profile not in profiles:
|
||||
|
||||
@@ -103,6 +103,8 @@ DELIVERY_RUN_FIELDS = {
|
||||
"evidence",
|
||||
"updatedAt",
|
||||
}
|
||||
DELIVERY_RUN_OPTIONAL_FIELDS = {"intent"}
|
||||
DELIVERY_RUN_INTENTS = {"testEnvironment", "release"}
|
||||
DELIVERY_STATUSES = {
|
||||
"planned",
|
||||
"running",
|
||||
@@ -481,7 +483,9 @@ def validate_delivery_runs(
|
||||
if not isinstance(run, dict):
|
||||
errors.append(f"{where}: 必须是对象")
|
||||
continue
|
||||
reject_unknown_fields(run, DELIVERY_RUN_FIELDS, where, errors)
|
||||
reject_unknown_fields(
|
||||
run, DELIVERY_RUN_FIELDS | DELIVERY_RUN_OPTIONAL_FIELDS, where, errors
|
||||
)
|
||||
missing = sorted(DELIVERY_RUN_FIELDS - set(run))
|
||||
for field in missing:
|
||||
errors.append(f"{where}.{field}: 必填")
|
||||
@@ -501,13 +505,23 @@ def validate_delivery_runs(
|
||||
if status not in DELIVERY_STATUSES:
|
||||
errors.append(f"{where}.status: 必须是 {sorted(DELIVERY_STATUSES)}")
|
||||
|
||||
intent = run.get("intent")
|
||||
if "intent" in run and intent not in DELIVERY_RUN_INTENTS:
|
||||
errors.append(
|
||||
f"{where}.intent: 必须是 {sorted(DELIVERY_RUN_INTENTS)}"
|
||||
)
|
||||
task_ids = run.get("taskIds")
|
||||
allow_empty_tasks = intent in DELIVERY_RUN_INTENTS
|
||||
if (
|
||||
not isinstance(task_ids, list)
|
||||
or not task_ids
|
||||
or any(not _nonempty_string(task_id) for task_id in task_ids)
|
||||
or (not task_ids and not allow_empty_tasks)
|
||||
or any(not _nonempty_string(task_id) for task_id in (task_ids or []))
|
||||
):
|
||||
errors.append(f"{where}.taskIds: 必须是非空任务 ID 列表")
|
||||
errors.append(
|
||||
f"{where}.taskIds: 必须是任务 ID 列表"
|
||||
if allow_empty_tasks
|
||||
else f"{where}.taskIds: 必须是非空任务 ID 列表"
|
||||
)
|
||||
task_ids = []
|
||||
elif len(task_ids) != len(set(task_ids)):
|
||||
errors.append(f"{where}.taskIds: 不能包含重复值")
|
||||
|
||||
@@ -24,12 +24,27 @@ LAUNCH_PROTOCOL_VERSION = 1
|
||||
MAX_ROUNDS = 3
|
||||
|
||||
ROLES = frozenset({"developer", "test"})
|
||||
CLIS = frozenset({"codex", "cursor-agent"})
|
||||
CLIS = frozenset({"codex", "cursor-agent", "grok"})
|
||||
TIERS = frozenset({"standard", "strong"})
|
||||
REASONING_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
|
||||
PERMISSION_MODES = frozenset({"read-only", "workspace-write"})
|
||||
ORCHESTRATION_MODES = frozenset({"orca", "manual"})
|
||||
DEFAULT_KEYS = frozenset({"developer", "test", "developerUpgraded"})
|
||||
CLI_REQUIRES_REASONING_EFFORT = frozenset({"codex", "grok"})
|
||||
CLI_REQUIRES_NULL_REASONING_EFFORT = frozenset({"cursor-agent"})
|
||||
|
||||
|
||||
def _cli_choice_text() -> str:
|
||||
return "/".join(sorted(CLIS))
|
||||
|
||||
|
||||
def executable_basename_matches_cli(executable: str, cli: str) -> bool:
|
||||
"""Return whether a resolved executable basename is valid for ``cli``."""
|
||||
|
||||
name = Path(executable).name
|
||||
if name == cli:
|
||||
return True
|
||||
return cli == "grok" and GROK_EXECUTABLE_NAME_RE.fullmatch(name) is not None
|
||||
|
||||
ORCHESTRATION_FIELDS = frozenset(
|
||||
{
|
||||
@@ -114,6 +129,9 @@ ROLE_DISPATCH_FIELDS = frozenset(
|
||||
|
||||
PROFILE_ID_RE = re.compile(r"^[a-z][a-z0-9-]{1,63}$")
|
||||
MODEL_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/+@-]{0,127}$")
|
||||
GROK_EXECUTABLE_NAME_RE = re.compile(
|
||||
r"^grok(?:-(?:linux|darwin|windows)-(?:x86_64|aarch64|arm64))?$"
|
||||
)
|
||||
RECEIPT_ID_RE = re.compile(r"^WR-[0-9a-f]{64}$")
|
||||
LAUNCH_ID_RE = re.compile(r"^[0-9a-f]{64}$")
|
||||
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||
@@ -214,7 +232,7 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
if not isinstance(role, str) or role not in ROLES:
|
||||
errors.append(f"{where}.role: must be developer/test")
|
||||
if not isinstance(cli, str) or cli not in CLIS:
|
||||
errors.append(f"{where}.cli: must be codex/cursor-agent")
|
||||
errors.append(f"{where}.cli: must be {_cli_choice_text()}")
|
||||
if not isinstance(tier, str) or tier not in TIERS:
|
||||
errors.append(f"{where}.tier: must be standard/strong")
|
||||
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
||||
@@ -223,12 +241,17 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
errors.append(
|
||||
f"{where}.permissionMode: must be read-only/workspace-write"
|
||||
)
|
||||
if cli == "codex":
|
||||
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT:
|
||||
if not isinstance(effort, str) or effort not in REASONING_EFFORTS:
|
||||
label = "Codex" if cli == "codex" else "Grok"
|
||||
errors.append(
|
||||
f"{where}.reasoningEffort: Codex requires low/medium/high/xhigh"
|
||||
f"{where}.reasoningEffort: {label} requires low/medium/high/xhigh"
|
||||
)
|
||||
elif cli == "cursor-agent" and effort is not None:
|
||||
elif (
|
||||
isinstance(cli, str)
|
||||
and cli in CLI_REQUIRES_NULL_REASONING_EFFORT
|
||||
and effort is not None
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||
|
||||
if role == "test" and tier != "standard":
|
||||
@@ -446,14 +469,15 @@ def render_worker_argv(
|
||||
raise ValueError("invalid profile: " + "; ".join(errors))
|
||||
if not _is_absolute_safe_path(executable):
|
||||
raise ValueError("executable must be a safe absolute path other than root")
|
||||
if Path(executable).name != profile["cli"]:
|
||||
if not executable_basename_matches_cli(executable, profile["cli"]):
|
||||
raise ValueError("executable basename must match profile.cli")
|
||||
if not _is_absolute_safe_path(worktree):
|
||||
raise ValueError("worktree must be a safe absolute path other than root")
|
||||
|
||||
model = profile["model"]
|
||||
permission = profile["permissionMode"]
|
||||
if profile["cli"] == "codex":
|
||||
cli = profile["cli"]
|
||||
if cli == "codex":
|
||||
return [
|
||||
executable,
|
||||
"--strict-config",
|
||||
@@ -469,13 +493,33 @@ def render_worker_argv(
|
||||
worktree,
|
||||
]
|
||||
|
||||
argv = [executable, "--model", model]
|
||||
if permission == "read-only":
|
||||
argv.extend(["--mode", "plan"])
|
||||
else:
|
||||
argv.append("--auto-review")
|
||||
argv.extend(["--sandbox", "enabled", "--workspace", worktree])
|
||||
return argv
|
||||
if cli == "cursor-agent":
|
||||
argv = [executable, "--model", model]
|
||||
if permission == "read-only":
|
||||
argv.extend(["--mode", "plan"])
|
||||
else:
|
||||
argv.append("--auto-review")
|
||||
argv.extend(["--sandbox", "enabled", "--workspace", worktree])
|
||||
return argv
|
||||
|
||||
if cli != "grok":
|
||||
raise ValueError(f"unsupported cli: {cli}")
|
||||
grok_permission = "plan" if permission == "read-only" else "acceptEdits"
|
||||
grok_sandbox = "read-only" if permission == "read-only" else "workspace"
|
||||
return [
|
||||
executable,
|
||||
"--model",
|
||||
model,
|
||||
"--reasoning-effort",
|
||||
profile["reasoningEffort"],
|
||||
"--permission-mode",
|
||||
grok_permission,
|
||||
"--always-approve",
|
||||
"--sandbox",
|
||||
grok_sandbox,
|
||||
"--cwd",
|
||||
worktree,
|
||||
]
|
||||
|
||||
|
||||
def receipt_hash(receipt: dict[str, Any]) -> str:
|
||||
@@ -532,25 +576,34 @@ def _validate_requested(value: Any, where: str) -> list[str]:
|
||||
tier = value.get("tier")
|
||||
permission = value.get("permissionMode")
|
||||
if not isinstance(cli, str) or cli not in CLIS:
|
||||
errors.append(f"{where}.cli: must be codex/cursor-agent")
|
||||
errors.append(f"{where}.cli: must be {_cli_choice_text()}")
|
||||
if not isinstance(tier, str) or tier not in TIERS:
|
||||
errors.append(f"{where}.tier: must be standard/strong")
|
||||
model = value.get("model")
|
||||
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
||||
errors.append(f"{where}.model: must be a safe model ID")
|
||||
effort = value.get("reasoningEffort")
|
||||
if cli == "codex" and (
|
||||
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT and (
|
||||
not isinstance(effort, str) or effort not in REASONING_EFFORTS
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: invalid Codex effort")
|
||||
if cli == "cursor-agent" and effort is not None:
|
||||
label = "Codex" if cli == "codex" else "Grok"
|
||||
errors.append(f"{where}.reasoningEffort: invalid {label} effort")
|
||||
if (
|
||||
isinstance(cli, str)
|
||||
and cli in CLI_REQUIRES_NULL_REASONING_EFFORT
|
||||
and effort is not None
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||
if not isinstance(permission, str) or permission not in PERMISSION_MODES:
|
||||
errors.append(f"{where}.permissionMode: must be read-only/workspace-write")
|
||||
executable = value.get("executable")
|
||||
if not _is_absolute_safe_path(executable):
|
||||
errors.append(f"{where}.executable: must be a safe absolute path")
|
||||
elif isinstance(cli, str) and cli in CLIS and Path(executable).name != cli:
|
||||
elif (
|
||||
isinstance(cli, str)
|
||||
and cli in CLIS
|
||||
and not executable_basename_matches_cli(str(executable), cli)
|
||||
):
|
||||
errors.append(f"{where}.executable: basename must match cli")
|
||||
if not _is_nonnegative_int(value.get("executableDevice")):
|
||||
errors.append(f"{where}.executableDevice: must be a non-negative integer")
|
||||
@@ -995,6 +1048,7 @@ __all__ = [
|
||||
"profile_hash",
|
||||
"receipt_hash",
|
||||
"render_worker_argv",
|
||||
"executable_basename_matches_cli",
|
||||
"validate_profile",
|
||||
"validate_orchestration",
|
||||
"validate_worker_receipt",
|
||||
|
||||
Reference in New Issue
Block a user