feat(ack): support project agent routing overrides
This commit is contained in:
@@ -24,7 +24,7 @@ LAUNCH_PROTOCOL_VERSION = 1
|
||||
MAX_ROUNDS = 3
|
||||
|
||||
ROLES = frozenset({"developer", "test"})
|
||||
CLIS = frozenset({"codex", "cursor-agent", "grok", "omp"})
|
||||
CLIS = frozenset({"codex", "cursor-agent", "grok", "omp", "pi"})
|
||||
TIERS = frozenset({"standard", "strong"})
|
||||
REASONING_EFFORTS = frozenset({"low", "medium", "high", "xhigh"})
|
||||
PERMISSION_MODES = frozenset({"read-only", "workspace-write"})
|
||||
@@ -32,6 +32,7 @@ ORCHESTRATION_MODES = frozenset({"orca", "manual"})
|
||||
DEFAULT_KEYS = frozenset({"developer", "test", "developerUpgraded"})
|
||||
CLI_REQUIRES_REASONING_EFFORT = frozenset({"codex", "grok", "omp"})
|
||||
CLI_REQUIRES_NULL_REASONING_EFFORT = frozenset({"cursor-agent"})
|
||||
CLI_ALLOWS_DEFAULT_MODEL = frozenset({"pi"})
|
||||
|
||||
|
||||
def _cli_choice_text() -> str:
|
||||
@@ -95,7 +96,9 @@ RECEIPT_FIELDS = frozenset(
|
||||
"receiptHash",
|
||||
}
|
||||
)
|
||||
RECEIPT_CONTEXT_FIELDS = frozenset({"projectRoot", "boardHash"})
|
||||
RECEIPT_CONTEXT_FIELDS = frozenset(
|
||||
{"projectRoot", "boardHash", "routingSource", "routingHash"}
|
||||
)
|
||||
CREATED_FOR_FIELDS = frozenset({"taskId", "attemptId", "role"})
|
||||
WORKTREE_FIELDS = frozenset(
|
||||
{
|
||||
@@ -247,8 +250,10 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
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:
|
||||
errors.append(f"{where}.model: must be a safe model ID")
|
||||
if not (isinstance(cli, str) and cli in CLI_ALLOWS_DEFAULT_MODEL and model is None) and (
|
||||
not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None
|
||||
):
|
||||
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:
|
||||
errors.append(
|
||||
f"{where}.permissionMode: must be read-only/workspace-write"
|
||||
@@ -265,6 +270,10 @@ def validate_profile(profile: Any, *, where: str = "profile") -> list[str]:
|
||||
and effort is not None
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||
elif isinstance(cli, str) and cli == "pi" and (
|
||||
not isinstance(effort, str) or effort not in REASONING_EFFORTS
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Pi requires low/medium/high/xhigh")
|
||||
|
||||
if role == "test" and tier != "standard":
|
||||
errors.append(f"{where}.tier: Test may only use standard")
|
||||
@@ -312,15 +321,22 @@ def _validate_model_allowlist(value: Any, where: str) -> list[str]:
|
||||
if not isinstance(models, list) or not models:
|
||||
errors.append(f"{tier_where}: must be a non-empty model list")
|
||||
continue
|
||||
seen: set[str] = set()
|
||||
seen: set[object] = set()
|
||||
for index, model in enumerate(models):
|
||||
item_where = f"{tier_where}[{index}]"
|
||||
if not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
||||
errors.append(f"{item_where}: must be a safe model ID")
|
||||
elif model in seen:
|
||||
if model is None and cli in CLI_ALLOWS_DEFAULT_MODEL:
|
||||
model_key: object = ("default",)
|
||||
elif not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None:
|
||||
errors.append(
|
||||
f"{item_where}: must be a safe model ID or null for Pi default"
|
||||
)
|
||||
continue
|
||||
else:
|
||||
model_key = model
|
||||
if model_key in seen:
|
||||
errors.append(f"{item_where}: duplicate model {model!r}")
|
||||
else:
|
||||
seen.add(model)
|
||||
seen.add(model_key)
|
||||
return errors
|
||||
|
||||
|
||||
@@ -329,7 +345,7 @@ def _allowed_models(
|
||||
cli: Any,
|
||||
role: Any,
|
||||
tier: Any,
|
||||
) -> list[str] | None:
|
||||
) -> list[Any] | None:
|
||||
if not isinstance(allowlist, dict):
|
||||
return None
|
||||
roles = allowlist.get(cli)
|
||||
@@ -519,6 +535,19 @@ def render_worker_argv(
|
||||
"--no-session",
|
||||
]
|
||||
|
||||
if cli == "pi":
|
||||
# Pi's omitted --model selects its configured startup default. Its
|
||||
# cwd is supplied by the trusted bootstrap process, because Pi has no
|
||||
# --cwd flag. Read-only profiles are restricted to observation tools;
|
||||
# workspace-write profiles use Pi's normal editing tool set.
|
||||
argv = [executable]
|
||||
if model is not None:
|
||||
argv.extend(["--model", model])
|
||||
argv.extend(["--thinking", profile["reasoningEffort"]])
|
||||
if permission == "read-only":
|
||||
argv.extend(["--tools", "read,grep,find,ls"])
|
||||
return argv
|
||||
|
||||
if cli != "grok":
|
||||
raise ValueError(f"unsupported cli: {cli}")
|
||||
grok_permission = "plan" if permission == "read-only" else "acceptEdits"
|
||||
@@ -597,8 +626,10 @@ def _validate_requested(value: Any, where: str) -> list[str]:
|
||||
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")
|
||||
if not (isinstance(cli, str) and cli in CLI_ALLOWS_DEFAULT_MODEL and model is None) and (
|
||||
not isinstance(model, str) or MODEL_ID_RE.fullmatch(model) is None
|
||||
):
|
||||
errors.append(f"{where}.model: must be a safe model ID or null for Pi default")
|
||||
effort = value.get("reasoningEffort")
|
||||
if isinstance(cli, str) and cli in CLI_REQUIRES_REASONING_EFFORT and (
|
||||
not isinstance(effort, str) or effort not in REASONING_EFFORTS
|
||||
@@ -611,6 +642,10 @@ def _validate_requested(value: Any, where: str) -> list[str]:
|
||||
and effort is not None
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Cursor requires null")
|
||||
if isinstance(cli, str) and cli == "pi" and (
|
||||
not isinstance(effort, str) or effort not in REASONING_EFFORTS
|
||||
):
|
||||
errors.append(f"{where}.reasoningEffort: Pi requires low/medium/high/xhigh")
|
||||
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")
|
||||
@@ -697,6 +732,16 @@ def validate_worker_receipt(
|
||||
not isinstance(board_hash, str) or SHA256_RE.fullmatch(board_hash) is None
|
||||
):
|
||||
errors.append(f"{where}.boardHash: must be a canonical sha256 hex digest")
|
||||
routing_source = receipt.get("routingSource")
|
||||
routing_hash = receipt.get("routingHash")
|
||||
if (routing_source is None) != (routing_hash is None):
|
||||
errors.append(f"{where}: routingSource and routingHash must be present together")
|
||||
if routing_source is not None and not _is_nonempty_text(routing_source, max_length=512):
|
||||
errors.append(f"{where}.routingSource: must be non-empty single-line text")
|
||||
if routing_hash is not None and (
|
||||
not isinstance(routing_hash, str) or SHA256_RE.fullmatch(routing_hash) is None
|
||||
):
|
||||
errors.append(f"{where}.routingHash: must be a canonical sha256 hex digest")
|
||||
|
||||
version = receipt.get("receiptVersion")
|
||||
if version != RECEIPT_VERSION or isinstance(version, bool):
|
||||
@@ -818,6 +863,9 @@ def validate_worker_receipt(
|
||||
if project_root is not None and board_hash is not None:
|
||||
facts["projectRoot"] = project_root
|
||||
facts["boardHash"] = board_hash
|
||||
if routing_source is not None and routing_hash is not None:
|
||||
facts["routingSource"] = routing_source
|
||||
facts["routingHash"] = routing_hash
|
||||
expected_fingerprint = canonical_sha256(facts)
|
||||
except ValueError:
|
||||
errors.append(f"{where}.launchFingerprint: cannot hash launch facts")
|
||||
@@ -1006,17 +1054,30 @@ def _validate_dispatch_links(
|
||||
return errors
|
||||
|
||||
|
||||
def validate_routing_document(data: Any) -> list[str]:
|
||||
"""Validate routing and receipts embedded in an ACK tasks document."""
|
||||
def validate_routing_document(
|
||||
data: Any,
|
||||
*,
|
||||
orchestration_override: dict[str, Any] | None = None,
|
||||
) -> list[str]:
|
||||
"""Validate routing and receipts in an ACK tasks document.
|
||||
|
||||
``orchestration_override`` is the optional project-local agents.yaml
|
||||
routing document. The task board remains the state source, while the
|
||||
override becomes the effective profile source for receipt checks.
|
||||
"""
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return ["<root>: must be an object"]
|
||||
project = data.get("project")
|
||||
if not isinstance(project, dict):
|
||||
return ["project: must be an object"]
|
||||
if "orchestration" not in project:
|
||||
if "orchestration" not in project and orchestration_override is None:
|
||||
return ["project.orchestration: is required"]
|
||||
orchestration = project.get("orchestration")
|
||||
orchestration = (
|
||||
orchestration_override
|
||||
if orchestration_override is not None
|
||||
else project.get("orchestration")
|
||||
)
|
||||
errors = validate_orchestration(orchestration)
|
||||
|
||||
task_ids: set[str] = set()
|
||||
|
||||
Reference in New Issue
Block a user