#agent with host level access

1 messages · Page 1 of 1 (latest)

umbral ocean
#

We're on OpenClaw 2026.4.20, Windows 11 host, OpenClaw deployed as a Docker Compose stack (build: .), gateway running in a Linux container.
We're building a three-tier agent architecture:

Tier 1 — main user-facing agent (inside container)
Tier 2 (orchestrator) — coordinates operational commands (inside container)
Tier 3 (HostOps) — intended host-side agent with Docker access, handles container lifecycle (restart, rebuild, upgrade)

Our upgrade skill scripts need to run docker compose up --build, docker exec, git fetch/checkout — all requiring host-level access. A few questions before we finalize the design:

  1. Can an agent run outside the gateway container? Is there a supported pattern for a host-side agent node in 2026.4.20, or do all agents always execute within the gateway container's exec environment?
  2. Docker access from inside the container — if HostOps must run inside, is bind-mounting /var/run/docker.sock the expected pattern, or is there a native OpenClaw mechanism?
  3. Subagent exec context — when an orchestrator spawns HostOps via sessions_spawn and it runs a script via exec, is execution always in the gateway container, or can it target a different exec context?
  4. Is there a first-class "host node" concept in 2026.4.20 that an agent can target for exec — separate from the gateway container?
    Our fallback is bind-mounting the Docker socket, but want to know if there's a cleaner native pattern first.
modest pikeBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

hardy zephyrBOT
finite zincBOT
#

Let me check the current docs/source so I don’t guess on the node/exec behavior.

#

I found the relevant node/exec docs. I’m just checking the current source for one nuance: whether subagents can point exec at a node independently, or whether that’s only a global/session default.

#

Short answer: yes, there is a cleaner native pattern than mounting docker.sock into the gateway container, and it is node host, not a host-side full agent.

  1. Can an agent run outside the gateway container?
    Not as a first-class OpenClaw agent runtime.
    In 2026.4.20, the agent/session/model loop still lives on the gateway.
    The supported host-side execution primitive is a paired node host. Nodes are “peripherals, not gateways”. So your HostOps design should be:

    • Tier 1, Tier 2: gateway-side agents
    • Tier 3: Windows headless node host
    • Tier 2 calls exec with host=node
  2. Docker access from inside the container, native mechanism or docker.sock?
    There is no separate Docker-native OpenClaw tool for lifecycle ops like docker compose up --build, docker exec, git fetch, etc.
    If you insist on doing that from inside the gateway container, then mounting the Docker daemon access is the normal infra pattern. OpenClaw’s Docker docs already support daemon/socket mounting for sandbox-related cases.

    But for your setup, I would not make the Linux gateway container your HostOps runner if the real target is the Windows host. A Windows node host is cleaner.

#
  1. When a subagent uses exec, where does it run?
    sessions_spawn does not move the child agent onto another machine by itself.
    The exec location is decided by the child’s exec call / session defaults:

    • host=auto → gateway container, unless sandbox is active
    • host=node → paired node host
    • host=gateway → gateway host/container

    So yes, a HostOps child can target a different exec context, but only because its exec calls do, not because sessions_spawn itself relocates the agent.

  2. Is there a first-class host node concept?
    Yes. This is the supported pattern:

    • openclaw node run ... or openclaw node install ...
    • pair it with openclaw devices approve ...
    • then use exec host=node
    • bind a default node with tools.exec.node if needed
#

What I’d recommend for your architecture

  • Keep Tier 1 and Tier 2 in the container.
  • Run Tier 3 HostOps as a Windows headless node host on the Docker host.
  • Let orchestrator/HostOps sessions use exec host=node.
  • Put approvals/allowlist on the node host for docker, git, pwsh, etc.
  • Prefer PowerShell 7 / direct binaries on Windows instead of cmd.exe /c ... in allowlist mode.

That gives you a real first-class separation:

  • gateway handles messaging/orchestration
  • Windows host node handles host-level ops

Docs:

