Skip to content

feat: stable machine-facing API — operations, idempotency, cancellation, shared audit path - #13

Merged
iamgp merged 2 commits into
feat/lineage-difffrom
feat/agent-api
Sep 15, 2026
Merged

iamgp merged 2 commits into
feat/lineage-difffrom
feat/agent-api

Conversation

@iamgp

@iamgp iamgp commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Bundle 9 — stable machine-facing API. Stacked on feat/lineage-diff (PR #12); do not merge before it.

Shared audit path (CLI ↔ API)

  • New phlo_transform_engine::audit module: the promotion-evidence helpers moved verbatim out of the CLI — audited_diff (branch_diff.json preferred, single-model diff.json fallback, both-side staleness, post-diff materialisation, self-comparison rejection, --full requirement), read_environment/read_environment_for/environment_artifact_name/write_environment_artifacts/remove_environment_artifacts, contract_breaking_changes (live desired-vs-recorded contract diff), plus catalog_name and compiled_catalog. The CLI keeps thin delegating wrappers — one evidence path, no drift.

Daemon — read plane (never mutates)

  • GET /v1/plan?select=&environment=&force= — plan DTO (needs adapter)
  • GET /v1/lineage — full lineage document (lineage --format graph)
  • GET /v1/diff/lineage?base=<git-ref> — semantic lineage diff, offline, read-only tree extraction
  • GET /v1/diff/branch?from=&to=&full= — branch diff (needs adapter), persists branch_diff.json like the CLI so promote audits it
  • GET /v1/state/runs[?environment=], /v1/state/runs/{id|prefix}, /v1/state/models/{id}[?environment=], /v1/state/promotions
  • POST /v1/reload — synchronous snapshot recompile

Daemon — operation plane (mutations, long-running)

  • POST /v1/operations {kind, idempotency_key?, params?} → handle; kinds run, test, promote, reload
  • Lifecycle queued → running → succeeded|failed|cancelled; GET /v1/operations[/{id}] polls status; POST .../{id}/cancel cancels via the engine CancelHandle
  • run ops expose live progress read back from the state store (the runner persists per-model transitions up front)
  • promote runs the full gated path: ref resolution, run history, audited diff artifact, live contract breaks, provisioning-base target-hash pin, merge check, promotion record + optional cleanup — identical to promote on the CLI
  • Idempotency: same idempotency_key (or Idempotency-Key header) replays the existing handle under one lock — no duplicate effects on retry, including across daemon restarts
  • Durability: every transition is appended to .phlo/transform/operations.jsonl and folded back at startup; an op in flight at shutdown restores as failed/interrupted rather than silently re-running
  • Single mutating-op gate: run/promote serialize (409 API008); test/reload ungated
  • Strict param schemas (deny_unknown_fields); ?select= accepts single or repeated keys

Auth — daemon --token <secret> requires Authorization: Bearer on every endpoint except /status; missing/wrong credentials answer 401 API015. No token = unauthenticated loopback mode.

Errors — {"error":{code,message}} with stable codes API001–API015 documented in docs/daemon.md; capability gaps answer 503 API007; op records carry the same codes plus cancelled and interrupted.

Wiring — phlo-transform daemon builds ServiceConfig from the global CLI flags (adapter/state/nessie/environment/catalog/schema/token), degrading to API007 per missing capability; /v1/status reports capabilities.

Results reuse CLI DTOs: run → RunResult (run.json), test → {tests,failed}, promote → gates + PromotionRecord, diffs/state → the exact --json shapes.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace — all suites green
  • run op end-to-end on real DuckDB + sqlite state (succeeded, run persisted, state endpoints serve it, idempotent replay)
  • promote end-to-end over HTTP — real DuckDB + sqlite state + recorded provisioning evidence + deep branch-diff artifact + in-memory Nessie: --check gates without merging, real promote merges and persists the promotion record, unaudited candidate fails run gate with ok:false
  • test op, GET /v1/plan selector narrowing + API006, lineage document + self-diff, API010 on bogus ref
  • Rejection matrix API007/008/009/012/015, conflict gate, cancel, POST /v1/reload
  • Journal durability: ops + idempotency survive restart, in-flight restores as interrupted
  • Nessie/Trino e2e (Docker, --ignored)

@iamgp
iamgp force-pushed the feat/agent-api branch 2 times, most recently from e24e729 to e8d21dc Compare September 14, 2026 09:53
@iamgp
iamgp force-pushed the feat/agent-api branch 2 times, most recently from 2e1b2b0 to e9b7ad0 Compare September 14, 2026 10:36
@iamgp
iamgp force-pushed the feat/agent-api branch 2 times, most recently from 573b1b9 to 2bb5fb0 Compare September 14, 2026 15:49
…on, shared audit path

Daemon gains a two-plane v1 API: reads (plan, lineage document, branch and
lineage diffs, state inspection) and operations (run/test/promote/reload)
with a stable queued→running→succeeded|failed|cancelled lifecycle, live
progress read back from the state store, cooperative cancellation via
CancelHandle, per-key idempotent replay, a single-mutating-operation gate,
strict param schemas, and structured API0xx errors. The promotion audit
path (audited_diff, environment artifacts, contract_breaking_changes,
catalog naming) moves into phlo-transform-engine::audit so the CLI and the
daemon apply identical evidence rules.
@iamgp
iamgp force-pushed the feat/agent-api branch 3 times, most recently from e085ae5 to 9d1fc61 Compare September 14, 2026 21:45
…ncy, durable journal, auth

Review fixes on the agent API bundle:

- run/resume/retry_failed provision the candidate Nessie branch and its
  branch-scoped catalog, then recompile the workspace retargeted at it —
  the same orchestration the CLI's --ref path runs; a passed op binds to
  the environment's post-run head. Without Nessie the environment stays a
  state label only; with Nessie configured but no adapter or catalog-facing
  URI, a non-base environment fails closed with API007 before execution
  rather than running on the default target and binding evidence to the
  candidate's head.
- Candidate catalog identity is collision-safe: names are
  phlo_<sanitised-ref>_<8-hex-sha256>, so punctuation-equivalent refs can
  never share a physical catalog. ensure_catalog reports CatalogStatus
  (Created/Unverified/Unmanaged) — an existing catalog is never adopted on
  name alone since its bound Nessie ref cannot be read back over SQL;
  unverified catalogs are accepted only via the self-naming convention or a
  recorded binding, and a catalog another candidate's artifacts claim is
  refused outright. Cleanup drops only catalogs phlo provably created.
- plan and run share one environment resolver (EnvironmentContext::resolve):
  Ensure provisions then compiles retargeted; ReadOnly — used by
  GET /v1/plan?environment=&base= — computes the same physical catalog
  (override → recorded binding → generated name) and a retargeted compile
  without creating branches or writing evidence, so the previewed targets
  are exactly what a run executes.
- resume keeps the interrupted run's id and reuses its passed work;
  retry_failed links a new run via continued_from; the stored run's
  environment is authoritative — continuations retarget it, not a
  requested one.
- /v1/diff/lineage runs the shared engine LineageDiffContext — identical
  merge-base/ref→ref orchestration, provenance artifact, and candidate
  lineage fingerprint as the CLI, so the API can no longer drift.
- Idempotency keys are global to POST /v1/operations and bound to a sha256
  of the canonical {kind, params} request: same key with different params
  or a different kind is 409 API016, and the reservation is journaled
  before the operation is acknowledged — a restart can never execute a
  keyed submission twice.
- Full read surface: /v1/impact (model, column, selection blast radius),
  /v1/state/runs/{id}/failed, /v1/diff/branch contract changes.
- Daemon e2e: an environment-targeted run over real Nessie + Trino is
  physically branch-isolated and its plan names identical targets (ignored
  by default; runs in CI).
@iamgp
iamgp added this pull request to stack #15 September 15, 2026 06:48
@iamgp
iamgp merged commit 8a072cb into main Sep 15, 2026
1 check passed
@iamgp
iamgp deleted the feat/agent-api branch September 15, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant