


























@@ -108,6 +108,7 @@ vi.mock("../agents/openclaw-tools.js", () => {
108108agentTo: lastCreateOpenClawToolsContext?.agentTo,
109109agentThreadId: lastCreateOpenClawToolsContext?.agentThreadId,
110110},
111+inheritedToolDenylist: lastCreateOpenClawToolsContext?.inheritedToolDenylist,
111112}),
112113},
113114{
@@ -122,6 +123,11 @@ vi.mock("../agents/openclaw-tools.js", () => {
122123throw toolInputError("invalid args");
123124},
124125},
126+{
127+name: "cron",
128+parameters: { type: "object", properties: {} },
129+execute: async () => ({ ok: true, result: "cron" }),
130+},
125131{
126132name: "exec",
127133parameters: { type: "object", properties: {} },
@@ -692,6 +698,34 @@ describe("POST /tools/invoke", () => {
692698});
693699});
694700701+it("propagates owner-only HTTP denies into spawned session inheritance", async () => {
702+cfg = {
703+ ...cfg,
704+agents: {
705+list: [
706+{
707+id: "main",
708+default: true,
709+tools: { allow: ["sessions_spawn", "cron", "gateway", "nodes"] },
710+},
711+],
712+},
713+gateway: { tools: { allow: ["sessions_spawn", "cron", "gateway", "nodes"] } },
714+};
715+716+const res = await invokeTool({
717+port: sharedPort,
718+headers: gatewayAuthHeaders(),
719+tool: "sessions_spawn",
720+sessionKey: "main",
721+});
722+723+const body = await expectOkInvokeResponse(res);
724+expect(body.result?.inheritedToolDenylist).toEqual(
725+expect.arrayContaining(["cron", "gateway", "nodes"]),
726+);
727+});
728+695729it("denies sessions_send via HTTP gateway", async () => {
696730setMainAllowedTools({ allow: ["sessions_send"] });
697731@@ -730,6 +764,47 @@ describe("POST /tools/invoke", () => {
730764expect(body.error?.type).toBe("tool_error");
731765});
732766767+it("keeps owner-only tools unavailable to non-owner HTTP callers despite gateway.tools.allow", async () => {
768+setMainAllowedTools({
769+allow: ["cron", "gateway", "nodes"],
770+gatewayAllow: ["cron", "gateway", "nodes"],
771+});
772+773+for (const tool of ["cron", "gateway", "nodes"]) {
774+const res = await invokeToolAuthed({
775+ tool,
776+sessionKey: "main",
777+});
778+779+expect(res.status, tool).toBe(404);
780+const body = await res.json();
781+expect(body.ok, tool).toBe(false);
782+expect(body.error?.type, tool).toBe("not_found");
783+}
784+});
785+786+it("keeps shared-secret bearer auth as owner for explicitly allowed owner-only tools", async () => {
787+setMainAllowedTools({ allow: ["nodes"], gatewayAllow: ["nodes"] });
788+vi.mocked(authorizeHttpGatewayConnect).mockResolvedValueOnce({
789+ok: true,
790+method: "token",
791+});
792+793+const res = await invokeTool({
794+port: sharedPort,
795+headers: {
796+authorization: "Bearer secret",
797+"x-openclaw-scopes": "operator.write",
798+},
799+tool: "nodes",
800+sessionKey: "main",
801+});
802+803+const body = await expectOkInvokeResponse(res);
804+expect(body.result).toEqual({ ok: true, result: "nodes" });
805+expect(lastCreateOpenClawToolsContext?.senderIsOwner).toBe(true);
806+});
807+733808it("treats gateway.tools.deny as higher priority than gateway.tools.allow", async () => {
734809setMainAllowedTools({
735810allow: ["gateway"],
@@ -995,6 +1070,47 @@ describe("tools.invoke Gateway RPC", () => {
9951070expect(hookCtx.sessionKey).toBe("agent:main:main");
9961071});
99710721073+it("keeps owner-only tools unavailable to non-owner RPC callers despite gateway.tools.allow", async () => {
1074+setMainAllowedTools({
1075+allow: ["cron", "gateway", "nodes"],
1076+gatewayAllow: ["cron", "gateway", "nodes"],
1077+});
1078+1079+for (const tool of ["cron", "gateway", "nodes"]) {
1080+const call = await invokeToolsRpc({
1081+name: tool,
1082+args: {},
1083+sessionKey: "main",
1084+});
1085+1086+expect(call?.[0], tool).toBe(true);
1087+expect(call?.[1]?.ok, tool).toBe(false);
1088+expect(call?.[1]?.toolName, tool).toBe(tool);
1089+const error = call?.[1]?.error as { code?: string; message?: string } | undefined;
1090+expect(error?.code, tool).toBe("not_found");
1091+}
1092+expect(lastCreateOpenClawToolsContext?.senderIsOwner).toBe(false);
1093+});
1094+1095+it("keeps operator.admin RPC callers as owner for explicitly allowed owner-only tools", async () => {
1096+setMainAllowedTools({ allow: ["nodes"], gatewayAllow: ["nodes"] });
1097+1098+const call = await invokeToolsRpc(
1099+{
1100+name: "nodes",
1101+args: {},
1102+sessionKey: "main",
1103+},
1104+["operator.admin"],
1105+);
1106+1107+expect(call?.[0]).toBe(true);
1108+expect(call?.[1]?.ok).toBe(true);
1109+expect(call?.[1]?.toolName).toBe("nodes");
1110+expect(call?.[1]?.output).toEqual({ ok: true, result: "nodes" });
1111+expect(lastCreateOpenClawToolsContext?.senderIsOwner).toBe(true);
1112+});
1113+9981114it("returns typed approval-needed refusal when the policy hook blocks", async () => {
9991115setMainAllowedTools({ allow: ["tools_invoke_test"] });
10001116hookMocks.runBeforeToolCallHook.mockResolvedValueOnce({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。