Skip to content

fix(cli): stop a deployment under aspect.<domain> from shadowing the Aspect account - #1368

Open
gregmagolan wants to merge 2 commits into
mainfrom
fix/reserved-deployment-name-collision
Open

fix(cli): stop a deployment under aspect.<domain> from shadowing the Aspect account#1368
gregmagolan wants to merge 2 commits into
mainfrom
fix/reserved-deployment-name-collision

Conversation

@gregmagolan

@gregmagolan gregmagolan commented Aug 1, 2026

Copy link
Copy Markdown
Member

A deployment served under aspect.<domain> took the built-in Aspect account's name and silently replaced it.

aspect auth configure remote.aspect.foo.com derived the name aspect: derivation stripped the host's registrable domain (via the public-suffix list), leaving the bare aspect. load_deployments overlays config.json onto the built-in seed by name, so that entry replaced the account. summarize_deployment then decided "is this the account?" with a name == "aspect" comparison, so the deployment was rendered under Aspect account: — showing its own issuer — while the real account disappeared. auth remove aspect refused to help, because the name looked built-in.

Account identity is a flag, not a name

Deployment carries a #[serde(skip)] builtin field set only by default_deployment(). A deployment merely named aspect is an ordinary deployment, and a hand-edited config.json cannot claim account status. Every d.name == DEFAULT_DEPLOYMENT_NAME identity check became d.builtin.

Reserved names

aspect and default may not be taken by a configured deployment — the first is the account's name, the second its credential profile (DEFAULT_PROFILE), and a deployment's credential is filed under its own name, so either would share the account's slot.

  • Derivation avoids them where it can. Dropping the leading service label and then only a trailing .aspect.build means a self-hosted host keeps its full domain: remote.aspect.foo.comaspect.foo.com, remote.foo.bar.comfoo.bar.com. Aspect-hosted hosts still reduce to a bare name (bes.gcp.awd-gha-test-dev.aspect.buildgcp.awd-gha-test-dev). This replaces the public-suffix approach and drops the psl dependency.
  • upsert_deployment rejects them. Every configure path funnels through it, so both an explicit --deployment=aspect and a name derived from an Aspect-hosted host (remote.aspect.aspect.buildaspect) are caught.
  • The loader skips them. A config.json entry under a reserved name is ignored rather than allowed to replace the seed. Skipping rather than erroring keeps a config that already holds such an entry usable — erroring would fail every auth command, including the auth remove needed to clean it up.

auth use default is unaffected: only DEFAULT_DEPLOYMENT_NAME is the "clear the configured default" sentinel, so default falls through to the unknown-deployment check instead of silently clearing every default.

Reporting and recovery

auth status warns for each ignored entry, naming the file that declares it. The remedy depends on that file: the user's config is fixable with auth remove <name>; a checked-in repo config is told to edit the entry directly, since auth remove only edits the user's file. auth remove deletes a reserved-name file entry while still refusing to remove the account itself.

Sharing one issuer across several deployments is unaffected — nothing keys on the issuer.

Existing configs holding an aspect entry need one manual step, since it predates the guard: aspect auth remove aspect, then re-run aspect auth configure <host>. auth status prints exactly that.


Changes are visible to end-users: yes

  • Searched for relevant documentation and updated as needed: yes (no docs reference the naming rule)
  • Breaking change (forces users to change their own code or config): no — but see the one-time auth remove above, which auth status prompts for
  • Suggested release notes appear below: yes

Suggested release notes

  • aspect auth configure no longer derives a name that collides with the built-in Aspect account, which silently replaced it and made the deployment appear missing from aspect auth status. Self-hosted hosts now keep their domain (remote.aspect.foo.comaspect.foo.com, remote.foo.bar.comfoo.bar.com); Aspect-hosted hosts are unchanged (remote.acme.aspect.buildacme).
  • aspect auth status warns when a config.json deployment is ignored for using a reserved name (aspect, default), naming the file that declares it and how to fix it.
  • aspect auth remove <name> can delete a config entry that took a reserved name, while still refusing to remove the built-in account.

Test plan

  • Covered by existing test cases
  • New test cases added
  • Manual testing; please provide instructions so we can reproduce:

With a ~/.aspect/config.json holding {"name": "aspect", "hosts": ["remote.aspect.<domain>"]}, aspect auth status shows the genuine account (Issuer auth.aspect.build) plus a warning naming the ignored entry and its file. aspect auth remove aspect deletes that entry; with no such entry it still errors with the built-in "aspect" account cannot be removed.

