feat: add /exec session overrides
This commit is contained in:
@@ -41,6 +41,16 @@ Notes:
|
|||||||
- `tools.exec.ask` (default: `on-miss`)
|
- `tools.exec.ask` (default: `on-miss`)
|
||||||
- `tools.exec.node` (default: unset)
|
- `tools.exec.node` (default: unset)
|
||||||
|
|
||||||
|
## Session overrides (`/exec`)
|
||||||
|
|
||||||
|
Use `/exec` to set **per-session** defaults for `host`, `security`, `ask`, and `node`.
|
||||||
|
Send `/exec` with no arguments to show the current values.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
/exec host=gateway security=allowlist ask=on-miss node=mac-1
|
||||||
|
```
|
||||||
|
|
||||||
## Exec approvals (macOS app)
|
## Exec approvals (macOS app)
|
||||||
|
|
||||||
Sandboxed agents can require per-request approval before `exec` runs on the gateway or node host.
|
Sandboxed agents can require per-request approval before `exec` runs on the gateway or node host.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ The host-only bash chat command uses `! <cmd>` (with `/bash <cmd>` as an alias).
|
|||||||
There are two related systems:
|
There are two related systems:
|
||||||
|
|
||||||
- **Commands**: standalone `/...` messages.
|
- **Commands**: standalone `/...` messages.
|
||||||
- **Directives**: `/think`, `/verbose`, `/reasoning`, `/elevated`, `/model`, `/queue`.
|
- **Directives**: `/think`, `/verbose`, `/reasoning`, `/elevated`, `/exec`, `/model`, `/queue`.
|
||||||
- Directives are stripped from the message before the model sees it.
|
- Directives are stripped from the message before the model sees it.
|
||||||
- In normal chat messages (not directive-only), they are treated as “inline hints” and do **not** persist session settings.
|
- In normal chat messages (not directive-only), they are treated as “inline hints” and do **not** persist session settings.
|
||||||
- In directive-only messages (the message contains only directives), they persist to the session and reply with an acknowledgement.
|
- In directive-only messages (the message contains only directives), they persist to the session and reply with an acknowledgement.
|
||||||
@@ -77,6 +77,7 @@ Text + native (when enabled):
|
|||||||
- `/verbose on|full|off` (alias: `/v`)
|
- `/verbose on|full|off` (alias: `/v`)
|
||||||
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
|
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
|
||||||
- `/elevated on|off` (alias: `/elev`)
|
- `/elevated on|off` (alias: `/elev`)
|
||||||
|
- `/exec host=<sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` (send `/exec` to show current)
|
||||||
- `/model <name>` (alias: `/models`; or `/<alias>` from `agents.defaults.models.*.alias`)
|
- `/model <name>` (alias: `/models`; or `/<alias>` from `agents.defaults.models.*.alias`)
|
||||||
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
|
- `/queue <mode>` (plus options like `debounce:2s cap:25 drop:summarize`; send `/queue` to see current settings)
|
||||||
- `/bash <command>` (host-only; alias for `! <command>`; requires `commands.bash: true` + `tools.elevated` allowlists)
|
- `/bash <command>` (host-only; alias for `! <command>`; requires `commands.bash: true` + `tools.elevated` allowlists)
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ export async function runEmbeddedPiAgent(
|
|||||||
verboseLevel: params.verboseLevel,
|
verboseLevel: params.verboseLevel,
|
||||||
reasoningLevel: params.reasoningLevel,
|
reasoningLevel: params.reasoningLevel,
|
||||||
toolResultFormat: resolvedToolResultFormat,
|
toolResultFormat: resolvedToolResultFormat,
|
||||||
|
execOverrides: params.execOverrides,
|
||||||
bashElevated: params.bashElevated,
|
bashElevated: params.bashElevated,
|
||||||
timeoutMs: params.timeoutMs,
|
timeoutMs: params.timeoutMs,
|
||||||
runId: params.runId,
|
runId: params.runId,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ import { prepareSessionManagerForRun } from "../session-manager-init.js";
|
|||||||
import { buildEmbeddedSystemPrompt, createSystemPromptOverride } from "../system-prompt.js";
|
import { buildEmbeddedSystemPrompt, createSystemPromptOverride } from "../system-prompt.js";
|
||||||
import { splitSdkTools } from "../tool-split.js";
|
import { splitSdkTools } from "../tool-split.js";
|
||||||
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../../date-time.js";
|
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../../date-time.js";
|
||||||
import { mapThinkingLevel, resolveExecToolDefaults } from "../utils.js";
|
import { mapThinkingLevel } from "../utils.js";
|
||||||
import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js";
|
import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js";
|
||||||
|
|
||||||
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
|
import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult } from "./types.js";
|
||||||
@@ -133,7 +133,7 @@ export async function runEmbeddedAttempt(
|
|||||||
|
|
||||||
const toolsRaw = createClawdbotCodingTools({
|
const toolsRaw = createClawdbotCodingTools({
|
||||||
exec: {
|
exec: {
|
||||||
...resolveExecToolDefaults(params.config),
|
...(params.execOverrides ?? {}),
|
||||||
elevated: params.bashElevated,
|
elevated: params.bashElevated,
|
||||||
},
|
},
|
||||||
sandbox,
|
sandbox,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ImageContent } from "@mariozechner/pi-ai";
|
|||||||
import type { ReasoningLevel, ThinkLevel, VerboseLevel } from "../../../auto-reply/thinking.js";
|
import type { ReasoningLevel, ThinkLevel, VerboseLevel } from "../../../auto-reply/thinking.js";
|
||||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||||
import type { enqueueCommand } from "../../../process/command-queue.js";
|
import type { enqueueCommand } from "../../../process/command-queue.js";
|
||||||
import type { ExecElevatedDefaults } from "../../bash-tools.js";
|
import type { ExecElevatedDefaults, ExecToolDefaults } from "../../bash-tools.js";
|
||||||
import type { BlockReplyChunking, ToolResultFormat } from "../../pi-embedded-subscribe.js";
|
import type { BlockReplyChunking, ToolResultFormat } from "../../pi-embedded-subscribe.js";
|
||||||
import type { SkillSnapshot } from "../../skills.js";
|
import type { SkillSnapshot } from "../../skills.js";
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ export type RunEmbeddedPiAgentParams = {
|
|||||||
verboseLevel?: VerboseLevel;
|
verboseLevel?: VerboseLevel;
|
||||||
reasoningLevel?: ReasoningLevel;
|
reasoningLevel?: ReasoningLevel;
|
||||||
toolResultFormat?: ToolResultFormat;
|
toolResultFormat?: ToolResultFormat;
|
||||||
|
execOverrides?: Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||||
bashElevated?: ExecElevatedDefaults;
|
bashElevated?: ExecElevatedDefaults;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
runId: string;
|
runId: string;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { discoverAuthStorage, discoverModels } from "@mariozechner/pi-codin
|
|||||||
|
|
||||||
import type { ReasoningLevel, ThinkLevel, VerboseLevel } from "../../../auto-reply/thinking.js";
|
import type { ReasoningLevel, ThinkLevel, VerboseLevel } from "../../../auto-reply/thinking.js";
|
||||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||||
import type { ExecElevatedDefaults } from "../../bash-tools.js";
|
import type { ExecElevatedDefaults, ExecToolDefaults } from "../../bash-tools.js";
|
||||||
import type { MessagingToolSend } from "../../pi-embedded-messaging.js";
|
import type { MessagingToolSend } from "../../pi-embedded-messaging.js";
|
||||||
import type { BlockReplyChunking, ToolResultFormat } from "../../pi-embedded-subscribe.js";
|
import type { BlockReplyChunking, ToolResultFormat } from "../../pi-embedded-subscribe.js";
|
||||||
import type { SkillSnapshot } from "../../skills.js";
|
import type { SkillSnapshot } from "../../skills.js";
|
||||||
@@ -39,6 +39,7 @@ export type EmbeddedRunAttemptParams = {
|
|||||||
verboseLevel?: VerboseLevel;
|
verboseLevel?: VerboseLevel;
|
||||||
reasoningLevel?: ReasoningLevel;
|
reasoningLevel?: ReasoningLevel;
|
||||||
toolResultFormat?: ToolResultFormat;
|
toolResultFormat?: ToolResultFormat;
|
||||||
|
execOverrides?: Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||||
bashElevated?: ExecElevatedDefaults;
|
bashElevated?: ExecElevatedDefaults;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
runId: string;
|
runId: string;
|
||||||
|
|||||||
@@ -367,6 +367,20 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
|||||||
],
|
],
|
||||||
argsMenu: "auto",
|
argsMenu: "auto",
|
||||||
}),
|
}),
|
||||||
|
defineChatCommand({
|
||||||
|
key: "exec",
|
||||||
|
nativeName: "exec",
|
||||||
|
description: "Set exec defaults for this session.",
|
||||||
|
textAlias: "/exec",
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: "options",
|
||||||
|
description: "host=... security=... ask=... node=...",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
argsParsing: "none",
|
||||||
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "model",
|
key: "model",
|
||||||
nativeName: "model",
|
nativeName: "model",
|
||||||
|
|||||||
@@ -147,6 +147,47 @@ describe("directive behavior", () => {
|
|||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it("shows current exec defaults when /exec has no argument", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/exec",
|
||||||
|
From: "+1222",
|
||||||
|
To: "+1222",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
exec: {
|
||||||
|
host: "gateway",
|
||||||
|
security: "allowlist",
|
||||||
|
ask: "always",
|
||||||
|
node: "mac-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain(
|
||||||
|
"Current exec defaults: host=gateway, security=allowlist, ask=always, node=mac-1.",
|
||||||
|
);
|
||||||
|
expect(text).toContain(
|
||||||
|
"Options: host=sandbox|gateway|node, security=deny|allowlist|full, ask=off|on-miss|always, node=<id>.",
|
||||||
|
);
|
||||||
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
it("persists elevated off and reflects it in /status (even when default is on)", async () => {
|
it("persists elevated off and reflects it in /status (even when default is on)", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { extractStatusDirective } from "./reply/directives.js";
|
import { extractStatusDirective } from "./reply/directives.js";
|
||||||
import {
|
import {
|
||||||
extractElevatedDirective,
|
extractElevatedDirective,
|
||||||
|
extractExecDirective,
|
||||||
extractQueueDirective,
|
extractQueueDirective,
|
||||||
extractReasoningDirective,
|
extractReasoningDirective,
|
||||||
extractReplyToTag,
|
extractReplyToTag,
|
||||||
@@ -112,6 +113,26 @@ describe("directive parsing", () => {
|
|||||||
expect(res.cleaned).toBe("");
|
expect(res.cleaned).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("matches exec directive with options", () => {
|
||||||
|
const res = extractExecDirective(
|
||||||
|
"please /exec host=gateway security=allowlist ask=on-miss node=mac-mini now",
|
||||||
|
);
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.execHost).toBe("gateway");
|
||||||
|
expect(res.execSecurity).toBe("allowlist");
|
||||||
|
expect(res.execAsk).toBe("on-miss");
|
||||||
|
expect(res.execNode).toBe("mac-mini");
|
||||||
|
expect(res.cleaned).toBe("please now");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures invalid exec host values", () => {
|
||||||
|
const res = extractExecDirective("/exec host=spaceship");
|
||||||
|
expect(res.hasDirective).toBe(true);
|
||||||
|
expect(res.execHost).toBeUndefined();
|
||||||
|
expect(res.rawExecHost).toBe("spaceship");
|
||||||
|
expect(res.invalidHost).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("matches queue directive", () => {
|
it("matches queue directive", () => {
|
||||||
const res = extractQueueDirective("please /queue interrupt now");
|
const res = extractQueueDirective("please /queue interrupt now");
|
||||||
expect(res.hasDirective).toBe(true);
|
expect(res.hasDirective).toBe(true);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export {
|
|||||||
extractVerboseDirective,
|
extractVerboseDirective,
|
||||||
} from "./reply/directives.js";
|
} from "./reply/directives.js";
|
||||||
export { getReplyFromConfig } from "./reply/get-reply.js";
|
export { getReplyFromConfig } from "./reply/get-reply.js";
|
||||||
|
export { extractExecDirective } from "./reply/exec.js";
|
||||||
export { extractQueueDirective } from "./reply/queue.js";
|
export { extractQueueDirective } from "./reply/queue.js";
|
||||||
export { extractReplyToTag } from "./reply/reply-tags.js";
|
export { extractReplyToTag } from "./reply/reply-tags.js";
|
||||||
export type { GetReplyOptions, ReplyPayload } from "./types.js";
|
export type { GetReplyOptions, ReplyPayload } from "./types.js";
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
thinkLevel: params.followupRun.run.thinkLevel,
|
thinkLevel: params.followupRun.run.thinkLevel,
|
||||||
verboseLevel: params.followupRun.run.verboseLevel,
|
verboseLevel: params.followupRun.run.verboseLevel,
|
||||||
reasoningLevel: params.followupRun.run.reasoningLevel,
|
reasoningLevel: params.followupRun.run.reasoningLevel,
|
||||||
|
execOverrides: params.followupRun.run.execOverrides,
|
||||||
toolResultFormat: (() => {
|
toolResultFormat: (() => {
|
||||||
const channel = resolveMessageChannel(
|
const channel = resolveMessageChannel(
|
||||||
params.sessionCtx.Surface,
|
params.sessionCtx.Surface,
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
|||||||
thinkLevel: params.followupRun.run.thinkLevel,
|
thinkLevel: params.followupRun.run.thinkLevel,
|
||||||
verboseLevel: params.followupRun.run.verboseLevel,
|
verboseLevel: params.followupRun.run.verboseLevel,
|
||||||
reasoningLevel: params.followupRun.run.reasoningLevel,
|
reasoningLevel: params.followupRun.run.reasoningLevel,
|
||||||
|
execOverrides: params.followupRun.run.execOverrides,
|
||||||
bashElevated: params.followupRun.run.bashElevated,
|
bashElevated: params.followupRun.run.bashElevated,
|
||||||
timeoutMs: params.followupRun.run.timeoutMs,
|
timeoutMs: params.followupRun.run.timeoutMs,
|
||||||
runId: flushRunId,
|
runId: flushRunId,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
import { resolveAgentConfig, resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
import { type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||||
|
import type { ExecAsk, ExecHost, ExecSecurity } from "../../infra/exec-approvals.js";
|
||||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||||
import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js";
|
import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js";
|
||||||
@@ -23,6 +24,38 @@ import {
|
|||||||
} from "./directive-handling.shared.js";
|
} from "./directive-handling.shared.js";
|
||||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js";
|
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js";
|
||||||
|
|
||||||
|
function resolveExecDefaults(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
sessionEntry?: SessionEntry;
|
||||||
|
agentId?: string;
|
||||||
|
}): { host: ExecHost; security: ExecSecurity; ask: ExecAsk; node?: string } {
|
||||||
|
const globalExec = params.cfg.tools?.exec;
|
||||||
|
const agentExec = params.agentId
|
||||||
|
? resolveAgentConfig(params.cfg, params.agentId)?.tools?.exec
|
||||||
|
: undefined;
|
||||||
|
return {
|
||||||
|
host:
|
||||||
|
(params.sessionEntry?.execHost as ExecHost | undefined) ??
|
||||||
|
(agentExec?.host as ExecHost | undefined) ??
|
||||||
|
(globalExec?.host as ExecHost | undefined) ??
|
||||||
|
"sandbox",
|
||||||
|
security:
|
||||||
|
(params.sessionEntry?.execSecurity as ExecSecurity | undefined) ??
|
||||||
|
(agentExec?.security as ExecSecurity | undefined) ??
|
||||||
|
(globalExec?.security as ExecSecurity | undefined) ??
|
||||||
|
"deny",
|
||||||
|
ask:
|
||||||
|
(params.sessionEntry?.execAsk as ExecAsk | undefined) ??
|
||||||
|
(agentExec?.ask as ExecAsk | undefined) ??
|
||||||
|
(globalExec?.ask as ExecAsk | undefined) ??
|
||||||
|
"on-miss",
|
||||||
|
node:
|
||||||
|
(params.sessionEntry?.execNode as string | undefined) ??
|
||||||
|
agentExec?.node ??
|
||||||
|
globalExec?.node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleDirectiveOnly(params: {
|
export async function handleDirectiveOnly(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
directives: InlineDirectives;
|
directives: InlineDirectives;
|
||||||
@@ -189,6 +222,42 @@ export async function handleDirectiveOnly(params: {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (directives.hasExecDirective) {
|
||||||
|
if (directives.invalidExecHost) {
|
||||||
|
return {
|
||||||
|
text: `Unrecognized exec host "${directives.rawExecHost ?? ""}". Valid hosts: sandbox, gateway, node.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (directives.invalidExecSecurity) {
|
||||||
|
return {
|
||||||
|
text: `Unrecognized exec security "${directives.rawExecSecurity ?? ""}". Valid: deny, allowlist, full.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (directives.invalidExecAsk) {
|
||||||
|
return {
|
||||||
|
text: `Unrecognized exec ask "${directives.rawExecAsk ?? ""}". Valid: off, on-miss, always.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (directives.invalidExecNode) {
|
||||||
|
return {
|
||||||
|
text: "Exec node requires a value.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!directives.hasExecOptions) {
|
||||||
|
const execDefaults = resolveExecDefaults({
|
||||||
|
cfg: params.cfg,
|
||||||
|
sessionEntry,
|
||||||
|
agentId: activeAgentId,
|
||||||
|
});
|
||||||
|
const nodeLabel = execDefaults.node ? `node=${execDefaults.node}` : "node=(unset)";
|
||||||
|
return {
|
||||||
|
text: withOptions(
|
||||||
|
`Current exec defaults: host=${execDefaults.host}, security=${execDefaults.security}, ask=${execDefaults.ask}, ${nodeLabel}.`,
|
||||||
|
"host=sandbox|gateway|node, security=deny|allowlist|full, ask=off|on-miss|always, node=<id>",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const queueAck = maybeHandleQueueDirective({
|
const queueAck = maybeHandleQueueDirective({
|
||||||
directives,
|
directives,
|
||||||
@@ -254,6 +323,20 @@ export async function handleDirectiveOnly(params: {
|
|||||||
elevatedChanged ||
|
elevatedChanged ||
|
||||||
(directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined);
|
(directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined);
|
||||||
}
|
}
|
||||||
|
if (directives.hasExecDirective && directives.hasExecOptions) {
|
||||||
|
if (directives.execHost) {
|
||||||
|
sessionEntry.execHost = directives.execHost;
|
||||||
|
}
|
||||||
|
if (directives.execSecurity) {
|
||||||
|
sessionEntry.execSecurity = directives.execSecurity;
|
||||||
|
}
|
||||||
|
if (directives.execAsk) {
|
||||||
|
sessionEntry.execAsk = directives.execAsk;
|
||||||
|
}
|
||||||
|
if (directives.execNode) {
|
||||||
|
sessionEntry.execNode = directives.execNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (modelSelection) {
|
if (modelSelection) {
|
||||||
if (modelSelection.isDefault) {
|
if (modelSelection.isDefault) {
|
||||||
delete sessionEntry.providerOverride;
|
delete sessionEntry.providerOverride;
|
||||||
@@ -355,6 +438,16 @@ export async function handleDirectiveOnly(params: {
|
|||||||
);
|
);
|
||||||
if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint());
|
if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint());
|
||||||
}
|
}
|
||||||
|
if (directives.hasExecDirective && directives.hasExecOptions) {
|
||||||
|
const execParts: string[] = [];
|
||||||
|
if (directives.execHost) execParts.push(`host=${directives.execHost}`);
|
||||||
|
if (directives.execSecurity) execParts.push(`security=${directives.execSecurity}`);
|
||||||
|
if (directives.execAsk) execParts.push(`ask=${directives.execAsk}`);
|
||||||
|
if (directives.execNode) execParts.push(`node=${directives.execNode}`);
|
||||||
|
if (execParts.length > 0) {
|
||||||
|
parts.push(formatDirectiveAck(`Exec defaults set (${execParts.join(", ")}).`));
|
||||||
|
}
|
||||||
|
}
|
||||||
if (shouldDowngradeXHigh) {
|
if (shouldDowngradeXHigh) {
|
||||||
parts.push(
|
parts.push(
|
||||||
`Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`,
|
`Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import type { ExecAsk, ExecHost, ExecSecurity } from "../../infra/exec-approvals.js";
|
||||||
import { extractModelDirective } from "../model.js";
|
import { extractModelDirective } from "../model.js";
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js";
|
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./directives.js";
|
||||||
import {
|
import {
|
||||||
extractElevatedDirective,
|
extractElevatedDirective,
|
||||||
|
extractExecDirective,
|
||||||
extractReasoningDirective,
|
extractReasoningDirective,
|
||||||
extractStatusDirective,
|
extractStatusDirective,
|
||||||
extractThinkDirective,
|
extractThinkDirective,
|
||||||
@@ -27,6 +29,20 @@ export type InlineDirectives = {
|
|||||||
hasElevatedDirective: boolean;
|
hasElevatedDirective: boolean;
|
||||||
elevatedLevel?: ElevatedLevel;
|
elevatedLevel?: ElevatedLevel;
|
||||||
rawElevatedLevel?: string;
|
rawElevatedLevel?: string;
|
||||||
|
hasExecDirective: boolean;
|
||||||
|
execHost?: ExecHost;
|
||||||
|
execSecurity?: ExecSecurity;
|
||||||
|
execAsk?: ExecAsk;
|
||||||
|
execNode?: string;
|
||||||
|
rawExecHost?: string;
|
||||||
|
rawExecSecurity?: string;
|
||||||
|
rawExecAsk?: string;
|
||||||
|
rawExecNode?: string;
|
||||||
|
hasExecOptions: boolean;
|
||||||
|
invalidExecHost: boolean;
|
||||||
|
invalidExecSecurity: boolean;
|
||||||
|
invalidExecAsk: boolean;
|
||||||
|
invalidExecNode: boolean;
|
||||||
hasStatusDirective: boolean;
|
hasStatusDirective: boolean;
|
||||||
hasModelDirective: boolean;
|
hasModelDirective: boolean;
|
||||||
rawModelDirective?: string;
|
rawModelDirective?: string;
|
||||||
@@ -83,10 +99,27 @@ export function parseInlineDirectives(
|
|||||||
hasDirective: false,
|
hasDirective: false,
|
||||||
}
|
}
|
||||||
: extractElevatedDirective(reasoningCleaned);
|
: extractElevatedDirective(reasoningCleaned);
|
||||||
|
const {
|
||||||
|
cleaned: execCleaned,
|
||||||
|
execHost,
|
||||||
|
execSecurity,
|
||||||
|
execAsk,
|
||||||
|
execNode,
|
||||||
|
rawExecHost,
|
||||||
|
rawExecSecurity,
|
||||||
|
rawExecAsk,
|
||||||
|
rawExecNode,
|
||||||
|
hasExecOptions,
|
||||||
|
invalidHost: invalidExecHost,
|
||||||
|
invalidSecurity: invalidExecSecurity,
|
||||||
|
invalidAsk: invalidExecAsk,
|
||||||
|
invalidNode: invalidExecNode,
|
||||||
|
hasDirective: hasExecDirective,
|
||||||
|
} = extractExecDirective(elevatedCleaned);
|
||||||
const allowStatusDirective = options?.allowStatusDirective !== false;
|
const allowStatusDirective = options?.allowStatusDirective !== false;
|
||||||
const { cleaned: statusCleaned, hasDirective: hasStatusDirective } = allowStatusDirective
|
const { cleaned: statusCleaned, hasDirective: hasStatusDirective } = allowStatusDirective
|
||||||
? extractStatusDirective(elevatedCleaned)
|
? extractStatusDirective(execCleaned)
|
||||||
: { cleaned: elevatedCleaned, hasDirective: false };
|
: { cleaned: execCleaned, hasDirective: false };
|
||||||
const {
|
const {
|
||||||
cleaned: modelCleaned,
|
cleaned: modelCleaned,
|
||||||
rawModel,
|
rawModel,
|
||||||
@@ -124,6 +157,20 @@ export function parseInlineDirectives(
|
|||||||
hasElevatedDirective,
|
hasElevatedDirective,
|
||||||
elevatedLevel,
|
elevatedLevel,
|
||||||
rawElevatedLevel,
|
rawElevatedLevel,
|
||||||
|
hasExecDirective,
|
||||||
|
execHost,
|
||||||
|
execSecurity,
|
||||||
|
execAsk,
|
||||||
|
execNode,
|
||||||
|
rawExecHost,
|
||||||
|
rawExecSecurity,
|
||||||
|
rawExecAsk,
|
||||||
|
rawExecNode,
|
||||||
|
hasExecOptions,
|
||||||
|
invalidExecHost,
|
||||||
|
invalidExecSecurity,
|
||||||
|
invalidExecAsk,
|
||||||
|
invalidExecNode,
|
||||||
hasStatusDirective,
|
hasStatusDirective,
|
||||||
hasModelDirective,
|
hasModelDirective,
|
||||||
rawModelDirective: rawModel,
|
rawModelDirective: rawModel,
|
||||||
@@ -156,6 +203,7 @@ export function isDirectiveOnly(params: {
|
|||||||
!directives.hasVerboseDirective &&
|
!directives.hasVerboseDirective &&
|
||||||
!directives.hasReasoningDirective &&
|
!directives.hasReasoningDirective &&
|
||||||
!directives.hasElevatedDirective &&
|
!directives.hasElevatedDirective &&
|
||||||
|
!directives.hasExecDirective &&
|
||||||
!directives.hasModelDirective &&
|
!directives.hasModelDirective &&
|
||||||
!directives.hasQueueDirective
|
!directives.hasQueueDirective
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -118,6 +118,24 @@ export async function persistInlineDirectives(params: {
|
|||||||
(directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined);
|
(directives.elevatedLevel !== prevElevatedLevel && directives.elevatedLevel !== undefined);
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
|
if (directives.hasExecDirective && directives.hasExecOptions) {
|
||||||
|
if (directives.execHost) {
|
||||||
|
sessionEntry.execHost = directives.execHost;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
if (directives.execSecurity) {
|
||||||
|
sessionEntry.execSecurity = directives.execSecurity;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
if (directives.execAsk) {
|
||||||
|
sessionEntry.execAsk = directives.execAsk;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
if (directives.execNode) {
|
||||||
|
sessionEntry.execNode = directives.execNode;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const modelDirective =
|
const modelDirective =
|
||||||
directives.hasModelDirective && params.effectiveModelDirective
|
directives.hasModelDirective && params.effectiveModelDirective
|
||||||
|
|||||||
@@ -153,3 +153,4 @@ export function extractStatusDirective(body?: string): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel };
|
export type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel };
|
||||||
|
export { extractExecDirective } from "./exec/directive.js";
|
||||||
|
|||||||
1
src/auto-reply/reply/exec.ts
Normal file
1
src/auto-reply/reply/exec.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { extractExecDirective } from "./exec/directive.js";
|
||||||
198
src/auto-reply/reply/exec/directive.ts
Normal file
198
src/auto-reply/reply/exec/directive.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import type { ExecAsk, ExecHost, ExecSecurity } from "../../../infra/exec-approvals.js";
|
||||||
|
|
||||||
|
type ExecDirectiveParse = {
|
||||||
|
cleaned: string;
|
||||||
|
hasDirective: boolean;
|
||||||
|
execHost?: ExecHost;
|
||||||
|
execSecurity?: ExecSecurity;
|
||||||
|
execAsk?: ExecAsk;
|
||||||
|
execNode?: string;
|
||||||
|
rawExecHost?: string;
|
||||||
|
rawExecSecurity?: string;
|
||||||
|
rawExecAsk?: string;
|
||||||
|
rawExecNode?: string;
|
||||||
|
hasExecOptions: boolean;
|
||||||
|
invalidHost: boolean;
|
||||||
|
invalidSecurity: boolean;
|
||||||
|
invalidAsk: boolean;
|
||||||
|
invalidNode: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeExecHost(value?: string): ExecHost | undefined {
|
||||||
|
const normalized = value?.trim().toLowerCase();
|
||||||
|
if (normalized === "sandbox" || normalized === "gateway" || normalized === "node") return normalized;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExecSecurity(value?: string): ExecSecurity | undefined {
|
||||||
|
const normalized = value?.trim().toLowerCase();
|
||||||
|
if (normalized === "deny" || normalized === "allowlist" || normalized === "full") return normalized;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExecAsk(value?: string): ExecAsk | undefined {
|
||||||
|
const normalized = value?.trim().toLowerCase();
|
||||||
|
if (normalized === "off" || normalized === "on-miss" || normalized === "always") {
|
||||||
|
return normalized as ExecAsk;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExecDirectiveArgs(raw: string): Omit<ExecDirectiveParse, "cleaned" | "hasDirective"> & {
|
||||||
|
consumed: number;
|
||||||
|
} {
|
||||||
|
let i = 0;
|
||||||
|
const len = raw.length;
|
||||||
|
while (i < len && /\s/.test(raw[i])) i += 1;
|
||||||
|
if (raw[i] === ":") {
|
||||||
|
i += 1;
|
||||||
|
while (i < len && /\s/.test(raw[i])) i += 1;
|
||||||
|
}
|
||||||
|
let consumed = i;
|
||||||
|
let execHost: ExecHost | undefined;
|
||||||
|
let execSecurity: ExecSecurity | undefined;
|
||||||
|
let execAsk: ExecAsk | undefined;
|
||||||
|
let execNode: string | undefined;
|
||||||
|
let rawExecHost: string | undefined;
|
||||||
|
let rawExecSecurity: string | undefined;
|
||||||
|
let rawExecAsk: string | undefined;
|
||||||
|
let rawExecNode: string | undefined;
|
||||||
|
let hasExecOptions = false;
|
||||||
|
let invalidHost = false;
|
||||||
|
let invalidSecurity = false;
|
||||||
|
let invalidAsk = false;
|
||||||
|
let invalidNode = false;
|
||||||
|
|
||||||
|
const takeToken = (): string | null => {
|
||||||
|
if (i >= len) return null;
|
||||||
|
const start = i;
|
||||||
|
while (i < len && !/\s/.test(raw[i])) i += 1;
|
||||||
|
if (start === i) return null;
|
||||||
|
const token = raw.slice(start, i);
|
||||||
|
while (i < len && /\s/.test(raw[i])) i += 1;
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitToken = (token: string): { key: string; value: string } | null => {
|
||||||
|
const eq = token.indexOf("=");
|
||||||
|
const colon = token.indexOf(":");
|
||||||
|
const idx =
|
||||||
|
eq === -1 ? colon : colon === -1 ? eq : Math.min(eq, colon);
|
||||||
|
if (idx === -1) return null;
|
||||||
|
const key = token.slice(0, idx).trim().toLowerCase();
|
||||||
|
const value = token.slice(idx + 1).trim();
|
||||||
|
if (!key) return null;
|
||||||
|
return { key, value };
|
||||||
|
};
|
||||||
|
|
||||||
|
while (i < len) {
|
||||||
|
const token = takeToken();
|
||||||
|
if (!token) break;
|
||||||
|
const parsed = splitToken(token);
|
||||||
|
if (!parsed) break;
|
||||||
|
const { key, value } = parsed;
|
||||||
|
if (key === "host") {
|
||||||
|
rawExecHost = value;
|
||||||
|
execHost = normalizeExecHost(value);
|
||||||
|
if (!execHost) invalidHost = true;
|
||||||
|
hasExecOptions = true;
|
||||||
|
consumed = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === "security") {
|
||||||
|
rawExecSecurity = value;
|
||||||
|
execSecurity = normalizeExecSecurity(value);
|
||||||
|
if (!execSecurity) invalidSecurity = true;
|
||||||
|
hasExecOptions = true;
|
||||||
|
consumed = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === "ask") {
|
||||||
|
rawExecAsk = value;
|
||||||
|
execAsk = normalizeExecAsk(value);
|
||||||
|
if (!execAsk) invalidAsk = true;
|
||||||
|
hasExecOptions = true;
|
||||||
|
consumed = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === "node") {
|
||||||
|
rawExecNode = value;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
invalidNode = true;
|
||||||
|
} else {
|
||||||
|
execNode = trimmed;
|
||||||
|
}
|
||||||
|
hasExecOptions = true;
|
||||||
|
consumed = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
consumed,
|
||||||
|
execHost,
|
||||||
|
execSecurity,
|
||||||
|
execAsk,
|
||||||
|
execNode,
|
||||||
|
rawExecHost,
|
||||||
|
rawExecSecurity,
|
||||||
|
rawExecAsk,
|
||||||
|
rawExecNode,
|
||||||
|
hasExecOptions,
|
||||||
|
invalidHost,
|
||||||
|
invalidSecurity,
|
||||||
|
invalidAsk,
|
||||||
|
invalidNode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractExecDirective(body?: string): ExecDirectiveParse {
|
||||||
|
if (!body) {
|
||||||
|
return {
|
||||||
|
cleaned: "",
|
||||||
|
hasDirective: false,
|
||||||
|
hasExecOptions: false,
|
||||||
|
invalidHost: false,
|
||||||
|
invalidSecurity: false,
|
||||||
|
invalidAsk: false,
|
||||||
|
invalidNode: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const re = /(?:^|\s)\/exec(?=$|\s|:)/i;
|
||||||
|
const match = re.exec(body);
|
||||||
|
if (!match) {
|
||||||
|
return {
|
||||||
|
cleaned: body.trim(),
|
||||||
|
hasDirective: false,
|
||||||
|
hasExecOptions: false,
|
||||||
|
invalidHost: false,
|
||||||
|
invalidSecurity: false,
|
||||||
|
invalidAsk: false,
|
||||||
|
invalidNode: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const start = match.index + match[0].indexOf("/exec");
|
||||||
|
const argsStart = start + "/exec".length;
|
||||||
|
const parsed = parseExecDirectiveArgs(body.slice(argsStart));
|
||||||
|
const cleanedRaw = `${body.slice(0, start)} ${body.slice(argsStart + parsed.consumed)}`;
|
||||||
|
const cleaned = cleanedRaw.replace(/\s+/g, " ").trim();
|
||||||
|
return {
|
||||||
|
cleaned,
|
||||||
|
hasDirective: true,
|
||||||
|
execHost: parsed.execHost,
|
||||||
|
execSecurity: parsed.execSecurity,
|
||||||
|
execAsk: parsed.execAsk,
|
||||||
|
execNode: parsed.execNode,
|
||||||
|
rawExecHost: parsed.rawExecHost,
|
||||||
|
rawExecSecurity: parsed.rawExecSecurity,
|
||||||
|
rawExecAsk: parsed.rawExecAsk,
|
||||||
|
rawExecNode: parsed.rawExecNode,
|
||||||
|
hasExecOptions: parsed.hasExecOptions,
|
||||||
|
invalidHost: parsed.invalidHost,
|
||||||
|
invalidSecurity: parsed.invalidSecurity,
|
||||||
|
invalidAsk: parsed.invalidAsk,
|
||||||
|
invalidNode: parsed.invalidNode,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -158,6 +158,7 @@ export function createFollowupRunner(params: {
|
|||||||
thinkLevel: queued.run.thinkLevel,
|
thinkLevel: queued.run.thinkLevel,
|
||||||
verboseLevel: queued.run.verboseLevel,
|
verboseLevel: queued.run.verboseLevel,
|
||||||
reasoningLevel: queued.run.reasoningLevel,
|
reasoningLevel: queued.run.reasoningLevel,
|
||||||
|
execOverrides: queued.run.execOverrides,
|
||||||
bashElevated: queued.run.bashElevated,
|
bashElevated: queued.run.bashElevated,
|
||||||
timeoutMs: queued.run.timeoutMs,
|
timeoutMs: queued.run.timeoutMs,
|
||||||
runId,
|
runId,
|
||||||
|
|||||||
@@ -110,6 +110,20 @@ export async function applyInlineDirectiveOverrides(params: {
|
|||||||
hasVerboseDirective: false,
|
hasVerboseDirective: false,
|
||||||
hasReasoningDirective: false,
|
hasReasoningDirective: false,
|
||||||
hasElevatedDirective: false,
|
hasElevatedDirective: false,
|
||||||
|
hasExecDirective: false,
|
||||||
|
execHost: undefined,
|
||||||
|
execSecurity: undefined,
|
||||||
|
execAsk: undefined,
|
||||||
|
execNode: undefined,
|
||||||
|
rawExecHost: undefined,
|
||||||
|
rawExecSecurity: undefined,
|
||||||
|
rawExecAsk: undefined,
|
||||||
|
rawExecNode: undefined,
|
||||||
|
hasExecOptions: false,
|
||||||
|
invalidExecHost: false,
|
||||||
|
invalidExecSecurity: false,
|
||||||
|
invalidExecAsk: false,
|
||||||
|
invalidExecNode: false,
|
||||||
hasStatusDirective: false,
|
hasStatusDirective: false,
|
||||||
hasModelDirective: false,
|
hasModelDirective: false,
|
||||||
hasQueueDirective: false,
|
hasQueueDirective: false,
|
||||||
@@ -206,6 +220,7 @@ export async function applyInlineDirectiveOverrides(params: {
|
|||||||
directives.hasVerboseDirective ||
|
directives.hasVerboseDirective ||
|
||||||
directives.hasReasoningDirective ||
|
directives.hasReasoningDirective ||
|
||||||
directives.hasElevatedDirective ||
|
directives.hasElevatedDirective ||
|
||||||
|
directives.hasExecDirective ||
|
||||||
directives.hasModelDirective ||
|
directives.hasModelDirective ||
|
||||||
directives.hasQueueDirective ||
|
directives.hasQueueDirective ||
|
||||||
directives.hasStatusDirective;
|
directives.hasStatusDirective;
|
||||||
|
|||||||
@@ -15,6 +15,20 @@ export function clearInlineDirectives(cleaned: string): InlineDirectives {
|
|||||||
hasElevatedDirective: false,
|
hasElevatedDirective: false,
|
||||||
elevatedLevel: undefined,
|
elevatedLevel: undefined,
|
||||||
rawElevatedLevel: undefined,
|
rawElevatedLevel: undefined,
|
||||||
|
hasExecDirective: false,
|
||||||
|
execHost: undefined,
|
||||||
|
execSecurity: undefined,
|
||||||
|
execAsk: undefined,
|
||||||
|
execNode: undefined,
|
||||||
|
rawExecHost: undefined,
|
||||||
|
rawExecSecurity: undefined,
|
||||||
|
rawExecAsk: undefined,
|
||||||
|
rawExecNode: undefined,
|
||||||
|
hasExecOptions: false,
|
||||||
|
invalidExecHost: false,
|
||||||
|
invalidExecSecurity: false,
|
||||||
|
invalidExecAsk: false,
|
||||||
|
invalidExecNode: false,
|
||||||
hasStatusDirective: false,
|
hasStatusDirective: false,
|
||||||
hasModelDirective: false,
|
hasModelDirective: false,
|
||||||
rawModelDirective: undefined,
|
rawModelDirective: undefined,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
|
||||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||||
import type { SkillCommandSpec } from "../../agents/skills.js";
|
import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
||||||
@@ -21,6 +22,7 @@ import { stripInlineStatus } from "./reply-inline.js";
|
|||||||
import type { TypingController } from "./typing.js";
|
import type { TypingController } from "./typing.js";
|
||||||
|
|
||||||
type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
|
type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
|
||||||
|
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||||
|
|
||||||
export type ReplyDirectiveContinuation = {
|
export type ReplyDirectiveContinuation = {
|
||||||
commandSource: string;
|
commandSource: string;
|
||||||
@@ -38,6 +40,7 @@ export type ReplyDirectiveContinuation = {
|
|||||||
resolvedVerboseLevel: VerboseLevel | undefined;
|
resolvedVerboseLevel: VerboseLevel | undefined;
|
||||||
resolvedReasoningLevel: ReasoningLevel;
|
resolvedReasoningLevel: ReasoningLevel;
|
||||||
resolvedElevatedLevel: ElevatedLevel;
|
resolvedElevatedLevel: ElevatedLevel;
|
||||||
|
execOverrides?: ExecOverrides;
|
||||||
blockStreamingEnabled: boolean;
|
blockStreamingEnabled: boolean;
|
||||||
blockReplyChunking?: {
|
blockReplyChunking?: {
|
||||||
minChars: number;
|
minChars: number;
|
||||||
@@ -59,6 +62,19 @@ export type ReplyDirectiveContinuation = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveExecOverrides(params: {
|
||||||
|
directives: InlineDirectives;
|
||||||
|
sessionEntry?: SessionEntry;
|
||||||
|
}): ExecOverrides | undefined {
|
||||||
|
const host = params.directives.execHost ?? (params.sessionEntry?.execHost as ExecOverrides["host"]);
|
||||||
|
const security =
|
||||||
|
params.directives.execSecurity ?? (params.sessionEntry?.execSecurity as ExecOverrides["security"]);
|
||||||
|
const ask = params.directives.execAsk ?? (params.sessionEntry?.execAsk as ExecOverrides["ask"]);
|
||||||
|
const node = params.directives.execNode ?? params.sessionEntry?.execNode;
|
||||||
|
if (!host && !security && !ask && !node) return undefined;
|
||||||
|
return { host, security, ask, node };
|
||||||
|
}
|
||||||
|
|
||||||
export type ReplyDirectiveResult =
|
export type ReplyDirectiveResult =
|
||||||
| { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
|
| { kind: "reply"; reply: ReplyPayload | ReplyPayload[] | undefined }
|
||||||
| { kind: "continue"; result: ReplyDirectiveContinuation };
|
| { kind: "continue"; result: ReplyDirectiveContinuation };
|
||||||
@@ -190,11 +206,33 @@ export async function resolveReplyDirectives(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isGroup && ctx.WasMentioned !== true && parsedDirectives.hasExecDirective) {
|
||||||
|
if (parsedDirectives.execSecurity !== "deny") {
|
||||||
|
parsedDirectives = {
|
||||||
|
...parsedDirectives,
|
||||||
|
hasExecDirective: false,
|
||||||
|
execHost: undefined,
|
||||||
|
execSecurity: undefined,
|
||||||
|
execAsk: undefined,
|
||||||
|
execNode: undefined,
|
||||||
|
rawExecHost: undefined,
|
||||||
|
rawExecSecurity: undefined,
|
||||||
|
rawExecAsk: undefined,
|
||||||
|
rawExecNode: undefined,
|
||||||
|
hasExecOptions: false,
|
||||||
|
invalidExecHost: false,
|
||||||
|
invalidExecSecurity: false,
|
||||||
|
invalidExecAsk: false,
|
||||||
|
invalidExecNode: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
const hasInlineDirective =
|
const hasInlineDirective =
|
||||||
parsedDirectives.hasThinkDirective ||
|
parsedDirectives.hasThinkDirective ||
|
||||||
parsedDirectives.hasVerboseDirective ||
|
parsedDirectives.hasVerboseDirective ||
|
||||||
parsedDirectives.hasReasoningDirective ||
|
parsedDirectives.hasReasoningDirective ||
|
||||||
parsedDirectives.hasElevatedDirective ||
|
parsedDirectives.hasElevatedDirective ||
|
||||||
|
parsedDirectives.hasExecDirective ||
|
||||||
parsedDirectives.hasModelDirective ||
|
parsedDirectives.hasModelDirective ||
|
||||||
parsedDirectives.hasQueueDirective;
|
parsedDirectives.hasQueueDirective;
|
||||||
if (hasInlineDirective) {
|
if (hasInlineDirective) {
|
||||||
@@ -405,6 +443,7 @@ export async function resolveReplyDirectives(params: {
|
|||||||
model = applyResult.model;
|
model = applyResult.model;
|
||||||
contextTokens = applyResult.contextTokens;
|
contextTokens = applyResult.contextTokens;
|
||||||
const { directiveAck, perMessageQueueMode, perMessageQueueOptions } = applyResult;
|
const { directiveAck, perMessageQueueMode, perMessageQueueOptions } = applyResult;
|
||||||
|
const execOverrides = resolveExecOverrides({ directives, sessionEntry });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: "continue",
|
kind: "continue",
|
||||||
@@ -424,6 +463,7 @@ export async function resolveReplyDirectives(params: {
|
|||||||
resolvedVerboseLevel,
|
resolvedVerboseLevel,
|
||||||
resolvedReasoningLevel,
|
resolvedReasoningLevel,
|
||||||
resolvedElevatedLevel,
|
resolvedElevatedLevel,
|
||||||
|
execOverrides,
|
||||||
blockStreamingEnabled,
|
blockStreamingEnabled,
|
||||||
blockReplyChunking,
|
blockReplyChunking,
|
||||||
resolvedBlockStreamingBreak,
|
resolvedBlockStreamingBreak,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
isProfileInCooldown,
|
isProfileInCooldown,
|
||||||
resolveAuthProfileOrder,
|
resolveAuthProfileOrder,
|
||||||
} from "../../agents/auth-profiles.js";
|
} from "../../agents/auth-profiles.js";
|
||||||
|
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
resolveSessionFilePath,
|
resolveSessionFilePath,
|
||||||
@@ -47,6 +48,7 @@ import type { TypingController } from "./typing.js";
|
|||||||
import { createTypingSignaler, resolveTypingMode } from "./typing-mode.js";
|
import { createTypingSignaler, resolveTypingMode } from "./typing-mode.js";
|
||||||
|
|
||||||
type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
|
type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
|
||||||
|
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||||
|
|
||||||
const BARE_SESSION_RESET_PROMPT =
|
const BARE_SESSION_RESET_PROMPT =
|
||||||
"A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning.";
|
"A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning.";
|
||||||
@@ -69,6 +71,7 @@ type RunPreparedReplyParams = {
|
|||||||
resolvedVerboseLevel: VerboseLevel | undefined;
|
resolvedVerboseLevel: VerboseLevel | undefined;
|
||||||
resolvedReasoningLevel: ReasoningLevel;
|
resolvedReasoningLevel: ReasoningLevel;
|
||||||
resolvedElevatedLevel: ElevatedLevel;
|
resolvedElevatedLevel: ElevatedLevel;
|
||||||
|
execOverrides?: ExecOverrides;
|
||||||
elevatedEnabled: boolean;
|
elevatedEnabled: boolean;
|
||||||
elevatedAllowed: boolean;
|
elevatedAllowed: boolean;
|
||||||
blockStreamingEnabled: boolean;
|
blockStreamingEnabled: boolean;
|
||||||
@@ -227,6 +230,7 @@ export async function runPreparedReply(
|
|||||||
resolvedVerboseLevel,
|
resolvedVerboseLevel,
|
||||||
resolvedReasoningLevel,
|
resolvedReasoningLevel,
|
||||||
resolvedElevatedLevel,
|
resolvedElevatedLevel,
|
||||||
|
execOverrides,
|
||||||
abortedLastRun,
|
abortedLastRun,
|
||||||
} = params;
|
} = params;
|
||||||
let currentSystemSent = systemSent;
|
let currentSystemSent = systemSent;
|
||||||
@@ -430,6 +434,7 @@ export async function runPreparedReply(
|
|||||||
verboseLevel: resolvedVerboseLevel,
|
verboseLevel: resolvedVerboseLevel,
|
||||||
reasoningLevel: resolvedReasoningLevel,
|
reasoningLevel: resolvedReasoningLevel,
|
||||||
elevatedLevel: resolvedElevatedLevel,
|
elevatedLevel: resolvedElevatedLevel,
|
||||||
|
execOverrides,
|
||||||
bashElevated: {
|
bashElevated: {
|
||||||
enabled: elevatedEnabled,
|
enabled: elevatedEnabled,
|
||||||
allowed: elevatedAllowed,
|
allowed: elevatedAllowed,
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export async function getReplyFromConfig(
|
|||||||
resolvedVerboseLevel,
|
resolvedVerboseLevel,
|
||||||
resolvedReasoningLevel,
|
resolvedReasoningLevel,
|
||||||
resolvedElevatedLevel,
|
resolvedElevatedLevel,
|
||||||
|
execOverrides,
|
||||||
blockStreamingEnabled,
|
blockStreamingEnabled,
|
||||||
blockReplyChunking,
|
blockReplyChunking,
|
||||||
resolvedBlockStreamingBreak,
|
resolvedBlockStreamingBreak,
|
||||||
@@ -241,6 +242,7 @@ export async function getReplyFromConfig(
|
|||||||
resolvedVerboseLevel,
|
resolvedVerboseLevel,
|
||||||
resolvedReasoningLevel,
|
resolvedReasoningLevel,
|
||||||
resolvedElevatedLevel,
|
resolvedElevatedLevel,
|
||||||
|
execOverrides,
|
||||||
elevatedEnabled,
|
elevatedEnabled,
|
||||||
elevatedAllowed,
|
elevatedAllowed,
|
||||||
blockStreamingEnabled,
|
blockStreamingEnabled,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ClawdbotConfig } from "../../../config/config.js";
|
|||||||
import type { SessionEntry } from "../../../config/sessions.js";
|
import type { SessionEntry } from "../../../config/sessions.js";
|
||||||
import type { OriginatingChannelType } from "../../templating.js";
|
import type { OriginatingChannelType } from "../../templating.js";
|
||||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../directives.js";
|
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../directives.js";
|
||||||
|
import type { ExecToolDefaults } from "../../../agents/bash-tools.js";
|
||||||
|
|
||||||
export type QueueMode = "steer" | "followup" | "collect" | "steer-backlog" | "interrupt" | "queue";
|
export type QueueMode = "steer" | "followup" | "collect" | "steer-backlog" | "interrupt" | "queue";
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ export type FollowupRun = {
|
|||||||
verboseLevel?: VerboseLevel;
|
verboseLevel?: VerboseLevel;
|
||||||
reasoningLevel?: ReasoningLevel;
|
reasoningLevel?: ReasoningLevel;
|
||||||
elevatedLevel?: ElevatedLevel;
|
elevatedLevel?: ElevatedLevel;
|
||||||
|
execOverrides?: Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
|
||||||
bashElevated?: {
|
bashElevated?: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
allowed: boolean;
|
allowed: boolean;
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ export type SessionEntry = {
|
|||||||
verboseLevel?: string;
|
verboseLevel?: string;
|
||||||
reasoningLevel?: string;
|
reasoningLevel?: string;
|
||||||
elevatedLevel?: string;
|
elevatedLevel?: string;
|
||||||
|
execHost?: string;
|
||||||
|
execSecurity?: string;
|
||||||
|
execAsk?: string;
|
||||||
|
execNode?: string;
|
||||||
responseUsage?: "on" | "off" | "tokens" | "full";
|
responseUsage?: "on" | "off" | "tokens" | "full";
|
||||||
providerOverride?: string;
|
providerOverride?: string;
|
||||||
modelOverride?: string;
|
modelOverride?: string;
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export const SessionsPatchParamsSchema = Type.Object(
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
|
execHost: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
|
execSecurity: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
|
execAsk: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
|
execNode: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
sendPolicy: Type.Optional(
|
sendPolicy: Type.Optional(
|
||||||
|
|||||||
@@ -30,6 +30,30 @@ function invalid(message: string): { ok: false; error: ErrorShape } {
|
|||||||
return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, message) };
|
return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, message) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeExecHost(raw: string): "sandbox" | "gateway" | "node" | undefined {
|
||||||
|
const normalized = raw.trim().toLowerCase();
|
||||||
|
if (normalized === "sandbox" || normalized === "gateway" || normalized === "node") {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExecSecurity(raw: string): "deny" | "allowlist" | "full" | undefined {
|
||||||
|
const normalized = raw.trim().toLowerCase();
|
||||||
|
if (normalized === "deny" || normalized === "allowlist" || normalized === "full") {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExecAsk(raw: string): "off" | "on-miss" | "always" | undefined {
|
||||||
|
const normalized = raw.trim().toLowerCase();
|
||||||
|
if (normalized === "off" || normalized === "on-miss" || normalized === "always") {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export async function applySessionsPatchToStore(params: {
|
export async function applySessionsPatchToStore(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
store: Record<string, SessionEntry>;
|
store: Record<string, SessionEntry>;
|
||||||
@@ -150,6 +174,50 @@ export async function applySessionsPatchToStore(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("execHost" in patch) {
|
||||||
|
const raw = patch.execHost;
|
||||||
|
if (raw === null) {
|
||||||
|
delete next.execHost;
|
||||||
|
} else if (raw !== undefined) {
|
||||||
|
const normalized = normalizeExecHost(String(raw));
|
||||||
|
if (!normalized) return invalid('invalid execHost (use "sandbox"|"gateway"|"node")');
|
||||||
|
next.execHost = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("execSecurity" in patch) {
|
||||||
|
const raw = patch.execSecurity;
|
||||||
|
if (raw === null) {
|
||||||
|
delete next.execSecurity;
|
||||||
|
} else if (raw !== undefined) {
|
||||||
|
const normalized = normalizeExecSecurity(String(raw));
|
||||||
|
if (!normalized) return invalid('invalid execSecurity (use "deny"|"allowlist"|"full")');
|
||||||
|
next.execSecurity = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("execAsk" in patch) {
|
||||||
|
const raw = patch.execAsk;
|
||||||
|
if (raw === null) {
|
||||||
|
delete next.execAsk;
|
||||||
|
} else if (raw !== undefined) {
|
||||||
|
const normalized = normalizeExecAsk(String(raw));
|
||||||
|
if (!normalized) return invalid('invalid execAsk (use "off"|"on-miss"|"always")');
|
||||||
|
next.execAsk = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("execNode" in patch) {
|
||||||
|
const raw = patch.execNode;
|
||||||
|
if (raw === null) {
|
||||||
|
delete next.execNode;
|
||||||
|
} else if (raw !== undefined) {
|
||||||
|
const trimmed = String(raw).trim();
|
||||||
|
if (!trimmed) return invalid("invalid execNode: empty");
|
||||||
|
next.execNode = trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("model" in patch) {
|
if ("model" in patch) {
|
||||||
const raw = patch.model;
|
const raw = patch.model;
|
||||||
if (raw === null) {
|
if (raw === null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user