Skip to content

Commit af746ba

Browse files
authored
Merge branch 'main' into vincentkoc-code/health-json-route-preload-fix
2 parents 11b3b1f + c80f34f commit af746ba

File tree

128 files changed

+5650
-1247
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+5650
-1247
lines changed

CHANGELOG.md

Lines changed: 67 additions & 50 deletions
Large diffs are not rendered by default.

docs/gateway/doctor.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ Doctor checks:
168168
(`~/Library/Mobile Documents/com~apple~CloudDocs/...`) or
169169
`~/Library/CloudStorage/...` because sync-backed paths can cause slower I/O
170170
and lock/sync races.
171+
- **Linux SD or eMMC state dir**: warns when state resolves to an `mmcblk*`
172+
mount source, because SD or eMMC-backed random I/O can be slower and wear
173+
faster under session and credential writes.
171174
- **Session dirs missing**: `sessions/` and the session store directory are
172175
required to persist history and avoid `ENOENT` crashes.
173176
- **Transcript mismatch**: warns when recent session entries have missing

docs/gateway/openai-http-api.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ Notes:
2828
- When `gateway.auth.mode="password"`, use `gateway.auth.password` (or `OPENCLAW_GATEWAY_PASSWORD`).
2929
- If `gateway.auth.rateLimit` is configured and too many auth failures occur, the endpoint returns `429` with `Retry-After`.
3030

31+
## Security boundary (important)
32+
33+
Treat this endpoint as a **full operator-access** surface for the gateway instance.
34+
35+
- HTTP bearer auth here is not a narrow per-user scope model.
36+
- A valid Gateway token/password for this endpoint should be treated like an owner/operator credential.
37+
- Requests run through the same control-plane agent path as trusted operator actions.
38+
- If the target agent policy allows sensitive tools, this endpoint can use them.
39+
- Keep this endpoint on loopback/tailnet/private ingress only; do not expose it directly to the public internet.
40+
41+
See [Security](/gateway/security) and [Remote access](/gateway/remote).
42+
3143
## Choosing an agent
3244

3345
No custom headers required: encode the agent id in the OpenAI `model` field:

docs/gateway/openresponses-http-api.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ Notes:
3030
- When `gateway.auth.mode="password"`, use `gateway.auth.password` (or `OPENCLAW_GATEWAY_PASSWORD`).
3131
- If `gateway.auth.rateLimit` is configured and too many auth failures occur, the endpoint returns `429` with `Retry-After`.
3232

33+
## Security boundary (important)
34+
35+
Treat this endpoint as a **full operator-access** surface for the gateway instance.
36+
37+
- HTTP bearer auth here is not a narrow per-user scope model.
38+
- A valid Gateway token/password for this endpoint should be treated like an owner/operator credential.
39+
- Requests run through the same control-plane agent path as trusted operator actions.
40+
- If the target agent policy allows sensitive tools, this endpoint can use them.
41+
- Keep this endpoint on loopback/tailnet/private ingress only; do not expose it directly to the public internet.
42+
43+
See [Security](/gateway/security) and [Remote access](/gateway/remote).
44+
3345
## Choosing an agent
3446

3547
No custom headers required: encode the agent id in the OpenResponses `model` field:

docs/gateway/security/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,12 @@ injected by Tailscale.
724724
HTTP API endpoints (for example `/v1/*`, `/tools/invoke`, and `/api/channels/*`)
725725
still require token/password auth.
726726

727+
Important boundary note:
728+
729+
- Gateway HTTP bearer auth is effectively all-or-nothing operator access.
730+
- Treat credentials that can call `/v1/chat/completions`, `/v1/responses`, `/tools/invoke`, or `/api/channels/*` as full-access operator secrets for that gateway.
731+
- Do not share these credentials with untrusted callers; prefer separate gateways per trust boundary.
732+
727733
**Trust assumption:** tokenless Serve auth assumes the gateway host is trusted.
728734
Do not treat this as protection against hostile same-host processes. If untrusted
729735
local code may run on the gateway host, disable `gateway.auth.allowTailscale`

docs/nodes/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ Notes:
277277

278278
- `system.run` returns stdout/stderr/exit code in the payload.
279279
- `system.notify` respects notification permission state on the macOS app.
280+
- Unrecognized node `platform` / `deviceFamily` metadata uses a conservative default allowlist that excludes `system.run` and `system.which`. If you intentionally need those commands for an unknown platform, add them explicitly via `gateway.nodes.allowCommands`.
280281
- `system.run` supports `--cwd`, `--env KEY=VAL`, `--command-timeout`, and `--needs-screen-recording`.
281282
- For shell wrappers (`bash|sh|zsh ... -c/-lc`), request-scoped `--env` values are reduced to an explicit allowlist (`TERM`, `LANG`, `LC_*`, `COLORTERM`, `NO_COLOR`, `FORCE_COLOR`).
282283
- For allow-always decisions in allowlist mode, known dispatch wrappers (`env`, `nice`, `nohup`, `stdbuf`, `timeout`) persist inner executable paths instead of wrapper paths. If unwrapping is not safe, no allowlist entry is persisted automatically.

extensions/acpx/openclaw.plugin.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"type": "string",
2525
"enum": ["deny", "fail"]
2626
},
27+
"strictWindowsCmdWrapper": {
28+
"type": "boolean"
29+
},
2730
"timeoutSeconds": {
2831
"type": "number",
2932
"minimum": 0.001
@@ -55,6 +58,11 @@
5558
"label": "Non-Interactive Permission Policy",
5659
"help": "acpx policy when interactive permission prompts are unavailable."
5760
},
61+
"strictWindowsCmdWrapper": {
62+
"label": "Strict Windows cmd Wrapper",
63+
"help": "When enabled on Windows, reject unresolved .cmd/.bat wrappers instead of shell fallback. Hardening-only; can break non-standard wrappers.",
64+
"advanced": true
65+
},
5866
"timeoutSeconds": {
5967
"label": "Prompt Timeout Seconds",
6068
"help": "Optional acpx timeout for each runtime turn.",

extensions/acpx/src/config.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe("acpx plugin config parsing", () => {
2020
expect(resolved.expectedVersion).toBe(ACPX_PINNED_VERSION);
2121
expect(resolved.allowPluginLocalInstall).toBe(true);
2222
expect(resolved.cwd).toBe(path.resolve("/tmp/workspace"));
23+
expect(resolved.strictWindowsCmdWrapper).toBe(false);
2324
});
2425

2526
it("accepts command override and disables plugin-local auto-install", () => {
@@ -109,4 +110,26 @@ describe("acpx plugin config parsing", () => {
109110

110111
expect(parsed.success).toBe(false);
111112
});
113+
114+
it("accepts strictWindowsCmdWrapper override", () => {
115+
const resolved = resolveAcpxPluginConfig({
116+
rawConfig: {
117+
strictWindowsCmdWrapper: true,
118+
},
119+
workspaceDir: "/tmp/workspace",
120+
});
121+
122+
expect(resolved.strictWindowsCmdWrapper).toBe(true);
123+
});
124+
125+
it("rejects non-boolean strictWindowsCmdWrapper", () => {
126+
expect(() =>
127+
resolveAcpxPluginConfig({
128+
rawConfig: {
129+
strictWindowsCmdWrapper: "yes",
130+
},
131+
workspaceDir: "/tmp/workspace",
132+
}),
133+
).toThrow("strictWindowsCmdWrapper must be a boolean");
134+
});
112135
});

extensions/acpx/src/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export type AcpxPluginConfig = {
2424
cwd?: string;
2525
permissionMode?: AcpxPermissionMode;
2626
nonInteractivePermissions?: AcpxNonInteractivePermissionPolicy;
27+
strictWindowsCmdWrapper?: boolean;
2728
timeoutSeconds?: number;
2829
queueOwnerTtlSeconds?: number;
2930
};
@@ -36,6 +37,7 @@ export type ResolvedAcpxPluginConfig = {
3637
cwd: string;
3738
permissionMode: AcpxPermissionMode;
3839
nonInteractivePermissions: AcpxNonInteractivePermissionPolicy;
40+
strictWindowsCmdWrapper: boolean;
3941
timeoutSeconds?: number;
4042
queueOwnerTtlSeconds: number;
4143
};
@@ -75,6 +77,7 @@ function parseAcpxPluginConfig(value: unknown): ParseResult {
7577
"cwd",
7678
"permissionMode",
7779
"nonInteractivePermissions",
80+
"strictWindowsCmdWrapper",
7881
"timeoutSeconds",
7982
"queueOwnerTtlSeconds",
8083
]);
@@ -133,6 +136,11 @@ function parseAcpxPluginConfig(value: unknown): ParseResult {
133136
return { ok: false, message: "timeoutSeconds must be a positive number" };
134137
}
135138

