feat(ACTIVITY-WP-0029): inventory callers, retarget sweep, bound execution
Map every State Hub/core-hub caller to a post-retirement owner. Keep the 15-minute sweep schedule here and point the engine at repo-manager (State Hub dual-run by default, REPO_MANAGER_URL when present). Publish GET /execution/semantics and 410 workplan launch routes so State Hub /execution/* is not re-homed as a task database. T03 still waits on HUB-WP-0004.
This commit is contained in:
parent
df3330f165
commit
f6cfc28c33
12 changed files with 359 additions and 12 deletions
|
|
@ -11,6 +11,7 @@ Endpoints:
|
|||
DELETE /activity-definitions/{id} — delete
|
||||
POST /activity-definitions/{id}/trigger — manual one-shot run
|
||||
GET/POST /ops/... — operator console (inventory, status, control)
|
||||
GET /execution/semantics — launch/ops_run contract (ACTIVITY-WP-0029)
|
||||
|
||||
Schedule lifecycle:
|
||||
- POST/PUT with trigger_type='cron' upserts a Temporal Schedule.
|
||||
|
|
@ -40,6 +41,7 @@ from temporalio.api.workflowservice.v1 import GetSystemInfoRequest
|
|||
from temporalio.client import Client
|
||||
|
||||
from activity_core.models import ActivityDefinition, CronTriggerConfig
|
||||
from activity_core.execution_api import router as execution_router
|
||||
from activity_core.ops_api import bind_ops_deps, router as ops_router
|
||||
from activity_core.ops_runs_api import bind_ops_runs_deps, router as ops_runs_router
|
||||
from activity_core.orm import ActivityDefinition as ActivityDefinitionRow, EventType as EventTypeRow
|
||||
|
|
@ -85,6 +87,7 @@ app = FastAPI(title="activity-core API", lifespan=lifespan)
|
|||
app.include_router(webhook_router)
|
||||
app.include_router(ops_router)
|
||||
app.include_router(ops_runs_router)
|
||||
app.include_router(execution_router)
|
||||
|
||||
|
||||
def _get_db() -> async_sessionmaker[AsyncSession]:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ Supported queries:
|
|||
- coding_retro: latest /progress/ item with event_type=coding_retro
|
||||
- daily_triage_digest: curated scalar JSON digest for daily WSJF triage
|
||||
- recently_on_scope_hourly: POST {STATE_HUB_URL}/recently-on-scope/hourly
|
||||
- consistency_sweep_remote_all: POST {STATE_HUB_URL}/consistency/sweep/remote-all
|
||||
- consistency_sweep_remote_all: POST {sweep_base}/consistency/sweep/remote-all
|
||||
sweep_base = CONSISTENCY_SWEEP_URL or REPO_MANAGER_URL or STATE_HUB_URL
|
||||
(ACTIVITY-WP-0029: engine is repo-manager; State Hub is the dual-run
|
||||
adapter until REPO_MANAGER_URL is set)
|
||||
- phase5_stabilization_check: hub-visible Phase 5 stabilization gates
|
||||
- legacy_meter_weekly_review: GET {STATE_HUB_URL}/legacy-meter/weekly-review
|
||||
- binky_rhythm_status: due-items for the Binky operating-rhythm definitions,
|
||||
|
|
@ -68,6 +71,33 @@ def _base_url() -> str:
|
|||
return os.environ.get("STATE_HUB_URL", _DEFAULT_STATE_HUB_URL).rstrip("/")
|
||||
|
||||
|
||||
def _sweep_target() -> tuple[str, str, str]:
|
||||
"""Return (base_url, path, adapter) for the consistency sweep POST.
|
||||
|
||||
Engine owner is always repo-manager. Default adapter is State Hub's
|
||||
dual-run ``POST /consistency/sweep/remote-all``. Point
|
||||
``CONSISTENCY_SWEEP_URL`` or ``REPO_MANAGER_URL`` at repo-manager when
|
||||
that HTTP surface exists.
|
||||
"""
|
||||
path = (os.environ.get("CONSISTENCY_SWEEP_PATH") or "/consistency/sweep/remote-all").strip()
|
||||
if not path.startswith("/"):
|
||||
path = f"/{path}"
|
||||
explicit = (os.environ.get("CONSISTENCY_SWEEP_URL") or "").strip().rstrip("/")
|
||||
repo_mgr = (os.environ.get("REPO_MANAGER_URL") or "").strip().rstrip("/")
|
||||
state_hub = _base_url()
|
||||
if explicit:
|
||||
if repo_mgr and explicit == repo_mgr:
|
||||
adapter = "repo-manager"
|
||||
elif explicit == state_hub:
|
||||
adapter = "state-hub-dual-run"
|
||||
else:
|
||||
adapter = "explicit"
|
||||
return explicit, path, adapter
|
||||
if repo_mgr:
|
||||
return repo_mgr, path, "repo-manager"
|
||||
return state_hub, path, "state-hub-dual-run"
|
||||
|
||||
|
||||
def _fetch_json(path: str, params: dict[str, Any] | None = None) -> Any:
|
||||
url = f"{_base_url()}{path}"
|
||||
try:
|
||||
|
|
@ -104,9 +134,10 @@ def _post_json(
|
|||
*,
|
||||
timeout: float = _TIMEOUT_SECONDS,
|
||||
retries: int | None = None,
|
||||
base_url: str | None = None,
|
||||
) -> Any:
|
||||
"""POST JSON with retries on transient edge/hub failures (ACTIVITY-WP-0027)."""
|
||||
url = f"{_base_url()}{path}"
|
||||
url = f"{(base_url or _base_url()).rstrip('/')}{path}"
|
||||
attempts = _post_retry_attempts() if retries is None else max(1, retries)
|
||||
backoff = _post_retry_backoff_seconds()
|
||||
last_exc: Exception | None = None
|
||||
|
|
@ -261,11 +292,13 @@ class StateHubContextResolver(ContextResolver):
|
|||
for key, value in params.items()
|
||||
if key not in {"required", "degrade_on_unavailable"}
|
||||
}
|
||||
sweep_base, sweep_path, adapter = _sweep_target()
|
||||
try:
|
||||
result = _post_json(
|
||||
"/consistency/sweep/remote-all",
|
||||
sweep_path,
|
||||
payload,
|
||||
timeout=_SWEEP_TIMEOUT_SECONDS,
|
||||
base_url=sweep_base,
|
||||
)
|
||||
except (httpx.HTTPError, ValueError) as exc:
|
||||
if not _want_degrade(params):
|
||||
|
|
@ -283,8 +316,15 @@ class StateHubContextResolver(ContextResolver):
|
|||
"skipped_budget": [],
|
||||
"degraded": True,
|
||||
"degraded_reason": str(exc)[:300],
|
||||
"engine_owner": "repo-manager",
|
||||
"adapter": adapter,
|
||||
"sweep_url": f"{sweep_base}{sweep_path}",
|
||||
}
|
||||
return _validate_consistency_sweep_remote_all(result)
|
||||
validated = _validate_consistency_sweep_remote_all(result)
|
||||
validated.setdefault("engine_owner", "repo-manager")
|
||||
validated.setdefault("adapter", adapter)
|
||||
validated.setdefault("sweep_url", f"{sweep_base}{sweep_path}")
|
||||
return validated
|
||||
if query == "phase5_stabilization_check":
|
||||
return _phase5_stabilization_check(params)
|
||||
if query == "legacy_meter_weekly_review":
|
||||
|
|
|
|||
88
src/activity_core/execution_api.py
Normal file
88
src/activity_core/execution_api.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
"""Execution-queue contract surface (ACTIVITY-WP-0029-T04).
|
||||
|
||||
Workplan launch rows stay out of this API. Callers that send State Hub
|
||||
``/execution/*`` workplan shapes get 410 with a replacement pointer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
router = APIRouter(prefix="/execution", tags=["execution"])
|
||||
|
||||
SEMANTICS = {
|
||||
"port": "port.schedule",
|
||||
"document": "docs/execution-queue-boundary.md",
|
||||
"activity_core_owns": [
|
||||
"schedules, wakeups, and recurring ActivityDefinition fires",
|
||||
"POST /activity-definitions/{id}/trigger",
|
||||
"ops_run claim, heartbeat, complete, and fail",
|
||||
],
|
||||
"activity_core_does_not_own": [
|
||||
"workplan and task files (ADR-001 / repo-manager index)",
|
||||
"task lifecycle assign/track/close (issue-core / work records)",
|
||||
"ITC Task Model types",
|
||||
"policy publication (policy-nexus)",
|
||||
"C-rule consistency engine (repo-manager)",
|
||||
],
|
||||
"replacements": {
|
||||
"GET /execution/semantics": "GET /execution/semantics",
|
||||
"GET /execution/launch-requests": "GET /ops-runs",
|
||||
"POST /execution/launch-requests": (
|
||||
"POST /activity-definitions/{id}/trigger for a schedule fire; "
|
||||
"do not POST workplan ids here"
|
||||
),
|
||||
"GET /execution/workplan-stack": "repo-manager port.work / hub-core projection",
|
||||
"PATCH /execution/workplans/{id}/intent": "edit the workplan file in its repo",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/semantics")
|
||||
async def execution_semantics() -> dict:
|
||||
return SEMANTICS
|
||||
|
||||
|
||||
def _gone(replacement: str, detail: str) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=410,
|
||||
content={
|
||||
"detail": detail,
|
||||
"replacement_ref": replacement,
|
||||
"document": "docs/execution-queue-boundary.md",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.api_route("/launch-requests", methods=["GET", "POST"])
|
||||
async def launch_requests_retired() -> JSONResponse:
|
||||
return _gone(
|
||||
SEMANTICS["replacements"]["GET /execution/launch-requests"],
|
||||
"Workplan launch-requests are not stored in activity-core. "
|
||||
"Use GET /ops-runs or POST /activity-definitions/{id}/trigger.",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/workplan-stack")
|
||||
async def workplan_stack_retired() -> JSONResponse:
|
||||
return _gone(
|
||||
SEMANTICS["replacements"]["GET /execution/workplan-stack"],
|
||||
"Workplan stacks are a work-record index, not an ops-run queue.",
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/workplans/{workplan_id}/intent")
|
||||
async def workplan_intent_retired(workplan_id: str) -> JSONResponse:
|
||||
return _gone(
|
||||
SEMANTICS["replacements"]["PATCH /execution/workplans/{id}/intent"],
|
||||
"Execution intent for a workplan belongs in the repo file, not here.",
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/workstreams/{workstream_id}/intent")
|
||||
async def workstream_intent_retired(workstream_id: str) -> JSONResponse:
|
||||
return _gone(
|
||||
SEMANTICS["replacements"]["PATCH /execution/workplans/{id}/intent"],
|
||||
"Legacy workstream execution intent is retired with State Hub.",
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue