mirror of https://github.com/openclaw/openclaw.git
refactor(gateway): clarify local mode guardrails
This commit is contained in:
parent
4e22e75697
commit
e2e1197fa9
|
|
@ -37,6 +37,7 @@ Notes:
|
|||
|
||||
- By default, the Gateway refuses to start unless `gateway.mode=local` is set in `~/.openclaw/openclaw.json`. Use `--allow-unconfigured` for ad-hoc/dev runs.
|
||||
- `openclaw onboard --mode local` and `openclaw setup` are expected to write `gateway.mode=local`. If the file exists but `gateway.mode` is missing, treat that as a broken or clobbered config and repair it instead of assuming local mode implicitly.
|
||||
- If the file exists and `gateway.mode` is missing, the Gateway treats that as suspicious config damage and refuses to “guess local” for you.
|
||||
- Binding beyond loopback without auth is blocked (safety guardrail).
|
||||
- `SIGUSR1` triggers an in-process restart when authorized (`commands.restart` is enabled by default; set `commands.restart: false` to block manual restart, while gateway tool/config apply/update remain allowed).
|
||||
- `SIGINT`/`SIGTERM` handlers stop the gateway process, but they don’t restore any custom terminal state. If you wrap the CLI with a TUI or raw-mode input, restore the terminal before exit.
|
||||
|
|
@ -51,7 +52,7 @@ Notes:
|
|||
- `--password-file <path>`: read the gateway password from a file.
|
||||
- `--tailscale <off|serve|funnel>`: expose the Gateway via Tailscale.
|
||||
- `--tailscale-reset-on-exit`: reset Tailscale serve/funnel config on shutdown.
|
||||
- `--allow-unconfigured`: allow gateway start without `gateway.mode=local` in config.
|
||||
- `--allow-unconfigured`: allow gateway start without `gateway.mode=local` in config. This bypasses the startup guard for ad-hoc/dev bootstrap only; it does not write or repair the config file.
|
||||
- `--dev`: create a dev config + workspace if missing (skips BOOTSTRAP.md).
|
||||
- `--reset`: reset dev config + credentials + sessions + workspace (requires `--dev`).
|
||||
- `--force`: kill any existing listener on the selected port before starting.
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ Gateway token options in non-interactive mode:
|
|||
- With `--install-daemon`, if token mode requires a token and the configured token SecretRef is unresolved, onboarding fails closed with remediation guidance.
|
||||
- With `--install-daemon`, if both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, onboarding blocks install until mode is set explicitly.
|
||||
- Local onboarding writes `gateway.mode="local"` into the config. If a later config file is missing `gateway.mode`, treat that as config damage or an incomplete manual edit, not as a valid local-mode shortcut.
|
||||
- `--allow-unconfigured` is a separate gateway runtime escape hatch. It does not mean onboarding may omit `gateway.mode`.
|
||||
|
||||
Example:
|
||||
|
||||
|
|
|
|||
|
|
@ -259,12 +259,12 @@ Events are not replayed. On sequence gaps, refresh state (`health`, `system-pres
|
|||
|
||||
## Common failure signatures
|
||||
|
||||
| Signature | Likely issue |
|
||||
| -------------------------------------------------------------- | ---------------------------------------- |
|
||||
| `refusing to bind gateway ... without auth` | Non-loopback bind without token/password |
|
||||
| `another gateway instance is already listening` / `EADDRINUSE` | Port conflict |
|
||||
| `Gateway start blocked: set gateway.mode=local` | Config set to remote mode |
|
||||
| `unauthorized` during connect | Auth mismatch between client and gateway |
|
||||
| Signature | Likely issue |
|
||||
| -------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `refusing to bind gateway ... without auth` | Non-loopback bind without token/password |
|
||||
| `another gateway instance is already listening` / `EADDRINUSE` | Port conflict |
|
||||
| `Gateway start blocked: set gateway.mode=local` | Config set to remote mode, or local-mode stamp is missing from a damaged config |
|
||||
| `unauthorized` during connect | Auth mismatch between client and gateway |
|
||||
|
||||
For full diagnosis ladders, use [Gateway Troubleshooting](/gateway/troubleshooting).
|
||||
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ Look for:
|
|||
|
||||
Common signatures:
|
||||
|
||||
- `Gateway start blocked: set gateway.mode=local` → local gateway mode is not enabled, or the config file was clobbered and lost `gateway.mode`. Fix: set `gateway.mode="local"` in your config, or re-run `openclaw onboard --mode local` / `openclaw setup` to restamp the expected local-mode config. If you are running OpenClaw via Podman, the default config path is `~/.openclaw/openclaw.json`.
|
||||
- `Gateway start blocked: set gateway.mode=local` or `existing config is missing gateway.mode` → local gateway mode is not enabled, or the config file was clobbered and lost `gateway.mode`. Fix: set `gateway.mode="local"` in your config, or re-run `openclaw onboard --mode local` / `openclaw setup` to restamp the expected local-mode config. If you are running OpenClaw via Podman, the default config path is `~/.openclaw/openclaw.json`.
|
||||
- `refusing to bind gateway ... without auth` → non-loopback bind without token/password.
|
||||
- `another gateway instance is already listening` / `EADDRINUSE` → port conflict.
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ flowchart TD
|
|||
|
||||
Common log signatures:
|
||||
|
||||
- `Gateway start blocked: set gateway.mode=local` → gateway mode is unset/remote.
|
||||
- `Gateway start blocked: set gateway.mode=local` or `existing config is missing gateway.mode` → gateway mode is remote, or the config file is missing the local-mode stamp and should be repaired.
|
||||
- `refusing to bind gateway ... without auth` → non-loopback bind without token/password.
|
||||
- `another gateway instance is already listening` or `EADDRINUSE` → port already taken.
|
||||
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ The lock file is at `/data/gateway.*.lock` (not in a subdirectory).
|
|||
|
||||
### Config Not Being Read
|
||||
|
||||
If using `--allow-unconfigured`, the gateway creates a minimal config. Your custom config at `/data/openclaw.json` should be read on restart.
|
||||
`--allow-unconfigured` only bypasses the startup guard. It does not create or repair `/data/openclaw.json`, so make sure your real config exists and includes `gateway.mode="local"` when you want a normal local gateway start.
|
||||
|
||||
Verify the config exists:
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ openclaw gateway --port 18789
|
|||
|
||||
```json5
|
||||
{
|
||||
gateway: { mode: "local" },
|
||||
channels: { whatsapp: { allowFrom: ["+15555550123"] } },
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -227,8 +227,9 @@ describe("gateway run option collisions", () => {
|
|||
await expect(runGatewayCli(["gateway", "run"])).rejects.toThrow("__exit__:1");
|
||||
|
||||
expect(runtimeErrors).toContain(
|
||||
"Gateway start blocked: set gateway.mode=local (current: unset) or pass --allow-unconfigured.",
|
||||
"Gateway start blocked: existing config is missing gateway.mode. Treat this as suspicious or clobbered config. Re-run `openclaw onboard --mode local` or `openclaw setup`, set gateway.mode=local manually, or pass --allow-unconfigured.",
|
||||
);
|
||||
expect(runtimeErrors).toContain("Config write audit: /tmp/logs/config-audit.jsonl");
|
||||
expect(startGatewayServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -141,6 +141,36 @@ function formatModeErrorList<T extends string>(modes: readonly T[]): string {
|
|||
return `${quoted.slice(0, -1).join(", ")}, or ${quoted[quoted.length - 1]}`;
|
||||
}
|
||||
|
||||
function getGatewayStartGuardErrors(params: {
|
||||
allowUnconfigured?: boolean;
|
||||
configExists: boolean;
|
||||
configAuditPath: string;
|
||||
mode: string | undefined;
|
||||
}): string[] {
|
||||
if (params.allowUnconfigured || params.mode === "local") {
|
||||
return [];
|
||||
}
|
||||
if (!params.configExists) {
|
||||
return [
|
||||
`Missing config. Run \`${formatCliCommand("openclaw setup")}\` or set gateway.mode=local (or pass --allow-unconfigured).`,
|
||||
];
|
||||
}
|
||||
if (params.mode === undefined) {
|
||||
return [
|
||||
[
|
||||
"Gateway start blocked: existing config is missing gateway.mode.",
|
||||
"Treat this as suspicious or clobbered config.",
|
||||
`Re-run \`${formatCliCommand("openclaw onboard --mode local")}\` or \`${formatCliCommand("openclaw setup")}\`, set gateway.mode=local manually, or pass --allow-unconfigured.`,
|
||||
].join(" "),
|
||||
`Config write audit: ${params.configAuditPath}`,
|
||||
];
|
||||
}
|
||||
return [
|
||||
`Gateway start blocked: set gateway.mode=local (current: ${params.mode}) or pass --allow-unconfigured.`,
|
||||
`Config write audit: ${params.configAuditPath}`,
|
||||
];
|
||||
}
|
||||
|
||||
function resolveGatewayRunOptions(opts: GatewayRunOpts, command?: Command): GatewayRunOpts {
|
||||
const resolved: GatewayRunOpts = { ...opts };
|
||||
|
||||
|
|
@ -349,16 +379,15 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
|||
const configAuditPath = path.join(resolveStateDir(process.env), "logs", "config-audit.jsonl");
|
||||
const effectiveCfg = snapshot?.valid ? snapshot.config : cfg;
|
||||
const mode = effectiveCfg.gateway?.mode;
|
||||
if (!opts.allowUnconfigured && mode !== "local") {
|
||||
if (!configExists) {
|
||||
defaultRuntime.error(
|
||||
`Missing config. Run \`${formatCliCommand("openclaw setup")}\` or set gateway.mode=local (or pass --allow-unconfigured).`,
|
||||
);
|
||||
} else {
|
||||
defaultRuntime.error(
|
||||
`Gateway start blocked: set gateway.mode=local (current: ${mode ?? "unset"}) or pass --allow-unconfigured.`,
|
||||
);
|
||||
defaultRuntime.error(`Config write audit: ${configAuditPath}`);
|
||||
const guardErrors = getGatewayStartGuardErrors({
|
||||
allowUnconfigured: opts.allowUnconfigured,
|
||||
configExists,
|
||||
configAuditPath,
|
||||
mode,
|
||||
});
|
||||
if (guardErrors.length > 0) {
|
||||
for (const error of guardErrors) {
|
||||
defaultRuntime.error(error);
|
||||
}
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
|
|
@ -536,7 +565,7 @@ export function addGatewayRunCommand(cmd: Command): Command {
|
|||
)
|
||||
.option(
|
||||
"--allow-unconfigured",
|
||||
"Allow gateway start without gateway.mode=local in config",
|
||||
"Allow gateway start without enforcing gateway.mode=local in config (does not repair config)",
|
||||
false,
|
||||
)
|
||||
.option("--dev", "Create a dev config + workspace if missing (no BOOTSTRAP.md)", false)
|
||||
|
|
|
|||
|
|
@ -255,18 +255,48 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
|
|||
|
||||
const configPath = resolveStateConfigPath(process.env, stateDir);
|
||||
const cfg = await readJsonFile<{
|
||||
gateway?: { auth?: { mode?: string; token?: string } };
|
||||
gateway?: { mode?: string; auth?: { mode?: string; token?: string } };
|
||||
agents?: { defaults?: { workspace?: string } };
|
||||
tools?: { profile?: string };
|
||||
}>(configPath);
|
||||
|
||||
expect(cfg?.agents?.defaults?.workspace).toBe(workspace);
|
||||
expect(cfg?.gateway?.mode).toBe("local");
|
||||
expect(cfg?.tools?.profile).toBe("coding");
|
||||
expect(cfg?.gateway?.auth?.mode).toBe("token");
|
||||
expect(cfg?.gateway?.auth?.token).toBe(token);
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
it("keeps gateway.mode=local on the install-daemon onboarding path", async () => {
|
||||
await withStateDir("state-install-daemon-local-mode-", async (stateDir) => {
|
||||
const workspace = path.join(stateDir, "openclaw");
|
||||
|
||||
await runNonInteractiveSetup(
|
||||
{
|
||||
nonInteractive: true,
|
||||
mode: "local",
|
||||
workspace,
|
||||
authChoice: "skip",
|
||||
skipSkills: true,
|
||||
skipHealth: true,
|
||||
installDaemon: true,
|
||||
gatewayBind: "loopback",
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const configPath = resolveStateConfigPath(process.env, stateDir);
|
||||
const cfg = await readJsonFile<{
|
||||
gateway?: { mode?: string; bind?: string };
|
||||
}>(configPath);
|
||||
|
||||
expect(cfg?.gateway?.mode).toBe("local");
|
||||
expect(cfg?.gateway?.bind).toBe("loopback");
|
||||
expect(installGatewayDaemonNonInteractiveMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
it("uses OPENCLAW_GATEWAY_TOKEN when --gateway-token is omitted", async () => {
|
||||
await withStateDir("state-env-token-", async (stateDir) => {
|
||||
const envToken = "tok_env_fallback_123";
|
||||
|
|
|
|||
Loading…
Reference in New Issue