Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ reserve, reject, or rewrite upstream command arguments.
OpenClaw inherits the terminal's working directory; the launcher does not make
the read-only application directory the workspace.

After each successful interactive `openclaw` invocation, the launcher performs
a file-only readiness check inside the isolated session. If the default
`.openclaw\openclaw.json` has `gateway.mode` set to `local` but the managed
gateway has never started or has stopped, it suggests
`clawctl gateway-service start` on standard error. It does not suggest a second
start when gateway status is starting, unhealthy, or unknown. The check stops
for the current Windows logon after that start command is invoked or a running
gateway is observed; a new Windows logon enables it again. On an interactive
terminal the hint uses the crab identity and the same warning/accent palette as
`clawctl`; redirected and explicitly color-disabled output remains plain.

### `clawctl`

`clawctl` owns setup and the isolated-session operations:
Expand All @@ -81,12 +92,12 @@ the read-only application directory the workspace.
|---|---|
| `clawctl setup` | Confirm packaged `app\openclaw.mjs` exists, provision or reuse the owned isolated session, and install the bundled Node.js runtime in the agent profile. It also configures gateway sign-in recovery without starting a gateway. On a machine that cannot host a session it fails with the Windows requirement described under [Requirements](#requirements). |
| `clawctl setup --fresh [--force]` | Remove this installation's owned session and package-local state, then run setup again. Without `--force`, incomplete external cleanup stops before local state is erased. `--force` is valid only with `--fresh`; it preserves an explicit warning when cleanup of owned external resources cannot be confirmed, but still stops if bounded local deletion fails. |
| `clawctl status` | Report the recorded isolated session, installed Node.js runtime, gateway, and sign-in recovery state without provisioning or replacing the session. It asks the backend to start the recorded provision as its status probe, so it is not a passive diagnostic. Use `clawctl gateway-service status` to inspect the gateway alone. |
| `clawctl status` | Report the recorded isolated session, installed Node.js runtime, gateway, sign-in recovery, and file-only config readiness when the gateway is not running. It asks the backend to start the recorded provision as its status probe, so it is not a passive diagnostic, but it does not provision a replacement or start the gateway. Use `clawctl gateway-service status` to inspect the gateway alone. |
| `clawctl teardown --force` | Confirm deletion, then stop and deprovision the owned session and remove its data and setup state. The MSIX remains installed. |
| `clawctl pwsh` | Open an interactive PowerShell session inside the agent session. |
| `clawctl collect-logs [--output <path>]` | Create a redacted host-and-agent diagnostics ZIP. |
| `clawctl gateway-service start` | Start the OpenClaw gateway in the isolated session and wait for it to listen. Requires setup. |
| `clawctl gateway-service status` | Inspect the gateway without starting it. |
| `clawctl gateway-service status` | Inspect the gateway without starting it. When the gateway is not running, it may start/probe only the already-recorded isolated session to report file-only config readiness; it never provisions a replacement or starts the gateway. |
| `clawctl gateway-service stop` | Stop the gateway while retaining the session and its data. |
| `clawctl --version` | Print the packaged launcher version. |

Expand All @@ -104,6 +115,12 @@ on standard output. Human diagnostics remain on standard error, and command
exit codes do not change. `clawctl pwsh --json` is rejected because the command
hands the terminal to an interactive shell.

When the gateway is not confirmed running, status JSON includes an optional
`gateway.readiness` object with `state`, stable `reason`, and failure `detail`
where applicable. The readiness states are `absent`, `not-ready`,
`startup-eligible`, `unavailable`, and `unknown`. A running gateway omits this
object and incurs no config-readiness probe.

Interactive terminals use color for headings and status marks. `--no-color`,
the `NO_COLOR` environment variable, redirected output, and CI disable color;
`FORCE_COLOR` enables it for redirected output or CI unless color was
Expand Down
12 changes: 12 additions & 0 deletions docs/clawctl-output-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ should describe that environment in terms an operator can act on, while
presents onboarding as mandatory nor launches it automatically.
- End neutral states with the command that moves the user forward, such as
`Run: clawctl setup`.
- When a gateway is not running, show the file-only agent config readiness
immediately after the gateway row. Suggest `clawctl gateway-service start`
only when readiness is `startup eligible`; absent, incomplete, unavailable,
and unknown config states need diagnosis or configuration instead.
- Keep a successful `clawctl pwsh` launch silent so the user reaches the shell
prompt directly. Its help text explains which commands are available inside
the session.
Expand All @@ -32,6 +36,14 @@ other non-actionable implementation details out of human output. Preserve them
in structured output and diagnostics when they are useful for automation or
support.

The post-OpenClaw gateway hint is a package-owned status surface even though it
is written after upstream output. On an interactive invocation it uses the crab
mark, warning-colored `Hint:`, and an accent-colored unquoted command. Resolve
foreground eligibility separately from the selected stderr handle: an
app-execution-alias proxy can carry ANSI without supporting console-mode
changes, while a native console handle still requires VT setup. Whole-command
redirection, CI, and `NO_COLOR` remain plain; `FORCE_COLOR` remains authoritative.

## Failures and diagnostics

Expected refusals should state the condition and the next usable action without
Expand Down
46 changes: 46 additions & 0 deletions docs/mxc-compatibility-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ shared session workspace. It does not stage the application tree. The helper
has distinct modes for launching a request, inspecting processes/listeners,
installing the agent runtime, and collecting requested diagnostics.

The helper also exposes an internal `--check-config <request-path>` mode used by
the launcher after packaged `openclaw` calls. It resolves the agent account's default
`.openclaw\openclaw.json` and classifies the file as `Absent`, `NotReady`, or
`StartupEligible` without starting Node.js or OpenClaw. This is deliberately a
file-only heuristic for the default OpenClaw-generated config:
`StartupEligible` means only that the file can be parsed and contains
`gateway.mode` set exactly to `local`. It does not resolve alternate profiles,
includes, environment substitution, secrets, plugins, bind/auth policy, ports,
or any other runtime dependency, and therefore does not claim that a gateway
will start or remain healthy.

On the local win-x64 NativeAOT publish, ten direct helper invocations took
53.5-108.9 ms (59.15 ms median), including process startup and file inspection
but excluding the MXC round trip. No invocation started Node.js or OpenClaw.

Opening the agent shell with `clawctl pwsh` installs an ASCII `openclaw.cmd`
shim in the shared workspace: `RunPowerShellAsync` calls `InstallToolsAsync`.
Setup alone does not guarantee that shim exists. The shim reads its Node.js and
Expand Down Expand Up @@ -104,6 +119,37 @@ owned by that process or one of its descendants. A missing record is
process is `Unhealthy`; and failed inspection is `Unknown` to avoid starting a
second gateway beside one that could still be healthy.

Both `clawctl status` and `clawctl gateway-service status` add the helper's
file-only config readiness whenever the managed gateway is not confirmed
running. The gateway-only command first observes gateway state, then starts
only an already-recorded session when necessary to reach the helper; it never
provisions a replacement or starts the gateway. Human output reports
`not configured`, `not ready`, `startup eligible`, `unavailable`, or `unknown`.
Structured output carries the same state under `gateway.readiness` with the
stable reason. A running gateway omits readiness and avoids the helper call.

After an `openclaw` child exits, the launcher uses the helper's file-only
readiness result before checking the managed gateway record and liveness. A
successful interactive call gets a start suggestion only for `NotStarted` or
`Stopped`; unsuccessful or redirected calls, and `Starting`, `Unhealthy`, or
`Unknown` gateway states, remain silent. The advisory path cannot change the
OpenClaw exit code.

The suggestion remains eligible until a manual
`clawctl gateway-service start` invocation or an observed `Running` state.
Acknowledgement is package-local and keyed to the Windows token authentication
ID, so it suppresses later checks only for the current Windows logon. The
sign-in recovery command carries a hidden provenance marker and does not count
as a manual acknowledgement; a later OpenClaw invocation that observes its
running gateway does.

Agent entrypoint startup captures and restores console state and initializes
UTF-8 just as the control entrypoint does. Postflight rendering treats
foreground interactivity and stderr's native console capability separately:
the app-alias stderr proxy can receive ANSI and the crab glyph without
supporting `GetConsoleMode`, while a native console still requires successful
VT enablement.

## Diagnostics and safe collection

`clawctl collect-logs [--output <path>]` creates a ZIP at the supplied path or
Expand Down
24 changes: 24 additions & 0 deletions src/OpenClaw.Launcher/ClawCtlColorPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,30 @@ internal static bool PrepareOutput(
enableVirtualTerminalProcessing());
}

internal static bool PrepareForegroundOutput(
bool noColor,
bool json,
bool outputIsProcessConsoleWriter,
bool invocationIsInteractive,
bool selectedStreamIsInteractive,
Func<string, string?> readEnvironmentVariable,
Func<bool> enableVirtualTerminalProcessing)
{
ArgumentNullException.ThrowIfNull(enableVirtualTerminalProcessing);

bool useColor = ShouldUseColor(
noColor,
json,
outputIsProcessConsoleWriter,
invocationIsInteractive,
readEnvironmentVariable);

return useColor &&
(!outputIsProcessConsoleWriter ||
!selectedStreamIsInteractive ||
enableVirtualTerminalProcessing());
}

internal static bool ShouldUseColor(
bool noColor,
bool json,
Expand Down
6 changes: 4 additions & 2 deletions src/OpenClaw.Launcher/ClawCtlCommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal sealed record ClawCtlHandlers
public required Func<string?, CancellationToken, Task<int>> CollectLogs { get; init; }
public required Func<bool, CancellationToken, Task<int>> Teardown { get; init; }
public required Func<CancellationToken, Task<int>> PowerShell { get; init; }
public required Func<CancellationToken, Task<int>> GatewayStart { get; init; }
public required Func<bool, CancellationToken, Task<int>> GatewayStart { get; init; }
public required Func<CancellationToken, Task<int>> GatewayStatus { get; init; }
public required Func<CancellationToken, Task<int>> GatewayStop { get; init; }
}
Expand Down Expand Up @@ -163,11 +163,13 @@ public static RootCommand Create(
"gateway-service",
"Manage the background OpenClaw gateway inside the isolated session.");
Command gatewayStart = new("start", "Start the gateway if needed.");
Option<bool> recovery = new("--recovery") { Hidden = true };
gatewayStart.Options.Add(recovery);
gatewayStart.SetAction((parsed, token) =>
{
outputOptions.Json = parsed.GetValue(json);
outputOptions.NoColor = parsed.GetValue(noColor);
return handlers.GatewayStart(token);
return handlers.GatewayStart(parsed.GetValue(recovery), token);
});
Command gatewayStatus = new("status", "Show whether the gateway is running.");
gatewayStatus.SetAction((parsed, token) =>
Expand Down
79 changes: 78 additions & 1 deletion src/OpenClaw.Launcher/ClawCtlConsole.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Globalization;
using OpenClaw.SessionProtocol;
using Spectre.Console;
using Spectre.Console.Rendering;

Expand Down Expand Up @@ -77,6 +78,27 @@ internal static void WriteResult(
}
}

