Skip to content

Admit first backend routes on an owned never-live production lane - #6058

Merged
shiny-code-app[bot] merged 2 commits into
mainfrom
work/lp6054-neverlive-domain-auth
Nov 12, 2026
Merged

shiny-code-app[bot] merged 2 commits into
mainfrom
work/lp6054-neverlive-domain-auth

Conversation

@shiny-code-app

Copy link
Copy Markdown
Contributor

Why

The Director agent could add the first backend hostname route to an empty, service-created compose only on a testing lane. An owned production lane whose product was never Live had no path, so ordinary first setup there was refused with authorization_denied before any provider or record effect. This happened to a reviewed TK production route apply (trace launchplane_req_4355b1e274c04d23bfefba0e12cbd77d). The cause is in setup_dokploy_target: director_testing_domain required instance == "testing", and no other admission path covers a production domain reconcile for this identity.

This extends the existing bounded first-hostname continuation to that case. It does not loosen the lane check, add a grant, add a product allowlist, or use a profile label.

What changed

  • setup_dokploy_target (control_plane/http_app.py): the Director-agent domain continuation (director_empty_domain) now also admits instance == "prod" when the context's exclusive owner is not Live. That is the same production_use != "live" invariant first_production_setup already uses, now shared as owner_not_live. The service-authenticated Director-agent marker, exclusive context ownership and the terminal-identity gate are unchanged.

  • setup_empty_lane_domain (control_plane/dokploy_testing_domain.py, formerly setup_empty_testing_domain) qualifies production with the same evidence as testing:

    • an active exclusive owner and its runtime port;
    • an unchanged service-created empty compose;
    • no deployments or serving inventory, and the provider proves the compose never deployed;
    • empty provider source, settings, mounts and networks;
    • no recorded use, data custody or resource sharing, and no alias or shared target;
    • hostnames no other lane, profile or route binding claims;
    • lane and authority locks, then compare-and-write.

    On production it also refuses a Live owner, holds the first-production setup lock, and refuses any unresolved original setup or production operation. Testing keeps exactly one hostname. Production accepts the reviewed hostname set, each matched or created exactly.

  • require_no_unresolved_production_setup (control_plane/dokploy_target_setup_http.py) is the empty auto-deploy correction's existing predecessor check, extracted so both paths share one copy.

  • The testing-only held state-volume check now applies only to testing lanes, so production does not inherit an unrelated hold.

  • docs/operations.md describes the generalized continuation.

Effect boundary (unchanged primitives): per hostname, at most one Dokploy domain.create of an HTTPS web route on the profile's runtime port with certificateType: none (the existing default certificate), or adoption of an exactly matching route. Then one compare-and-write of the tracked target's domains. There is no certificate issuance and no env, compose source or file, mounts, auto-deploy, deploy, workload, DNS, front-proxy, profile, policy, grant, classification or release-exemption change.

production_use: unknown is admitted only together with that positive never-used evidence. It is not prelaunch_bootstrap_authorized and grants no bootstrap or release exemption.

Verification

  • Failing first: before the source change, all 18 new production witnesses got 403 authorization_denied with the real trace's message.
  • tests/test_director_testing_domain_setup.py, 40 tests:
    • Accepted: owned never-live production gets only its routes, with exact payloads, replay and no deploy, profile or provider-record change. The existing testing success is unchanged.
    • Refused before any provider contact: a Live owner, a scoped (non-Director) caller, and shared context ownership.
    • Refused at request validation (400 invalid_request): caller-supplied authority fields.
    • Refused before effects: recorded use, only-copy data or shared resources; deployment history; served inventory; a changed, adopted or replaced target binding; a target id aliasing another lane; changed provider facts (env, mounts, compose file, git source, active status, foreign route) and provider deployment history; broader or mismatched inputs; hostnames claimed by another lane or a route binding; an unresolved original setup; an active executor.
    • Recovery: a lost second-route response, and a lost receipt (only the exact recorded hostnames).
    • Original key: a request refused at authorization binds nothing to its key. The identical request continues later, and a changed request under that key after success gets 409 idempotency_key_reused.
  • Full touched safety and authorization modules, 19 whole modules, 580 tests, OK (4 skipped).
  • Ruff check and format, mypy, role words: pass.
  • Native JetBrains inspection (PyCharm, 5 changed files): GREEN, 0 findings on changed lines.

Independent review

OpenAI (Codex CLI default; the CLI did not report the model), two rounds.

  • Round 1, P2: partial hostname effects do not bind the Idempotency-Key, because this route stores its receipt only on success.
    • Acted on the sibling this branch introduced: a subset check on tracked domains let a same-key retry add hostnames after the routes were recorded but the receipt was lost. Tracked domains must now be empty or exactly the requested set, as testing already required. A new witness fails under the subset rule.
    • Declined the pre-commit remainder, with reason. Round 2 confirmed that a changed or unreceipted retry gets nothing a fresh, fully qualified request on the same state would not also get. It bypasses no ownership, Live, protection, claim, never-deployed, unresolved-setup or alias check. This success-only receipt behavior predates the branch and is shared with the testing continuation and the generic path.
  • Round 2: no safety findings.

Removed: the duplicated unresolved-setup loop in dokploy_empty_automation.py and the testing-only instance assumptions in the shared first-hostname helpers.
Net lines: source +92 (−128 / +220: multi-hostname matching, the production lock and the split predicates), docs +3, tests +279.

Refs #6054

The Director agent's first-hostname continuation only covered testing, so an owned production lane whose product was never Live had no path for its first backend routes. Extend the same bounded continuation to that lane: the owner must not be Live, the compose must be the service-created empty target that never deployed, and an unresolved original setup keeps custody. Reuse the empty auto-deploy correction's unresolved-setup check instead of a second copy.
A subset check let a retry under the same key add hostnames after the routes were recorded but the receipt was lost. Require the tracked domains to be empty or exactly the requested set, as testing already did.
@shiny-code-app

Copy link
Copy Markdown
Contributor Author

Review result: weighed
Head: aae2ad8

Independent review by OpenAI (Codex CLI default; the CLI did not report the model), two rounds, read-only, static. Prompts and results are kept in local task evidence.

Acted on (round 1, P2 sibling): a subset check on tracked domains let a same-key retry add hostnames after the routes were recorded but the receipt was lost. Tracked domains must again be empty or exactly the requested set (aae2ad8). test_missing_receipt_recovers_only_the_recorded_exact_hostnames fails under the subset rule and passes now.

Declined, with reason (round 1, P2 remainder): this route stores its receipt only on success, so a changed retry after a pre-commit partial outcome is not refused by key. Round 2 confirmed this gives no effect a fresh, fully qualified request on the same state would not also get, and bypasses no ownership, Live, protection, claim, never-deployed, unresolved-setup or alias check. The behavior predates this branch and is shared with the testing continuation and the generic path. Same-original continuation guidance stays: the identical request and key.

Round 2: no safety findings.

@shiny-code-app

Copy link
Copy Markdown
Contributor Author

Ready for the merge train

Head: aae2ad8

@shiny-code-app

shiny-code-app Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Launchplane completed the merge-train step for this pull request.

Launchplane finished the merge-train step for this pull request.

Launchplane manages this comment and will update it as the train moves.

shiny-code-app Bot added a commit that referenced this pull request Nov 12, 2026
@shiny-code-app
shiny-code-app Bot merged commit c447fe5 into main Nov 12, 2026
49 checks passed
@shiny-code-app
shiny-code-app Bot deleted the work/lp6054-neverlive-domain-auth branch November 12, 2026 14:58
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.

0 participants