Rust unit tests (cargo test -p axl-runtime, 370 passed):

  • deployment_name_from_host_strips_service_label_and_aspect_domain — table-driven over Aspect-hosted, self-hosted, multi-label suffixes (.co.uk), IP literals, single labels, trailing dots, and dot-anchoring (remote.acme.notaspect.build keeps its domain; an unanchored suffix would corrupt it to acme.not).
  • deployment_name_from_host_collides_only_under_aspects_own_domain — self-hosted hosts can never derive a reserved name; Aspect-hosted ones can, and upsert_deployment rejects those.
  • upsert_deployment_rejects_reserved_names, config_json_entry_cannot_shadow_the_account, remove_clears_a_shadowed_entry_but_protects_the_account.
  • shadowed_entries_are_attributed_to_their_config_file — repo vs user attribution, dedup across both files, both reserved names.
  • overlay_replaces_by_name_in_source_order — overlay precedence and seed-default reconciliation.
  • seed_identity_is_the_builtin_flag_not_the_name, builtin_flag_is_not_deserialized_from_config, apply_set_default_switches_and_clears (covers auth use default).

AXL: aspect dev test-auth — 4 tests, including _test_shadowed_warning for the warning wording in both the user-config and repo-config cases.

cargo fmt --check clean; cargo clippy findings in auth.rs unchanged from main (9 pre-existing).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48670e761f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/aspect-cli/src/builtins/aspect/auth.axl Outdated
Comment thread crates/axl-runtime/src/engine/aspect/auth.rs Outdated
@aspect-workflows

aspect-workflows Bot commented Aug 1, 2026

Copy link
Copy Markdown

✨ Aspect Workflows Tasks

📅 Sun Aug 2 06:43:43 UTC 2026

❌ 1 failed task

  • ❌ delivery-uncacheable [delivery] · ⏱ 43.3s · ✨ Aspect · 🐙 GitHub Actions
    💬 failed in deliver · Delivery failed (1 delivery fail)

⚠️ 3 flagged tasks

  • ⚠️ delivery-gha-debug [delivery] · ⏱ 48.9s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Delivery complete (1 delivered · 2 warn · 4 skipped)
  • ⚠️ delivery-gha [delivery] · ⏱ 49.8s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Delivery complete (1 delivered · 2 warn · 4 skipped)
  • ⚠️ delivery-uncacheable-warn [delivery] · ⏱ 14.5s · ✨ Aspect · 🐙 GitHub Actions
    💬 Delivery complete (1 warn)

✅ 28 successful tasks

  • ✅ axl-smoke-gha-bootstrap [build] · ⏱ 22.7s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (1 built)
  • ✅ run-axl-smoke [run] · ⏱ 27.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Ran //examples/deliverable:py_deliverable
  • ✅ run-axl-smoke-2 [run] · ⏱ 14s · 🐙 GitHub Actions · ☑️ Check
    💬 Ran //examples/deliverable:sh_deliverable
  • ✅ axl-tests-gha-bootstrap [build] · ⏱ 1m 6s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (1 built)
  • ✅ build-gha-debug [build] · ⏱ 12m 27s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (172 built)
  • ✅ build-gha [build] · ⏱ 14m 11s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (172 built)
  • ✅ build-gha-ephemeral [build] · ⏱ 35.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (10 built)
  • ✅ buildifier-gha-debug [buildifier] · ⏱ 31.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ buildifier-gha [buildifier] · ⏱ 46.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ delivery-no-remote-exec [delivery] · ⏱ 6.8s · ✨ Aspect · 🐙 GitHub Actions
    💬 Delivery complete (no deliveries)
  • ✅ format-gha-debug [format] · ⏱ 1m 12s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task [format] · ⏱ 1m 4s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-2 [format] · ⏱ 12.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-3 [format] · ⏱ 12s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-4 [format] · ⏱ 11s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-gha [format] · ⏱ 1m 15s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ gazelle-gha-debug [gazelle] · ⏱ 55.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-from-source-gha-debug [gazelle] · ⏱ 2m 2s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-from-source-gha [gazelle] · ⏱ 2m 3s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-gha [gazelle] · ⏱ 36.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ init-shell [build] · ⏱ 30.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (10 built)
  • ✅ lint-gha-debug [lint] · ⏱ 48.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Lint complete (clean)
  • ✅ lint-gha [lint] · ⏱ 33.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Lint complete (clean)
  • ✅ test-gha-debug [test] · ⏱ 7m 14s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (26/26 passed · 23 cached)
  • ✅ test-gha-coverage [test] · ⏱ 21.3s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-gha-target-pattern-file [test] · ⏱ 12.9s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-gha [test] · ⏱ 9m 26s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (26/26 passed · 26 cached)
  • ✅ test-gha-ephemeral [test] · ⏱ 43.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)

🔁 Reproduce

❌ delivery (delivery-uncacheable · delivery-gha-debug · delivery-gha · delivery-uncacheable-warn)