umbral ocean
#

yes.. that would be helpful

finite zincBOT
#

Yep. I’d wire it like this.

Recommended shape

  • Tier 1: user-facing agent, inside gateway
  • Tier 2: orchestrator, inside gateway
  • Tier 3: Windows headless node host, not a separate off-gateway agent runtime

So the agent still lives on the gateway, but host-level exec goes to the Windows node via host=node.

Why this is cleaner than docker.sock

For your setup, docker.sock inside the Linux gateway container is the wrong abstraction if the real work is:

  • docker compose up --build
  • git fetch/checkout
  • Windows-host-local paths/scripts
#

A Windows node host runs those commands on the actual Windows machine, with local Docker Desktop / Git / PowerShell, and with OpenClaw approvals around it.

Setup sketch

1. Start a Windows node host

On the Windows host:

$env:OPENCLAW_GATEWAY_TOKEN="<your-gateway-token>"
openclaw node install --host 127.0.0.1 --port 18789 --display-name "HostOps-Win"
openclaw node restart
openclaw node status

That pattern is documented here:
https://docs.openclaw.ai/cli/node

#

https://docs.openclaw.ai/nodes

Because Docker install defaults to host-reachable bind, 127.0.0.1:18789 is usually the right target for a local Windows host talking to the containerized gateway:
https://docs.openclaw.ai/install/docker

2. Pair the node

On the gateway side:

openclaw devices list
openclaw devices approve <requestId>
openclaw nodes status
openclaw nodes describe --node HostOps-Win

If you're using the bundled compose stack, same thing via:

#
docker compose run --rm openclaw-cli devices list
docker compose run --rm openclaw-cli devices approve <requestId>

3. Set node exec approvals

On the Windows host, first find the real binary paths:

where.exe docker
where.exe git
where.exe pwsh

Then allowlist those exact resolved paths for the node:

#
openclaw approvals allowlist add --node HostOps-Win "<resolved-path-to-docker.exe>"
openclaw approvals allowlist add --node HostOps-Win "<resolved-path-to-git.exe>"
openclaw approvals allowlist add --node HostOps-Win "<resolved-path-to-pwsh.exe>"
openclaw approvals get --node HostOps-Win

Important nuance from the docs:

  • allowlist matches resolved binary paths
  • on Windows, prefer pwsh -File script.ps1 or direct binaries
  • avoid cmd.exe /c ... in allowlist mode, it stays approval-gated

Docs:
https://docs.openclaw.ai/tools/exec
https://docs.openclaw.ai/tools/exec-approvals
https://docs.openclaw.ai/nodes/troubleshooting

#

4. Bind HostOps exec to that node

For a HostOps session, set:

/exec host=node security=allowlist ask=on-miss node=HostOps-Win

Or do it in each exec call explicitly.

Important nuance: if Tier 2 spawns Tier 3 with sessions_spawn, the child session does not magically move to Windows. The child still runs on the gateway, but its exec can target the node if:

  • the child session sets /exec host=node ..., or
  • the child explicitly calls exec with host=node, or
  • you bind that HostOps agent’s tools.exec.* defaults to the node

5. Example HostOps exec call

#
{
  "command": "pwsh -File C:\\HostOps\\upgrade-openclaw.ps1",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}

That PowerShell script can then safely do your:

  • git fetch
  • git checkout
  • docker compose up --build
  • docker exec ...
#

My recommendation

For your exact architecture, I would not use docker.sock as the primary design.

I would use:

  • containerized gateway for agents/orchestration
  • Windows node host for HostOps
  • node-scoped exec approvals for Docker/Git/PowerShell

Use docker.sock only if you specifically want the Linux gateway container itself to manage sibling Docker containers. That is infra-valid, but it is not the cleanest HostOps pattern here.

If you want, I can draft a minimal upgrade-openclaw.ps1 plus the exact HostOps prompt contract for Tier 2 -> Tier 3.

umbral ocean
#

ok.. that would be wonderful

