fix(api): normalize trailing slashes — no slash on param routes

Rule: trailing slash only on collection roots (/). Any route containing
a path parameter {…} uses no trailing slash. Applies across all routers,
scripts, Makefile, and tests. Fixes 307-redirect fragility on POST/PATCH
from naive clients (curl, Codex HTTP calls).

Also adds POST /repos/{slug}/sync — runs ADR-001 consistency check with
--fix via HTTP, so non-MCP agents (Codex) can self-service DB sync without
operator intervention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 15:13:01 +02:00
parent cadeb4a3b5
commit 768a8ba9c7
13 changed files with 74 additions and 21 deletions

View File

@@ -37,7 +37,7 @@ PROMPT_FILE = SCRIPT_DIR.parent / "prompts" / "sbom-capture-agent.md"
def resolve_repo_path(repo_slug: str) -> Path | None:
"""Look up the registered path for a repo slug via the state-hub API."""
url = f"{API_BASE}/repos/{repo_slug}/"
url = f"{API_BASE}/repos/{repo_slug}"
try:
with urllib.request.urlopen(url, timeout=10) as resp:
data = json.loads(resp.read())

View File

@@ -69,7 +69,7 @@ def _print_report(report, use_color: bool = True) -> None:
async def check_repo(slug: str, as_json: bool) -> bool:
try:
repo = _get(f"/repos/{slug}/")
repo = _get(f"/repos/{slug}")
except Exception as e:
print(f"✗ Could not fetch repo '{slug}': {e}", file=sys.stderr)
return False

View File

@@ -1083,7 +1083,7 @@ def fix_repo(
hostname = socket.gethostname()
if (repo_record.get("host_paths") or {}).get(hostname) != repo_path:
result = _api_post(
api_base, f"/repos/{repo_slug}/paths/",
api_base, f"/repos/{repo_slug}/paths",
{"host": hostname, "path": repo_path},
)
if result and "_error" not in result:
@@ -1333,7 +1333,7 @@ def fix_repo(
from datetime import timezone as _tz
import datetime as _dt
now_iso = _dt.datetime.now(_tz.utc).isoformat()
_api_patch(api_base, f"/repos/{repo_slug}/", {"last_state_synced_at": now_iso})
_api_patch(api_base, f"/repos/{repo_slug}", {"last_state_synced_at": now_iso})
# Write the worker orientation brief (.custodian-brief.md)
if repo_path:

View File

@@ -509,7 +509,7 @@ def post_ingest(api_base: str, repo_slug: str, entries: list[dict]) -> dict:
def _resolve_repo_path_from_hub(api_base: str, repo_slug: str) -> Path | None:
"""Query the hub for this host's registered path for repo_slug."""
try:
url = f"{api_base}/repos/{repo_slug}/"
url = f"{api_base}/repos/{repo_slug}"
with urllib.request.urlopen(url) as resp:
data = json.loads(resp.read())
hostname = socket.gethostname()

View File

@@ -45,7 +45,7 @@ resolve_path() {
local slug="$1"
# Try the registered local_path first
local api_path
api_path=$(curl -sf "${API_BASE}/repos/${slug}/" | python3 -c \
api_path=$(curl -sf "${API_BASE}/repos/${slug}" | python3 -c \
"import json,sys; d=json.load(sys.stdin); print(d.get('local_path') or '')" 2>/dev/null || true)
if [[ -n "$api_path" && -d "$api_path" ]]; then
echo "$api_path"

View File

@@ -73,7 +73,7 @@ echo " API OK"
# ── Step 2: Verify domain exists ───────────────────────────────────────────────
echo "==> Verifying domain '$DOMAIN' ..."
DOMAIN_JSON="$(curl -sf "$API_BASE/domains/$DOMAIN/" 2>/dev/null || echo 'NOT_FOUND')"
DOMAIN_JSON="$(curl -sf "$API_BASE/domains/$DOMAIN" 2>/dev/null || echo 'NOT_FOUND')"
if [[ "$DOMAIN_JSON" == "NOT_FOUND" ]] || ! echo "$DOMAIN_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get('slug') else 1)" 2>/dev/null; then
echo "ERROR: Domain '$DOMAIN' not found in the State Hub."
echo " To create: make add-domain DOMAIN=$DOMAIN NAME=\"<display name>\""
@@ -252,7 +252,7 @@ fi
# ── Step 7: Register this machine's local path ────────────────────────────────
echo "==> Registering host path for $(hostname) ..."
curl -sf -X POST "$API_BASE/repos/$REPO_SLUG/paths/" \
curl -sf -X POST "$API_BASE/repos/$REPO_SLUG/paths" \
-H "Content-Type: application/json" \
-d "{\"host\": \"$(hostname)\", \"path\": \"$PROJECT_PATH\"}" > /dev/null \
&& echo " host_paths[$(hostname)] = $PROJECT_PATH"