mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
docs(security): clarify computer act authorization
This commit is contained in:
@@ -90,6 +90,7 @@ These are frequently reported but are typically closed with no code change:
|
||||
- Prompt-injection-only chains without a boundary bypass (prompt injection is out of scope).
|
||||
- Operator-intended local features (for example TUI local `!` shell) presented as remote injection.
|
||||
- Reports that treat explicit operator-control surfaces (for example `canvas.eval`, browser evaluate/script execution, or direct `node.invoke` execution primitives) as vulnerabilities without demonstrating an auth/policy/sandbox boundary bypass. These capabilities are intentional when enabled and are trusted-operator features, not standalone security bugs.
|
||||
- Reports that treat an admin-gated enablement or arming step as requiring `operator.admin` for every subsequent action, when the documented contract delegates use of the enabled capability to `operator.write` and no auth, arming, allowlist, sandbox, or policy bypass is shown. This is an arm-then-use operator guardrail, not privilege escalation.
|
||||
- Authorized user-triggered local actions presented as privilege escalation. Example: an allowlisted/owner sender running `/export-session /absolute/path.html` to write on the host. In this trust model, authorized user actions are trusted host actions unless you demonstrate an auth/sandbox/boundary bypass.
|
||||
- Reports that only show a malicious plugin executing privileged actions after a trusted operator installs/enables it.
|
||||
- Reports that assume per-user multi-tenant authorization on a shared gateway host/config.
|
||||
|
||||
@@ -104,6 +104,13 @@ command list:
|
||||
| non-exec node commands | `operator.pairing` + `operator.write` |
|
||||
| `system.run`, `system.run.prepare`, or `system.which` | `operator.pairing` + `operator.admin` |
|
||||
|
||||
Approving a node declaration does not enable commands that have a separate
|
||||
runtime allowlist gate. For example, approving a node that declares
|
||||
`computer.act` requires pairing plus write scope, but only records the surface.
|
||||
An administrator or owner must still arm `computer.act`. While it remains
|
||||
armed, invoking it through the write-scoped `node.invoke` method does not
|
||||
require admin scope for each action.
|
||||
|
||||
Node pairing establishes identity and trust; it does not replace a node's own
|
||||
`system.run` exec approval policy.
|
||||
|
||||
|
||||
@@ -71,6 +71,14 @@ Reads reuse `screen.snapshot`; there is no second capture path. See [Camera and
|
||||
|
||||
For persistent authorization, add `computer.act` to `gateway.nodes.allowCommands` **and remove it from** `gateway.nodes.denyCommands`; the deny list wins. Persistent authorization does not auto-expire. Entries already present before `/phone arm` remain after `/phone disarm`; do not convert a temporary grant to persistent while it is armed.
|
||||
|
||||
Authorization is deliberately split between enabling and use. Arming or
|
||||
persistently configuring `computer.act` requires administrative authority.
|
||||
Once armed, an authenticated operator with `operator.write` can invoke
|
||||
`computer.act` through `node.invoke` until the grant expires or is disarmed;
|
||||
there is no per-action admin check. Approving a node that declares
|
||||
`computer.act` only records the surface so it can be armed later and does not
|
||||
enable invocation by itself.
|
||||
|
||||
## Safety
|
||||
|
||||
- Before authorization, every layer (tool policy, gateway command policy, macOS setting, Accessibility, and Screen Recording) must agree. Once armed, actions execute without a per-action confirmation until expiry or `/phone disarm`.
|
||||
|
||||
@@ -554,6 +554,43 @@ describe("node.invoke APNs wake path", () => {
|
||||
expect(nodeRegistry.invoke).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows an armed computer.act command for write-scoped operators", async () => {
|
||||
mocks.getRuntimeConfig.mockReturnValue({
|
||||
gateway: { nodes: { allowCommands: ["computer.act"] } },
|
||||
});
|
||||
mocks.resolveNodeCommandAllowlist.mockReturnValue(new Set(["computer.act"]));
|
||||
const nodeRegistry = {
|
||||
get: vi.fn(() => ({
|
||||
nodeId: "computer-node",
|
||||
commands: ["computer.act"],
|
||||
platform: "macOS 26.0.0",
|
||||
})),
|
||||
invoke: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
payloadJSON: '{"ok":true}',
|
||||
}),
|
||||
};
|
||||
|
||||
const respond = await invokeNode({
|
||||
nodeRegistry,
|
||||
client: createOperatorClient({ scopes: ["operator.write"] }),
|
||||
requestParams: {
|
||||
nodeId: "computer-node",
|
||||
command: "computer.act",
|
||||
params: { action: "type", text: "hello" },
|
||||
},
|
||||
});
|
||||
|
||||
const call = firstRespondCall(respond);
|
||||
expect(call[0]).toBe(true);
|
||||
expect(nodeRegistry.invoke).toHaveBeenCalledTimes(1);
|
||||
expectRecordFields(mockArg(nodeRegistry.invoke, 0, 0), "node invoke payload", {
|
||||
nodeId: "computer-node",
|
||||
command: "computer.act",
|
||||
params: { action: "type", text: "hello" },
|
||||
});
|
||||
});
|
||||
|
||||
it("explains the explicit opt-in required for dangerous commands", async () => {
|
||||
mocks.isNodeCommandAllowed.mockReturnValue({
|
||||
ok: false,
|
||||
|
||||
@@ -17,6 +17,13 @@ describe("resolveNodePairApprovalScopes", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats computer.act pairing approval as non-exec surface approval", () => {
|
||||
expect(resolveNodePairApprovalScopes(["computer.act"])).toEqual([
|
||||
"operator.pairing",
|
||||
"operator.write",
|
||||
]);
|
||||
});
|
||||
|
||||
it("requires only operator.pairing without commands", () => {
|
||||
expect(resolveNodePairApprovalScopes(undefined)).toEqual(["operator.pairing"]);
|
||||
expect(resolveNodePairApprovalScopes([])).toEqual(["operator.pairing"]);
|
||||
|
||||
Reference in New Issue
Block a user