finite zincBOT
#

Nice, here’s a practical pattern you can lift.

1. Minimal HostOps contract

I’d make Tier 3 follow rules like these:

  • All host operations must use exec with host=node
  • Always target the Windows node, for example node=HostOps-Win
  • Prefer pwsh -File ... scripts over inline shell
  • Never use cmd.exe /c ...
  • Default to security=allowlist, ask=on-miss
  • Refuse destructive flags unless explicitly requested, for example git reset --hard, docker compose down -v, docker system prune

Expected output from HostOps:

  • repo path
  • previous ref and new ref
#
  • fetched commit
  • compose services rebuilt/restarted
  • health check result
  • any approval needed / failure point

2. Example Tier 2 -> Tier 3 handoff

Something like this works well as the spawned task:

You are HostOps for a Windows node host named HostOps-Win.

Rules:
- Every shell action must use exec with host=node, node=HostOps-Win, security=allowlist, ask=on-miss.
- Use PowerShell scripts via pwsh -File, not cmd.exe /c.
- Operate only inside the provided repo path.
#
- Do not run destructive cleanup unless the request explicitly asks for it.
- Return a short structured report with git ref, docker result, and health status.

Task:
Upgrade OpenClaw from repo <REPO_PATH>.
Target ref: <REF_OR_BRANCH>.
Compose project path: <COMPOSE_DIR>.
After upgrade, verify the gateway is healthy.

3. Minimal PowerShell upgrade script

On the Windows host, save something like:

#
param(
  [Parameter(Mandatory = $true)][string]$RepoPath,
  [Parameter(Mandatory = $true)][string]$ComposePath,
  [string]$Ref = "main",
  [string]$ServiceName = "openclaw-gateway"
)

$ErrorActionPreference = "Stop"

function Step($msg) {
  Write-Host ""
  Write-Host "==> $msg"
}

Step "Validate paths"
#
if (!(Test-Path $RepoPath)) { throw "RepoPath not found: $RepoPath" }
if (!(Test-Path $ComposePath)) { throw "ComposePath not found: $ComposePath" }

Push-Location $RepoPath
try {
  Step "Capture current git state"
  $before = git rev-parse --short HEAD
  $branch = git rev-parse --abbrev-ref HEAD

  Step "Fetch target ref"
  git fetch --all --prune

  Step "Checkout target ref"
  git checkout $Ref
#
  Step "Pull latest"
  git pull --ff-only

  $after = git rev-parse --short HEAD
}
finally {
  Pop-Location
}

Push-Location $ComposePath
try {
  Step "Rebuild and restart gateway"
  docker compose up -d --build $ServiceName

  Step "Container status"
#
  docker compose ps

  Step "Recent gateway logs"
  docker compose logs --tail=80 $ServiceName
}
finally {
  Pop-Location
}

Write-Host ""
Write-Host "Upgrade complete"
Write-Host "Previous: $before"
Write-Host "Current:  $after"
Write-Host "Branch:   $branch"
#

4. Example exec call from HostOps

{
  "command": "pwsh -File C:\\HostOps\\upgrade-openclaw.ps1 -RepoPath C:\\OpenClaw -ComposePath C:\\OpenClaw -Ref main -ServiceName openclaw-gateway",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}

5. Allowlist I’d start with

On the node host, allowlist exact resolved paths for:

  • pwsh.exe
#
  • Tier 3 as a Windows node host
  • HostOps executes one approved PowerShell script per operation

That keeps the model/orchestration centralized, but the risky machine control stays on the real host with node approvals.

If you want, I can turn this into a slightly more production-ready version next, with:

  1. rollback support
  2. health check / smoke test
  3. structured JSON output for Tier 2 to parse
umbral ocean
#

sure.. would love that

finite zincBOT
#

Awesome. Here’s the version I’d actually ship.

One important correction to my earlier suggestion: I would not broadly allowlist pwsh.exe for HostOps in production. OpenClaw allowlist matching is based on the resolved binary path, so allowlisting pwsh turns it into a general shell. I’m a little wary of that.

