feat: fallback elevated allowlist to discord dms
This commit is contained in:
@@ -442,10 +442,10 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
||||
|
||||
`agent.elevated` controls elevated (host) bash access:
|
||||
- `enabled`: allow elevated mode (default true)
|
||||
- `allowFrom`: per-surface allowlists (required to enable; empty = disabled)
|
||||
- `allowFrom`: per-surface allowlists (empty = disabled)
|
||||
- `whatsapp`: E.164 numbers
|
||||
- `telegram`: chat ids or usernames
|
||||
- `discord`: user ids or usernames
|
||||
- `discord`: user ids or usernames (falls back to `discord.dm.allowFrom` if omitted)
|
||||
- `signal`: E.164 numbers
|
||||
- `imessage`: handles/chat ids
|
||||
- `webchat`: session ids or usernames
|
||||
|
||||
@@ -24,6 +24,7 @@ read_when:
|
||||
- Feature gate: `agent.elevated.enabled` (default can be off via config even if the code supports it).
|
||||
- Sender allowlist: `agent.elevated.allowFrom` with per-surface allowlists (e.g. `discord`, `whatsapp`).
|
||||
- Both must pass; otherwise elevated is treated as unavailable.
|
||||
- Discord fallback: if `agent.elevated.allowFrom.discord` is omitted, the `discord.dm.allowFrom` list is used as a fallback. Set `agent.elevated.allowFrom.discord` (even `[]`) to override.
|
||||
|
||||
## Logging + status
|
||||
- Elevated bash calls are logged at info level.
|
||||
|
||||
@@ -299,6 +299,38 @@ describe("directive parsing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid elevated level", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/elevated maybe",
|
||||
From: "+1222",
|
||||
To: "+1222",
|
||||
Surface: "whatsapp",
|
||||
SenderE164: "+1222",
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1222"] },
|
||||
},
|
||||
},
|
||||
whatsapp: { allowFrom: ["+1222"] },
|
||||
session: { store: path.join(home, "sessions.json") },
|
||||
},
|
||||
);
|
||||
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Unrecognized elevated level");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("acks queue directive and persists override", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||
@@ -751,4 +783,49 @@ describe("directive parsing", () => {
|
||||
expect(call?.thinkLevel).toBe("low");
|
||||
});
|
||||
});
|
||||
|
||||
it("passes elevated defaults when sender is approved", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const storePath = path.join(home, "sessions.json");
|
||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||
payloads: [{ text: "done" }],
|
||||
meta: {
|
||||
durationMs: 5,
|
||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||
},
|
||||
});
|
||||
|
||||
await getReplyFromConfig(
|
||||
{
|
||||
Body: "hello",
|
||||
From: "+1004",
|
||||
To: "+2000",
|
||||
Surface: "whatsapp",
|
||||
SenderE164: "+1004",
|
||||
},
|
||||
{},
|
||||
{
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: path.join(home, "clawd"),
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1004"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
session: { store: storePath },
|
||||
},
|
||||
);
|
||||
|
||||
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
|
||||
const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0];
|
||||
expect(call?.bashElevated).toEqual({
|
||||
enabled: true,
|
||||
allowed: true,
|
||||
defaultLevel: "on",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,6 +222,112 @@ describe("trigger handling", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects elevated inline directive for unapproved sender", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
elevated: {
|
||||
allowFrom: { whatsapp: ["+1000"] },
|
||||
},
|
||||
},
|
||||
whatsapp: {
|
||||
allowFrom: ["+1000"],
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "please /elevated on now",
|
||||
From: "+2000",
|
||||
To: "+2000",
|
||||
Surface: "whatsapp",
|
||||
SenderE164: "+2000",
|
||||
},
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("elevated is not available right now.");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to discord dm allowFrom for elevated approval", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
},
|
||||
discord: {
|
||||
dm: {
|
||||
allowFrom: ["steipete"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/elevated on",
|
||||
From: "discord:123",
|
||||
To: "user:123",
|
||||
Surface: "discord",
|
||||
SenderName: "steipete",
|
||||
},
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toContain("Elevated mode enabled");
|
||||
|
||||
const storeRaw = await fs.readFile(cfg.session.store, "utf-8");
|
||||
const store = JSON.parse(storeRaw) as Record<
|
||||
string,
|
||||
{ elevatedLevel?: string }
|
||||
>;
|
||||
expect(store.main?.elevatedLevel).toBe("on");
|
||||
});
|
||||
});
|
||||
|
||||
it("treats explicit discord elevated allowlist as override", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
const cfg = {
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
workspace: join(home, "clawd"),
|
||||
elevated: {
|
||||
allowFrom: { discord: [] },
|
||||
},
|
||||
},
|
||||
discord: {
|
||||
dm: {
|
||||
allowFrom: ["steipete"],
|
||||
},
|
||||
},
|
||||
session: { store: join(home, "sessions.json") },
|
||||
};
|
||||
|
||||
const res = await getReplyFromConfig(
|
||||
{
|
||||
Body: "/elevated on",
|
||||
From: "discord:123",
|
||||
To: "user:123",
|
||||
Surface: "discord",
|
||||
SenderName: "steipete",
|
||||
},
|
||||
{},
|
||||
cfg,
|
||||
);
|
||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||
expect(text).toBe("elevated is not available right now.");
|
||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a context overflow fallback when the embedded agent throws", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValue(
|
||||
|
||||
@@ -98,14 +98,20 @@ function stripSenderPrefix(value?: string) {
|
||||
function resolveElevatedAllowList(
|
||||
allowFrom: AgentElevatedAllowFromConfig | undefined,
|
||||
surface: string,
|
||||
discordFallback?: Array<string | number>,
|
||||
): Array<string | number> | undefined {
|
||||
switch (surface) {
|
||||
case "whatsapp":
|
||||
return allowFrom?.whatsapp;
|
||||
case "telegram":
|
||||
return allowFrom?.telegram;
|
||||
case "discord":
|
||||
return allowFrom?.discord;
|
||||
case "discord": {
|
||||
const hasExplicit = Boolean(
|
||||
allowFrom && Object.prototype.hasOwnProperty.call(allowFrom, "discord"),
|
||||
);
|
||||
if (hasExplicit) return allowFrom?.discord;
|
||||
return discordFallback;
|
||||
}
|
||||
case "signal":
|
||||
return allowFrom?.signal;
|
||||
case "imessage":
|
||||
@@ -121,8 +127,13 @@ function isApprovedElevatedSender(params: {
|
||||
surface: string;
|
||||
ctx: MsgContext;
|
||||
allowFrom?: AgentElevatedAllowFromConfig;
|
||||
discordFallback?: Array<string | number>;
|
||||
}): boolean {
|
||||
const rawAllow = resolveElevatedAllowList(params.allowFrom, params.surface);
|
||||
const rawAllow = resolveElevatedAllowList(
|
||||
params.allowFrom,
|
||||
params.surface,
|
||||
params.discordFallback,
|
||||
);
|
||||
if (!rawAllow || rawAllow.length === 0) return false;
|
||||
|
||||
const allowTokens = rawAllow
|
||||
@@ -250,6 +261,8 @@ export async function getReplyFromConfig(
|
||||
ctx.Surface?.trim().toLowerCase() ??
|
||||
"";
|
||||
const elevatedConfig = agentCfg?.elevated;
|
||||
const discordElevatedFallback =
|
||||
surfaceKey === "discord" ? cfg.discord?.dm?.allowFrom : undefined;
|
||||
const elevatedEnabled = elevatedConfig?.enabled !== false;
|
||||
const elevatedAllowed =
|
||||
elevatedEnabled &&
|
||||
@@ -259,6 +272,7 @@ export async function getReplyFromConfig(
|
||||
surface: surfaceKey,
|
||||
ctx,
|
||||
allowFrom: elevatedConfig?.allowFrom,
|
||||
discordFallback: discordElevatedFallback,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user