feat(ack): add one-off delivery routing
This commit is contained in:
@@ -41,6 +41,7 @@ from validate_tasks import load_document, validate_builtin # noqa: E402
|
||||
from worker_profiles import ( # noqa: E402
|
||||
LAUNCH_PROTOCOL_VERSION,
|
||||
canonical_sha256,
|
||||
environment_policy_for_role,
|
||||
profile_hash,
|
||||
render_worker_argv,
|
||||
validate_routing_document,
|
||||
@@ -48,7 +49,6 @@ from worker_profiles import ( # noqa: E402
|
||||
|
||||
PROTOCOL_VERSION = LAUNCH_PROTOCOL_VERSION
|
||||
RECEIPT_VERSION = 1
|
||||
ENVIRONMENT_POLICY = "per-cli-allowlist-v1"
|
||||
TASKS_RELATIVE_PATH = Path("docs/ack/tasks.yaml")
|
||||
MAX_CONTROL_OUTPUT = 1024 * 1024
|
||||
MAX_RECORD_SIZE = 256 * 1024
|
||||
@@ -82,6 +82,15 @@ WORKER_CREDENTIAL_NAMES = {
|
||||
"codex": frozenset({"AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"}),
|
||||
"cursor-agent": frozenset({"CURSOR_API_KEY"}),
|
||||
}
|
||||
OPERATOR_CREDENTIAL_NAMES = frozenset(
|
||||
{
|
||||
"DEB_REPOSITORY",
|
||||
"DEB_SERVER_URL",
|
||||
"DEB_TOKEN",
|
||||
"DEB_UPLOAD_PATH",
|
||||
"SSH_AUTH_SOCK",
|
||||
}
|
||||
)
|
||||
INHERITED_ENVIRONMENT_PREFIXES = (
|
||||
"LC_",
|
||||
)
|
||||
@@ -184,13 +193,23 @@ def control_environment() -> dict[str, str]:
|
||||
return _sanitized_environment(CONTROL_ENVIRONMENT_NAMES)
|
||||
|
||||
|
||||
def worker_environment(cli: str) -> dict[str, str]:
|
||||
def worker_environment(cli: str, role: str | None = None) -> dict[str, str]:
|
||||
"""Return only the supported CLI's own credentials and common runtime data."""
|
||||
|
||||
credential_names = WORKER_CREDENTIAL_NAMES.get(cli)
|
||||
if credential_names is None:
|
||||
raise LaunchError(f"不支持的 worker CLI 环境: {cli}")
|
||||
return _sanitized_environment(WORKER_ENVIRONMENT_NAMES | credential_names)
|
||||
if role is not None:
|
||||
try:
|
||||
environment_policy_for_role(role)
|
||||
except ValueError as exc:
|
||||
raise LaunchError(f"不支持的 worker role 环境: {role}") from exc
|
||||
role_credentials = (
|
||||
OPERATOR_CREDENTIAL_NAMES if role == "operator" else frozenset()
|
||||
)
|
||||
return _sanitized_environment(
|
||||
WORKER_ENVIRONMENT_NAMES | credential_names | role_credentials
|
||||
)
|
||||
|
||||
|
||||
def reject_duplicate_or_separator_args(argv: list[str]) -> None:
|
||||
@@ -633,15 +652,20 @@ def build_plan(
|
||||
raise LaunchError("task-id 只允许字母、数字、点、下划线和连字符")
|
||||
if attempt_id not in {f"{task_id}-A1", f"{task_id}-A2", f"{task_id}-A3"}:
|
||||
raise LaunchError("attempt-id 必须精确为 <task-id>-A1..A3")
|
||||
if role not in {"developer", "test"}:
|
||||
raise LaunchError("role 必须是 developer 或 test")
|
||||
if role not in {"developer", "test", "operator"}:
|
||||
raise LaunchError("role 必须是 developer、test 或 operator")
|
||||
if not PROFILE_ID_RE.fullmatch(profile_id):
|
||||
raise LaunchError("profile-id 格式非法")
|
||||
if not isinstance(slot, int) or isinstance(slot, bool) or not 1 <= slot <= 99:
|
||||
raise LaunchError("slot 必须是 1..99 的整数")
|
||||
|
||||
project_root, board = load_authoritative_board(project_root_value)
|
||||
find_task(board, task_id)
|
||||
task = find_task(board, task_id)
|
||||
is_delivery_operation = task.get("type") == "delivery-operation"
|
||||
if role == "operator" and not is_delivery_operation:
|
||||
raise LaunchError("operator 只能用于 delivery-operation 任务")
|
||||
if role != "operator" and is_delivery_operation:
|
||||
raise LaunchError("delivery-operation 任务只能由 operator 执行")
|
||||
project = board["project"]
|
||||
orchestration = project.get("orchestration")
|
||||
if not isinstance(orchestration, dict):
|
||||
@@ -681,7 +705,7 @@ def build_plan(
|
||||
"cliVersion": cli_version,
|
||||
"argv": argv,
|
||||
"argvHash": canonical_sha256(argv),
|
||||
"environmentPolicy": ENVIRONMENT_POLICY,
|
||||
"environmentPolicy": environment_policy_for_role(role),
|
||||
}
|
||||
current_profile_hash = profile_hash(
|
||||
profile,
|
||||
@@ -705,7 +729,11 @@ def build_plan(
|
||||
}
|
||||
)
|
||||
cli_label = "CODEX" if profile["cli"] == "codex" else "CURSOR"
|
||||
role_label = "DEV" if role == "developer" else "TEST"
|
||||
role_label = {
|
||||
"developer": "DEV",
|
||||
"test": "TEST",
|
||||
"operator": "OP",
|
||||
}[role]
|
||||
digest_short = launch_fingerprint.split(":", 1)[-1][:10]
|
||||
title = (
|
||||
f"ACK-{role_label}-{cli_label}-{str(profile['tier']).upper()}-"
|
||||
@@ -1453,7 +1481,10 @@ def bootstrap_worker(launch_id: str) -> int:
|
||||
rebuilt["requested"]["argv"],
|
||||
shell=False,
|
||||
cwd=rebuilt["worktree"]["path"],
|
||||
env=worker_environment(str(rebuilt["requested"]["cli"])),
|
||||
env=worker_environment(
|
||||
str(rebuilt["requested"]["cli"]),
|
||||
str(rebuilt["role"]),
|
||||
),
|
||||
)
|
||||
current_record.update(
|
||||
state="bootstrap-ready",
|
||||
@@ -1488,7 +1519,11 @@ def add_launch_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--project-root", required=True)
|
||||
parser.add_argument("--task-id", required=True)
|
||||
parser.add_argument("--attempt-id", required=True)
|
||||
parser.add_argument("--role", required=True, choices=("developer", "test"))
|
||||
parser.add_argument(
|
||||
"--role",
|
||||
required=True,
|
||||
choices=("developer", "test", "operator"),
|
||||
)
|
||||
parser.add_argument("--profile-id", required=True)
|
||||
parser.add_argument("--worktree", required=True)
|
||||
parser.add_argument("--slot", type=int, default=1)
|
||||
|
||||
Reference in New Issue
Block a user