internal static void WriteGatewayHint(
TextWriter output,
bool useColor = false,
bool? useUnicode = null)
{
ArgumentNullException.ThrowIfNull(output);

bool unicode = useUnicode ?? SupportsUnicode(output);
var paragraph = new Paragraph();
if (unicode)
{
paragraph.Append($"{IdentityMark} ", WarningStyle);
}

paragraph.Append("Hint:", WarningStyle);
paragraph.Append(" The OpenClaw gateway is not running. Run ");
paragraph.Append("clawctl gateway-service start", AccentStyle);
paragraph.Append(" to start it.");
Render(output, paragraph, useColor, unicode);
}

internal static void WriteVersion(TextWriter output, bool useColor = false)
{
ArgumentNullException.ThrowIfNull(output);
Expand Down Expand Up @@ -391,6 +413,8 @@ Session.SessionAvailability.BackendError or
view.Detail(result.Gateway.Detail);
}

WriteReadiness(view, result.Readiness);

view.Row("Recovery", DescribeRecovery(view, result.Recovery));
if (!string.IsNullOrWhiteSpace(result.Recovery.Detail))
{
Expand Down Expand Up @@ -514,6 +538,8 @@ private static void WriteGateway(ResultView view, GatewayCommandResult result)
view.Detail(explanation);
}

WriteReadiness(view, result.Readiness);

// Reaching the Control UI needs the shared token, and the command that
// reveals it belongs to OpenClaw rather than to this package.
if (result.State == Gateway.GatewayState.Running && result.Port is not null)
Expand All @@ -523,13 +549,64 @@ private static void WriteGateway(ResultView view, GatewayCommandResult result)
}

if (result.State == Gateway.GatewayState.NotStarted &&
result.Action == "status")
result.Action == "status" &&
result.Readiness?.State ==
Session.AgentConfigReadinessState.StartupEligible)
{
view.Blank();
view.Command("Run", "clawctl gateway-service start");
}
}

private static void WriteReadiness(
ResultView view,
Session.AgentConfigReadinessStatus? readiness)
{
if (readiness is null)
{
return;
}

view.Row("Readiness", readiness.State switch
{
Session.AgentConfigReadinessState.Absent =>
Status(view, StatusKind.Neutral, "not configured"),
Session.AgentConfigReadinessState.NotReady =>
Status(view, StatusKind.Warning, "not ready"),
Session.AgentConfigReadinessState.StartupEligible =>
Status(view, StatusKind.Success, "startup eligible"),
Session.AgentConfigReadinessState.Unavailable =>
Status(view, StatusKind.Neutral, "unavailable"),
_ => Status(
view,
readiness.ProbeFailed ? StatusKind.Failure : StatusKind.Warning,
"unknown")
});

string? detail = readiness.Reason switch
{
SessionConfigReadinessReason.ConfigFileMissing =>
"the default config file is missing",
SessionConfigReadinessReason.ConfigFileUnreadable =>
"the default config file could not be read",
SessionConfigReadinessReason.ConfigFileInvalid =>
"the default config file is invalid",
SessionConfigReadinessReason.GatewayMissing =>
"the config has no gateway object",
SessionConfigReadinessReason.GatewayModeMissing =>
"gateway.mode is missing",
SessionConfigReadinessReason.GatewayModeNotLocal =>
"gateway.mode is not local",
SessionConfigReadinessReason.GatewayModeLocal =>
"gateway.mode is local",
_ => readiness.Detail
};
if (!string.IsNullOrWhiteSpace(detail))
{
view.Detail(detail);
}
}

private static Paragraph DescribeSession(
ResultView view,
Session.SessionAvailability availability) =>
Expand Down
Loading
Loading