# --mode=always --track-state=false for off-runner with no state backend.
aspect delivery \
  --commit-sha=44f76db5a2cd0794a81423601586aa74b5cbff81 \
  --mode=always \
  --track-state=false \
  --dry-run=true

Install aspect: aspect.build/docs/cli/install


⏱ Last updated Sun Aug 2 07:03:52 UTC 2026 · 📊 GitHub API quota 1,034/15,000 (7% used, resets in 10m)
🚀 Powered by Aspect CLI (v0.0.0-dev)  |  Aspect Build · X · LinkedIn · YouTube

@gregmagolan
gregmagolan force-pushed the fix/reserved-deployment-name-collision branch from 48670e7 to 44f76db Compare August 2, 2026 06:36
@gregmagolan gregmagolan changed the title fix(cli): stop a configured deployment from shadowing the Aspect account fix(cli): stop a deployment under aspect.<domain> from shadowing the Aspect account Aug 6, 2026
`aspect auth configure remote.aspect.foo.com` derived the deployment name
`aspect` — the public-suffix list makes `foo.com` the registrable domain, so
stripping it from `aspect.foo.com` leaves `aspect`, which is
`DEFAULT_DEPLOYMENT_NAME`. Because `load_deployments` overlaid config.json onto
the built-in seed by name, the new entry silently *replaced* the Aspect account:
`auth status` rendered the deployment under "Aspect account:" (its `builtin` flag
was a `name == "aspect"` comparison), the account itself vanished, and `auth
remove aspect` refused to help because the name looked built-in.

Seed identity is now a `#[serde(skip)]` `builtin` field set only by
`default_deployment()`, so a deployment merely *named* `aspect` is an ordinary
deployment and a hand-edited config.json cannot claim account status. Three
layers then keep the name from being taken at all: derivation keeps the
registrable domain when it would produce a reserved name (`aspect.foo.com`),
`upsert_deployment` rejects a reserved name whatever its origin (the choke point
every `configure` path shares, covering explicit `--deployment`), and
`load_deployments` skips — rather than honors — a reserved-name config entry.

Skipping rather than erroring on load is deliberate: an already-shadowed
config.json would otherwise fail every auth command, including the `auth remove`
needed to clean it up. `auth status` now warns with that recovery step, and
`auth remove` deletes a reserved-name *file entry* while still refusing to remove
the account itself.

`default` is reserved alongside `aspect`: a deployment's credential is filed
under its own name, and `DEFAULT_PROFILE` is `"default"`, so such a deployment
would share the account's credential slot. It is reserved from being
*configured* only — `apply_set_default` still treats `DEFAULT_DEPLOYMENT_NAME`
alone as the "clear the default" sentinel, so `auth use default` fails the
unknown-deployment check instead of silently clearing every configured default.

The `auth status` warning names the config file that declares the ignored entry
and adapts its advice: `auth remove` only edits the user's config, so a
checked-in repo config is told to edit the file directly.

Sharing an issuer across deployments is unaffected — nothing keys on the issuer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gregmagolan
gregmagolan force-pushed the fix/reserved-deployment-name-collision branch from 0528632 to 226a58a Compare August 6, 2026 02:59
Deployment-name derivation used the public-suffix list to strip a host's
registrable domain, so a self-hosted endpoint lost its domain:
`remote.foo.bar.com` became `foo`, and `remote.aspect.foo.com` became the bare
`aspect` — the built-in account's reserved name.

Derivation now drops the leading service label and then only a trailing
`.aspect.build`, keeping everything else verbatim. Aspect-hosted endpoints still
yield a bare deployment name (`bes.gcp.awd-gha-test-dev.aspect.build` →
`gcp.awd-gha-test-dev`), while self-hosted ones keep their full domain
(`remote.foo.bar.com` → `foo.bar.com`), which reads unambiguously and can never
collide with a reserved name. This drops the `psl` dependency.

An Aspect-hosted host can still derive a reserved name
(`remote.aspect.aspect.build` → `aspect`), so `upsert_deployment` remains the
guarantee rather than a backstop; the docstrings now say so.

Also consolidates the config overlay: `load_deployments` and the `auth status`
shadowed-entry report walked the two config files separately, reading and parsing
both twice per `auth status` and risking drift. Both now come from one
`load_deployments_and_shadowed` over a `ConfigSource` list, with the pure overlay
split out so the overlay and shadowing rules are directly testable.

Test coverage: derivation is now table-driven, covering Aspect-hosted,
self-hosted, multi-label suffixes, IP literals, single labels, trailing dots, and
dot-anchoring (`remote.acme.notaspect.build` must keep its domain — an unanchored
suffix would corrupt it to `acme.not`). Adds overlay precedence coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gregmagolan
gregmagolan force-pushed the fix/reserved-deployment-name-collision branch from 226a58a to e1d42db Compare August 7, 2026 00:26
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