feat: complete Epic 1 production foundation
This commit is contained in:
@@ -100,6 +100,25 @@ python3 scripts/check-config-boundaries.py --root .
|
||||
committed machine contract. PostgreSQL DDL вне `crank-registry::migrations`
|
||||
блокируется Rust module boundary check.
|
||||
|
||||
Операторские auth-команды живут в том же `crank-migrate` binary и используют
|
||||
database-only config projection:
|
||||
|
||||
```bash
|
||||
crank-migrate admin-auth bootstrap-create --email owner@example.local
|
||||
|
||||
crank-migrate admin-auth recover \
|
||||
--email owner@example.local \
|
||||
--password-file /secure/new-admin-password.txt \
|
||||
--password-pepper-file /secure/password-pepper.txt \
|
||||
--master-key-file /secure/current-master.key
|
||||
```
|
||||
|
||||
Команда создаёт одноразовый local bootstrap contract и выводит token для UI
|
||||
`/login`; static startup password не является production bootstrap authority.
|
||||
Recovery проверяет active master-key identity, заменяет verifier существующего
|
||||
Admin account, отзывает browser sessions и не выводит password, pepper,
|
||||
master key или Secret material.
|
||||
|
||||
## Typed metrics contract
|
||||
|
||||
`just metrics-contract-check` сверяет Rust registry с versioned JSON snapshot и
|
||||
@@ -107,6 +126,22 @@ committed machine contract. PostgreSQL DDL вне `crank-registry::migrations`
|
||||
Checker распознаёт прямые macro calls, imports, aliases и re-exports; новые
|
||||
Rust-файлы можно передать явно через `--files`.
|
||||
|
||||
## Unified execution boundary
|
||||
|
||||
`check-execution-boundaries.py` запрещает Admin/MCP route и service слоям
|
||||
обходить typed `RuntimeExecutionRequest`: напрямую импортировать REST adapter,
|
||||
Reqwest, legacy `execute_with_*` helpers или запускать execution через
|
||||
persistence. Проверка видит tracked и untracked файлы, а explicit mode
|
||||
fail-closed отклоняет missing, symlink и path escape:
|
||||
|
||||
```bash
|
||||
python3 scripts/check-execution-boundaries.py --root .
|
||||
python3 scripts/check-execution-boundaries.py --root . --files <paths...>
|
||||
```
|
||||
|
||||
Canonical composition roots создают adapters, но продуктовые Admin Draft Test
|
||||
и MCP snapshot calls проходят только через единый runtime outcome contract.
|
||||
|
||||
## `check-community-scope.sh`
|
||||
|
||||
Проверяет, что в community-репозиторий не попали функции и тексты за пределами
|
||||
|
||||
@@ -276,6 +276,17 @@ def run_operation_test(
|
||||
raise safe_error("operation_test", "outcome_not_ok")
|
||||
|
||||
|
||||
def operation_etag(client: Client, workspace_id: str, operation_id: str) -> str:
|
||||
response = client.request_json(
|
||||
"GET",
|
||||
admin_path(workspace_id, f"/operations/{operation_id}"),
|
||||
)
|
||||
etag = response.headers.get("ETag") if response.headers is not None else None
|
||||
if not isinstance(etag, str) or len(etag) > 128 or not etag.startswith('"') or not etag.endswith('"'):
|
||||
raise safe_error("operation_precondition", "invalid_response")
|
||||
return etag
|
||||
|
||||
|
||||
def publish_operation(
|
||||
client: Client,
|
||||
workspace_id: str,
|
||||
@@ -286,6 +297,7 @@ def publish_operation(
|
||||
"POST",
|
||||
admin_path(workspace_id, f"/operations/{operation_id}/publish"),
|
||||
{"version": operation_version},
|
||||
headers={"If-Match": operation_etag(client, workspace_id, operation_id)},
|
||||
).body
|
||||
try:
|
||||
published_version = int(published["published_version"])
|
||||
@@ -314,6 +326,35 @@ def create_agent(client: Client, workspace_id: str, agent_slug: str) -> tuple[st
|
||||
raise safe_error("agent_create", "invalid_response") from error
|
||||
|
||||
|
||||
def edit_and_archive_operation(
|
||||
client: Client,
|
||||
workspace_id: str,
|
||||
operation_id: str,
|
||||
operation_name: str,
|
||||
internal_upstream: str,
|
||||
) -> int:
|
||||
payload = build_operation_payload(operation_name, internal_upstream)
|
||||
payload.pop("name", None)
|
||||
payload.pop("protocol", None)
|
||||
payload["display_name"] = "Internal Health Smoke Edited Draft"
|
||||
updated = client.request_json(
|
||||
"PATCH",
|
||||
admin_path(workspace_id, f"/operations/{operation_id}"),
|
||||
payload,
|
||||
headers={"If-Match": operation_etag(client, workspace_id, operation_id)},
|
||||
).body
|
||||
try:
|
||||
draft_version = int(updated["version"])
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
raise safe_error("operation_edit", "invalid_response") from error
|
||||
client.request_json(
|
||||
"POST",
|
||||
admin_path(workspace_id, f"/operations/{operation_id}/archive"),
|
||||
headers={"If-Match": operation_etag(client, workspace_id, operation_id)},
|
||||
)
|
||||
return draft_version
|
||||
|
||||
|
||||
def bind_and_publish_agent(
|
||||
client: Client,
|
||||
workspace_id: str,
|
||||
@@ -407,6 +448,7 @@ def cleanup_smoke_assets(
|
||||
client.request_json(
|
||||
"DELETE",
|
||||
admin_path(workspace_id, f"/operations/{operation_id}"),
|
||||
headers={"If-Match": operation_etag(client, workspace_id, operation_id)},
|
||||
expected=(200, 404),
|
||||
)
|
||||
except SmokeError:
|
||||
@@ -414,6 +456,7 @@ def cleanup_smoke_assets(
|
||||
client.request_json(
|
||||
"POST",
|
||||
admin_path(workspace_id, f"/operations/{operation_id}/archive"),
|
||||
headers={"If-Match": operation_etag(client, workspace_id, operation_id)},
|
||||
expected=(200, 404),
|
||||
)
|
||||
except SmokeError as error:
|
||||
@@ -524,7 +567,15 @@ def build_safe_summary(
|
||||
"command_id": "authenticated-product-smoke",
|
||||
"operation_id": operation_id[:128],
|
||||
"operation_version": operation_version,
|
||||
"stages": ["admin_test", "operation_publish", "agent_publish", "mcp_list", "mcp_call"],
|
||||
"stages": [
|
||||
"admin_test",
|
||||
"operation_publish",
|
||||
"agent_publish",
|
||||
"mcp_list",
|
||||
"mcp_call",
|
||||
"operation_edit_archive",
|
||||
"pinned_mcp_list_call",
|
||||
],
|
||||
"verdict": "pass",
|
||||
}
|
||||
|
||||
@@ -614,6 +665,31 @@ def run(args: argparse.Namespace) -> None:
|
||||
)
|
||||
validate_tool_call_result(result)
|
||||
print("tools/call: ok")
|
||||
edited_draft_version = edit_and_archive_operation(
|
||||
client,
|
||||
workspace_id,
|
||||
operation_id,
|
||||
operation_name,
|
||||
args.internal_upstream,
|
||||
)
|
||||
print(f"operation edited and archived: draft_version={edited_draft_version}")
|
||||
tools = call_mcp(
|
||||
client,
|
||||
mcp_url,
|
||||
api_key,
|
||||
session_id,
|
||||
{"jsonrpc": "2.0", "id": 4, "method": "tools/list", "params": {}},
|
||||
)
|
||||
validate_tools_list(tools, operation_name)
|
||||
pinned_result = call_mcp(
|
||||
client,
|
||||
mcp_url,
|
||||
api_key,
|
||||
session_id,
|
||||
tools_call_payload(operation_name, {"probe": "ok"}),
|
||||
)
|
||||
validate_tool_call_result(pinned_result)
|
||||
print("pinned tools/list and tools/call after Operation archive: ok")
|
||||
summary = build_safe_summary(
|
||||
operation_id, published_operation_version, agent_id, published_agent_version
|
||||
)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MAX_FILE_BYTES = 2 * 1024 * 1024
|
||||
FORBIDDEN = (
|
||||
(re.compile(r"\bcrank_adapter_rest\b|\bRestAdapterError\b"), "direct REST adapter dependency"),
|
||||
(re.compile(r"\breqwest\b"), "direct HTTP client dependency"),
|
||||
(
|
||||
re.compile(r"\.execute_with_(?:auth|context|auth_and_context)\s*\("),
|
||||
"legacy runtime execution bypass",
|
||||
),
|
||||
(
|
||||
re.compile(r"\.(?:execute_request|prepare_request|invoke_unary)\s*\("),
|
||||
"raw execution pipeline bypass",
|
||||
),
|
||||
(
|
||||
re.compile(r"\bpub\s+use\s+[^;]*(?:RuntimeExecutor|ProtocolAdapter)\b"),
|
||||
"execution boundary re-export",
|
||||
),
|
||||
(re.compile(r"\bsqlx\s*::"), "direct execution persistence dependency"),
|
||||
(
|
||||
re.compile(r"\.(?:validate_shape|apply_mapping|apply)\s*\("),
|
||||
"direct execution validation or mapping bypass",
|
||||
),
|
||||
)
|
||||
SCOPED_PREFIXES = (
|
||||
"apps/admin-api/src/routes/",
|
||||
"apps/admin-api/src/service/",
|
||||
"apps/admin-api/src/service.rs",
|
||||
)
|
||||
SCOPED_MCP_FILES = {
|
||||
"crates/crank-community-mcp/src/app.rs",
|
||||
"crates/crank-community-mcp/src/approval_execution.rs",
|
||||
"crates/crank-community-mcp/src/tool_error.rs",
|
||||
"crates/crank-community-mcp/src/app/invocation_history.rs",
|
||||
"crates/crank-community-mcp/src/app/tool_resolution.rs",
|
||||
"crates/crank-community-mcp/src/tool_search.rs",
|
||||
}
|
||||
SCOPED_MCP_PREFIXES = ("crates/crank-community-mcp/src/app/",)
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Check the canonical execution boundary.")
|
||||
parser.add_argument("--root", type=Path, default=Path.cwd())
|
||||
parser.add_argument("--files", nargs="*")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def candidate_paths(root: Path, explicit: list[str] | None) -> list[Path]:
|
||||
if explicit is None:
|
||||
result = subprocess.run(
|
||||
["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
||||
cwd=root,
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
)
|
||||
names = [name for name in result.stdout.decode("utf-8").split("\0") if name]
|
||||
else:
|
||||
names = explicit
|
||||
return [root / name for name in sorted(set(names))]
|
||||
|
||||
|
||||
def logical_path(root: Path, path: Path) -> str:
|
||||
if path.is_absolute():
|
||||
resolved = path.resolve(strict=False)
|
||||
else:
|
||||
resolved = (root / path).resolve(strict=False)
|
||||
try:
|
||||
return resolved.relative_to(root.resolve()).as_posix()
|
||||
except ValueError as error:
|
||||
raise ValueError("path escapes repository root") from error
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = parse_args(argv)
|
||||
root = args.root.resolve()
|
||||
failures: list[str] = []
|
||||
try:
|
||||
paths = candidate_paths(root, args.files)
|
||||
except (OSError, subprocess.SubprocessError, UnicodeError) as error:
|
||||
print(f"error: execution boundary input unavailable ({type(error).__name__})", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
for supplied in paths:
|
||||
if supplied.is_symlink():
|
||||
failures.append("INVALID_PATH")
|
||||
continue
|
||||
try:
|
||||
logical = logical_path(root, supplied)
|
||||
except ValueError:
|
||||
failures.append("INVALID_PATH")
|
||||
continue
|
||||
if (
|
||||
not logical.endswith(".rs")
|
||||
or not (
|
||||
logical.startswith(SCOPED_PREFIXES)
|
||||
or logical.startswith(SCOPED_MCP_PREFIXES)
|
||||
or logical in SCOPED_MCP_FILES
|
||||
)
|
||||
):
|
||||
continue
|
||||
path = root / logical
|
||||
if path.is_symlink() or not path.is_file():
|
||||
failures.append(f"INVALID_PATH {logical[:256]}")
|
||||
continue
|
||||
if path.stat().st_size > MAX_FILE_BYTES:
|
||||
failures.append(f"INPUT_TOO_LARGE {logical[:256]}")
|
||||
continue
|
||||
try:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError):
|
||||
failures.append(f"INVALID_SOURCE {logical[:256]}")
|
||||
continue
|
||||
for pattern, reason in FORBIDDEN:
|
||||
if pattern.search(source):
|
||||
failures.append(f"EXECUTION_BOUNDARY {logical[:256]} {reason}")
|
||||
|
||||
if failures:
|
||||
for failure in sorted(failures)[:1000]:
|
||||
print(f"error: {failure}", file=sys.stderr)
|
||||
return 1
|
||||
print("Execution boundary check passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -209,6 +209,14 @@ def validate_runtime_reference(
|
||||
raise ContractError(f"divergent Compose inline default: {name}")
|
||||
|
||||
|
||||
def compose_optional(specification: dict[str, object]) -> bool:
|
||||
compatibility = specification.get("compatibility")
|
||||
return (
|
||||
isinstance(compatibility, str)
|
||||
and "deprecated startup-bootstrap password" in compatibility
|
||||
)
|
||||
|
||||
|
||||
def validate(root: Path) -> None:
|
||||
effective, specifications, deployment = load_contract(root)
|
||||
declared_values: dict[str, set[str]] = {name: set() for name in effective}
|
||||
@@ -235,6 +243,7 @@ def validate(root: Path) -> None:
|
||||
name
|
||||
for name in effective
|
||||
if specifications[name]["process"] in {"shared", "admin_api"}
|
||||
and not compose_optional(specifications[name])
|
||||
}
|
||||
required_mcp = {
|
||||
name
|
||||
|
||||
@@ -23,6 +23,7 @@ echo "Rust module boundary check: checking module-level imports"
|
||||
python3 "$ROOT_DIR/scripts/check-config-boundaries.py" --root "$ROOT_DIR" || status=1
|
||||
python3 "$ROOT_DIR/scripts/check-migration-boundaries.py" --root "$ROOT_DIR" || status=1
|
||||
python3 "$ROOT_DIR/scripts/check-metrics-boundaries.py" --root "$ROOT_DIR" || status=1
|
||||
python3 "$ROOT_DIR/scripts/check-execution-boundaries.py" --root "$ROOT_DIR" || status=1
|
||||
|
||||
check_no_match \
|
||||
"admin-api service modules must not depend on axum HTTP types" \
|
||||
|
||||
Reference in New Issue
Block a user