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)
|
||||
|
||||
@@ -128,8 +128,15 @@ DISPATCH_FIELDS = {
|
||||
"worker",
|
||||
"developer",
|
||||
"test",
|
||||
"operator",
|
||||
"rounds",
|
||||
}
|
||||
DELIVERY_OPERATION_FIELDS = {"skill", "request"}
|
||||
DELIVERY_OPERATION_SKILLS = {
|
||||
"manage-release",
|
||||
"deb-publisher",
|
||||
"publish-docker-image",
|
||||
}
|
||||
KNOWLEDGE_CANDIDATE_REQUIRED_FIELDS = {
|
||||
"kind",
|
||||
"title",
|
||||
@@ -432,11 +439,14 @@ def validate_delivery_runs(
|
||||
value: object,
|
||||
task_statuses: dict[str, object],
|
||||
errors: list[str],
|
||||
*,
|
||||
delivery_operation_ids: set[str] | None = None,
|
||||
) -> None:
|
||||
if not isinstance(value, list):
|
||||
errors.append("deliveryRuns 必须是列表")
|
||||
return
|
||||
|
||||
excluded_task_ids = delivery_operation_ids or set()
|
||||
seen_run_ids: set[str] = set()
|
||||
for index, run in enumerate(value):
|
||||
where = f"deliveryRuns[{index}]"
|
||||
@@ -476,6 +486,11 @@ def validate_delivery_runs(
|
||||
for task_id in task_ids:
|
||||
if task_id not in task_statuses:
|
||||
errors.append(f"{where}.taskIds: 未知任务 {task_id!r}")
|
||||
elif task_id in excluded_task_ids:
|
||||
errors.append(
|
||||
f"{where}.taskIds: deliveryRuns 不能引用 delivery-operation "
|
||||
f"{task_id!r}"
|
||||
)
|
||||
elif task_statuses[task_id] != "verified":
|
||||
errors.append(
|
||||
f"{where}: delivery run 只能引用 verified 任务,"
|
||||
@@ -807,6 +822,30 @@ def validate_builtin(data: dict) -> list[str]:
|
||||
if not _nonempty_string(source.get("updatedAt")):
|
||||
errors.append(f"{where}.source.updatedAt: 必须是非空字符串")
|
||||
|
||||
operation = task.get("operation")
|
||||
if operation is not None:
|
||||
if not isinstance(operation, dict):
|
||||
errors.append(f"{where}.operation: 必须是对象")
|
||||
else:
|
||||
reject_unknown_fields(
|
||||
operation,
|
||||
DELIVERY_OPERATION_FIELDS,
|
||||
f"{where}.operation",
|
||||
errors,
|
||||
)
|
||||
skill = operation.get("skill")
|
||||
if skill not in DELIVERY_OPERATION_SKILLS:
|
||||
errors.append(
|
||||
f"{where}.operation.skill: 必须是 "
|
||||
"manage-release/deb-publisher/publish-docker-image"
|
||||
)
|
||||
if not _nonempty_string(operation.get("request")):
|
||||
errors.append(f"{where}.operation.request: 必须保留非空用户请求")
|
||||
|
||||
if task.get("type") == "delivery-operation":
|
||||
if not isinstance(operation, dict):
|
||||
errors.append(f"{where}: delivery-operation 必须声明 operation")
|
||||
|
||||
validate_knowledge_fields(task, where, status, errors)
|
||||
|
||||
if "dispatch" not in task:
|
||||
@@ -829,6 +868,12 @@ def validate_builtin(data: dict) -> list[str]:
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
if task.get("type") == "delivery-operation" and (
|
||||
not isinstance(dispatch, dict)
|
||||
or not isinstance(dispatch.get("operator"), dict)
|
||||
):
|
||||
errors.append(f"{where}: delivery-operation 必须声明 dispatch.operator")
|
||||
|
||||
rounds = dispatch.get("rounds", [])
|
||||
if not isinstance(rounds, list):
|
||||
errors.append(f"{where}.dispatch.rounds: 必须是列表")
|
||||
@@ -929,7 +974,19 @@ def validate_builtin(data: dict) -> list[str]:
|
||||
for task in tasks
|
||||
if isinstance(task, dict) and _nonempty_string(task.get("id"))
|
||||
}
|
||||
validate_delivery_runs(data["deliveryRuns"], task_statuses, errors)
|
||||
delivery_operation_ids = {
|
||||
task["id"]
|
||||
for task in tasks
|
||||
if isinstance(task, dict)
|
||||
and _nonempty_string(task.get("id"))
|
||||
and task.get("type") == "delivery-operation"
|
||||
}
|
||||
validate_delivery_runs(
|
||||
data["deliveryRuns"],
|
||||
task_statuses,
|
||||
errors,
|
||||
delivery_operation_ids=delivery_operation_ids,
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@@ -23,13 +23,22 @@ RECEIPT_VERSION = 1
|
||||
LAUNCH_PROTOCOL_VERSION = 1
|
||||
MAX_ROUNDS = 3
|
||||
|
||||
ROLES = frozenset({"developer", "test"})
|
||||
ROLES = frozenset({"developer", "test", "operator"})
|
||||
REQUIRED_DEFAULT_ROLES = frozenset({"developer", "test"})
|
||||
STANDARD_ONLY_ROLES = frozenset({"test", "operator"})
|
||||
CLIS = frozenset({"codex", "cursor-agent"})
|
||||
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"})
|
||||
BASE_ENVIRONMENT_POLICY = "per-cli-allowlist-v1"
|
||||
OPERATOR_ENVIRONMENT_POLICY = "per-cli-plus-operator-publish-v1"
|
||||
ENVIRONMENT_POLICIES = frozenset(
|
||||
{BASE_ENVIRONMENT_POLICY, OPERATOR_ENVIRONMENT_POLICY}
|
||||
)
|
||||
DEFAULT_KEYS = frozenset(
|
||||
{"developer", "test", "operator", "developerUpgraded"}
|
||||
)
|
||||
|
||||
ORCHESTRATION_FIELDS = frozenset(
|
||||
{
|
||||
@@ -184,6 +193,16 @@ def _is_positive_int(value: Any) -> bool:
|
||||
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
||||
|
||||
|
||||
def environment_policy_for_role(role: Any) -> str:
|
||||
"""Return the fixed credential policy for a validated worker role."""
|
||||
|
||||
if role == "operator":
|
||||
return OPERATOR_ENVIRONMENT_POLICY
|
||||
if role in {"developer", "test"}:
|
||||
return BASE_ENVIRONMENT_POLICY
|
||||
raise ValueError("role must be developer/test/operator")
|
||||
|
||||
|
||||
def _is_timestamp(value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
@@ -211,7 +230,7 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
permission = profile.get("permissionMode")
|
||||
|
||||
if not isinstance(role, str) or role not in ROLES:
|
||||
errors.append(f"{where}.role: must be developer/test")
|
||||
errors.append(f"{where}.role: must be developer/test/operator")
|
||||
if not isinstance(cli, str) or cli not in CLIS:
|
||||
errors.append(f"{where}.cli: must be codex/cursor-agent")
|
||||
if not isinstance(tier, str) or tier not in TIERS:
|
||||
@@ -231,8 +250,13 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
elif cli == "cursor-agent" and effort is not None:
|
||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||
|
||||
if role == "test" and tier != "standard":
|
||||
errors.append(f"{where}.tier: Test may only use standard")
|
||||
if (
|
||||
isinstance(role, str)
|
||||
and role in STANDARD_ONLY_ROLES
|
||||
and tier != "standard"
|
||||
):
|
||||
label = "Test" if role == "test" else "Operator"
|
||||
errors.append(f"{where}.tier: {label} may only use standard")
|
||||
if tier == "strong" and role != "developer":
|
||||
errors.append(f"{where}.tier: strong may only be used by Developer")
|
||||
|
||||
@@ -268,8 +292,11 @@ def _validate_model_allowlist(value: Any, where: str) -> list[str]:
|
||||
errors.append(f"{role_where}: must not be empty")
|
||||
for tier in sorted(set(tiers) - TIERS, key=repr):
|
||||
errors.append(f"{role_where}: unknown tier {tier!r}")
|
||||
if role == "test" and "strong" in tiers:
|
||||
errors.append(f"{role_where}: Test cannot define a strong allowlist")
|
||||
if role in STANDARD_ONLY_ROLES and "strong" in tiers:
|
||||
label = "Test" if role == "test" else "Operator"
|
||||
errors.append(
|
||||
f"{role_where}: {label} cannot define a strong allowlist"
|
||||
)
|
||||
for tier, models in tiers.items():
|
||||
tier_where = f"{role_where}.{tier}"
|
||||
if tier not in TIERS:
|
||||
@@ -383,7 +410,7 @@ def validate_orchestration(
|
||||
for default_key in sorted(set(defaults) - DEFAULT_KEYS, key=repr):
|
||||
errors.append(f"{where}.defaults: unknown key {default_key!r}")
|
||||
if mode == "orca":
|
||||
for role in sorted(ROLES - set(defaults)):
|
||||
for role in sorted(REQUIRED_DEFAULT_ROLES - set(defaults)):
|
||||
errors.append(f"{where}.defaults: missing role {role!r}")
|
||||
for default_key, profile_id in defaults.items():
|
||||
default_where = f"{where}.defaults.{default_key}"
|
||||
@@ -407,6 +434,22 @@ def validate_orchestration(
|
||||
if profile.get("permissionMode") not in PERMISSION_MODES:
|
||||
errors.append(f"{default_where}: default profile has unsafe permissions")
|
||||
|
||||
operator_profile_id = defaults.get("operator")
|
||||
test_profile_id = defaults.get("test")
|
||||
operator_profile = valid_profiles.get(operator_profile_id)
|
||||
test_profile = valid_profiles.get(test_profile_id)
|
||||
if operator_profile_id is not None and test_profile is None:
|
||||
errors.append(
|
||||
f"{where}.defaults.operator: requires a valid Test default profile"
|
||||
)
|
||||
elif operator_profile is not None and test_profile is not None:
|
||||
for field in ("cli", "tier", "model", "reasoningEffort"):
|
||||
if operator_profile.get(field) != test_profile.get(field):
|
||||
errors.append(
|
||||
f"{where}.defaults.operator: operator default must use "
|
||||
f"the Test default {field}"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@@ -502,7 +545,7 @@ def _validate_created_for(value: Any, where: str) -> list[str]:
|
||||
errors.append(f"{where}.attemptId: must belong to taskId")
|
||||
role = value.get("role")
|
||||
if not isinstance(role, str) or role not in ROLES:
|
||||
errors.append(f"{where}.role: must be developer/test")
|
||||
errors.append(f"{where}.role: must be developer/test/operator")
|
||||
return errors
|
||||
|
||||
|
||||
@@ -571,9 +614,10 @@ def _validate_requested(value: Any, where: str) -> list[str]:
|
||||
elif isinstance(argv, list) and all(isinstance(arg, str) for arg in argv):
|
||||
if argv_hash != canonical_sha256(argv):
|
||||
errors.append(f"{where}.argvHash: does not match argv")
|
||||
if value.get("environmentPolicy") != "per-cli-allowlist-v1":
|
||||
if value.get("environmentPolicy") not in ENVIRONMENT_POLICIES:
|
||||
errors.append(
|
||||
f"{where}.environmentPolicy: must be 'per-cli-allowlist-v1'"
|
||||
f"{where}.environmentPolicy: must be 'per-cli-allowlist-v1' or "
|
||||
"'per-cli-plus-operator-publish-v1'"
|
||||
)
|
||||
return errors
|
||||
|
||||
@@ -722,6 +766,19 @@ def validate_worker_receipt(
|
||||
if binding.get("observedWorktreePath") != worktree.get("path"):
|
||||
errors.append(f"{where}.binding.observedWorktreePath: does not match worktree.path")
|
||||
|
||||
if isinstance(created_for, dict) and isinstance(requested, dict):
|
||||
try:
|
||||
expected_environment_policy = environment_policy_for_role(
|
||||
created_for.get("role")
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if requested.get("environmentPolicy") != expected_environment_policy:
|
||||
errors.append(
|
||||
f"{where}.requested.environmentPolicy: does not match role"
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(created_for, dict)
|
||||
and isinstance(worktree, dict)
|
||||
@@ -977,7 +1034,10 @@ __all__ = [
|
||||
"RECEIPT_VERSION",
|
||||
"LAUNCH_PROTOCOL_VERSION",
|
||||
"MAX_ROUNDS",
|
||||
"BASE_ENVIRONMENT_POLICY",
|
||||
"OPERATOR_ENVIRONMENT_POLICY",
|
||||
"canonical_sha256",
|
||||
"environment_policy_for_role",
|
||||
"profile_hash",
|
||||
"receipt_hash",
|
||||
"render_worker_argv",
|
||||
|
||||
Reference in New Issue
Block a user