My recommended split

Option A, safer

  • HostOps does multiple direct exec calls
  • Allowlist exact paths for:
    • git.exe
    • docker.exe
  • Use host=node, node=HostOps-Win, security=allowlist, ask=on-miss

Option B, more convenient

  • HostOps runs one reviewed PowerShell script
  • Still use host=node
  • But keep that call approval-gated, ideally ask=always
#

If you want rollback + one-shot JSON output, Option B is nicer.


Production-ready upgrade-openclaw.ps1

param(
  [Parameter(Mandatory = $true)][string]$RepoPath,
  [Parameter(Mandatory = $true)][string]$ComposePath,
  [string]$Ref = "main",
  [string]$ServiceName = "openclaw-gateway",
  [string]$GatewayBaseUrl = "http://127.0.0.1:18789",
  [int]$HealthTimeoutSec = 120,
  [switch]$RollbackOnFailure,
  [switch]$AllowDirty
#
)

$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"

$result = [ordered]@{
  success = $false
  repoPath = $RepoPath
  composePath = $ComposePath
  requestedRef = $Ref
  serviceName = $ServiceName
  beforeSha = $null
  beforeBranch = $null
  afterSha = $null
  afterBranch = $null
#
  rollbackPerformed = $false
  healthz = $false
  readyz = $false
  error = $null
  logsTail = $null
}

function Invoke-Step {
  param(
    [Parameter(Mandatory = $true)][string]$File,
    [Parameter(Mandatory = $true)][string[]]$Args
  )

  Write-Host "==> $File $($Args -join ' ')"
  $output = & $File @Args 2>&1
#
  $code = $LASTEXITCODE
  $text = ($output -join "`n").Trim()

  if ($code -ne 0) {
    throw "Command failed ($code): $File $($Args -join ' ')`n$text"
  }

  return $text
}

function Test-Http200 {
  param([Parameter(Mandatory = $true)][string]$Url)

  try {
    $resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5
#
    return ($resp.StatusCode -eq 200)
  } catch {
    return $false
  }
}

function Wait-GatewayHealthy {
  param(
    [Parameter(Mandatory = $true)][string]$BaseUrl,
    [Parameter(Mandatory = $true)][int]$TimeoutSec
  )

  $deadline = (Get-Date).AddSeconds($TimeoutSec)
  $hz = $false
  $rz = $false
#

  do {
    $hz = Test-Http200 "$BaseUrl/healthz"
    $rz = Test-Http200 "$BaseUrl/readyz"
    if ($hz -and $rz) {
      return @{ healthz = $hz; readyz = $rz }
    }
    Start-Sleep -Seconds 3
  } while ((Get-Date) -lt $deadline)

  return @{ healthz = $hz; readyz = $rz }
}

$beforeSha = $null
$beforeBranch = $null
#

