OpenClaw EnterpriseDOCSGitHub

Upgrade a split-layout Installation

Single-cluster Installations created by the 2026-09-28 release keep each Namespace's Gateways, Gateway state and canonical Secrets in a separate oce-gateways-<hash> namespace. The current controller refuses to start on that layout; split-layout installations says how to tell. This page moves each tenant to the shared layout before the control-plane upgrade. Adopt in place where you can; use the export fallback for tenants that can't be adopted.

Adopt in place

Each oce-gateways-<hash> namespace becomes its tenant's namespace. OCC's database is not changed, so IDs, Namespace names, versions, Secrets, service-account credentials, chat history and Harness workspaces all stay. The old namespace's workspace and embedded Gateway claims move into it by PersistentVolume rebind, and its Agent Secrets are copied.

In a three-tenant test, the moves took about 20 seconds and the old API started again in about 20 more; each helper release then stopped the API for about 20 seconds. apply also waits for the old Pods to stop, up to 5.5 minutes for a Gateway that is not ready, so stop such Agents first. Dedicated Gateways keep serving until they are deployed again; Harnesses and embedded Gateways are down from apply until then. Run the commands from a checkout of the target release, with cluster-admin kubectl for the cluster (--context/--kubeconfig are passed through).

  1. Plan. This only reads:

    bash
    node scripts/split-layout-adopt.mjs plan --out /secure/occ/adopt-plan.json

    It lists, per tenant, the claims, Secrets and routes to move and the Agents that run. It exits 2 and names the problem when a tenant can't be adopted: an existing namespace selected with existingNamespace, an object OCE did not create in the old namespace, a name taken in the new one, or a missing tenant RoleBinding in the oce-gateways-* namespace. Grant the bindings there, move or delete foreign objects (--drop-resource <resource>, as the refusal names it, deletes a kind you checked is disposable), or use the fallback below for that tenant (--namespace-id selects tenants). Codex OAuth sign-ins are bound to the old claim, so plan refuses those Agents unless you pass --accept-oauth-reconnect. Their deploy in step 5 then fails until someone connects again, and the new sign-in clears the Agent's Codex home, including its earlier sessions; the export fallback loses those too.

  2. Back up the volumes the plan lists with your storage's snapshot or backup tool. The script does not back them up.

  3. Adopt. This stops OCC's API and worker and the old namespace's workloads, moves everything, then starts the old API again for the upgrade helper. The worker stays stopped:

    bash
    node scripts/split-layout-adopt.mjs apply --archive /secure/occ/adopt --yes

    It records each step on the oce-gateways-* namespace, so running it again after any failure continues. Until step 4 starts the new release, revert --archive /secure/occ/adopt --yes restores the old layout and restarts OCC. With --namespace-id, revert restores only those tenants and restarts the old API; the worker stays stopped until the rest are reverted or the new release starts. Keep the archive directory: it holds the old routes. Adopt every tenant before the upgrade: once the controller runs other images, apply refuses.

    apply returns once the old API serves. Use it only for the upgrade helper: requests that touch an adopted tenant's Secrets, Configurations or Gateways fail with an ownership error until the new release starts, and Agent deploys wait for the new worker.

  4. Upgrade the control plane and the runtime as usual.

  5. Deploy each Agent apply listed as running (occ agent deploy <id>) that is not running yet, and check it. A Gateway from the 2026-09-28 release migrates its agent database at its first start, about 15 seconds longer.

  6. Delete the old namespaces. After this there is no way back:

    bash
    node scripts/split-layout-adopt.mjs finalize --yes

    It refuses while the controller still runs the images apply recorded, and while the old namespace holds anything that would be lost with it.

Export and re-create

Use this for tenants that can't be adopted.

The tenants are exported, deleted and re-created under new IDs. A deleted Namespace's name stays reserved, so each Namespace comes back under a new name. These come back: Secrets (you supply the values again), Configurations that an Agent uses, Presets, credential sources, Roles, access bindings, service accounts (without issued credentials) and Agents. Agents that were running are deployed again, and a copy of each running Agent's Harness workspace can be put back. These do not come back:

Accounts, Installation service keys and the audit history stay in the database.

Tested on a 2026-09-28 release Installation with three Namespaces and three Agents: the commands took about two minutes, plus the two image releases (about 35 seconds each). Run them from a checkout of the target release, with occ set up for the old release as an administrator (OCC_URL, OCC_SERVICE_KEY_FILE, OCC_CA_BUNDLE) and kubectl for the cluster. The script talks to OCC only. Keep every file below private: they hold Configurations, workspace files, transcripts and Secret values.

  1. Export every Namespace from the old release:

    bash
    node scripts/split-layout-tenants.ts export --out /secure/occ/tenants.json

    If a successful API response is malformed, lacks required restoration fields, or reports unreadable saved Agent settings, export exits nonzero without writing a bundle. Restore the API response path and rerun export, then check its Namespace inventory before continuing.

    It reads AGENTS.md, SOUL.md, IDENTITY.md and USER.md only where workspace routing is configured and the Agent runs. Step 2 copies the whole workspace instead.

  2. Copy each running Agent's Harness workspace and Gateway state:

    bash
    ARCHIVE=/secure/occ/tenant-archivemkdir -m 700 -p "$ARCHIVE"kubectl get pods -A -l openclaw.dev/agent --field-selector status.phase=Running -o jsonpath='{range .items[*]}{.metadata.namespace} {.metadata.name} {.metadata.labels.openclaw\.dev/workload-role} {.metadata.labels.openclaw\.dev/agent}{"\n"}{end}' |while read -r ns pod role agent; do  case $role in    gateway) paths='.openclaw/state .openclaw/agents/main/agent .openclaw/media .openclaw/agents/main/sessions' ;;    agent) paths='workspace .codex/generated_images' ;;    *) continue ;;  esac  kubectl -n "$ns" exec "$pod" -c "$role" -- tar -C /home/node --ignore-failed-read \    --exclude=codex-home --exclude='.oce-workspace-setup.*' -cf - $paths \    >"$ARCHIVE/$agent-$role.tar" </dev/null || echo "failed: $agent $role"done
  3. Write the Secret values to a 0600 file such as /secure/occ/tenant-secrets.json, keyed by the old Namespace name, then Secret name: {"team-a": {"model-key": "..."}}. List the names with jq '.namespaces[] | {name, secrets: [.secrets[].name]}' /secure/occ/tenants.json.

  4. Delete the tenants on the old release. The command refuses ambiguous source IDs that the import map cannot represent, preserving those resources for manual restoration. It also refuses if a Namespace gained an Agent or Secret after the export, or if the export could not read an Agent's workspace files; once step 2 has the copy, add --allow-unread-workspace-files:

    bash
    node scripts/split-layout-tenants.ts discard --bundle /secure/occ/tenants.json \  --yes --allow-unread-workspace-fileskubectl get namespaces -l openclaw.dev/gateway-namespace

    Continue when no oce-gateways-* namespace is left. A Configuration that no Agent uses is not exported and blocks its Namespace's delete; the error says how to find and delete it. Then run discard again.

  5. Upgrade the control plane and the runtime as usual. The preflight now passes.

  6. Re-create the tenants under new Namespace names. --skip leaves out a Namespace you no longer need, such as an unused default:

    bash
    node scripts/split-layout-tenants.ts import --bundle /secure/occ/tenants.json \  --secret-values /secure/occ/tenant-secrets.json --map /secure/occ/tenant-ids.json \  --rename team-a=team-a-2 --skip default

    Give each selected source Namespace a distinct target name. Import refuses overlapping --rename targets or two selected sources that resolve to one Namespace through saved IDs or existing names, before it changes the target or saves the map. A skipped source without a saved destination reserves nothing; a saved destination stays reserved even while its source is skipped. Choose separate targets or skip a source; review an old conflicting map and any already imported resources before retrying. Malformed saved IDs and duplicate source Namespace IDs, including skipped sources, are refused before any API request. Import also refuses source IDs shared by different resource kinds or Namespaces, including skipped sources: its flat map cannot keep their destinations separate. Preserve the export and restore those colliding resources manually; do not alter IDs in the bundle.

    It creates the Namespaces and exits 3 while they are provisioning. Grant tenant RoleBindings for each one, then run the same command again. The ID map records every old and new ID. If a write's response has an unexpected status, resource identity, or success/error envelope, reconcile that write before resuming; it may already have committed. Redirects are refused: correct OCC_URL instead of replaying a write through a redirect. Failed or deleting Namespaces require lifecycle recovery, not another RoleBinding wait. Exit 4 lists what needs a hand: Agents whose deploy failed, such as a service-account Agent without an issued credential, and access bindings it could not re-create. The ID map does not record those bindings, so each rerun lists them again and exits 4, also after you re-create them by hand. Exit 0 does not restore bindings to old Agent revisions; the import only logs them.

    Presets are restored after service-account IDs are mapped, so their Harness authentication refers to the restored account. Existing mapped Presets are left unchanged; inspect their account references before resuming an older import. See the tenant restore flow.

  7. Put each Harness workspace back once the new Agent's Harness Pod runs:

    bash
    for tarball in "$ARCHIVE"/*-agent.tar; do  old=$(basename "$tarball" -agent.tar)  new=$(jq -r --arg old "$old" '.ids[$old] // empty' /secure/occ/tenant-ids.json)  [ -n "$new" ] || { echo "skipped: $old was not imported"; continue; }  read -r ns pod < <(kubectl get pods -A --field-selector status.phase=Running \    -l "openclaw.dev/agent=$new,openclaw.dev/workload-role=agent" \    -o jsonpath='{.items[0].metadata.namespace} {.items[0].metadata.name}')  [ -n "${pod:-}" ] || { echo "skipped: $new has no running Harness"; continue; }  kubectl -n "$ns" exec -i "$pod" -c agent -- tar -C /home/node --no-overwrite-dir -xf - <"$tarball" \    && echo "restored $old -> $new" || echo "failed: $old -> $new"done
  8. Check the Agents (occ agent list) and re-issue any service-account credentials.

Search documentation