139+
const strictWindowsCmdWrapper = value.strictWindowsCmdWrapper;
140+
if (strictWindowsCmdWrapper !== undefined && typeof strictWindowsCmdWrapper !== "boolean") {
141+
return { ok: false, message: "strictWindowsCmdWrapper must be a boolean" };
142+
}
143+
136144
const queueOwnerTtlSeconds = value.queueOwnerTtlSeconds;
137145
if (
138146
queueOwnerTtlSeconds !== undefined &&
@@ -152,6 +160,8 @@ function parseAcpxPluginConfig(value: unknown): ParseResult {
152160
permissionMode: typeof permissionMode === "string" ? permissionMode : undefined,
153161
nonInteractivePermissions:
154162
typeof nonInteractivePermissions === "string" ? nonInteractivePermissions : undefined,
163+
strictWindowsCmdWrapper:
164+
typeof strictWindowsCmdWrapper === "boolean" ? strictWindowsCmdWrapper : undefined,
155165
timeoutSeconds: typeof timeoutSeconds === "number" ? timeoutSeconds : undefined,
156166
queueOwnerTtlSeconds:
157167
typeof queueOwnerTtlSeconds === "number" ? queueOwnerTtlSeconds : undefined,
@@ -205,6 +215,7 @@ export function createAcpxPluginConfigSchema(): OpenClawPluginConfigSchema {
205215
type: "string",
206216
enum: [...ACPX_NON_INTERACTIVE_POLICIES],
207217
},
218+
strictWindowsCmdWrapper: { type: "boolean" },
208219
timeoutSeconds: { type: "number", minimum: 0.001 },
209220
queueOwnerTtlSeconds: { type: "number", minimum: 0 },
210221
},
@@ -244,6 +255,7 @@ export function resolveAcpxPluginConfig(params: {
244255
permissionMode: normalized.permissionMode ?? DEFAULT_PERMISSION_MODE,
245256
nonInteractivePermissions:
246257
normalized.nonInteractivePermissions ?? DEFAULT_NON_INTERACTIVE_POLICY,
258+
strictWindowsCmdWrapper: normalized.strictWindowsCmdWrapper ?? false,
247259
timeoutSeconds: normalized.timeoutSeconds,
248260
queueOwnerTtlSeconds: normalized.queueOwnerTtlSeconds ?? DEFAULT_QUEUE_OWNER_TTL_SECONDS,
249261
};

extensions/acpx/src/ensure.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import fs from "node:fs";
22
import path from "node:path";
33
import type { PluginLogger } from "openclaw/plugin-sdk";
44
import { ACPX_PINNED_VERSION, ACPX_PLUGIN_ROOT, buildAcpxLocalInstallCommand } from "./config.js";
5-
import { resolveSpawnFailure, spawnAndCollect } from "./runtime-internals/process.js";
5+
import {
6+
resolveSpawnFailure,
7+
type SpawnCommandOptions,
8+
spawnAndCollect,
9+
} from "./runtime-internals/process.js";
610

711
const SEMVER_PATTERN = /\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b/;
812

@@ -76,17 +80,32 @@ export async function checkAcpxVersion(params: {
7680
command: string;
7781
cwd?: string;
7882
expectedVersion?: string;
83+
spawnOptions?: SpawnCommandOptions;
7984
}): Promise<AcpxVersionCheckResult> {
8085
const expectedVersion = params.expectedVersion?.trim() || undefined;
8186
const installCommand = buildAcpxLocalInstallCommand(expectedVersion ?? ACPX_PINNED_VERSION);
8287
const cwd = params.cwd ?? ACPX_PLUGIN_ROOT;
8388
const hasExpectedVersion = isExpectedVersionConfigured(expectedVersion);
8489
const probeArgs = hasExpectedVersion ? ["--version"] : ["--help"];
85-
const result = await spawnAndCollect({
90+
const spawnParams = {
8691
command: params.command,
8792
args: probeArgs,
8893
cwd,
89-
});
94+
};
95+
let result: Awaited<ReturnType<typeof spawnAndCollect>>;
96+
try {
97+
result = params.spawnOptions
98+
? await spawnAndCollect(spawnParams, params.spawnOptions)
99+
: await spawnAndCollect(spawnParams);
100+
} catch (error) {
101+
return {
102+
ok: false,
103+
reason: "execution-failed",
104+
message: error instanceof Error ? error.message : String(error),
105+
expectedVersion,
106+
installCommand,
107+
};
108+
}
90109

91110
if (result.error) {
92111
const spawnFailure = resolveSpawnFailure(result.error, cwd);
@@ -186,6 +205,7 @@ export async function ensureAcpx(params: {
186205
pluginRoot?: string;
187206
expectedVersion?: string;
188207
allowInstall?: boolean;
208+
spawnOptions?: SpawnCommandOptions;
189209
}): Promise<void> {
190210
if (pendingEnsure) {
191211
return await pendingEnsure;
@@ -201,6 +221,7 @@ export async function ensureAcpx(params: {
201221
command: params.command,
202222
cwd: pluginRoot,
203223
expectedVersion,
224+
spawnOptions: params.spawnOptions,
204225
});
205226
if (precheck.ok) {
206227
return;
@@ -238,6 +259,7 @@ export async function ensureAcpx(params: {
238259
command: params.command,
239260
cwd: pluginRoot,
240261
expectedVersion,
262+
spawnOptions: params.spawnOptions,
241263
});
242264

243265
if (!postcheck.ok) {

0 commit comments

Comments
 (0)