try {
  if (!(Test-Path $RepoPath)) { throw "RepoPath not found: $RepoPath" }
  if (!(Test-Path $ComposePath)) { throw "ComposePath not found: $ComposePath" }

  Push-Location $RepoPath
  try {
    $status = Invoke-Step git @("status", "--porcelain")
    if (-not $AllowDirty -and $status) {
      throw "Working tree is dirty. Refusing upgrade."
    }

    $beforeSha = Invoke-Step git @("rev-parse", "HEAD")
    $beforeBranch = (& git symbolic-ref --short -q HEAD 2>$null).Trim()
#
    $result.beforeSha = $beforeSha
    $result.beforeBranch = if ($beforeBranch) { $beforeBranch } else { $null }

    Invoke-Step git @("fetch", "--all", "--prune") | Out-Null
    Invoke-Step git @("checkout", $Ref) | Out-Null

    $currentBranch = (& git symbolic-ref --short -q HEAD 2>$null).Trim()
    if ($currentBranch) {
      Invoke-Step git @("pull", "--ff-only") | Out-Null
    }

    $result.afterSha = Invoke-Step git @("rev-parse", "HEAD")
    $result.afterBranch = if ($currentBranch) { $currentBranch } else { $null }
  }
  finally {
#
    Pop-Location
  }

  Push-Location $ComposePath
  try {
    Invoke-Step docker @("compose", "up", "-d", "--build", $ServiceName) | Out-Null

    $health = Wait-GatewayHealthy -BaseUrl $GatewayBaseUrl -TimeoutSec $HealthTimeoutSec
    $result.healthz = $health.healthz
    $result.readyz = $health.readyz

    if (-not ($health.healthz -and $health.readyz)) {
      throw "Gateway did not become healthy within timeout."
    }
  }
#
  finally {
    Pop-Location
  }

  $result.success = $true
}
catch {
  $result.error = $_.Exception.Message

  if ($RollbackOnFailure -and $beforeSha) {
    try {
      Push-Location $RepoPath
      try {
        if ($beforeBranch) {
          Invoke-Step git @("checkout", $beforeBranch) | Out-Null
#
          Invoke-Step git @("reset", "--hard", $beforeSha) | Out-Null
        } else {
          Invoke-Step git @("checkout", $beforeSha) | Out-Null
        }
      }
      finally {
        Pop-Location
      }

      Push-Location $ComposePath
      try {
        Invoke-Step docker @("compose", "up", "-d", "--build", $ServiceName) | Out-Null
      }
      finally {
        Pop-Location
#
      }

      $result.rollbackPerformed = $true
    }
    catch {
      $result.rollbackError = $_.Exception.Message
    }
  }
}
finally {
  try {
    Push-Location $ComposePath
    try {
      $result.logsTail = Invoke-Step docker @("compose", "logs", "--tail=60", $ServiceName)
    }
#
    finally {
      Pop-Location
    }
  }
  catch {}

  Write-Output ("OPENCLAW_RESULT_JSON:" + ($result | ConvertTo-Json -Compress -Depth 8))

  if (-not $result.success) {
    exit 1
  }
}

#

Why I like this shape

  • refuses dirty repos by default
  • captures before and after
  • rebuilds only the target service
  • waits for /healthz and /readyz
  • can rollback on failure
  • emits a single machine-readable final line

Tier 2 can just parse the line starting with:

OPENCLAW_RESULT_JSON:

#

Example HostOps exec call

If you use the script-wrapper pattern, I’d start with this:

{
  "command": "pwsh -File C:\\HostOps\\upgrade-openclaw.ps1 -RepoPath C:\\OpenClaw -ComposePath C:\\OpenClaw -Ref main -RollbackOnFailure",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "always"
}

I’d use ask=always at first because pwsh is powerful. Once the flow is boring and proven, then decide whether to relax it.


#

Better Tier 2 contract

Tell Tier 3 to return only:

  • requested ref
  • before sha
  • after sha
  • rollback performed
  • healthz / readyz
  • logs tail on failure

That keeps the orchestration layer simple.

Example parsed result:

#
{
  "success": true,
  "requestedRef": "main",
  "beforeSha": "abc123",
  "afterSha": "def456",
  "rollbackPerformed": false,
  "healthz": true,
  "readyz": true
}

Windows-specific sharp edges

A few gotchas I’d watch for:

#
  • the node host service user must be able to run docker.exe and git.exe
  • node hosts ignore PATH overrides for exec, so install tools in standard locations or fix the service environment
  • prefer docker compose ..., not legacy docker-compose
  • use docker compose exec -T for any non-interactive container exec in automation
  • avoid cmd.exe /c ..., it stays awkward in allowlist mode on Windows nodes

Docs

If this gets you over the line, please right-click the solution message → Apps → ✅ Mark Solution so Answer Overflow can index it. 🎉

#

If you want, I can do one more pass and give you the direct-exec version too, which is stricter than the PowerShell wrapper.

umbral ocean
#

sure

