Skip to content

Commit 6ead092

Browse files
authored
fix(acp): require owner for runtime controls (#97953)
1 parent 606bcc8 commit 6ead092

4 files changed

Lines changed: 49 additions & 5 deletions

File tree

docs/tools/acp-agents.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,12 @@ If no target resolves, OpenClaw returns a clear error
790790
| `/acp doctor` | Backend health, capabilities, actionable fixes. | `/acp doctor` |
791791
| `/acp install` | Print deterministic install and enable steps. | `/acp install` |
792792

793+
Runtime controls (`spawn`, `cancel`, `steer`, `close`, `status`, `set-mode`,
794+
`set`, `cwd`, `permissions`, `timeout`, `model`, and `reset-options`) require
795+
owner identity from external channels and `operator.admin` from internal Gateway
796+
clients. Authorized non-owner senders can still use `sessions`, `doctor`,
797+
`install`, and `help`.
798+
793799
`/acp status` shows the effective runtime options plus runtime-level and
794800
backend-level session identifiers. Unsupported-control errors surface
795801
clearly when a backend lacks a capability. `/acp sessions` reads the

docs/tools/slash-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ plugins.
256256
| Command | Description |
257257
| --- | --- |
258258
| `/subagents list\|log\|info` | Inspect sub-agent runs for the current session |
259-
| `/acp spawn\|cancel\|steer\|close\|sessions\|status\|set-mode\|set\|cwd\|permissions\|timeout\|model\|reset-options\|doctor\|install\|help` | Manage ACP sessions and runtime options |
259+
| `/acp spawn\|cancel\|steer\|close\|sessions\|status\|set-mode\|set\|cwd\|permissions\|timeout\|model\|reset-options\|doctor\|install\|help` | Manage ACP sessions and runtime options. Runtime controls require external owner or internal Gateway admin identity |
260260
| `/focus <target>` | Bind the current Discord thread or Telegram topic to a session target |
261261
| `/unfocus` | Remove the current thread binding |
262262
| `/agents` | List thread-bound agents for the current session |

src/auto-reply/reply/commands-acp.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,7 @@ function createDiscordParams(commandBody: string, cfg: OpenClawConfig = baseCfg)
518518
AccountId: "default",
519519
});
520520
params.command.senderId = "user-1";
521+
params.command.senderIsOwner = true;
521522
return params;
522523
}
523524

@@ -752,6 +753,7 @@ function createConversationParams(
752753
...(fixture.threadParentId ? { ThreadParentId: fixture.threadParentId } : {}),
753754
});
754755
params.command.senderId = fixture.senderId ?? "user-1";
756+
params.command.senderIsOwner = true;
755757
return params;
756758
}
757759

@@ -896,7 +898,6 @@ async function runInternalAcpCommand(params: {
896898
});
897899
commandParams.command.channel = INTERNAL_MESSAGE_CHANNEL;
898900
commandParams.command.senderId = "user-1";
899-
commandParams.command.senderIsOwner = true;
900901
return handleAcpCommand(commandParams, true);
901902
}
902903

@@ -1142,6 +1143,37 @@ describe("/acp command", () => {
11421143
expect(result?.reply?.text).toContain("/acp spawn");
11431144
});
11441145

1146+
it.each([
1147+
"spawn codex",
1148+
"cancel",
1149+
"steer continue",
1150+
"close",
1151+
"status",
1152+
"set-mode plan",
1153+
"set model gpt-5.5",
1154+
"cwd /tmp",
1155+
"permissions approve-all",
1156+
"timeout 120",
1157+
"model openai/gpt-5.5",
1158+
"reset-options",
1159+
])("blocks authorized non-owners from /acp %s", async (action) => {
1160+
const params = createDiscordParams(`/acp ${action}`);
1161+
params.command.senderIsOwner = false;
1162+
1163+
const result = await handleAcpCommand(params, true);
1164+
1165+
expect(result).toEqual({ shouldContinue: false });
1166+
});
1167+
1168+
it("keeps read-only /acp actions available to authorized non-owners", async () => {
1169+
const params = createDiscordParams("/acp sessions");
1170+
params.command.senderIsOwner = false;
1171+
1172+
const result = await handleAcpCommand(params, true);
1173+
1174+
expect(result?.reply?.text).toContain("ACP sessions:");
1175+
});
1176+
11451177
it("spawns an ACP session and binds a Discord thread", async () => {
11461178
hoisted.ensureSessionMock.mockResolvedValueOnce({
11471179
sessionKey: "agent:codex:acp:s1",

src/auto-reply/reply/commands-acp.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Implements ACP session commands and runtime status formatting.
22
import { logVerbose } from "../../globals.js";
33
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
4-
import { requireGatewayClientScope } from "./command-gates.js";
4+
import { rejectNonOwnerCommand, requireGatewayClientScope } from "./command-gates.js";
55
import {
66
COMMAND,
77
type AcpAction,
@@ -71,7 +71,7 @@ async function loadAcpActionHandler(action: Exclude<AcpAction, "help">): Promise
7171
return diagnosticHandlers[action];
7272
}
7373

74-
const ACP_MUTATING_ACTIONS = new Set<AcpAction>([
74+
const ACP_OWNER_REQUIRED_ACTIONS = new Set<AcpAction>([
7575
"spawn",
7676
"cancel",
7777
"steer",
@@ -104,7 +104,7 @@ export const handleAcpCommand: CommandHandler = async (params, _allowTextCommand
104104
return stopWithText(resolveAcpHelpText());
105105
}
106106

107-
if (ACP_MUTATING_ACTIONS.has(action)) {
107+
if (ACP_OWNER_REQUIRED_ACTIONS.has(action)) {
108108
const scopeBlock = requireGatewayClientScope(params, {
109109
label: "/acp",
110110
allowedScopes: ["operator.admin"],
@@ -113,6 +113,12 @@ export const handleAcpCommand: CommandHandler = async (params, _allowTextCommand
113113
if (scopeBlock) {
114114
return scopeBlock;
115115
}
116+
// Command auth maps internal operator.admin scope to owner identity, so this
117+
// second gate rejects external non-owners without blocking Gateway admins.
118+
const nonOwner = rejectNonOwnerCommand(params, "/acp");
119+
if (nonOwner) {
120+
return nonOwner;
121+
}
116122
}
117123

118124
const handler = await loadAcpActionHandler(action);

0 commit comments

Comments
 (0)