Skip to content

fix(host-core): make clawctl create truly idempotent#39

Merged
TimBeyer merged 4 commits into
mainfrom
fix/clawctl-create-truly-idempotent
May 13, 2026
Merged

fix(host-core): make clawctl create truly idempotent#39
TimBeyer merged 4 commits into
mainfrom
fix/clawctl-create-truly-idempotent

Conversation

@TimBeyer
Copy link
Copy Markdown
Owner

@TimBeyer TimBeyer commented May 13, 2026

Summary

  • Gate first-run-only steps in bootstrapOpenclaw on the existing data/config sentinel, so re-running clawctl create --config <path> against an existing instance no longer re-runs openclaw onboard, rotates the gateway auth token, or re-sends the bootstrap prompt.
  • Generalise patchAuthProfiles (now a thin IO wrapper around the new pure applyAuthProfileSwap) to handle a provider change between runs: the prior :default profile is evicted, the new one bound, lastGood reset, and orphaned usageStats dropped.

Together these make clawctl create idempotent in the strong sense the project conventions promise: first run bootstraps, subsequent runs apply the diff in clawctl.json. No new commands; no parallel reconfigure path.

Motivation

The intended workflow when changing an instance's config is to edit clawctl.json and re-run clawctl create. In practice the path was almost right — capabilities are idempotent and provisionVM skips Lima VM creation if the VM exists — but bootstrapOpenclaw conflated first-run-only steps with apply-state steps:

  1. openclaw onboard ran unconditionally each invocation.
  2. The gateway auth token was regenerated via randomBytes(24) every run and pushed through openclaw config set gateway.auth.token, rotating a token that may be wired into remote tooling.
  3. The bootstrap prompt re-sent on every reapply.
  4. patchAuthProfiles looked up <currentProvider>:default and patched it in place — when the provider type had changed it logged "Profile not found — skipping" and the prior provider's profile stayed bound.

Test plan

  • Unit tests for applyAuthProfileSwap: first-run plaintext→tokenRef migration, same-provider no-op re-apply, provider switch (removes old, adds new, resets lastGood, filters usageStats), conservative handling of unknown/non-:default profiles, no input mutation.
  • bun test (266 pass, 0 fail), bun run lint, bun run format:check clean.
  • End-to-end smoke on an existing instance: re-run clawctl create against a clawctl.json with a different provider, assert gateway.auth.token byte-for-byte preserved against a data/config.bak.* snapshot, prior auth profile cleanly evicted, openclaw doctor green.

🤖 Generated with Claude Code

TimBeyer and others added 3 commits May 13, 2026 23:07
`clawctl create --config <path>` against an existing instance was not
truly idempotent: `bootstrapOpenclaw` re-ran `openclaw onboard`,
rotated the gateway auth token on every invocation, and re-sent the
bootstrap prompt. `patchAuthProfiles` also no-op'd when the configured
provider differed from what was already on disk, leaving the prior
provider's profile bound.

Plan two narrow fixes in the existing path — gate first-run-only steps
on the `data/config` sentinel and generalise `patchAuthProfiles` to
converge on the configured provider — rather than adding a parallel
reconfigure command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the pure transformation as `applyAuthProfileSwap`. On first run
it still migrates plaintext `token` → `tokenRef`. When the configured
provider differs from what's already on disk it now evicts the prior
provider's `:default` profile, adds the new one, resets `lastGood`, and
drops orphaned `usageStats` entries.

Previously the function looked up `<currentProviderType>:default` and
patched it in place — when the provider type had changed relative to
the existing file it would log "Profile not found — skipping" and
leave the dead profile active.

Conservative: only evicts `:default` profiles whose `provider` field is
set and differs from the new provider. Unknown/legacy profile shapes
are preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`clawctl create` re-runs `bootstrapOpenclaw` on an existing instance
(provisionVM detects the VM exists and skips Lima creation but still
calls bootstrap). That meant:

- `openclaw onboard` ran every time — onboard issues the gateway auth
  token and configures the daemon, so re-running it would rotate
  credentials that may be wired into remote tooling.
- The gateway token was regenerated each run via `randomBytes(24)` and
  pushed through `openclaw config set gateway.auth.token`, rotating it
  on every reapply.
- The bootstrap prompt re-sent on every reapply.

Use `${PROJECT_MOUNT_POINT}/data/config` (the file onboard creates) as
the "already onboarded" sentinel. On reapply, skip onboard, read the
existing `gateway.auth.token` from data/config, and skip the bootstrap
prompt. Continue with all the apply-state steps (`openclaw config set`,
`models set`, channels, secret migration, daemon restart, doctor,
bootstrap-phase capability hooks) so a clawctl.json edit converges
state. Together with the patchAuthProfiles provider-switch fix this
makes `clawctl create` idempotent in the strong sense.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@TimBeyer TimBeyer force-pushed the fix/clawctl-create-truly-idempotent branch from dfe1729 to 6ed53cf Compare May 13, 2026 21:08
Direct editing of auth-profiles.json is the standing exception to
bootstrap.ts's "delegate to openclaw" pattern. Document the rationale
on the function itself, with links to the upstream issues that, once
resolved, would let us replace the surgery with two delegate calls
(openclaw/openclaw#16134, openclaw/openclaw#10244).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@TimBeyer TimBeyer merged commit a7902ca into main May 13, 2026
4 checks passed
@TimBeyer TimBeyer deleted the fix/clawctl-create-truly-idempotent branch May 13, 2026 21:42
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