finite zincBOT
#

Yep, here’s the direct-exec pattern. This is the stricter one, and honestly the one I’d prefer first.

Why this is safer

Instead of allowlisting a general shell like pwsh.exe, Tier 3 only gets exact binaries:

  • git.exe
  • docker.exe
  • optionally curl.exe for /healthz + /readyz

That keeps HostOps much narrower.

Recommended Tier 3 flow

Have Tier 2 orchestrate these as separate exec calls with:

  • host=node
#
  • node=HostOps-Win
  • security=allowlist
  • ask=on-miss
  • workdir set explicitly, instead of cd && ...

Allowlist

On the node, allowlist the exact resolved paths:

where.exe git
where.exe docker
where.exe curl
#

Then add those exact paths with:

openclaw approvals allowlist add --node HostOps-Win "<git.exe path>"
openclaw approvals allowlist add --node HostOps-Win "<docker.exe path>"
openclaw approvals allowlist add --node HostOps-Win "<curl.exe path>"
openclaw approvals get --node HostOps-Win

Docs: https://docs.openclaw.ai/tools/exec-approvals


Direct-exec upgrade sequence

Assume:

#
  • repo path: C:\OpenClaw
  • compose path: C:\OpenClaw
  • service: openclaw-gateway

1. Refuse dirty repo unless explicitly allowed

{
  "command": "git status --porcelain",
  "workdir": "C:\\OpenClaw",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}
#

If output is non-empty, abort.

2. Capture current SHA

{
  "command": "git rev-parse HEAD",
  "workdir": "C:\\OpenClaw",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}

3. Fetch

#
{
  "command": "git fetch --all --prune",
  "workdir": "C:\\OpenClaw",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}

4. Checkout target ref

{
  "command": "git checkout main",
#
  "workdir": "C:\\OpenClaw",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}

5. Fast-forward pull

{
  "command": "git pull --ff-only",
  "workdir": "C:\\OpenClaw",
  "host": "node",
#
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}

6. Capture new SHA

{
  "command": "git rev-parse HEAD",
  "workdir": "C:\\OpenClaw",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
#
  "ask": "on-miss"
}

7. Rebuild and restart

{
  "command": "docker compose up -d --build openclaw-gateway",
  "workdir": "C:\\OpenClaw",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}
#

8. Inspect service state

{
  "command": "docker compose ps",
  "workdir": "C:\\OpenClaw",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}

9. Health checks

#
{
  "command": "curl.exe -fsS http://127.0.0.1:18789/healthz",
  "workdir": "C:\\OpenClaw",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}
{
  "command": "curl.exe -fsS http://127.0.0.1:18789/readyz",
  "workdir": "C:\\OpenClaw",
  "host": "node",
#
  "node": "HostOps-Win",
  "security": "allowlist",
  "ask": "on-miss"
}

10. Logs on failure

{
  "command": "docker compose logs --tail=80 openclaw-gateway",
  "workdir": "C:\\OpenClaw",
  "host": "node",
  "node": "HostOps-Win",
  "security": "allowlist",
#
  "ask": "on-miss"
}

Rollback flow

If health fails after step 7:

  1. git checkout <previous-sha-or-branch>
  2. docker compose up -d --build openclaw-gateway
  3. collect logs
  4. return failure + rollback status

That keeps rollback logic in Tier 2 instead of hiding it in a script.

#

Tier 2 contract I’d use

Have Tier 3 return a small structured report:

{
  "success": true,
  "requestedRef": "main",
  "beforeSha": "abc123",
  "afterSha": "def456",
  "dirtyRepo": false,
  "composeRebuilt": true,
  "healthz": true,
  "readyz": true,
#
  "rollbackPerformed": false
}

My recommendation

  • Start with direct-exec
  • Keep allowlist to git.exe, docker.exe, curl.exe
  • Only move to the PowerShell wrapper if you really want one-shot rollback logic and single-call output

That gives you the cleanest trust boundary.

Docs: