mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor: remove sender owner tool gating
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Gateway/plugins: reuse a compatible Gateway startup plugin registry during dispatch so safe plugin dispatches avoid redundant registry loading. (#84324) Thanks @ai-hpc.
|
||||
- Dependencies: refresh provider, plugin, UI, and tooling packages, update `protobufjs` to 8.4.0 to clear the current npm advisory, and carry the Claude ACP completion patch forward to `@agentclientprotocol/claude-agent-acp` 0.36.1.
|
||||
- Agents/tools: remove the old sender-owner tool gating path so configured tools stay visible for trusted sessions while command and channel-action auth still carry real sender identity.
|
||||
- Tests/perf: isolate doctor core health check unit coverage from real skills/workspace discovery so `doctor-core-checks` no longer dominates unit perf while keeping one real skills-readiness smoke. (#84493) Thanks @frankekn.
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -105,7 +105,6 @@ describe("ClickClack account resolution", () => {
|
||||
enabled: true,
|
||||
reconnectMs: 1_500,
|
||||
replyMode: "agent",
|
||||
senderIsOwner: false,
|
||||
token: "ccb_live",
|
||||
workspace: "wsp_1",
|
||||
});
|
||||
@@ -125,7 +124,6 @@ describe("ClickClack account resolution", () => {
|
||||
replyMode: "model",
|
||||
model: "openai/gpt-5.4-mini",
|
||||
toolsAllow: ["web_search"],
|
||||
senderIsOwner: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -144,7 +142,6 @@ describe("ClickClack account resolution", () => {
|
||||
enabled: true,
|
||||
model: "openai/gpt-5.4-mini",
|
||||
replyMode: "model",
|
||||
senderIsOwner: true,
|
||||
token: "ccb_peter",
|
||||
toolsAllow: ["web_search"],
|
||||
workspace: "wsp_1",
|
||||
@@ -155,7 +152,6 @@ describe("ClickClack account resolution", () => {
|
||||
model: "openai/gpt-5.4-mini",
|
||||
reconnectMs: 1_500,
|
||||
replyMode: "model",
|
||||
senderIsOwner: true,
|
||||
token: "ccb_peter",
|
||||
toolsAllow: ["web_search"],
|
||||
workspace: "wsp_1",
|
||||
|
||||
@@ -126,7 +126,6 @@ export function resolveClickClackAccount(params: {
|
||||
systemPrompt: normalizeOptionalString(merged.systemPrompt),
|
||||
timeoutSeconds: merged.timeoutSeconds,
|
||||
toolsAllow: merged.toolsAllow,
|
||||
senderIsOwner: merged.senderIsOwner === true,
|
||||
defaultTo: merged.defaultTo?.trim() || "channel:general",
|
||||
allowFrom: merged.allowFrom ?? ["*"],
|
||||
reconnectMs: merged.reconnectMs ?? DEFAULT_RECONNECT_MS,
|
||||
|
||||
@@ -16,7 +16,6 @@ const ClickClackAccountConfigSchema = z
|
||||
systemPrompt: z.string().optional(),
|
||||
timeoutSeconds: z.number().int().min(1).max(3_600).optional(),
|
||||
toolsAllow: z.array(z.string()).optional(),
|
||||
senderIsOwner: z.boolean().optional(),
|
||||
defaultTo: z.string().optional(),
|
||||
allowFrom: z.array(z.string()).optional(),
|
||||
reconnectMs: z.number().int().min(100).max(60_000).optional(),
|
||||
|
||||
@@ -94,7 +94,6 @@ describe("handleClickClackInbound", () => {
|
||||
agentId: "service-bot",
|
||||
replyMode: "model",
|
||||
model: "openai/gpt-5.4-mini",
|
||||
senderIsOwner: false,
|
||||
toolsAllow: [],
|
||||
defaultTo: "channel:general",
|
||||
allowFrom: ["*"],
|
||||
|
||||
@@ -13,7 +13,6 @@ export type ClickClackAccountConfig = {
|
||||
systemPrompt?: string;
|
||||
timeoutSeconds?: number;
|
||||
toolsAllow?: string[];
|
||||
senderIsOwner?: boolean;
|
||||
defaultTo?: string;
|
||||
allowFrom?: string[];
|
||||
reconnectMs?: number;
|
||||
@@ -45,7 +44,6 @@ export type ResolvedClickClackAccount = {
|
||||
systemPrompt?: string;
|
||||
timeoutSeconds?: number;
|
||||
toolsAllow?: string[];
|
||||
senderIsOwner: boolean;
|
||||
defaultTo: string;
|
||||
allowFrom: string[];
|
||||
reconnectMs: number;
|
||||
|
||||
@@ -3343,7 +3343,6 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
allowGatewaySubagentBinding:
|
||||
params.allowGatewaySubagentBinding || isForcedPrivateQaCodexRuntime(),
|
||||
...sessionKeys,
|
||||
|
||||
@@ -104,8 +104,6 @@ export class AgentComponentButton extends Button {
|
||||
enqueueSystemEvent(eventText, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `discord:agent-button:${channelId}:${componentId}:${userId}`,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
|
||||
await ackComponentInteraction({ interaction, replyOpts, label: "agent button" });
|
||||
@@ -198,8 +196,6 @@ export class AgentSelectMenu extends StringSelectMenu {
|
||||
enqueueSystemEvent(eventText, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `discord:agent-select:${channelId}:${componentId}:${userId}`,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
|
||||
await ackComponentInteraction({ interaction, replyOpts, label: "agent select" });
|
||||
|
||||
@@ -501,8 +501,6 @@ async function handleDiscordReactionEvent(
|
||||
enqueueSystemEvent(text, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
};
|
||||
const shouldNotifyReaction = (options: {
|
||||
|
||||
@@ -724,8 +724,6 @@ export async function preflightDiscordMessage(
|
||||
enqueueSystemEvent(systemText, {
|
||||
sessionKey: effectiveRoute.sessionKey,
|
||||
contextKey: `discord:system:${messageChannelId}:${message.id}`,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -140,8 +140,6 @@ describe("agent components", () => {
|
||||
{
|
||||
sessionKey: defaultDmSessionKey,
|
||||
contextKey: "discord:agent-button:dm-channel:hello:123456789",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
);
|
||||
if (params.expectPairingStoreRead) {
|
||||
@@ -269,8 +267,6 @@ describe("agent components", () => {
|
||||
{
|
||||
sessionKey: defaultGroupDmSessionKey,
|
||||
contextKey: "discord:agent-button:group-dm-channel:hello:123456789",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
);
|
||||
expect(peekSystemEvents(defaultDmSessionKey)).toStrictEqual([]);
|
||||
@@ -351,8 +347,6 @@ describe("agent components", () => {
|
||||
{
|
||||
sessionKey: defaultDmSessionKey,
|
||||
contextKey: "discord:agent-select:dm-channel:hello:123456789",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
@@ -376,8 +370,6 @@ describe("agent components", () => {
|
||||
{
|
||||
sessionKey: defaultDmSessionKey,
|
||||
contextKey: "discord:agent-button:dm-channel:hello_cid:123456789",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
@@ -401,8 +393,6 @@ describe("agent components", () => {
|
||||
{
|
||||
sessionKey: defaultDmSessionKey,
|
||||
contextKey: "discord:agent-button:dm-channel:hello%2G:123456789",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
);
|
||||
expect(readAllowFromStoreMock).not.toHaveBeenCalled();
|
||||
|
||||
@@ -139,7 +139,6 @@ export async function runDiscordVoiceAgentTurn(params: {
|
||||
messageChannel: "discord",
|
||||
messageProvider: DISCORD_VOICE_MESSAGE_PROVIDER,
|
||||
extraSystemPrompt: context.extraSystemPrompt,
|
||||
senderIsOwner: context.senderIsOwner,
|
||||
allowModelOverride: Boolean(voiceModel),
|
||||
model: voiceModel,
|
||||
toolsAllow: params.toolsAllow,
|
||||
|
||||
@@ -2000,7 +2000,6 @@ describe("DiscordVoiceManager", () => {
|
||||
ownerTurn?.sendInputAudio(Buffer.alloc(8));
|
||||
await new Promise((resolve) => setTimeout(resolve, 260));
|
||||
|
||||
expect(lastAgentCommandArgs().senderIsOwner).toBe(false);
|
||||
expect(realtimeSessionMock.handleBargeIn).not.toHaveBeenCalled();
|
||||
expectUserMessageIncludes("non-owner answer");
|
||||
});
|
||||
@@ -2053,10 +2052,8 @@ describe("DiscordVoiceManager", () => {
|
||||
|
||||
const guestCommandArgs = agentCommandArgsAt(0);
|
||||
expect(guestCommandArgs.message).toContain("guest question");
|
||||
expect(guestCommandArgs.senderIsOwner).toBe(false);
|
||||
const ownerCommandArgs = agentCommandArgsAt(1);
|
||||
expect(ownerCommandArgs.message).toContain("owner question");
|
||||
expect(ownerCommandArgs.senderIsOwner).toBe(true);
|
||||
expectUserMessageIncludes("guest answer");
|
||||
expectUserMessageIncludes("owner answer");
|
||||
});
|
||||
@@ -2420,10 +2417,8 @@ describe("DiscordVoiceManager", () => {
|
||||
|
||||
const ownerCommandArgs = agentCommandArgsAt(0);
|
||||
expect(ownerCommandArgs.message).toContain("owner question");
|
||||
expect(ownerCommandArgs.senderIsOwner).toBe(true);
|
||||
const guestCommandArgs = agentCommandArgsAt(1);
|
||||
expect(guestCommandArgs.message).toContain("guest question");
|
||||
expect(guestCommandArgs.senderIsOwner).toBe(false);
|
||||
expect(realtimeSessionMock.submitToolResult).toHaveBeenCalledWith("call-owner", {
|
||||
text: "owner answer",
|
||||
});
|
||||
@@ -2652,7 +2647,6 @@ describe("DiscordVoiceManager", () => {
|
||||
expect(agentCommandMock).toHaveBeenCalledTimes(2);
|
||||
const followupCommandArgs = agentCommandArgsAt(1);
|
||||
expect(followupCommandArgs.message).toContain("guest followup");
|
||||
expect(followupCommandArgs.senderIsOwner).toBe(false);
|
||||
expectUserMessageIncludes("guest answer");
|
||||
});
|
||||
|
||||
@@ -2788,7 +2782,6 @@ describe("DiscordVoiceManager", () => {
|
||||
bridgeParams?.onTranscript?.("user", "guest question", true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 260));
|
||||
|
||||
expect(lastAgentCommandArgs().senderIsOwner).toBe(false);
|
||||
expectUserMessageIncludes("guest answer");
|
||||
});
|
||||
|
||||
@@ -2880,7 +2873,6 @@ describe("DiscordVoiceManager", () => {
|
||||
);
|
||||
expect(workingToolResultCall?.[2]).toEqual({ willContinue: true });
|
||||
const commandArgs = lastAgentCommandArgs();
|
||||
expect(commandArgs.senderIsOwner).toBe(true);
|
||||
expect(commandArgs.toolsAllow).toEqual([
|
||||
"read",
|
||||
"web_search",
|
||||
@@ -3074,7 +3066,6 @@ describe("DiscordVoiceManager", () => {
|
||||
await Promise.resolve();
|
||||
|
||||
const commandArgs = lastAgentCommandArgs();
|
||||
expect(commandArgs.senderIsOwner).toBe(false);
|
||||
expect(commandArgs.toolsAllow).toEqual([
|
||||
"read",
|
||||
"web_search",
|
||||
@@ -3147,7 +3138,6 @@ describe("DiscordVoiceManager", () => {
|
||||
await Promise.resolve();
|
||||
|
||||
const commandArgs = lastAgentCommandArgs();
|
||||
expect(commandArgs.senderIsOwner).toBe(false);
|
||||
expect(commandArgs.toolsAllow).toEqual([
|
||||
"read",
|
||||
"web_search",
|
||||
@@ -3447,7 +3437,7 @@ describe("DiscordVoiceManager", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("passes senderIsOwner=true for allowlisted voice speakers", async () => {
|
||||
it("accepts allowlisted voice speakers", async () => {
|
||||
const client = createClient();
|
||||
client.fetchMember.mockResolvedValue({
|
||||
nickname: "Owner Nick",
|
||||
@@ -3460,12 +3450,9 @@ describe("DiscordVoiceManager", () => {
|
||||
});
|
||||
const manager = createManager({ groupPolicy: "open", allowFrom: ["discord:u-owner"] }, client);
|
||||
await processVoiceSegment(manager, "u-owner");
|
||||
|
||||
const commandArgs = lastAgentCommandArgs() as { senderIsOwner?: boolean } | undefined;
|
||||
expect(commandArgs?.senderIsOwner).toBe(true);
|
||||
});
|
||||
|
||||
it("passes senderIsOwner=false for non-owner voice speakers", async () => {
|
||||
it("accepts open-policy voice speakers", async () => {
|
||||
const client = createClient();
|
||||
client.fetchMember.mockResolvedValue({
|
||||
nickname: "Guest Nick",
|
||||
@@ -3480,9 +3467,6 @@ describe("DiscordVoiceManager", () => {
|
||||
commands: { useAccessGroups: false },
|
||||
});
|
||||
await processVoiceSegment(manager, "u-guest");
|
||||
|
||||
const commandArgs = lastAgentCommandArgs() as { senderIsOwner?: boolean } | undefined;
|
||||
expect(commandArgs?.senderIsOwner).toBe(false);
|
||||
});
|
||||
|
||||
it("passes configured model override to agent command in voice flow", async () => {
|
||||
|
||||
@@ -33,8 +33,6 @@ describe("enqueueIMessageReactionSystemEvent", () => {
|
||||
{
|
||||
sessionKey: "agent:main:main",
|
||||
contextKey: "imessage:reaction:added:3:lobster-reply-guid:+15555550123:👎",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
);
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
|
||||
@@ -23,8 +23,6 @@ export function enqueueIMessageReactionSystemEvent(params: {
|
||||
const queued = enqueueSystemEvent(decision.text, {
|
||||
sessionKey: decision.route.sessionKey,
|
||||
contextKey: decision.contextKey,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
runtime.log?.(
|
||||
`imessage: reaction system event ${queued ? "queued" : "deduped"} session=${
|
||||
|
||||
@@ -97,8 +97,8 @@ describe("matrixMessageActions account propagation", () => {
|
||||
await matrixMessageActions.handleAction?.(
|
||||
createContext({
|
||||
action: profileAction,
|
||||
senderIsOwner: true,
|
||||
accountId: "ops",
|
||||
senderIsOwner: true,
|
||||
params: {
|
||||
displayName: "Ops Bot",
|
||||
avatarUrl: "mxc://example/avatar",
|
||||
@@ -115,53 +115,46 @@ describe("matrixMessageActions account propagation", () => {
|
||||
expect(call.options).toEqual({ mediaLocalRoots: undefined });
|
||||
});
|
||||
|
||||
it("rejects self-profile updates for non-owner callers", async () => {
|
||||
try {
|
||||
await matrixMessageActions.handleAction?.(
|
||||
it("rejects self-profile updates without sender owner context", async () => {
|
||||
await expect(
|
||||
matrixMessageActions.handleAction?.(
|
||||
createContext({
|
||||
action: profileAction,
|
||||
senderIsOwner: false,
|
||||
accountId: "ops",
|
||||
params: {
|
||||
displayName: "Ops Bot",
|
||||
},
|
||||
}),
|
||||
);
|
||||
throw new Error("expected non-owner self-profile update to reject");
|
||||
} catch (error) {
|
||||
expect((error as Error).name).toBe("ToolAuthorizationError");
|
||||
expect((error as Error).message).toBe("Matrix profile updates require owner access.");
|
||||
}
|
||||
|
||||
expect(mocks.handleMatrixAction).not.toHaveBeenCalled();
|
||||
),
|
||||
).rejects.toThrow("Matrix profile updates require owner access.");
|
||||
});
|
||||
|
||||
it("rejects self-profile updates when owner status is unknown", async () => {
|
||||
try {
|
||||
await matrixMessageActions.handleAction?.(
|
||||
createContext({
|
||||
action: profileAction,
|
||||
accountId: "ops",
|
||||
params: {
|
||||
displayName: "Ops Bot",
|
||||
},
|
||||
}),
|
||||
);
|
||||
throw new Error("expected unknown-owner self-profile update to reject");
|
||||
} catch (error) {
|
||||
expect((error as Error).name).toBe("ToolAuthorizationError");
|
||||
expect((error as Error).message).toBe("Matrix profile updates require owner access.");
|
||||
}
|
||||
it("dispatches self-profile updates with sender owner context", async () => {
|
||||
await matrixMessageActions.handleAction?.(
|
||||
createContext({
|
||||
action: profileAction,
|
||||
accountId: "ops",
|
||||
senderIsOwner: true,
|
||||
params: {
|
||||
displayName: "Ops Bot",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mocks.handleMatrixAction).not.toHaveBeenCalled();
|
||||
const call = matrixActionCall();
|
||||
expect(call.input).toMatchObject({
|
||||
action: "setProfile",
|
||||
accountId: "ops",
|
||||
displayName: "Ops Bot",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards local avatar paths for self-profile updates", async () => {
|
||||
await matrixMessageActions.handleAction?.(
|
||||
createContext({
|
||||
action: profileAction,
|
||||
senderIsOwner: true,
|
||||
accountId: "ops",
|
||||
senderIsOwner: true,
|
||||
params: {
|
||||
path: "/tmp/avatar.jpg",
|
||||
},
|
||||
|
||||
@@ -108,20 +108,7 @@ describe("matrixMessageActions", () => {
|
||||
expect(properties.avatarPath).toHaveProperty("type", "string");
|
||||
});
|
||||
|
||||
it("hides self-profile updates for non-owner discovery", () => {
|
||||
const discovery = matrixMessageActions.describeMessageTool({
|
||||
cfg: createConfiguredMatrixConfig(),
|
||||
senderIsOwner: false,
|
||||
} as never);
|
||||
if (!discovery) {
|
||||
throw new Error("describeMessageTool returned null");
|
||||
}
|
||||
|
||||
expect(discovery.actions).not.toContain(profileAction);
|
||||
expect(discovery.schema).toBeNull();
|
||||
});
|
||||
|
||||
it("hides self-profile updates when owner status is unknown", () => {
|
||||
it("hides self-profile updates without owner identity context", () => {
|
||||
const discovery = matrixMessageActions.describeMessageTool({
|
||||
cfg: createConfiguredMatrixConfig(),
|
||||
} as never);
|
||||
@@ -130,7 +117,6 @@ describe("matrixMessageActions", () => {
|
||||
}
|
||||
|
||||
expect(discovery.actions).not.toContain(profileAction);
|
||||
expect(discovery.schema).toBeNull();
|
||||
});
|
||||
|
||||
it("hides gated actions when the default Matrix account disables them", () => {
|
||||
|
||||
@@ -645,10 +645,7 @@ describe("msteams monitor handler authz", () => {
|
||||
if (!systemEventCall) {
|
||||
throw new Error("expected skipped Teams message system event");
|
||||
}
|
||||
expect(systemEventCall[1]).toMatchObject({
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
expect(systemEventCall[1]).toMatchObject({});
|
||||
});
|
||||
|
||||
it("keeps dispatched primary message system events owner-neutral", async () => {
|
||||
@@ -690,12 +687,6 @@ describe("msteams monitor handler authz", () => {
|
||||
if (!systemEventCall) {
|
||||
throw new Error("expected active Teams message system event");
|
||||
}
|
||||
expect(systemEventCall[1]).not.toMatchObject({
|
||||
forceSenderIsOwnerFalse: true,
|
||||
});
|
||||
expect(systemEventCall[1]).not.toMatchObject({
|
||||
trusted: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("authorizes text control commands from static access groups", async () => {
|
||||
|
||||
@@ -43,8 +43,6 @@ describe("msteams thread parent context injection", () => {
|
||||
{
|
||||
sessionKey: string;
|
||||
contextKey?: string;
|
||||
forceSenderIsOwnerFalse?: boolean;
|
||||
trusted?: boolean;
|
||||
},
|
||||
];
|
||||
|
||||
@@ -102,10 +100,7 @@ describe("msteams thread parent context injection", () => {
|
||||
expect(parentCall[0]).toBe("Replying to @Alice: Can someone investigate the latency spike?");
|
||||
expect(parentCall[1]?.contextKey).toContain("msteams:thread-parent:");
|
||||
expect(parentCall[1]?.contextKey).toContain("thread-root-123");
|
||||
expect(parentCall[1]).toMatchObject({
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
expect(parentCall[1]).toMatchObject({});
|
||||
});
|
||||
|
||||
it("caches parent fetches across thread replies in the same session", async () => {
|
||||
|
||||
@@ -498,14 +498,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
? `Teams DM from ${senderName}`
|
||||
: `Teams message in ${conversationType} from ${senderName}`;
|
||||
|
||||
const enqueuePrimaryMessageSystemEvent = (opts?: {
|
||||
forceSenderIsOwnerFalse?: boolean;
|
||||
trusted?: boolean;
|
||||
}) =>
|
||||
const enqueuePrimaryMessageSystemEvent = () =>
|
||||
core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `msteams:message:${conversationId}:${activity.id ?? "unknown"}`,
|
||||
...opts,
|
||||
});
|
||||
|
||||
const channelId = conversationId;
|
||||
@@ -541,10 +537,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
requireMention,
|
||||
mentioned,
|
||||
});
|
||||
enqueuePrimaryMessageSystemEvent({
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
enqueuePrimaryMessageSystemEvent();
|
||||
createChannelHistoryWindow({ historyMap: conversationHistories }).record({
|
||||
historyKey: conversationId,
|
||||
limit: historyLimit,
|
||||
@@ -675,8 +668,6 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `msteams:thread-parent:${conversationId}:${activity.replyToId}`,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
markParentContextInjected(route.sessionKey, activity.replyToId);
|
||||
}
|
||||
|
||||
@@ -207,8 +207,6 @@ describe("createMSTeamsReactionHandler", () => {
|
||||
expect(label).toContain("added");
|
||||
expect(meta.sessionKey).toBe("test-session");
|
||||
expect(meta.contextKey).toContain("added");
|
||||
expect(meta.forceSenderIsOwnerFalse).toBe(true);
|
||||
expect(meta.trusted).toBe(false);
|
||||
});
|
||||
|
||||
it("enqueues system event for reactionsRemoved", async () => {
|
||||
|
||||
@@ -116,8 +116,6 @@ export function createMSTeamsReactionHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
core.system.enqueueSystemEvent(label, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey: `msteams:reaction:${conversationId}:${targetMessageId}:${senderId}:${reactionType}:${direction}`,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,15 +33,9 @@ describe("bridge/tools/remind", () => {
|
||||
callGatewayToolMock.mockResolvedValue({ ok: true });
|
||||
});
|
||||
|
||||
it("marks qqbot_remind as owner-only", () => {
|
||||
const tool = createRemindTool();
|
||||
expect(tool.ownerOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("schedules reminders directly through Gateway cron with ambient QQ delivery context", async () => {
|
||||
callGatewayToolMock.mockResolvedValue({ id: "job-1" });
|
||||
const tool = createRemindTool({
|
||||
senderIsOwner: true,
|
||||
deliveryContext: { to: "qqbot:c2c:user-openid", accountId: "bot2" },
|
||||
});
|
||||
|
||||
@@ -73,7 +67,7 @@ describe("bridge/tools/remind", () => {
|
||||
});
|
||||
|
||||
it("routes list and remove through Gateway cron without exposing generic cron to the model", async () => {
|
||||
const tool = createRemindTool({ senderIsOwner: true });
|
||||
const tool = createRemindTool({});
|
||||
|
||||
await tool.execute("tool-call-1", { action: "list" });
|
||||
await tool.execute("tool-call-2", { action: "remove", jobId: "job-1" });
|
||||
@@ -91,7 +85,6 @@ describe("bridge/tools/remind", () => {
|
||||
const callCron = vi.fn(async (_params: unknown) => ({ id: "job-1" }));
|
||||
const tool = createRemindTool(
|
||||
{
|
||||
senderIsOwner: true,
|
||||
deliveryContext: { to: "qqbot:c2c:user-openid", accountId: "bot2" },
|
||||
},
|
||||
{ callCron },
|
||||
@@ -117,7 +110,7 @@ describe("bridge/tools/remind", () => {
|
||||
expect(callGatewayToolMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not schedule when sender ownership is missing", async () => {
|
||||
it("schedules when sender ownership is missing", async () => {
|
||||
const callCron = vi.fn(async (_params: unknown) => ({ id: "job-1" }));
|
||||
const tool = createRemindTool(
|
||||
{
|
||||
@@ -126,16 +119,13 @@ describe("bridge/tools/remind", () => {
|
||||
{ callCron },
|
||||
);
|
||||
|
||||
const result = await tool.execute("tool-call-1", {
|
||||
await tool.execute("tool-call-1", {
|
||||
action: "add",
|
||||
content: "drink water",
|
||||
time: "5m",
|
||||
});
|
||||
|
||||
expect(callCron).not.toHaveBeenCalled();
|
||||
expect(callCron).toHaveBeenCalledTimes(1);
|
||||
expect(callGatewayToolMock).not.toHaveBeenCalled();
|
||||
expect(result.details).toEqual({
|
||||
error: "QQ reminders require an owner-authorized sender.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,7 +49,6 @@ export function createRemindTool(
|
||||
return {
|
||||
name: "qqbot_remind",
|
||||
label: "QQBot Reminder",
|
||||
ownerOnly: true,
|
||||
description:
|
||||
"Create, list, and remove QQ reminders. " +
|
||||
"This tool schedules Gateway cron jobs directly; do not call the cron tool after it succeeds.\n" +
|
||||
@@ -60,19 +59,6 @@ export function createRemindTool(
|
||||
'Time examples: "5m", "1h", "0 8 * * *"',
|
||||
parameters: RemindSchema,
|
||||
async execute(_toolCallId, params) {
|
||||
if (toolContext.senderIsOwner !== true) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: JSON.stringify({
|
||||
error: "QQ reminders require an owner-authorized sender.",
|
||||
}),
|
||||
},
|
||||
],
|
||||
details: { error: "QQ reminders require an owner-authorized sender." },
|
||||
};
|
||||
}
|
||||
const ctx = getRequestContext();
|
||||
return await executeScheduledRemind(
|
||||
params as RemindParams,
|
||||
|
||||
+1
-3
@@ -75,9 +75,7 @@ function hasQueuedReactionEventFor(sender: string) {
|
||||
typeof options === "object" &&
|
||||
options !== null &&
|
||||
"sessionKey" in options &&
|
||||
(options as { sessionKey?: string; forceSenderIsOwnerFalse?: boolean }).sessionKey ===
|
||||
route.sessionKey &&
|
||||
(options as { forceSenderIsOwnerFalse?: boolean }).forceSenderIsOwnerFalse === true
|
||||
(options as { sessionKey?: string }).sessionKey === route.sessionKey
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -518,8 +518,6 @@ describe("signal createSignalEventHandler inbound context", () => {
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith("reaction added", {
|
||||
sessionKey: "agent:main:signal:group:g1",
|
||||
contextKey: "signal:reaction:added:1700000000000:+15550001111:+1:g1",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -490,8 +490,6 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
enqueueSystemEvent(text, {
|
||||
sessionKey: route.sessionKey,
|
||||
contextKey,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -81,8 +81,6 @@ describe("registerSlackChannelEvents", () => {
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith("Slack channel created: #general.", {
|
||||
sessionKey: "agent:main:main",
|
||||
contextKey: "slack:channel:created:C1",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,8 +46,6 @@ export function registerSlackChannelEvents(params: {
|
||||
enqueueSystemEvent(`Slack channel ${params.kind}: ${label}.`, {
|
||||
sessionKey,
|
||||
contextKey: `slack:channel:${params.kind}:${params.channelId ?? params.channelName ?? "unknown"}`,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -794,8 +794,6 @@ function enqueueSlackBlockActionEvent(params: {
|
||||
accountId: params.ctx.accountId,
|
||||
threadId: params.parsed.threadTs,
|
||||
},
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
if (queued) {
|
||||
requestHeartbeat({
|
||||
|
||||
@@ -764,8 +764,6 @@ describe("registerSlackInteractionEvents", () => {
|
||||
to: "channel:C1",
|
||||
},
|
||||
sessionKey: "agent:ops:slack:channel:C1",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
);
|
||||
expect(resolveSessionKey).toHaveBeenCalledWith({
|
||||
|
||||
@@ -230,8 +230,6 @@ describe("registerSlackReactionEvents", () => {
|
||||
expect(reactionQueueMock).toHaveBeenCalledWith(expect.any(String), {
|
||||
sessionKey: "agent:main:main",
|
||||
contextKey: "slack:reaction:added:D1:123.456:U1:thumbsup",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -88,8 +88,6 @@ export function registerSlackReactionEvents(params: {
|
||||
enqueueSystemEvent(text, {
|
||||
sessionKey: ingressContext.sessionKey,
|
||||
contextKey: `slack:reaction:${action}:${item.channel}:${item.ts}:${event.user}:${emojiLabel}`,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.runtime.error?.(danger(`slack reaction handler failed: ${formatErrorMessage(err)}`));
|
||||
|
||||
@@ -164,8 +164,6 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith("Slack DM from Alice: hi", {
|
||||
sessionKey: prepared.ctxPayload.SessionKey,
|
||||
contextKey: "slack:message:D123:1.000",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1116,8 +1116,6 @@ export async function prepareSlackMessage(params: {
|
||||
enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
||||
sessionKey,
|
||||
contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
|
||||
const envelopeFrom =
|
||||
|
||||
@@ -12,7 +12,6 @@ export function createWhatsAppLoginTool(): ChannelAgentTool {
|
||||
return {
|
||||
label: "WhatsApp Login",
|
||||
name: "whatsapp_login",
|
||||
ownerOnly: true,
|
||||
description: "Generate a WhatsApp QR code for linking, or wait for the scan to complete.",
|
||||
// NOTE: Using Type.Unsafe for action enum instead of Type.Union([Type.Literal(...)]
|
||||
// because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
|
||||
|
||||
@@ -521,7 +521,6 @@ export async function monitorWebChannel(
|
||||
});
|
||||
enqueueSystemEvent(`WhatsApp gateway connected${selfE164 ? ` as ${selfE164}` : ""}.`, {
|
||||
sessionKey: connectRoute.sessionKey,
|
||||
trusted: true,
|
||||
});
|
||||
|
||||
const normalizedAccountId = normalizeReconnectAccountId(account.accountId);
|
||||
@@ -615,7 +614,6 @@ export async function monitorWebChannel(
|
||||
`WhatsApp gateway disconnected (status ${decision.normalized.statusLabel})`,
|
||||
{
|
||||
sessionKey: connectRoute.sessionKey,
|
||||
trusted: true,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -2695,62 +2695,6 @@ rules:
|
||||
detector-bucket: precise
|
||||
source-run: 2026-04-17T07-37-10Z
|
||||
source-rule-id: replay-key-derived-from-idempotency-header
|
||||
- id: ghsa-gfmx-pph7-g46x.openclaw.system-event-missing-explicit-trust
|
||||
languages:
|
||||
- typescript
|
||||
- javascript
|
||||
severity: ERROR
|
||||
message: |
|
||||
enqueueSystemEvent() is called with interpolated or variable text without an explicit owner downgrade. External content — channel messages, user IDs, event payloads, exec output — MUST be explicitly downgraded with `forceSenderIsOwnerFalse: true` (or legacy `trusted: false`) to prevent prompt injection. See GHSA-GFMX-PPH7-G46X.
|
||||
TRIAGE NOTE: If ALL interpolated values in the template literal are boolean flags or enum/const expressions (e.g. `${x ? "on" : "off"}`), or if the variable text is formatted from fully-internal state (not external channel content), the finding may be low-risk. Add `trusted: true` explicitly to self-document that the text is intentionally trusted.
|
||||
metadata:
|
||||
category: security
|
||||
cwe:
|
||||
- CWE-74
|
||||
- CWE-77
|
||||
ghsas:
|
||||
- GHSA-GFMX-PPH7-G46X
|
||||
ghsa: GHSA-GFMX-PPH7-G46X
|
||||
advisory-url: https://github.com/openclaw/openclaw/security/advisories/GHSA-GFMX-PPH7-G46X
|
||||
detector-bucket: precise
|
||||
source-run: 2026-04-17T07-37-10Z
|
||||
source-rule-id: openclaw.system-event-missing-explicit-trust
|
||||
paths:
|
||||
exclude:
|
||||
- "**/*.test.*"
|
||||
- "**/*.spec.*"
|
||||
- src/auto-reply/reply/directive-handling.impl.ts
|
||||
- src/auto-reply/reply/directive-handling.persist.ts
|
||||
- src/auto-reply/reply/get-reply-directives-apply.ts
|
||||
- src/gateway/config-recovery-notice.ts
|
||||
- src/gateway/server-restart-sentinel.ts
|
||||
- src/infra/session-maintenance-warning.ts
|
||||
pattern-either:
|
||||
- patterns:
|
||||
- pattern: |
|
||||
enqueueSystemEvent(`...${$X}...`, $OPTS)
|
||||
- pattern-not: |
|
||||
enqueueSystemEvent(`...${$X}...`, { ..., trusted: $V, ... })
|
||||
- pattern-not: |
|
||||
enqueueSystemEvent(`...${$X}...`, { ..., forceSenderIsOwnerFalse: true, ... })
|
||||
- pattern-not-inside: |
|
||||
enqueueSystemEvent(`...${$X}...`, { ..., trusted: $V, ... })
|
||||
- pattern-not-inside: |
|
||||
enqueueSystemEvent(`...${$X}...`, { ..., forceSenderIsOwnerFalse: true, ... })
|
||||
- patterns:
|
||||
- pattern: |
|
||||
enqueueSystemEvent($TEXT, $OPTS)
|
||||
- pattern-not: |
|
||||
enqueueSystemEvent($TEXT, { ..., trusted: $V, ... })
|
||||
- pattern-not: |
|
||||
enqueueSystemEvent($TEXT, { ..., forceSenderIsOwnerFalse: true, ... })
|
||||
- pattern-not-inside: |
|
||||
enqueueSystemEvent($TEXT, { ..., trusted: $V, ... })
|
||||
- pattern-not-inside: |
|
||||
enqueueSystemEvent($TEXT, { ..., forceSenderIsOwnerFalse: true, ... })
|
||||
- metavariable-regex:
|
||||
metavariable: $TEXT
|
||||
regex: ^[a-zA-Z_$][a-zA-Z0-9_$]*$
|
||||
- id: ghsa-gg9v-mgcp-v6m7.openclaw-bootstrap-token-legacy-acceptance-without-profile-record
|
||||
languages:
|
||||
- typescript
|
||||
@@ -4802,7 +4746,7 @@ rules:
|
||||
source-run: 2026-04-17T07-37-10Z
|
||||
source-rule-id: gateway-http-json-endpoint-missing-required-operator-method
|
||||
- id: ghsa-x2m8-53h4-6hch.discord-voice-ingress-missing-authorize-review
|
||||
message: Discord voice ingress forwards senderIsOwner to agentCommandFromIngress without calling authorizeDiscordVoiceIngress in the same file. See GHSA-X2M8-53H4-6HCH.
|
||||
message: Discord voice ingress calls agentCommandFromIngress without calling authorizeDiscordVoiceIngress in the same file. See GHSA-X2M8-53H4-6HCH.
|
||||
severity: ERROR
|
||||
languages:
|
||||
- typescript
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("classifyAcpToolApproval", () => {
|
||||
expectedClass: "exec_capable",
|
||||
},
|
||||
] as const)(
|
||||
"classifies shared owner-only ACP backstops for $expectedToolName",
|
||||
"classifies shared ACP backstop tools for $expectedToolName",
|
||||
({ title, rawInput, expectedToolName, expectedClass }) => {
|
||||
expect(
|
||||
classify({
|
||||
|
||||
@@ -2,7 +2,6 @@ import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { isKnownCoreToolId } from "../agents/tool-catalog.js";
|
||||
import { isMutatingToolCall } from "../agents/tool-mutation.js";
|
||||
import { resolveOwnerOnlyToolApprovalClass } from "../agents/tool-policy.js";
|
||||
import { isPathInside } from "../infra/path-guards.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
@@ -19,8 +18,15 @@ const EXEC_CAPABLE_TOOL_IDS = new Set([
|
||||
"bash",
|
||||
"process",
|
||||
"code_execution",
|
||||
"nodes",
|
||||
]);
|
||||
const CONTROL_PLANE_TOOL_IDS = new Set([
|
||||
"cron",
|
||||
"gateway",
|
||||
"sessions_spawn",
|
||||
"sessions_send",
|
||||
"session_status",
|
||||
]);
|
||||
const CONTROL_PLANE_TOOL_IDS = new Set(["sessions_spawn", "sessions_send", "session_status"]);
|
||||
|
||||
export type AcpApprovalClass =
|
||||
| "readonly_scoped"
|
||||
@@ -209,10 +215,6 @@ export function classifyAcpToolApproval(params: {
|
||||
if (SAFE_SEARCH_TOOL_IDS.has(toolName) && isTrustedToolId) {
|
||||
return { toolName, approvalClass: "readonly_search", autoApprove: true };
|
||||
}
|
||||
const ownerOnlyApprovalClass = resolveOwnerOnlyToolApprovalClass(toolName);
|
||||
if (ownerOnlyApprovalClass) {
|
||||
return { toolName, approvalClass: ownerOnlyApprovalClass, autoApprove: false };
|
||||
}
|
||||
if (EXEC_CAPABLE_TOOL_IDS.has(toolName)) {
|
||||
return { toolName, approvalClass: "exec_capable", autoApprove: false };
|
||||
}
|
||||
|
||||
@@ -437,7 +437,7 @@ describe("resolvePermissionRequest", () => {
|
||||
},
|
||||
},
|
||||
] as const)(
|
||||
"prompts for shared owner-only backstop tools: $toolName",
|
||||
"prompts for shared backstop tools: $toolName",
|
||||
async ({ toolName, title, rawInput }) => {
|
||||
const prompt = vi.fn(async () => true);
|
||||
const res = await resolvePermissionRequest(
|
||||
|
||||
@@ -145,8 +145,6 @@ describe("startAcpSpawnParentStreamRelay", () => {
|
||||
contextKey?: string;
|
||||
sessionKey?: string;
|
||||
deliveryContext?: unknown;
|
||||
forceSenderIsOwnerFalse?: boolean;
|
||||
trusted?: boolean;
|
||||
},
|
||||
]
|
||||
>;
|
||||
@@ -155,30 +153,22 @@ describe("startAcpSpawnParentStreamRelay", () => {
|
||||
contextKey: options.contextKey,
|
||||
sessionKey: options.sessionKey,
|
||||
deliveryContext: options.deliveryContext,
|
||||
forceSenderIsOwnerFalse: options.forceSenderIsOwnerFalse,
|
||||
trusted: options.trusted,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
contextKey: "acp-spawn:run-1:start",
|
||||
sessionKey: "agent:main:main",
|
||||
deliveryContext,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
{
|
||||
contextKey: "acp-spawn:run-1:progress",
|
||||
sessionKey: "agent:main:main",
|
||||
deliveryContext,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
{
|
||||
contextKey: "acp-spawn:run-1:done",
|
||||
sessionKey: "agent:main:main",
|
||||
deliveryContext,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
},
|
||||
]);
|
||||
const heartbeatCalls = requestHeartbeatMock.mock.calls as Array<
|
||||
@@ -233,11 +223,10 @@ describe("startAcpSpawnParentStreamRelay", () => {
|
||||
);
|
||||
expect(progressEvent?.[0]).toContain("codex: hello from child");
|
||||
const progressOptions = progressEvent?.[1] as
|
||||
| { contextKey?: unknown; sessionKey?: unknown; forceSenderIsOwnerFalse?: unknown }
|
||||
| { contextKey?: unknown; sessionKey?: unknown }
|
||||
| undefined;
|
||||
expect(progressOptions?.contextKey).toBe("acp-spawn:run-cron:progress");
|
||||
expect(progressOptions?.sessionKey).toBe("global");
|
||||
expect(progressOptions?.forceSenderIsOwnerFalse).toBe(true);
|
||||
const heartbeatOptions = firstMockCall(requestHeartbeatMock, "heartbeat request")[0] as
|
||||
| { agentId?: string; reason?: string }
|
||||
| undefined;
|
||||
|
||||
@@ -234,8 +234,6 @@ export function startAcpSpawnParentStreamRelay(params: {
|
||||
sessionKey: resolveEventSessionKey(parentSessionKey, params.mainKey, params.sessionScope),
|
||||
contextKey,
|
||||
deliveryContext: params.deliveryContext,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
wake();
|
||||
};
|
||||
|
||||
@@ -706,7 +706,6 @@ async function runBasicAgentCommand() {
|
||||
await agentCommand({
|
||||
message: "hello",
|
||||
to: "+1234567890",
|
||||
senderIsOwner: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -855,7 +854,6 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
await agentCommand({
|
||||
message: "hello",
|
||||
to: "+1234567890",
|
||||
senderIsOwner: true,
|
||||
thinking: "xhigh",
|
||||
});
|
||||
|
||||
@@ -914,7 +912,6 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
await agentCommand({
|
||||
message: "hello",
|
||||
to: "+1234567890",
|
||||
senderIsOwner: true,
|
||||
thinking: "xhigh",
|
||||
});
|
||||
|
||||
@@ -1029,7 +1026,6 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
await agentCommand({
|
||||
message: "internal handoff",
|
||||
to: "+1234567890",
|
||||
senderIsOwner: true,
|
||||
suppressPromptPersistence: true,
|
||||
});
|
||||
|
||||
@@ -1045,7 +1041,6 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
agentCommand({
|
||||
message: "hello",
|
||||
to: "+1234567890",
|
||||
senderIsOwner: true,
|
||||
}),
|
||||
).rejects.toThrow("provider down");
|
||||
|
||||
@@ -1372,7 +1367,6 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
INTERNAL_RUNTIME_CONTEXT_END,
|
||||
].join("\n"),
|
||||
sessionKey: "agent:main",
|
||||
senderIsOwner: true,
|
||||
internalEvents: [
|
||||
{
|
||||
type: "task_completion",
|
||||
@@ -1423,7 +1417,6 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
await agentCommand({
|
||||
message: "bootstrap ACP child",
|
||||
sessionKey: "agent:main",
|
||||
senderIsOwner: true,
|
||||
acpTurnSource: "manual_spawn",
|
||||
});
|
||||
|
||||
@@ -1448,7 +1441,6 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
agentCommand({
|
||||
message: "automatic ACP turn",
|
||||
sessionKey: "agent:main",
|
||||
senderIsOwner: true,
|
||||
}),
|
||||
).rejects.toThrow("ACP dispatch is disabled");
|
||||
|
||||
|
||||
@@ -300,10 +300,7 @@ function normalizeExplicitOverrideInput(raw: string, kind: "provider" | "model")
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
async function prepareAgentCommandExecution(
|
||||
opts: AgentCommandOpts & { senderIsOwner: boolean },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
async function prepareAgentCommandExecution(opts: AgentCommandOpts, runtime: RuntimeEnv) {
|
||||
const isRawModelRun = opts.modelRun === true || opts.promptMode === "none";
|
||||
const message = opts.message ?? "";
|
||||
if (!message.trim()) {
|
||||
@@ -483,7 +480,7 @@ async function prepareAgentCommandExecution(
|
||||
}
|
||||
|
||||
async function agentCommandInternal(
|
||||
opts: AgentCommandOpts & { senderIsOwner: boolean },
|
||||
opts: AgentCommandOpts,
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
deps?: CliDeps,
|
||||
) {
|
||||
@@ -1474,7 +1471,6 @@ async function agentCommandInternal(
|
||||
skillsSnapshot,
|
||||
messageChannel,
|
||||
agentAccountId: runContext.accountId,
|
||||
senderIsOwner: opts.senderIsOwner,
|
||||
thinkLevel: resolvedThinkLevel,
|
||||
extraSystemPrompt: opts.extraSystemPrompt,
|
||||
});
|
||||
@@ -1606,7 +1602,7 @@ export async function agentCommand(
|
||||
{
|
||||
...opts,
|
||||
// agentCommand is the trusted-operator entrypoint used by CLI/local flows.
|
||||
// Ingress callers must opt into owner semantics explicitly via
|
||||
// Ingress callers must opt into owner identity explicitly via
|
||||
// agentCommandFromIngress so network-facing paths cannot inherit this default by accident.
|
||||
senderIsOwner: opts.senderIsOwner ?? true,
|
||||
// Local/CLI callers are trusted by default for per-run model overrides.
|
||||
@@ -1623,20 +1619,11 @@ export async function agentCommandFromIngress(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
deps?: CliDeps,
|
||||
) {
|
||||
if (typeof opts.senderIsOwner !== "boolean") {
|
||||
// HTTP/WS ingress must declare the trust level explicitly at the boundary.
|
||||
// This keeps network-facing callers from silently picking up the local trusted default.
|
||||
throw new Error("senderIsOwner must be explicitly set for ingress agent runs.");
|
||||
}
|
||||
if (typeof opts.allowModelOverride !== "boolean") {
|
||||
throw new Error("allowModelOverride must be explicitly set for ingress agent runs.");
|
||||
}
|
||||
return await agentCommandInternal(
|
||||
{
|
||||
...opts,
|
||||
senderIsOwner: opts.senderIsOwner,
|
||||
allowModelOverride: opts.allowModelOverride,
|
||||
},
|
||||
{ ...opts, senderIsOwner: opts.senderIsOwner === true },
|
||||
runtime,
|
||||
deps,
|
||||
);
|
||||
|
||||
@@ -197,7 +197,7 @@ async function runAuthContractAttempt(params: {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: AUTH_PROFILE_RUNTIME_CONTRACT.runId,
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
|
||||
@@ -489,8 +489,6 @@ describe("emitExecSystemEvent", () => {
|
||||
to: "telegram:-100123:topic:47",
|
||||
threadId: 47,
|
||||
},
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
const heartbeat = requireHeartbeatCall();
|
||||
expect(heartbeat.coalesceMs).toBe(0);
|
||||
@@ -508,8 +506,6 @@ describe("emitExecSystemEvent", () => {
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith("Exec finished", {
|
||||
sessionKey: "agent:ops:primary",
|
||||
contextKey: "exec:run-cron",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
expect(requestHeartbeatMock).toHaveBeenCalledTimes(1);
|
||||
const [[heartbeatParams]] = requestHeartbeatMock.mock.calls as unknown as Array<
|
||||
@@ -530,8 +526,6 @@ describe("emitExecSystemEvent", () => {
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith("Exec finished", {
|
||||
sessionKey: "global",
|
||||
contextKey: "exec:run-global",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
expect(requestHeartbeatMock).toHaveBeenCalledTimes(1);
|
||||
const [[heartbeatParams]] = requestHeartbeatMock.mock.calls as unknown as Array<
|
||||
@@ -552,8 +546,6 @@ describe("emitExecSystemEvent", () => {
|
||||
expect(enqueueSystemEventMock).toHaveBeenCalledWith("Exec finished", {
|
||||
sessionKey: "global",
|
||||
contextKey: "exec:run-global",
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
const heartbeat = requireHeartbeatCall();
|
||||
expect(heartbeat.coalesceMs).toBe(0);
|
||||
@@ -580,8 +572,6 @@ describe("emitExecSystemEvent", () => {
|
||||
sessionKey: "agent:main:subagent:abc-123",
|
||||
contextKey: "exec:run-sub",
|
||||
deliveryContext: undefined,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
expect(requestHeartbeatMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -343,8 +343,6 @@ function maybeNotifyOnExit(session: ProcessSession, status: "completed" | "faile
|
||||
enqueueSystemEvent(summary, {
|
||||
sessionKey: resolveEventSessionKey(sessionKey, session.mainKey, session.sessionScope),
|
||||
deliveryContext: session.notifyDeliveryContext,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
// Subagent sessions receive exec results via process poll and announce flow;
|
||||
// the heartbeat would fall back to the main session and cause spurious wakes.
|
||||
@@ -447,8 +445,6 @@ export function emitExecSystemEvent(
|
||||
sessionKey: resolveEventSessionKey(sessionKey, opts.mainKey, opts.sessionScope),
|
||||
contextKey: opts.contextKey,
|
||||
deliveryContext: opts.deliveryContext,
|
||||
forceSenderIsOwnerFalse: true,
|
||||
trusted: false,
|
||||
});
|
||||
// Subagent sessions receive exec results via process poll and announce flow;
|
||||
// the heartbeat would fall back to the main session and cause spurious wakes.
|
||||
|
||||
@@ -773,7 +773,7 @@ describe("exec notifyOnExit", () => {
|
||||
expect(finished?.status).toBe(PROCESS_STATUS_COMPLETED);
|
||||
expect(finished?.exitCode).toBe(0);
|
||||
expect(hasEvent).toBe(true);
|
||||
expect(queuedEvent?.forceSenderIsOwnerFalse).toBe(true);
|
||||
expect(queuedEvent).toBeDefined();
|
||||
expect(formatted).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -793,7 +793,7 @@ describe("exec notifyOnExit", () => {
|
||||
event.text.includes(sessionId.slice(0, 8)),
|
||||
);
|
||||
|
||||
expect(queuedEvent?.forceSenderIsOwnerFalse).toBe(true);
|
||||
expect(queuedEvent).toBeDefined();
|
||||
expect(queuedEvent?.deliveryContext?.channel).toBe("telegram");
|
||||
expect(queuedEvent?.deliveryContext?.to).toBe("telegram:-1003774691294:topic:47");
|
||||
expect(queuedEvent?.deliveryContext?.threadId).toBe("47");
|
||||
|
||||
@@ -31,7 +31,6 @@ type ChannelMessageActionDiscoveryParams = {
|
||||
sessionId?: string | null;
|
||||
agentId?: string | null;
|
||||
requesterSenderId?: string | null;
|
||||
senderIsOwner?: boolean;
|
||||
};
|
||||
|
||||
const channelAgentToolMeta = new WeakMap<ChannelAgentTool, ChannelAgentToolMeta>();
|
||||
|
||||
@@ -571,22 +571,6 @@ describe("runCliAgent spawn path", () => {
|
||||
expect(JSON.stringify(params)).not.toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
|
||||
});
|
||||
|
||||
it("forwards senderIsOwner through the compat wrapper", () => {
|
||||
const params = buildRunClaudeCliAgentParams({
|
||||
sessionId: "openclaw-session",
|
||||
sessionKey: "agent:main:matrix:room:123",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
prompt: "hi",
|
||||
model: "opus",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-claude-owner-wrapper",
|
||||
senderIsOwner: false,
|
||||
});
|
||||
|
||||
expect(params.senderIsOwner).toBe(false);
|
||||
});
|
||||
|
||||
it("forwards channel context through the compat wrapper", () => {
|
||||
const params = buildRunClaudeCliAgentParams({
|
||||
sessionId: "openclaw-session",
|
||||
|
||||
@@ -564,7 +564,6 @@ export async function runPreparedCliAgent(
|
||||
}),
|
||||
channelId: hookContext.channelId,
|
||||
accountId: params.agentAccountId,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
},
|
||||
buildAgentHookContext(hookContext),
|
||||
);
|
||||
@@ -725,7 +724,6 @@ export function buildRunClaudeCliAgentParams(params: RunClaudeCliAgentParams): R
|
||||
images: params.images,
|
||||
messageChannel: params.messageChannel,
|
||||
messageProvider: params.messageProvider,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,6 @@ function buildCliEnvMcpLog(childEnv: Record<string, string>): string {
|
||||
`agentId=${childEnv.OPENCLAW_MCP_AGENT_ID || "<empty>"}`,
|
||||
`accountId=${childEnv.OPENCLAW_MCP_ACCOUNT_ID || "<empty>"}`,
|
||||
`messageChannel=${childEnv.OPENCLAW_MCP_MESSAGE_CHANNEL || "<empty>"}`,
|
||||
`senderIsOwner=${childEnv.OPENCLAW_MCP_SENDER_IS_OWNER || "<empty>"}`,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
@@ -427,11 +426,7 @@ export async function executePreparedCliRun(
|
||||
});
|
||||
cliBackendLog.info(`cli argv: ${backend.command} ${logArgs.join(" ")}`);
|
||||
cliBackendLog.info(`cli env auth: ${buildCliEnvAuthLog(env)}`);
|
||||
if (
|
||||
env.OPENCLAW_MCP_TOKEN ||
|
||||
env.OPENCLAW_MCP_SESSION_KEY ||
|
||||
env.OPENCLAW_MCP_SENDER_IS_OWNER
|
||||
) {
|
||||
if (env.OPENCLAW_MCP_TOKEN || env.OPENCLAW_MCP_SESSION_KEY || env.OPENCLAW_MCP_AGENT_ID) {
|
||||
cliBackendLog.info(`cli env mcp: ${buildCliEnvMcpLog(env)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,9 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
getActiveMcpLoopbackRuntime: vi.fn(() => undefined),
|
||||
ensureMcpLoopbackServer: vi.fn(createTestMcpLoopbackServer),
|
||||
createMcpLoopbackServerConfig: vi.fn(createTestMcpLoopbackServerConfig),
|
||||
resolveMcpLoopbackBearerToken: vi.fn((runtime, senderIsOwner) =>
|
||||
senderIsOwner ? runtime.ownerToken : runtime.nonOwnerToken,
|
||||
),
|
||||
resolveMcpLoopbackScopedTools: vi.fn(() => ({ agentId: "main", tools: [] })),
|
||||
resolveOpenClawReferencePaths: vi.fn(async () => ({ docsPath: null, sourcePath: null })),
|
||||
});
|
||||
@@ -955,8 +958,8 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
try {
|
||||
const getActiveMcpLoopbackRuntime = vi.fn(() => ({
|
||||
port: 31783,
|
||||
ownerToken: "owner-token",
|
||||
nonOwnerToken: "non-owner-token",
|
||||
ownerToken: "loopback-owner-token",
|
||||
nonOwnerToken: "loopback-non-owner-token",
|
||||
}));
|
||||
const ensureMcpLoopbackServer = vi.fn(createTestMcpLoopbackServer);
|
||||
const createMcpLoopbackServerConfig = vi.fn(createTestMcpLoopbackServerConfig);
|
||||
@@ -1000,8 +1003,8 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
);
|
||||
const getActiveMcpLoopbackRuntime = vi.fn(() => ({
|
||||
port: 31783,
|
||||
ownerToken: "owner-token",
|
||||
nonOwnerToken: "non-owner-token",
|
||||
ownerToken: "loopback-owner-token",
|
||||
nonOwnerToken: "loopback-non-owner-token",
|
||||
}));
|
||||
const ensureMcpLoopbackServer = vi.fn(createTestMcpLoopbackServer);
|
||||
const createMcpLoopbackServerConfig = vi.fn(createTestMcpLoopbackServerConfig);
|
||||
@@ -1067,7 +1070,6 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
messageProvider: undefined,
|
||||
accountId: undefined,
|
||||
inboundEventKind: undefined,
|
||||
senderIsOwner: undefined,
|
||||
});
|
||||
expect(context.systemPrompt).toContain("## Memory Recall");
|
||||
expect(context.systemPrompt).toContain("tools=memory_search");
|
||||
@@ -1167,8 +1169,8 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
try {
|
||||
const getActiveMcpLoopbackRuntime = vi.fn(() => ({
|
||||
port: 31783,
|
||||
ownerToken: "owner-token",
|
||||
nonOwnerToken: "non-owner-token",
|
||||
ownerToken: "loopback-owner-token",
|
||||
nonOwnerToken: "loopback-non-owner-token",
|
||||
}));
|
||||
const ensureMcpLoopbackServer = vi.fn(createTestMcpLoopbackServer);
|
||||
const createMcpLoopbackServerConfig = vi.fn(createTestMcpLoopbackServerConfig);
|
||||
@@ -1225,8 +1227,8 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
try {
|
||||
const getActiveMcpLoopbackRuntime = vi.fn(() => ({
|
||||
port: 31783,
|
||||
ownerToken: "owner-token",
|
||||
nonOwnerToken: "non-owner-token",
|
||||
ownerToken: "loopback-owner-token",
|
||||
nonOwnerToken: "loopback-non-owner-token",
|
||||
}));
|
||||
setCliRunnerPrepareTestDeps({
|
||||
getActiveMcpLoopbackRuntime,
|
||||
@@ -1260,8 +1262,8 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
try {
|
||||
const getActiveMcpLoopbackRuntime = vi.fn(() => ({
|
||||
port: 31783,
|
||||
ownerToken: "owner-token",
|
||||
nonOwnerToken: "non-owner-token",
|
||||
ownerToken: "loopback-owner-token",
|
||||
nonOwnerToken: "loopback-non-owner-token",
|
||||
}));
|
||||
setCliRunnerPrepareTestDeps({
|
||||
getActiveMcpLoopbackRuntime,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ensureMcpLoopbackServer } from "../../gateway/mcp-http.js";
|
||||
import {
|
||||
createMcpLoopbackServerConfig,
|
||||
getActiveMcpLoopbackRuntime,
|
||||
resolveMcpLoopbackBearerToken,
|
||||
} from "../../gateway/mcp-http.loopback-runtime.js";
|
||||
import { resolveMcpLoopbackScopedTools } from "../../gateway/mcp-http.runtime.js";
|
||||
import { isClaudeCliProvider } from "../../plugin-sdk/anthropic-cli.js";
|
||||
@@ -68,6 +69,7 @@ const prepareDeps = {
|
||||
getActiveMcpLoopbackRuntime,
|
||||
ensureMcpLoopbackServer,
|
||||
createMcpLoopbackServerConfig,
|
||||
resolveMcpLoopbackBearerToken,
|
||||
resolveMcpLoopbackScopedTools,
|
||||
resolveOpenClawReferencePaths: async (
|
||||
params: Parameters<typeof import("../docs-path.js").resolveOpenClawReferencePaths>[0],
|
||||
@@ -223,10 +225,10 @@ export async function prepareCliRunContext(
|
||||
: undefined,
|
||||
env: mcpLoopbackRuntime
|
||||
? {
|
||||
OPENCLAW_MCP_TOKEN:
|
||||
params.senderIsOwner === true
|
||||
? mcpLoopbackRuntime.ownerToken
|
||||
: mcpLoopbackRuntime.nonOwnerToken,
|
||||
OPENCLAW_MCP_TOKEN: prepareDeps.resolveMcpLoopbackBearerToken(
|
||||
mcpLoopbackRuntime,
|
||||
params.senderIsOwner === true,
|
||||
),
|
||||
OPENCLAW_MCP_AGENT_ID: sessionAgentId ?? "",
|
||||
OPENCLAW_MCP_ACCOUNT_ID: params.agentAccountId ?? "",
|
||||
OPENCLAW_MCP_SESSION_KEY: params.sessionKey ?? "",
|
||||
|
||||
@@ -61,6 +61,7 @@ export type RunCliAgentParams = {
|
||||
messageChannel?: string;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
/** Trusted sender identity bit for channel action auth. */
|
||||
senderIsOwner?: boolean;
|
||||
/** Runtime tool allow-list. CLI harnesses fail closed when this is set. */
|
||||
toolsAllow?: string[];
|
||||
|
||||
@@ -172,7 +172,7 @@ describe("CLI attempt execution", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: params.runId,
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -270,7 +270,7 @@ describe("CLI attempt execution", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-cli-expired",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -489,7 +489,7 @@ describe("CLI attempt execution", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-codex-cli-auth-alias",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -894,7 +894,6 @@ describe("CLI attempt execution", () => {
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-cli-channel-context",
|
||||
opts: {
|
||||
senderIsOwner: false,
|
||||
messageProvider: "discord-voice",
|
||||
} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
@@ -945,7 +944,6 @@ describe("CLI attempt execution", () => {
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-cli-tools-allow",
|
||||
opts: {
|
||||
senderIsOwner: true,
|
||||
toolsAllow: ["read", "web_search"],
|
||||
} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
@@ -1001,7 +999,7 @@ describe("CLI attempt execution", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-canonical-claude-cli",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: "telegram",
|
||||
@@ -1056,7 +1054,7 @@ describe("CLI attempt execution", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-shorthand-claude-cli",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: "telegram",
|
||||
@@ -1118,7 +1116,7 @@ describe("CLI attempt execution", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-canonical-codex-cli",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: "telegram",
|
||||
@@ -1196,7 +1194,7 @@ describe("CLI attempt execution", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-openai-codex-api-key-backup",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -1256,7 +1254,6 @@ describe("CLI attempt execution", () => {
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-model-run-raw",
|
||||
opts: {
|
||||
senderIsOwner: false,
|
||||
modelRun: true,
|
||||
promptMode: "none",
|
||||
messageProvider: "discord-voice",
|
||||
@@ -1328,7 +1325,6 @@ describe("CLI attempt execution", () => {
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-elevated-followup",
|
||||
opts: {
|
||||
senderIsOwner: false,
|
||||
bashElevated,
|
||||
} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
@@ -1378,7 +1374,6 @@ describe("CLI attempt execution", () => {
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-cleanup-claude-cli",
|
||||
opts: {
|
||||
senderIsOwner: false,
|
||||
cleanupBundleMcpOnRunEnd: true,
|
||||
cleanupCliLiveSessionOnRunEnd: true,
|
||||
} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
@@ -1441,7 +1436,7 @@ describe("embedded attempt harness pinning", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-legacy-pi-pin",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -1482,7 +1477,7 @@ describe("embedded attempt harness pinning", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-mixed-provider-auto-runtime",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -1523,7 +1518,6 @@ describe("embedded attempt harness pinning", () => {
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-tools-allow",
|
||||
opts: {
|
||||
senderIsOwner: true,
|
||||
toolsAllow: ["read", "web_search"],
|
||||
} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
@@ -1575,7 +1569,7 @@ describe("embedded attempt harness pinning", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-codex-no-pi-pin",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -1639,7 +1633,7 @@ describe("embedded attempt harness pinning", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-codex-auto-auth-profile",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -1686,7 +1680,7 @@ describe("embedded attempt harness pinning", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-fresh-no-pin",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -1727,7 +1721,7 @@ describe("embedded attempt harness pinning", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-stale-openai-pi-pin",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -1782,7 +1776,7 @@ describe("embedded attempt harness pinning", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-openai-pi-codex-oauth",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
@@ -1835,7 +1829,7 @@ describe("embedded attempt harness pinning", () => {
|
||||
resolvedThinkLevel: "medium",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-openai-fallback-with-cli-runtime",
|
||||
opts: { senderIsOwner: false } as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
opts: {} as Parameters<typeof runAgentAttempt>[0]["opts"],
|
||||
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
|
||||
spawnedBy: undefined,
|
||||
messageChannel: undefined,
|
||||
|
||||
@@ -381,7 +381,7 @@ export function runAgentAttempt(params: {
|
||||
fastMode?: boolean;
|
||||
timeoutMs: number;
|
||||
runId: string;
|
||||
opts: AgentCommandOpts & { senderIsOwner: boolean };
|
||||
opts: AgentCommandOpts;
|
||||
runContext: ReturnType<typeof resolveAgentRunContext>;
|
||||
spawnedBy: string | undefined;
|
||||
messageChannel: ReturnType<typeof resolveMessageChannel>;
|
||||
|
||||
@@ -127,7 +127,6 @@ async function compactCliTranscript(params: {
|
||||
skillsSnapshot?: SkillSnapshot;
|
||||
messageChannel?: string;
|
||||
agentAccountId?: string;
|
||||
senderIsOwner?: boolean;
|
||||
thinkLevel?: Parameters<typeof buildEmbeddedCompactionRuntimeContext>[0]["thinkLevel"];
|
||||
extraSystemPrompt?: string;
|
||||
}) {
|
||||
@@ -142,7 +141,6 @@ async function compactCliTranscript(params: {
|
||||
agentDir: params.agentDir,
|
||||
config: params.cfg,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
provider: params.provider,
|
||||
modelId: params.model,
|
||||
thinkLevel: params.thinkLevel,
|
||||
@@ -211,7 +209,6 @@ export async function runCliTurnCompactionLifecycle(params: {
|
||||
skillsSnapshot?: SkillSnapshot;
|
||||
messageChannel?: string;
|
||||
agentAccountId?: string;
|
||||
senderIsOwner?: boolean;
|
||||
thinkLevel?: Parameters<typeof buildEmbeddedCompactionRuntimeContext>[0]["thinkLevel"];
|
||||
extraSystemPrompt?: string;
|
||||
}): Promise<SessionEntry | undefined> {
|
||||
@@ -275,7 +272,6 @@ export async function runCliTurnCompactionLifecycle(params: {
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
messageChannel: params.messageChannel,
|
||||
agentAccountId: params.agentAccountId,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
thinkLevel: params.thinkLevel,
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ export type AgentCommandOpts = {
|
||||
runContext?: AgentRunContext;
|
||||
/** Internal trusted exec approval follow-up elevated defaults. */
|
||||
bashElevated?: ExecElevatedDefaults;
|
||||
/** Whether this caller is authorized for owner-only tools (defaults true for local CLI calls). */
|
||||
/** Trusted sender identity bit for command/channel-action auth; defaults true for local CLI calls. */
|
||||
senderIsOwner?: boolean;
|
||||
/** Whether this caller is authorized to use provider/model per-run overrides. */
|
||||
allowModelOverride?: boolean;
|
||||
@@ -134,8 +134,8 @@ export type AgentCommandIngressOpts = Omit<
|
||||
AgentCommandOpts,
|
||||
"senderIsOwner" | "allowModelOverride" | "resultMetaOverrides"
|
||||
> & {
|
||||
/** Ingress callsites must always pass explicit owner-tool authorization state. */
|
||||
senderIsOwner: boolean;
|
||||
/** Trusted sender identity bit for command/channel-action auth; defaults false for ingress. */
|
||||
senderIsOwner?: boolean;
|
||||
/** Ingress callsites must always pass explicit model-override authorization state. */
|
||||
allowModelOverride: boolean;
|
||||
};
|
||||
|
||||
@@ -164,11 +164,6 @@ describe("gateway tool", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("marks gateway as owner-only", () => {
|
||||
const tool = requireGatewayTool();
|
||||
expect(tool.ownerOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("exposes restart and config actions in the gateway tool schema", () => {
|
||||
const tool = requireGatewayTool();
|
||||
const parameters = tool.parameters as {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
isOpenClawOwnerOnlyCoreToolName,
|
||||
OPENCLAW_OWNER_ONLY_CORE_TOOL_NAMES,
|
||||
} from "./tools/owner-only-tools.js";
|
||||
|
||||
describe("createOpenClawTools owner authorization", () => {
|
||||
it("marks owner-only core tool names", () => {
|
||||
expect(OPENCLAW_OWNER_ONLY_CORE_TOOL_NAMES).toEqual(["cron", "gateway", "nodes"]);
|
||||
expect(isOpenClawOwnerOnlyCoreToolName("cron")).toBe(true);
|
||||
expect(isOpenClawOwnerOnlyCoreToolName("gateway")).toBe(true);
|
||||
expect(isOpenClawOwnerOnlyCoreToolName("nodes")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps canvas non-owner-only", () => {
|
||||
expect(isOpenClawOwnerOnlyCoreToolName("canvas")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -10,12 +10,10 @@ describe("openclaw plugin tool context", () => {
|
||||
options: {
|
||||
config: {} as never,
|
||||
requesterSenderId: "trusted-sender",
|
||||
senderIsOwner: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.context.requesterSenderId).toBe("trusted-sender");
|
||||
expect(result.context.senderIsOwner).toBe(true);
|
||||
});
|
||||
|
||||
it("forwards fs policy for plugin tool sandbox enforcement", () => {
|
||||
|
||||
@@ -20,7 +20,6 @@ export type OpenClawPluginToolOptions = {
|
||||
modelId?: string;
|
||||
requesterSenderId?: string | null;
|
||||
requesterAgentIdOverride?: string;
|
||||
senderIsOwner?: boolean;
|
||||
sessionId?: string;
|
||||
sandboxBrowserBridgeUrl?: string;
|
||||
allowHostBrowserControl?: boolean;
|
||||
@@ -82,7 +81,6 @@ export function resolveOpenClawPluginToolInputs(params: {
|
||||
agentAccountId: options?.agentAccountId,
|
||||
deliveryContext,
|
||||
requesterSenderId: options?.requesterSenderId ?? undefined,
|
||||
senderIsOwner: options?.senderIsOwner ?? undefined,
|
||||
sandboxed: options?.sandboxed,
|
||||
},
|
||||
allowGatewaySubagentBinding: options?.allowGatewaySubagentBinding,
|
||||
|
||||
@@ -116,6 +116,8 @@ export function createOpenClawTools(
|
||||
allowMediaInvokeCommands?: boolean;
|
||||
/** Explicit agent ID override for cron/hook sessions. */
|
||||
requesterAgentIdOverride?: string;
|
||||
/** Trusted sender identity bit for channel action auth. */
|
||||
senderIsOwner?: boolean;
|
||||
/** Restrict the cron tool to self-removing this active cron job. */
|
||||
cronSelfRemoveOnlyJobId?: string;
|
||||
/** Require explicit message targets (no implicit last-route sends). */
|
||||
@@ -143,8 +145,6 @@ export function createOpenClawTools(
|
||||
requesterSenderId?: string | null;
|
||||
/** Auth profiles already loaded for this run; used for prompt-time tool availability. */
|
||||
authProfileStore?: AuthProfileStore;
|
||||
/** Whether the requesting sender is an owner. */
|
||||
senderIsOwner?: boolean;
|
||||
/** Ephemeral session UUID — regenerated on /new and /reset. */
|
||||
sessionId?: string;
|
||||
/**
|
||||
|
||||
@@ -371,7 +371,6 @@ function buildCompactionContextEngineRuntimeContext(params: {
|
||||
agentDir: params.agentDir,
|
||||
config: params.params.config,
|
||||
skillsSnapshot: params.params.skillsSnapshot,
|
||||
senderIsOwner: params.params.senderIsOwner,
|
||||
senderId: params.params.senderId,
|
||||
provider: params.params.provider,
|
||||
modelId: params.params.model,
|
||||
|
||||
@@ -742,7 +742,6 @@ async function compactEmbeddedPiSessionDirectOnce(
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
|
||||
agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
@@ -809,7 +808,6 @@ async function compactEmbeddedPiSessionDirectOnce(
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
warn: (message) => log.warn(message),
|
||||
});
|
||||
const normalizedBundledTools =
|
||||
@@ -852,7 +850,6 @@ async function compactEmbeddedPiSessionDirectOnce(
|
||||
sessionId: params.sessionId,
|
||||
agentId: sessionAgentId,
|
||||
senderId: params.senderId,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -33,8 +33,6 @@ export type CompactEmbeddedPiSessionParams = {
|
||||
groupSpace?: string | null;
|
||||
/** Parent session key for subagent policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
/** Whether the sender is an owner (required for owner-only tools). */
|
||||
senderIsOwner?: boolean;
|
||||
sessionFile: string;
|
||||
/** Optional caller-observed live prompt tokens used for compaction diagnostics. */
|
||||
currentTokenCount?: number;
|
||||
|
||||
@@ -25,7 +25,6 @@ describe("buildEmbeddedCompactionRuntimeContext", () => {
|
||||
workspaceDir: "/tmp/workspace",
|
||||
agentDir: "/tmp/agent",
|
||||
config: {} as OpenClawConfig,
|
||||
senderIsOwner: true,
|
||||
senderId: "user-123",
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
|
||||
@@ -21,7 +21,6 @@ export type EmbeddedCompactionRuntimeContext = {
|
||||
agentDir: string;
|
||||
config?: OpenClawConfig;
|
||||
skillsSnapshot?: SkillSnapshot;
|
||||
senderIsOwner?: boolean;
|
||||
senderId?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
@@ -89,7 +88,6 @@ export function buildEmbeddedCompactionRuntimeContext(params: {
|
||||
agentDir: string;
|
||||
config?: OpenClawConfig;
|
||||
skillsSnapshot?: SkillSnapshot;
|
||||
senderIsOwner?: boolean;
|
||||
senderId?: string | null;
|
||||
provider?: string | null;
|
||||
modelId?: string | null;
|
||||
@@ -127,7 +125,6 @@ export function buildEmbeddedCompactionRuntimeContext(params: {
|
||||
agentDir: params.agentDir,
|
||||
config: params.config,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
senderId: params.senderId ?? undefined,
|
||||
provider: resolved.provider,
|
||||
model: resolved.model,
|
||||
|
||||
@@ -7,13 +7,12 @@ import { providerAliasCases } from "../test-helpers/provider-alias-cases.js";
|
||||
import type { AnyAgentTool } from "../tools/common.js";
|
||||
import { applyFinalEffectiveToolPolicy } from "./effective-tool-policy.js";
|
||||
|
||||
function makeTool(name: string, ownerOnly = false): AnyAgentTool {
|
||||
function makeTool(name: string): AnyAgentTool {
|
||||
return {
|
||||
name,
|
||||
label: name,
|
||||
description: name,
|
||||
parameters: { type: "object", properties: {} },
|
||||
ownerOnly,
|
||||
execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }),
|
||||
};
|
||||
}
|
||||
@@ -152,16 +151,6 @@ describe("applyFinalEffectiveToolPolicy", () => {
|
||||
expect(filtered.map((tool) => tool.name)).toEqual(["mcp__bundle__read"]);
|
||||
});
|
||||
|
||||
it("applies owner-only filtering to bundled tools", () => {
|
||||
const filtered = applyFinalEffectiveToolPolicy({
|
||||
bundledTools: [makeTool("mcp__bundle__read"), makeTool("mcp__bundle__admin", true)],
|
||||
senderIsOwner: false,
|
||||
warn: () => {},
|
||||
});
|
||||
|
||||
expect(filtered.map((tool) => tool.name)).toEqual(["mcp__bundle__read"]);
|
||||
});
|
||||
|
||||
it("returns the empty array unchanged when there are no bundled tools", () => {
|
||||
const filtered = applyFinalEffectiveToolPolicy({
|
||||
bundledTools: [],
|
||||
|
||||
@@ -17,11 +17,7 @@ import {
|
||||
buildDefaultToolPolicyPipelineSteps,
|
||||
type ToolPolicyPipelineStep,
|
||||
} from "../tool-policy-pipeline.js";
|
||||
import {
|
||||
applyOwnerOnlyToolPolicy,
|
||||
mergeAlsoAllowPolicy,
|
||||
resolveToolProfilePolicy,
|
||||
} from "../tool-policy.js";
|
||||
import { mergeAlsoAllowPolicy, resolveToolProfilePolicy } from "../tool-policy.js";
|
||||
import type { AnyAgentTool } from "../tools/common.js";
|
||||
|
||||
/**
|
||||
@@ -36,7 +32,7 @@ import type { AnyAgentTool } from "../tools/common.js";
|
||||
*/
|
||||
type FinalEffectiveToolPolicyParams = {
|
||||
// Tools appended to the core tool set after `createOpenClawCodingTools()`
|
||||
// has already applied owner-only and tool-policy filtering (e.g. bundled
|
||||
// has already applied the shared tool-policy pipeline (e.g. bundled
|
||||
// MCP/LSP tools). Only these are filtered here; re-running the pipeline over
|
||||
// the already-filtered core tools would drop plugin tools whose WeakMap
|
||||
// metadata no longer survives core-tool wrapping/normalization.
|
||||
@@ -57,8 +53,6 @@ type FinalEffectiveToolPolicyParams = {
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
senderIsOwner?: boolean;
|
||||
ownerOnlyToolAllowlist?: string[];
|
||||
warn: (message: string) => void;
|
||||
};
|
||||
|
||||
@@ -144,11 +138,6 @@ export function applyFinalEffectiveToolPolicy(
|
||||
store: subagentStore,
|
||||
},
|
||||
);
|
||||
const ownerFiltered = applyOwnerOnlyToolPolicy(
|
||||
params.bundledTools,
|
||||
params.senderIsOwner === true,
|
||||
params.ownerOnlyToolAllowlist,
|
||||
);
|
||||
// Suppress unavailable-core-tool warnings on every step of this pass.
|
||||
// `applyToolPolicyPipeline` infers `coreToolNames` from the `tools` array
|
||||
// it's filtering, and this pass only sees the bundled MCP/LSP subset.
|
||||
@@ -180,7 +169,7 @@ export function applyFinalEffectiveToolPolicy(
|
||||
{ policy: inheritedToolPolicy, label: "inherited tools" },
|
||||
].map((step) => Object.assign({}, step, { suppressUnavailableCoreToolWarning: true }));
|
||||
return applyToolPolicyPipeline({
|
||||
tools: ownerFiltered,
|
||||
tools: params.bundledTools,
|
||||
toolMeta: (tool) => getPluginToolMeta(tool),
|
||||
warn: params.warn,
|
||||
steps: pipelineSteps,
|
||||
|
||||
@@ -14,7 +14,6 @@ describe("buildEmbeddedMessageActionDiscoveryInput", () => {
|
||||
sessionId: "session-1",
|
||||
agentId: "main",
|
||||
senderId: "user-123",
|
||||
senderIsOwner: false,
|
||||
}),
|
||||
).toEqual({
|
||||
cfg: undefined,
|
||||
@@ -27,7 +26,6 @@ describe("buildEmbeddedMessageActionDiscoveryInput", () => {
|
||||
sessionId: "session-1",
|
||||
agentId: "main",
|
||||
requesterSenderId: "user-123",
|
||||
senderIsOwner: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +41,6 @@ describe("buildEmbeddedMessageActionDiscoveryInput", () => {
|
||||
sessionId: null,
|
||||
agentId: null,
|
||||
senderId: null,
|
||||
senderIsOwner: false,
|
||||
}),
|
||||
).toEqual({
|
||||
cfg: undefined,
|
||||
@@ -56,28 +53,6 @@ describe("buildEmbeddedMessageActionDiscoveryInput", () => {
|
||||
sessionId: undefined,
|
||||
agentId: undefined,
|
||||
requesterSenderId: undefined,
|
||||
senderIsOwner: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves owner authorization for downstream channel action gating", () => {
|
||||
expect(
|
||||
buildEmbeddedMessageActionDiscoveryInput({
|
||||
channel: "matrix",
|
||||
senderIsOwner: true,
|
||||
}),
|
||||
).toEqual({
|
||||
cfg: undefined,
|
||||
channel: "matrix",
|
||||
currentChannelId: undefined,
|
||||
currentThreadTs: undefined,
|
||||
currentMessageId: undefined,
|
||||
accountId: undefined,
|
||||
sessionKey: undefined,
|
||||
sessionId: undefined,
|
||||
agentId: undefined,
|
||||
requesterSenderId: undefined,
|
||||
senderIsOwner: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ export function buildEmbeddedMessageActionDiscoveryInput(params: {
|
||||
sessionId?: string | null;
|
||||
agentId?: string | null;
|
||||
senderId?: string | null;
|
||||
senderIsOwner?: boolean;
|
||||
senderIsOwner?: boolean | null;
|
||||
}) {
|
||||
return {
|
||||
cfg: params.cfg,
|
||||
@@ -24,6 +24,6 @@ export function buildEmbeddedMessageActionDiscoveryInput(params: {
|
||||
sessionId: params.sessionId ?? undefined,
|
||||
agentId: params.agentId ?? undefined,
|
||||
requesterSenderId: params.senderId ?? undefined,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
senderIsOwner: params.senderIsOwner ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ function makeForwardingCase(internalEvents: AgentInternalEvent[]) {
|
||||
runId: "forward-attempt-params",
|
||||
params: {
|
||||
toolsAllow: ["exec", "read"],
|
||||
ownerOnlyToolAllowlist: ["cron"],
|
||||
bootstrapContextMode: "lightweight",
|
||||
bootstrapContextRunKind: "cron",
|
||||
disableMessageTool: true,
|
||||
@@ -58,7 +57,6 @@ function makeForwardingCase(internalEvents: AgentInternalEvent[]) {
|
||||
},
|
||||
expected: {
|
||||
toolsAllow: ["exec", "read"],
|
||||
ownerOnlyToolAllowlist: ["cron"],
|
||||
bootstrapContextMode: "lightweight",
|
||||
bootstrapContextRunKind: "cron",
|
||||
disableMessageTool: true,
|
||||
|
||||
@@ -49,7 +49,6 @@ type CompactRuntimeContext = {
|
||||
currentThreadTs?: string;
|
||||
currentMessageId?: string;
|
||||
senderId?: string;
|
||||
senderIsOwner?: boolean;
|
||||
authProfileId?: string;
|
||||
};
|
||||
|
||||
@@ -240,7 +239,6 @@ describe("timeout-triggered compaction", () => {
|
||||
currentThreadTs: "thread-1",
|
||||
currentMessageId: "message-1",
|
||||
senderId: "sender-1",
|
||||
senderIsOwner: true,
|
||||
});
|
||||
|
||||
expect(mockedCompactDirect).toHaveBeenCalledTimes(1);
|
||||
@@ -252,7 +250,6 @@ describe("timeout-triggered compaction", () => {
|
||||
expect(compactParams.runtimeContext?.currentThreadTs).toBe("thread-1");
|
||||
expect(compactParams.runtimeContext?.currentMessageId).toBe("message-1");
|
||||
expect(compactParams.runtimeContext?.senderId).toBe("sender-1");
|
||||
expect(compactParams.runtimeContext?.senderIsOwner).toBe(true);
|
||||
});
|
||||
|
||||
it("falls through to normal handling when timeout compaction fails", async () => {
|
||||
|
||||
@@ -1403,7 +1403,6 @@ export async function runEmbeddedPiAgent(
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
currentMessageId: params.currentMessageId,
|
||||
@@ -1495,7 +1494,6 @@ export async function runEmbeddedPiAgent(
|
||||
bootstrapContextRunKind: params.bootstrapContextRunKind,
|
||||
jobId: params.jobId,
|
||||
toolsAllow: params.toolsAllow,
|
||||
ownerOnlyToolAllowlist: params.ownerOnlyToolAllowlist,
|
||||
disableMessageTool: params.disableMessageTool,
|
||||
forceMessageTool: params.forceMessageTool,
|
||||
enableHeartbeatTool: params.enableHeartbeatTool,
|
||||
@@ -1733,7 +1731,6 @@ export async function runEmbeddedPiAgent(
|
||||
agentDir,
|
||||
config: params.config,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
senderId: params.senderId,
|
||||
provider,
|
||||
modelId,
|
||||
@@ -1916,7 +1913,6 @@ export async function runEmbeddedPiAgent(
|
||||
agentDir,
|
||||
config: params.config,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
senderId: params.senderId,
|
||||
provider,
|
||||
modelId,
|
||||
|
||||
@@ -503,7 +503,6 @@ type AfterTurnRuntimeContextAttempt = Pick<
|
||||
| "currentMessageId"
|
||||
| "config"
|
||||
| "skillsSnapshot"
|
||||
| "senderIsOwner"
|
||||
| "senderId"
|
||||
| "provider"
|
||||
| "modelId"
|
||||
@@ -542,7 +541,6 @@ export function buildAfterTurnRuntimeContext(params: {
|
||||
agentDir: params.agentDir,
|
||||
config: params.attempt.config,
|
||||
skillsSnapshot: params.attempt.skillsSnapshot,
|
||||
senderIsOwner: params.attempt.senderIsOwner,
|
||||
senderId: params.attempt.senderId,
|
||||
provider: params.attempt.provider,
|
||||
modelId: params.attempt.modelId,
|
||||
|
||||
@@ -113,7 +113,7 @@ describe("embedded attempt context injection", () => {
|
||||
expect(resolver).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("forwards senderIsOwner into embedded message-action discovery", () => {
|
||||
it("builds embedded message-action discovery routing context", () => {
|
||||
const input = buildEmbeddedMessageActionDiscoveryInput({
|
||||
cfg: {},
|
||||
channel: "matrix",
|
||||
@@ -125,7 +125,6 @@ describe("embedded attempt context injection", () => {
|
||||
sessionId: "session",
|
||||
agentId: "main",
|
||||
senderId: "@alice:example.org",
|
||||
senderIsOwner: false,
|
||||
});
|
||||
|
||||
expect(input.channel).toBe("matrix");
|
||||
@@ -137,7 +136,6 @@ describe("embedded attempt context injection", () => {
|
||||
expect(input.sessionId).toBe("session");
|
||||
expect(input.agentId).toBe("main");
|
||||
expect(input.requesterSenderId).toBe("@alice:example.org");
|
||||
expect(input.senderIsOwner).toBe(false);
|
||||
});
|
||||
|
||||
it("never skips heartbeat bootstrap filtering", async () => {
|
||||
|
||||
@@ -1204,7 +1204,6 @@ export async function createContextEngineAttemptRunner(params: {
|
||||
authProfileStore: { version: 1, profiles: {} },
|
||||
modelRegistry: {} as never,
|
||||
thinkLevel: "off",
|
||||
senderIsOwner: true,
|
||||
disableTools: true,
|
||||
disableMessageTool: true,
|
||||
contextTokenBudget: 2048,
|
||||
|
||||
@@ -3627,7 +3627,6 @@ describe("buildAfterTurnRuntimeContext", () => {
|
||||
sessionId: "session-123",
|
||||
config: {} as OpenClawConfig,
|
||||
skillsSnapshot: undefined,
|
||||
senderIsOwner: true,
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
thinkLevel: "off",
|
||||
@@ -3666,7 +3665,6 @@ describe("buildAfterTurnRuntimeContext", () => {
|
||||
authProfileId: "openai:p1",
|
||||
config: {} as OpenClawConfig,
|
||||
skillsSnapshot: undefined,
|
||||
senderIsOwner: true,
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
thinkLevel: "off",
|
||||
@@ -3700,7 +3698,6 @@ describe("buildAfterTurnRuntimeContext", () => {
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
skillsSnapshot: undefined,
|
||||
senderIsOwner: true,
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
thinkLevel: "off",
|
||||
@@ -3739,7 +3736,6 @@ describe("buildAfterTurnRuntimeContext", () => {
|
||||
authProfileId: "openai:p1",
|
||||
config: { plugins: { slots: { contextEngine: "lossless-claw" } } } as OpenClawConfig,
|
||||
skillsSnapshot: undefined,
|
||||
senderIsOwner: true,
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
thinkLevel: "off",
|
||||
@@ -3782,7 +3778,6 @@ describe("buildAfterTurnRuntimeContext", () => {
|
||||
authProfileId: "openai:p1",
|
||||
config: { plugins: { slots: { contextEngine: "lossless-claw" } } } as OpenClawConfig,
|
||||
skillsSnapshot: undefined,
|
||||
senderIsOwner: true,
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
thinkLevel: "off",
|
||||
@@ -3814,7 +3809,6 @@ describe("buildAfterTurnRuntimeContext", () => {
|
||||
authProfileId: "openai:p1",
|
||||
config: {} as OpenClawConfig,
|
||||
skillsSnapshot: undefined,
|
||||
senderIsOwner: true,
|
||||
senderId: "user-123",
|
||||
provider: "openai-codex",
|
||||
modelId: "gpt-5.4",
|
||||
|
||||
@@ -1394,7 +1394,6 @@ export async function runEmbeddedAttempt(
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
ownerOnlyToolAllowlist: params.ownerOnlyToolAllowlist,
|
||||
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
|
||||
sessionKey: sandboxSessionKey,
|
||||
// When sandboxSessionKey differs from the real run session key (e.g. Telegram
|
||||
@@ -1685,8 +1684,6 @@ export async function runEmbeddedAttempt(
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
ownerOnlyToolAllowlist: params.ownerOnlyToolAllowlist,
|
||||
warn: (message) => log.warn(message),
|
||||
});
|
||||
const normalizedBundledTools =
|
||||
|
||||
@@ -70,13 +70,8 @@ export type RunEmbeddedPiAgentParams = {
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
/** Whether the sender is an owner (required for owner-only tools). */
|
||||
/** Trusted sender identity bit for command/channel-action auth. */
|
||||
senderIsOwner?: boolean;
|
||||
/**
|
||||
* Additional owner-only tools authorized by a server-side runtime grant.
|
||||
* This must stay narrow; it does not make the sender an owner.
|
||||
*/
|
||||
ownerOnlyToolAllowlist?: string[];
|
||||
/** Current channel ID for auto-threading (Slack). */
|
||||
currentChannelId?: string;
|
||||
/** Current thread timestamp for auto-threading (Slack). */
|
||||
|
||||
@@ -187,7 +187,6 @@ describe("Agent-specific tool filtering", () => {
|
||||
sessionKey: "main",
|
||||
workspaceDir: "/tmp/test",
|
||||
agentDir: "/tmp/agent-local",
|
||||
senderIsOwner: true,
|
||||
modelProvider: "lmstudio",
|
||||
modelId: "gemma-4-e4b-it",
|
||||
});
|
||||
|
||||
+5
-25
@@ -9,7 +9,6 @@ vi.mock("./channel-tools.js", () => {
|
||||
name,
|
||||
description: `${name} stub`,
|
||||
parameters: { type: "object", properties: {} },
|
||||
ownerOnly: true,
|
||||
execute: vi.fn(),
|
||||
});
|
||||
return {
|
||||
@@ -19,18 +18,9 @@ vi.mock("./channel-tools.js", () => {
|
||||
};
|
||||
});
|
||||
|
||||
describe("owner-only tool gating", () => {
|
||||
it("removes owner-only tools for unauthorized senders", () => {
|
||||
const tools = createOpenClawCodingTools({ senderIsOwner: false });
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
expect(toolNames).not.toContain("plugin_login");
|
||||
expect(toolNames).not.toContain("cron");
|
||||
expect(toolNames).not.toContain("gateway");
|
||||
expect(toolNames).not.toContain("nodes");
|
||||
});
|
||||
|
||||
it("keeps owner-only tools for authorized senders", () => {
|
||||
const tools = createOpenClawCodingTools({ senderIsOwner: true });
|
||||
describe("tool availability", () => {
|
||||
it("keeps control-plane tools available", () => {
|
||||
const tools = createOpenClawCodingTools();
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
expect(toolNames).toContain("plugin_login");
|
||||
expect(toolNames).toContain("cron");
|
||||
@@ -38,24 +28,14 @@ describe("owner-only tool gating", () => {
|
||||
expect(toolNames).toContain("nodes");
|
||||
});
|
||||
|
||||
it("keeps canvas available to unauthorized senders by current trust model", () => {
|
||||
const tools = createOpenClawCodingTools({ senderIsOwner: false });
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
expect(toolNames).toContain("canvas");
|
||||
});
|
||||
|
||||
it("defaults to removing owner-only tools when owner status is unknown", () => {
|
||||
it("keeps canvas available by current trust model", () => {
|
||||
const tools = createOpenClawCodingTools();
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
expect(toolNames).not.toContain("plugin_login");
|
||||
expect(toolNames).not.toContain("cron");
|
||||
expect(toolNames).not.toContain("gateway");
|
||||
expect(toolNames).not.toContain("nodes");
|
||||
expect(toolNames).toContain("canvas");
|
||||
});
|
||||
|
||||
it("restricts node-originated runs to the node-safe tool subset", () => {
|
||||
const tools = createOpenClawCodingTools({ messageProvider: "node", senderIsOwner: false });
|
||||
const tools = createOpenClawCodingTools({ messageProvider: "node" });
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
expect(toolNames).toContain("canvas");
|
||||
expect(toolNames).not.toContain("exec");
|
||||
@@ -157,7 +157,7 @@ describe("createOpenClawCodingTools", () => {
|
||||
});
|
||||
|
||||
it("exposes gateway config and restart actions to owner sessions", () => {
|
||||
const tools = createOpenClawCodingTools({ config: testConfig, senderIsOwner: true });
|
||||
const tools = createOpenClawCodingTools({ config: testConfig });
|
||||
const gateway = requireTool(tools, "gateway");
|
||||
|
||||
const parameters = gateway.parameters as {
|
||||
@@ -313,51 +313,30 @@ describe("createOpenClawCodingTools", () => {
|
||||
expect(names.has("exec")).toBe(false);
|
||||
});
|
||||
|
||||
it("exposes only an explicitly authorized owner-only tool to non-owner sessions", () => {
|
||||
it("exposes control-plane tools to configured sessions", () => {
|
||||
const tools = createOpenClawCodingTools({
|
||||
config: testConfig,
|
||||
senderIsOwner: false,
|
||||
ownerOnlyToolAllowlist: ["cron"],
|
||||
});
|
||||
const names = new Set(tools.map((tool) => tool.name));
|
||||
|
||||
expect(names.has("cron")).toBe(true);
|
||||
expect(names.has("gateway")).toBe(false);
|
||||
expect(names.has("nodes")).toBe(false);
|
||||
expect(names.has("gateway")).toBe(true);
|
||||
expect(names.has("nodes")).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves isolated cron runtime toolsAllow after the cron owner-only grant", () => {
|
||||
const withoutGrant = applyRuntimeToolsAllow(
|
||||
it("resolves isolated cron runtime toolsAllow", () => {
|
||||
const allowed = applyRuntimeToolsAllow(
|
||||
createOpenClawCodingTools({
|
||||
config: testConfig,
|
||||
senderIsOwner: false,
|
||||
}),
|
||||
["cron"],
|
||||
);
|
||||
const errorWithoutGrant = buildEmptyExplicitToolAllowlistError({
|
||||
sources: [{ label: "runtime toolsAllow", entries: ["cron"] }],
|
||||
callableToolNames: withoutGrant.map((tool) => tool.name),
|
||||
toolsEnabled: true,
|
||||
});
|
||||
|
||||
expect(errorWithoutGrant?.message).toContain(
|
||||
"No callable tools remain after resolving explicit tool allowlist (runtime toolsAllow: cron); no registered tools matched.",
|
||||
);
|
||||
|
||||
const withGrant = applyRuntimeToolsAllow(
|
||||
createOpenClawCodingTools({
|
||||
config: testConfig,
|
||||
senderIsOwner: false,
|
||||
ownerOnlyToolAllowlist: ["cron"],
|
||||
}),
|
||||
["cron"],
|
||||
);
|
||||
|
||||
expect(withGrant.map((tool) => tool.name)).toEqual(["cron"]);
|
||||
expect(allowed.map((tool) => tool.name)).toEqual(["cron"]);
|
||||
expect(
|
||||
buildEmptyExplicitToolAllowlistError({
|
||||
sources: [{ label: "runtime toolsAllow", entries: ["cron"] }],
|
||||
callableToolNames: withGrant.map((tool) => tool.name),
|
||||
callableToolNames: allowed.map((tool) => tool.name),
|
||||
toolsEnabled: true,
|
||||
}),
|
||||
).toBeNull();
|
||||
@@ -656,7 +635,6 @@ describe("createOpenClawCodingTools", () => {
|
||||
createOpenClawCodingTools({
|
||||
config: testConfig,
|
||||
recordToolPrepStage: (name) => stages.push(name),
|
||||
senderIsOwner: true,
|
||||
});
|
||||
|
||||
expectListIncludes(stages, [
|
||||
@@ -683,7 +661,7 @@ describe("createOpenClawCodingTools", () => {
|
||||
});
|
||||
|
||||
it("preserves action enums in normalized schemas", () => {
|
||||
const defaultTools = createOpenClawCodingTools({ config: testConfig, senderIsOwner: true });
|
||||
const defaultTools = createOpenClawCodingTools({ config: testConfig });
|
||||
const toolNames = ["canvas", "nodes", "cron", "gateway", "message"];
|
||||
const missingNames = toolNames.filter(
|
||||
(name) => !defaultTools.some((candidate) => candidate.name === name),
|
||||
@@ -707,7 +685,7 @@ describe("createOpenClawCodingTools", () => {
|
||||
});
|
||||
|
||||
it("enforces apply_patch availability and canonical names across model/provider constraints", () => {
|
||||
const defaultTools = createOpenClawCodingTools({ config: testConfig, senderIsOwner: true });
|
||||
const defaultTools = createOpenClawCodingTools({ config: testConfig });
|
||||
expect(toolNameList(defaultTools)).toContain("exec");
|
||||
expect(toolNameList(defaultTools)).toContain("process");
|
||||
expect(toolNameList(defaultTools)).not.toContain("apply_patch");
|
||||
@@ -956,7 +934,6 @@ describe("createOpenClawCodingTools", () => {
|
||||
browser: { enabled: true },
|
||||
plugins: { entries: { browser: { enabled: true } } },
|
||||
} as OpenClawConfig,
|
||||
senderIsOwner: true,
|
||||
});
|
||||
const names = new Set(tools.map((tool) => tool.name));
|
||||
// full profile must not filter any tools — browser, canvas, etc. must be present.
|
||||
@@ -966,23 +943,20 @@ describe("createOpenClawCodingTools", () => {
|
||||
expect(names.has("message")).toBe(true);
|
||||
});
|
||||
|
||||
it("includes browser tool with full profile for non-owner senders (#76507)", () => {
|
||||
it("includes browser tool with full profile (#76507)", () => {
|
||||
const tools = createOpenClawCodingTools({
|
||||
config: {
|
||||
tools: { profile: "full" },
|
||||
browser: { enabled: true },
|
||||
plugins: { entries: { browser: { enabled: true } } },
|
||||
} as OpenClawConfig,
|
||||
senderIsOwner: false,
|
||||
});
|
||||
const names = new Set(tools.map((tool) => tool.name));
|
||||
// browser is NOT owner-only; it must be available to non-owner senders.
|
||||
expect(names.has("browser")).toBe(true);
|
||||
expect(names.has("canvas")).toBe(true);
|
||||
// owner-only tools should be filtered for non-owners
|
||||
expect(names.has("gateway")).toBe(false);
|
||||
expect(names.has("cron")).toBe(false);
|
||||
expect(names.has("nodes")).toBe(false);
|
||||
expect(names.has("gateway")).toBe(true);
|
||||
expect(names.has("cron")).toBe(true);
|
||||
expect(names.has("nodes")).toBe(true);
|
||||
});
|
||||
|
||||
it("includes browser tool without explicit profile (defaults to no filtering) (#76507)", () => {
|
||||
@@ -1156,7 +1130,6 @@ describe("createOpenClawCodingTools", () => {
|
||||
it("removes unsupported JSON Schema keywords for Cloud Code Assist API compatibility", () => {
|
||||
const googleTools = createOpenClawCodingTools({
|
||||
modelProvider: "google",
|
||||
senderIsOwner: true,
|
||||
});
|
||||
for (const tool of googleTools) {
|
||||
const violations = findUnsupportedSchemaKeywords(
|
||||
@@ -1177,7 +1150,6 @@ describe("createOpenClawCodingTools", () => {
|
||||
nativeWebSearchTool: true,
|
||||
toolCallArgumentsEncoding: "html-entities",
|
||||
},
|
||||
senderIsOwner: true,
|
||||
});
|
||||
|
||||
expect(toolNameList(xaiTools)).not.toContain("web_search");
|
||||
|
||||
@@ -2,13 +2,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const stubTool = (name: string, ownerOnly = false) =>
|
||||
const stubTool = (name: string) =>
|
||||
({
|
||||
name,
|
||||
label: name,
|
||||
displaySummary: name,
|
||||
description: name,
|
||||
ownerOnly,
|
||||
parameters: { type: "object", properties: {} },
|
||||
execute: vi.fn(),
|
||||
}) satisfies AnyAgentTool;
|
||||
@@ -22,7 +21,7 @@ const mocks = vi.hoisted(() => {
|
||||
vi.mock("./openclaw-tools.js", () => ({
|
||||
createOpenClawTools: (options: unknown) => {
|
||||
mocks.createOpenClawToolsOptions(options);
|
||||
return [mocks.stubTool("cron", true)];
|
||||
return [mocks.stubTool("cron")];
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -41,23 +40,20 @@ describe("createOpenClawCodingTools cron scope", () => {
|
||||
mocks.createOpenClawToolsOptions.mockClear();
|
||||
});
|
||||
|
||||
it("scopes the cron owner-only runtime grant to self-removal", () => {
|
||||
it("scopes cron-triggered jobs to self-removal", () => {
|
||||
const tools = createOpenClawCodingTools({
|
||||
trigger: "cron",
|
||||
jobId: "job-current",
|
||||
senderIsOwner: false,
|
||||
ownerOnlyToolAllowlist: ["cron"],
|
||||
});
|
||||
|
||||
expect(tools.map((tool) => tool.name)).toContain("cron");
|
||||
expect(firstOpenClawToolsOptions()?.cronSelfRemoveOnlyJobId).toBe("job-current");
|
||||
});
|
||||
|
||||
it("does not scope ordinary owner cron sessions", () => {
|
||||
it("does not scope non-cron sessions", () => {
|
||||
createOpenClawCodingTools({
|
||||
trigger: "cron",
|
||||
trigger: "user",
|
||||
jobId: "job-current",
|
||||
senderIsOwner: true,
|
||||
});
|
||||
|
||||
expect(firstOpenClawToolsOptions()?.cronSelfRemoveOnlyJobId).toBeUndefined();
|
||||
|
||||
@@ -2,11 +2,11 @@ import { describe, expect, it } from "vitest";
|
||||
import { applyDeferredFollowupToolDescriptions } from "./pi-tools.deferred-followup.js";
|
||||
import type { AnyAgentTool } from "./pi-tools.types.js";
|
||||
|
||||
function findToolDescription(toolName: string, senderIsOwner: boolean) {
|
||||
function findToolDescription(toolName: string, includeCron: boolean) {
|
||||
const tools = applyDeferredFollowupToolDescriptions([
|
||||
{ name: "exec", description: "exec base" },
|
||||
{ name: "process", description: "process base" },
|
||||
...(senderIsOwner ? [{ name: "cron", description: "cron base" }] : []),
|
||||
...(includeCron ? [{ name: "cron", description: "cron base" }] : []),
|
||||
] as AnyAgentTool[]);
|
||||
const tool = tools.find((entry) => entry.name === toolName);
|
||||
return {
|
||||
|
||||
+6
-22
@@ -79,7 +79,6 @@ import {
|
||||
buildDefaultToolPolicyPipelineSteps,
|
||||
} from "./tool-policy-pipeline.js";
|
||||
import {
|
||||
applyOwnerOnlyToolPolicy,
|
||||
collectExplicitAllowlist,
|
||||
collectExplicitDenylist,
|
||||
expandToolGroups,
|
||||
@@ -458,13 +457,8 @@ export function createOpenClawCodingTools(options?: {
|
||||
toolSearchCatalogRef?: ToolSearchCatalogRef;
|
||||
/** Limits which tool families are materialized before the shared policy pipeline runs. */
|
||||
toolConstructionPlan?: OpenClawCodingToolConstructionPlan;
|
||||
/** Whether the sender is an owner (required for owner-only tools). */
|
||||
/** Trusted sender identity bit for command/channel-action auth; does not filter model tools. */
|
||||
senderIsOwner?: boolean;
|
||||
/**
|
||||
* Additional owner-only tools authorized by a server-side runtime grant.
|
||||
* Keep this narrowly scoped; it is not a replacement for sender ownership.
|
||||
*/
|
||||
ownerOnlyToolAllowlist?: string[];
|
||||
/** Auth profiles already loaded for this run; used for prompt-time tool availability. */
|
||||
authProfileStore?: AuthProfileStore;
|
||||
/** Callback invoked when sessions_yield tool is called. */
|
||||
@@ -484,11 +478,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
}
|
||||
const memoryFlushWritePath = isMemoryFlushRun ? options.memoryFlushWritePath : undefined;
|
||||
const cronSelfRemoveOnlyJobId =
|
||||
options?.trigger === "cron" &&
|
||||
options.jobId?.trim() &&
|
||||
options.ownerOnlyToolAllowlist?.some((toolName) => normalizeToolName(toolName) === "cron")
|
||||
? options.jobId.trim()
|
||||
: undefined;
|
||||
options?.trigger === "cron" && options.jobId?.trim() ? options.jobId.trim() : undefined;
|
||||
const {
|
||||
agentId,
|
||||
globalPolicy,
|
||||
@@ -854,7 +844,6 @@ export function createOpenClawCodingTools(options?: {
|
||||
config: options?.config,
|
||||
fsPolicy,
|
||||
requesterSenderId: options?.senderId,
|
||||
senderIsOwner: options?.senderIsOwner,
|
||||
sessionId: options?.sessionId,
|
||||
sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl,
|
||||
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
|
||||
@@ -965,8 +954,8 @@ export function createOpenClawCodingTools(options?: {
|
||||
...(cronSelfRemoveOnlyJobId ? { cronSelfRemoveOnlyJobId } : {}),
|
||||
requesterAgentIdOverride: agentId,
|
||||
requesterSenderId: options?.senderId,
|
||||
authProfileStore: options?.authProfileStore,
|
||||
senderIsOwner: options?.senderIsOwner,
|
||||
authProfileStore: options?.authProfileStore,
|
||||
sessionId: options?.sessionId,
|
||||
inheritedToolAllowlist,
|
||||
inheritedToolDenylist,
|
||||
@@ -1022,15 +1011,10 @@ export function createOpenClawCodingTools(options?: {
|
||||
suppressManagedWebSearch: options?.suppressManagedWebSearch,
|
||||
});
|
||||
options?.recordToolPrepStage?.("model-provider-policy");
|
||||
// Security: treat unknown/undefined as unauthorized (opt-in, not opt-out)
|
||||
const senderIsOwner = options?.senderIsOwner === true;
|
||||
const toolsByAuthorization = applyOwnerOnlyToolPolicy(
|
||||
toolsForModelProvider,
|
||||
senderIsOwner,
|
||||
options?.ownerOnlyToolAllowlist,
|
||||
);
|
||||
// Sender identity is carried for command/channel-action auth; tool visibility
|
||||
// comes from configured tool policies, not per-turn sender ownership.
|
||||
const subagentFiltered = applyToolPolicyPipeline({
|
||||
tools: toolsByAuthorization,
|
||||
tools: toolsForModelProvider,
|
||||
toolMeta: (tool) => getPluginToolMeta(tool),
|
||||
warn: logWarn,
|
||||
steps: [
|
||||
|
||||
@@ -352,8 +352,7 @@ describe("spawnSubagentDirect seam flow", () => {
|
||||
// Admin-only methods must be pinned to operator.admin.
|
||||
expect(call.scopes).toEqual(["operator.admin"]);
|
||||
} else {
|
||||
// Non-admin methods (e.g. "agent") must NOT be forced to admin scope
|
||||
// so the gateway preserves least-privilege and senderIsOwner stays false.
|
||||
// Non-admin methods (e.g. "agent") must NOT be forced to admin scope.
|
||||
expect(call.scopes).toBeUndefined();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,8 +205,7 @@ async function callSubagentGateway(
|
||||
// complete interactively, causing close(1008) "pairing required" (#59428).
|
||||
//
|
||||
// Only admin-only methods are pinned to ADMIN_SCOPE; other methods (e.g.
|
||||
// "agent" → write) keep their least-privilege scope so that the gateway does
|
||||
// not treat the caller as owner (senderIsOwner) and expose owner-only tools.
|
||||
// "agent" -> write) keep their least-privilege scope.
|
||||
const scopes = params.scopes ?? (isAdminOnlyMethod(params.method) ? [ADMIN_SCOPE] : undefined);
|
||||
return await subagentSpawnDeps.callGateway({
|
||||
...params,
|
||||
|
||||
@@ -1,47 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "../security/dangerous-tools.js";
|
||||
import { pickSandboxToolPolicy } from "./sandbox-tool-policy.js";
|
||||
import { isToolAllowed, resolveSandboxToolPolicyForAgent } from "./sandbox/tool-policy.js";
|
||||
import type { SandboxToolPolicy } from "./sandbox/types.js";
|
||||
import { isToolAllowedByPolicyName } from "./tool-policy-match.js";
|
||||
import { TOOL_POLICY_CONFORMANCE } from "./tool-policy.conformance.js";
|
||||
import {
|
||||
applyOwnerOnlyToolPolicy,
|
||||
collectExplicitAllowlist,
|
||||
DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY,
|
||||
expandToolGroups,
|
||||
isOwnerOnlyToolName,
|
||||
normalizeToolName,
|
||||
resolveOwnerOnlyToolApprovalClass,
|
||||
resolveToolProfilePolicy,
|
||||
TOOL_GROUPS,
|
||||
} from "./tool-policy.js";
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
function createOwnerPolicyTools() {
|
||||
return [
|
||||
{
|
||||
name: "read",
|
||||
execute: async () => ({ content: [], details: {} }) as any,
|
||||
},
|
||||
{
|
||||
name: "cron",
|
||||
ownerOnly: true,
|
||||
execute: async () => ({ content: [], details: {} }) as any,
|
||||
},
|
||||
{
|
||||
name: "gateway",
|
||||
ownerOnly: true,
|
||||
execute: async () => ({ content: [], details: {} }) as any,
|
||||
},
|
||||
{
|
||||
name: "nodes",
|
||||
ownerOnly: true,
|
||||
execute: async () => ({ content: [], details: {} }) as any,
|
||||
},
|
||||
] as unknown as AnyAgentTool[];
|
||||
}
|
||||
|
||||
describe("tool-policy", () => {
|
||||
it("expands groups and normalizes aliases", () => {
|
||||
@@ -79,70 +50,6 @@ describe("tool-policy", () => {
|
||||
expect(normalizeToolName("READ")).toBe("read");
|
||||
});
|
||||
|
||||
it("identifies owner-only tools", () => {
|
||||
expect(isOwnerOnlyToolName("cron")).toBe(true);
|
||||
expect(isOwnerOnlyToolName("gateway")).toBe(true);
|
||||
expect(isOwnerOnlyToolName("nodes")).toBe(true);
|
||||
expect(isOwnerOnlyToolName("read")).toBe(false);
|
||||
});
|
||||
|
||||
it("exposes stable approval classes for shared owner-only fallbacks", () => {
|
||||
expect(resolveOwnerOnlyToolApprovalClass("cron")).toBe("control_plane");
|
||||
expect(resolveOwnerOnlyToolApprovalClass("gateway")).toBe("control_plane");
|
||||
expect(resolveOwnerOnlyToolApprovalClass("nodes")).toBe("exec_capable");
|
||||
expect(resolveOwnerOnlyToolApprovalClass("read")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps ACP owner-only backstops aligned with the HTTP deny list", () => {
|
||||
const sharedBackstops = DEFAULT_GATEWAY_HTTP_TOOL_DENY.flatMap((name) => {
|
||||
const approvalClass = resolveOwnerOnlyToolApprovalClass(name);
|
||||
return approvalClass ? ([[name, approvalClass]] as const) : [];
|
||||
});
|
||||
|
||||
expect(Object.fromEntries(sharedBackstops)).toEqual({
|
||||
cron: "control_plane",
|
||||
gateway: "control_plane",
|
||||
nodes: "exec_capable",
|
||||
});
|
||||
});
|
||||
|
||||
it("strips owner-only tools for non-owner senders", () => {
|
||||
const tools = createOwnerPolicyTools();
|
||||
const filtered = applyOwnerOnlyToolPolicy(tools, false);
|
||||
expect(filtered.map((t) => t.name)).toEqual(["read"]);
|
||||
});
|
||||
|
||||
it("keeps owner-only tools for the owner sender", () => {
|
||||
const tools = createOwnerPolicyTools();
|
||||
const filtered = applyOwnerOnlyToolPolicy(tools, true);
|
||||
expect(filtered.map((t) => t.name)).toEqual(["read", "cron", "gateway", "nodes"]);
|
||||
});
|
||||
|
||||
it("keeps only explicitly authorized owner-only tools for non-owner senders", async () => {
|
||||
const tools = createOwnerPolicyTools();
|
||||
const filtered = applyOwnerOnlyToolPolicy(tools, false, ["cron"]);
|
||||
expect(filtered.map((t) => t.name)).toEqual(["read", "cron"]);
|
||||
|
||||
await expect(
|
||||
filtered.find((tool) => tool.name === "cron")?.execute?.("call_1", {}),
|
||||
).resolves.toEqual({
|
||||
content: [],
|
||||
details: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("honors ownerOnly metadata for custom tool names", () => {
|
||||
const tools = [
|
||||
{
|
||||
name: "custom_admin_tool",
|
||||
ownerOnly: true,
|
||||
execute: async () => ({ content: [], details: {} }) as any,
|
||||
},
|
||||
] as unknown as AnyAgentTool[];
|
||||
expect(applyOwnerOnlyToolPolicy(tools, false)).toStrictEqual([]);
|
||||
expect(applyOwnerOnlyToolPolicy(tools, true)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("collects explicit allowlist entries", () => {
|
||||
expect(
|
||||
collectExplicitAllowlist([
|
||||
@@ -169,25 +76,6 @@ describe("tool-policy", () => {
|
||||
"*",
|
||||
]);
|
||||
});
|
||||
|
||||
it("strips nodes for non-owner senders via fallback policy", () => {
|
||||
const tools = [
|
||||
{
|
||||
name: "read",
|
||||
execute: async () => ({ content: [], details: {} }) as any,
|
||||
},
|
||||
{
|
||||
name: "nodes",
|
||||
execute: async () => ({ content: [], details: {} }) as any,
|
||||
},
|
||||
] as unknown as AnyAgentTool[];
|
||||
|
||||
expect(applyOwnerOnlyToolPolicy(tools, false).map((tool) => tool.name)).toEqual(["read"]);
|
||||
expect(applyOwnerOnlyToolPolicy(tools, true).map((tool) => tool.name)).toEqual([
|
||||
"read",
|
||||
"nodes",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TOOL_POLICY_CONFORMANCE", () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { IMPLICIT_ALLOW_ALL_FROM_ALSO_ALLOW } from "./sandbox-tool-policy.js";
|
||||
import { expandToolGroups, normalizeToolList, normalizeToolName } from "./tool-policy-shared.js";
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
export {
|
||||
expandToolGroups,
|
||||
normalizeToolList,
|
||||
@@ -11,67 +10,6 @@ export {
|
||||
} from "./tool-policy-shared.js";
|
||||
export type { ToolProfileId } from "./tool-policy-shared.js";
|
||||
|
||||
export type OwnerOnlyToolApprovalClass = "control_plane" | "exec_capable" | "interactive";
|
||||
|
||||
// Keep tool-policy browser-safe: do not import tools/common at runtime.
|
||||
function wrapOwnerOnlyToolExecution(tool: AnyAgentTool, authorized: boolean): AnyAgentTool {
|
||||
if (tool.ownerOnly !== true || authorized || !tool.execute) {
|
||||
return tool;
|
||||
}
|
||||
return {
|
||||
...tool,
|
||||
execute: async () => {
|
||||
throw new Error("Tool restricted to owner senders.");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const OWNER_ONLY_TOOL_APPROVAL_CLASS_FALLBACKS = new Map<string, OwnerOnlyToolApprovalClass>([
|
||||
["cron", "control_plane"],
|
||||
["gateway", "control_plane"],
|
||||
["nodes", "exec_capable"],
|
||||
]);
|
||||
|
||||
export function resolveOwnerOnlyToolApprovalClass(
|
||||
name: string,
|
||||
): OwnerOnlyToolApprovalClass | undefined {
|
||||
return OWNER_ONLY_TOOL_APPROVAL_CLASS_FALLBACKS.get(normalizeToolName(name));
|
||||
}
|
||||
|
||||
export function isOwnerOnlyToolName(name: string) {
|
||||
return resolveOwnerOnlyToolApprovalClass(name) !== undefined;
|
||||
}
|
||||
|
||||
function isOwnerOnlyTool(tool: AnyAgentTool) {
|
||||
return tool.ownerOnly === true || isOwnerOnlyToolName(tool.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters owner-only tools unless the sender is an owner or a server-side
|
||||
* runtime grant authorizes a specific owner-only tool for this run.
|
||||
*/
|
||||
export function applyOwnerOnlyToolPolicy(
|
||||
tools: AnyAgentTool[],
|
||||
senderIsOwner: boolean,
|
||||
ownerOnlyToolAllowlist?: string[],
|
||||
) {
|
||||
const allowedOwnerOnlyTools = new Set(
|
||||
ownerOnlyToolAllowlist?.map((name) => normalizeToolName(name)) ?? [],
|
||||
);
|
||||
const isAuthorized = (tool: AnyAgentTool) =>
|
||||
senderIsOwner || allowedOwnerOnlyTools.has(normalizeToolName(tool.name));
|
||||
const withGuard = tools.map((tool) => {
|
||||
if (!isOwnerOnlyTool(tool)) {
|
||||
return tool;
|
||||
}
|
||||
return wrapOwnerOnlyToolExecution(tool, isAuthorized(tool));
|
||||
});
|
||||
if (senderIsOwner) {
|
||||
return withGuard;
|
||||
}
|
||||
return withGuard.filter((tool) => !isOwnerOnlyTool(tool) || isAuthorized(tool));
|
||||
}
|
||||
|
||||
export type ToolPolicyLike = {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
|
||||
@@ -216,7 +216,6 @@ export function resolveEffectiveToolInventory(
|
||||
modelId: params.modelId,
|
||||
modelCompat,
|
||||
messageProvider: params.messageProvider,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName ?? undefined,
|
||||
senderUsername: params.senderUsername ?? undefined,
|
||||
|
||||
@@ -41,7 +41,6 @@ export type ResolveEffectiveToolInventoryParams = {
|
||||
workspaceDir?: string;
|
||||
agentDir?: string;
|
||||
messageProvider?: string;
|
||||
senderIsOwner?: boolean;
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
|
||||
@@ -74,7 +74,6 @@ export async function runAgentStep(params: {
|
||||
runId: stepIdem,
|
||||
extraSystemPrompt: params.extraSystemPrompt,
|
||||
inputProvenance,
|
||||
senderIsOwner: false,
|
||||
allowModelOverride: false,
|
||||
});
|
||||
await retireSessionMcpRuntimeForSessionKey({
|
||||
|
||||
@@ -14,7 +14,6 @@ export type AgentToolWithMeta<TParameters extends TSchema, TResult> = AgentTool<
|
||||
TParameters,
|
||||
TResult
|
||||
> & {
|
||||
ownerOnly?: boolean;
|
||||
displaySummary?: string;
|
||||
};
|
||||
|
||||
@@ -30,7 +29,6 @@ type ErasedAgentToolExecute = {
|
||||
|
||||
export type AnyAgentTool = Omit<AgentTool<TSchema, unknown>, "execute"> &
|
||||
ErasedAgentToolExecute & {
|
||||
ownerOnly?: boolean;
|
||||
displaySummary?: string;
|
||||
};
|
||||
|
||||
@@ -52,8 +50,6 @@ export type ActionGate<T extends Record<string, boolean | undefined>> = (
|
||||
defaultValue?: boolean,
|
||||
) => boolean;
|
||||
|
||||
export const OWNER_ONLY_TOOL_ERROR = "Tool restricted to owner senders.";
|
||||
|
||||
export class ToolInputError extends Error {
|
||||
readonly status: number = 400;
|
||||
|
||||
@@ -303,21 +299,6 @@ export function jsonResult(payload: unknown): AgentToolResult<unknown> {
|
||||
return textResult(JSON.stringify(payload, null, 2), payload);
|
||||
}
|
||||
|
||||
export function wrapOwnerOnlyToolExecution(
|
||||
tool: AnyAgentTool,
|
||||
senderIsOwner: boolean,
|
||||
): AnyAgentTool {
|
||||
if (tool.ownerOnly !== true || senderIsOwner || !tool.execute) {
|
||||
return tool;
|
||||
}
|
||||
return {
|
||||
...tool,
|
||||
execute: async () => {
|
||||
throw new Error(OWNER_ONLY_TOOL_ERROR);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function imageResult(params: {
|
||||
label: string;
|
||||
path: string;
|
||||
|
||||
@@ -171,11 +171,6 @@ describe("cron tool", () => {
|
||||
extractDeliveryInfoMock.mockReturnValue({ deliveryContext: undefined, threadId: undefined });
|
||||
});
|
||||
|
||||
it("marks cron as owner-only", () => {
|
||||
const tool = createTestCronTool();
|
||||
expect(tool.ownerOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("allows scoped isolated cron runs to remove the current job", async () => {
|
||||
const tool = createTestCronTool({ selfRemoveOnlyJobId: "job-current" });
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import { optionalStringEnum, stringEnum } from "../schema/typebox.js";
|
||||
import { CRON_TOOL_DISPLAY_SUMMARY } from "../tool-description-presets.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool, readGatewayCallOptions, type GatewayCallOptions } from "./gateway.js";
|
||||
import { isOpenClawOwnerOnlyCoreToolName } from "./owner-only-tools.js";
|
||||
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./sessions-helpers.js";
|
||||
|
||||
// We spell out job/patch properties so that LLMs know what fields to send.
|
||||
@@ -492,7 +491,6 @@ export function createCronTool(opts?: CronToolOptions, deps?: CronToolDeps): Any
|
||||
return {
|
||||
label: "Cron",
|
||||
name: "cron",
|
||||
ownerOnly: isOpenClawOwnerOnlyCoreToolName("cron"),
|
||||
displaySummary: CRON_TOOL_DISPLAY_SUMMARY,
|
||||
description: `Manage Gateway cron jobs and wake events: reminders, check-back-later, delayed follow-ups, recurring work. Do not emulate scheduling with exec sleep/process polling.
|
||||
|
||||
|
||||
@@ -19,15 +19,14 @@ import { normalizeOptionalString, readStringValue } from "../../shared/string-co
|
||||
import { stringEnum } from "../schema/typebox.js";
|
||||
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
|
||||
import { callGatewayTool, readGatewayCallOptions } from "./gateway.js";
|
||||
import { isOpenClawOwnerOnlyCoreToolName } from "./owner-only-tools.js";
|
||||
|
||||
const log = createSubsystemLogger("gateway-tool");
|
||||
|
||||
const DEFAULT_UPDATE_TIMEOUT_MS = 20 * 60_000;
|
||||
// Security: the agent-facing `gateway` tool is owner-only, but per SECURITY.md the model/agent
|
||||
// itself is not a trusted principal. `assertGatewayConfigMutationAllowed` is the explicit
|
||||
// model -> operator trust-boundary control on `config.apply`/`config.patch`, so the runtime
|
||||
// tool must fail closed and allow only a narrow set of agent-tunable paths.
|
||||
// Per SECURITY.md the model/agent itself is not a trusted principal.
|
||||
// `assertGatewayConfigMutationAllowed` is the explicit model -> operator
|
||||
// trust-boundary control on `config.apply`/`config.patch`, so the runtime tool
|
||||
// must fail closed and allow only a narrow set of agent-tunable paths.
|
||||
const ALLOWED_GATEWAY_CONFIG_PATHS = [
|
||||
// Agent prompt/model tuning.
|
||||
"agents.defaults.systemPromptOverride",
|
||||
@@ -370,7 +369,6 @@ export function createGatewayTool(opts?: {
|
||||
return {
|
||||
label: "Gateway",
|
||||
name: "gateway",
|
||||
ownerOnly: isOpenClawOwnerOnlyCoreToolName("gateway"),
|
||||
description:
|
||||
"Gateway restart/config/update. Before config edits, use config.schema.lookup with targeted dot path. Prefer config.patch for partial merge; config.apply only full replace. Writes hot-reload or restart as needed. Always pass human `note` for post-restart delivery. If still owe the user a reply, pass one-shot `continuationMessage`; do not write restart sentinel files directly.",
|
||||
parameters: GatewayToolSchema,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user