refactor: extract shared dedupe helpers for runtime paths

This commit is contained in:
Peter Steinberger
2026-02-23 05:43:21 +00:00
parent 9f508056d3
commit 8af19ddc5b
16 changed files with 307 additions and 275 deletions

View File

@@ -42,3 +42,27 @@ export async function requestExecApprovalDecision(
: undefined;
return typeof decisionValue === "string" ? decisionValue : null;
}
export async function requestExecApprovalDecisionForHost(params: {
approvalId: string;
command: string;
workdir: string;
host: "gateway" | "node";
security: ExecSecurity;
ask: ExecAsk;
agentId?: string;
resolvedPath?: string;
sessionKey?: string;
}): Promise<string | null> {
return await requestExecApprovalDecision({
id: params.approvalId,
command: params.command,
cwd: params.workdir,
host: params.host,
security: params.security,
ask: params.ask,
agentId: params.agentId,
resolvedPath: params.resolvedPath,
sessionKey: params.sessionKey,
});
}

View File

@@ -16,7 +16,7 @@ import {
} from "../infra/exec-approvals.js";
import type { SafeBinProfile } from "../infra/exec-safe-bin-policy.js";
import { markBackgrounded, tail } from "./bash-process-registry.js";
import { requestExecApprovalDecision } from "./bash-tools.exec-approval-request.js";
import { requestExecApprovalDecisionForHost } from "./bash-tools.exec-approval-request.js";
import {
DEFAULT_APPROVAL_TIMEOUT_MS,
DEFAULT_NOTIFY_TAIL_CHARS,
@@ -81,6 +81,19 @@ export async function processGatewayAllowlist(
const analysisOk = allowlistEval.analysisOk;
const allowlistSatisfied =
hostSecurity === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
const recordMatchedAllowlistUse = (resolvedPath?: string) => {
if (allowlistMatches.length === 0) {
return;
}
const seen = new Set<string>();
for (const match of allowlistMatches) {
if (seen.has(match.pattern)) {
continue;
}
seen.add(match.pattern);
recordAllowlistUse(approvals.file, params.agentId, match, params.command, resolvedPath);
}
};
const hasHeredocSegment = allowlistEval.segments.some((segment) =>
segment.argv.some((token) => token.startsWith("<<")),
);
@@ -113,10 +126,10 @@ export async function processGatewayAllowlist(
void (async () => {
let decision: string | null = null;
try {
decision = await requestExecApprovalDecision({
id: approvalId,
decision = await requestExecApprovalDecisionForHost({
approvalId,
command: params.command,
cwd: params.workdir,
workdir: params.workdir,
host: "gateway",
security: hostSecurity,
ask: hostAsk,
@@ -186,22 +199,7 @@ export async function processGatewayAllowlist(
return;
}
if (allowlistMatches.length > 0) {
const seen = new Set<string>();
for (const match of allowlistMatches) {
if (seen.has(match.pattern)) {
continue;
}
seen.add(match.pattern);
recordAllowlistUse(
approvals.file,
params.agentId,
match,
params.command,
resolvedPath ?? undefined,
);
}
}
recordMatchedAllowlistUse(resolvedPath ?? undefined);
let run: Awaited<ReturnType<typeof runExecProcess>> | null = null;
try {
@@ -321,22 +319,7 @@ export async function processGatewayAllowlist(
}
}
if (allowlistMatches.length > 0) {
const seen = new Set<string>();
for (const match of allowlistMatches) {
if (seen.has(match.pattern)) {
continue;
}
seen.add(match.pattern);
recordAllowlistUse(
approvals.file,
params.agentId,
match,
params.command,
allowlistEval.segments[0]?.resolution?.resolvedPath,
);
}
}
recordMatchedAllowlistUse(allowlistEval.segments[0]?.resolution?.resolvedPath);
return { execCommandOverride };
}

View File

@@ -12,7 +12,7 @@ import {
resolveExecApprovalsFromFile,
} from "../infra/exec-approvals.js";
import { buildNodeShellCommand } from "../infra/node-shell.js";
import { requestExecApprovalDecision } from "./bash-tools.exec-approval-request.js";
import { requestExecApprovalDecisionForHost } from "./bash-tools.exec-approval-request.js";
import {
DEFAULT_APPROVAL_TIMEOUT_MS,
createApprovalSlug,
@@ -178,10 +178,10 @@ export async function executeNodeHostCommand(
void (async () => {
let decision: string | null = null;
try {
decision = await requestExecApprovalDecision({
id: approvalId,
decision = await requestExecApprovalDecisionForHost({
approvalId,
command: params.command,
cwd: params.workdir,
workdir: params.workdir,
host: "node",
security: hostSecurity,
ask: hostAsk,

View File

@@ -317,35 +317,10 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
}
return `\`\`\`txt\n${trimmed}\n\`\`\``;
};
const emitToolSummary = (toolName?: string, meta?: string) => {
const emitToolResultMessage = (toolName: string | undefined, message: string) => {
if (!params.onToolResult) {
return;
}
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
markdown: useMarkdown,
});
const { text: cleanedText, mediaUrls } = parseReplyDirectives(agg);
const filteredMediaUrls = filterToolResultMediaUrls(toolName, mediaUrls ?? []);
if (!cleanedText && filteredMediaUrls.length === 0) {
return;
}
try {
void params.onToolResult({
text: cleanedText,
mediaUrls: filteredMediaUrls.length ? filteredMediaUrls : undefined,
});
} catch {
// ignore tool result delivery failures
}
};
const emitToolOutput = (toolName?: string, meta?: string, output?: string) => {
if (!params.onToolResult || !output) {
return;
}
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
markdown: useMarkdown,
});
const message = `${agg}\n${formatToolOutputBlock(output)}`;
const { text: cleanedText, mediaUrls } = parseReplyDirectives(message);
const filteredMediaUrls = filterToolResultMediaUrls(toolName, mediaUrls ?? []);
if (!cleanedText && filteredMediaUrls.length === 0) {
@@ -360,6 +335,22 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
// ignore tool result delivery failures
}
};
const emitToolSummary = (toolName?: string, meta?: string) => {
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
markdown: useMarkdown,
});
emitToolResultMessage(toolName, agg);
};
const emitToolOutput = (toolName?: string, meta?: string, output?: string) => {
if (!output) {
return;
}
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
markdown: useMarkdown,
});
const message = `${agg}\n${formatToolOutputBlock(output)}`;
emitToolResultMessage(toolName, message);
};
const stripBlockTags = (
text: string,

View File

@@ -1,6 +1,6 @@
import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import { evaluateEntryMetadataRequirementsForCurrentPlatform } from "../shared/entry-status.js";
import { evaluateEntryRequirementsForCurrentPlatform } from "../shared/entry-status.js";
import type { RequirementConfigCheck, Requirements } from "../shared/requirements.js";
import { CONFIG_DIR } from "../utils.js";
import {
@@ -191,17 +191,15 @@ function buildSkillStatus(
? bundledNames.has(entry.skill.name)
: entry.skill.source === "openclaw-bundled";
const requirementStatus = evaluateEntryMetadataRequirementsForCurrentPlatform({
always,
metadata: entry.metadata,
frontmatter: entry.frontmatter,
hasLocalBin: hasBinary,
remote: eligibility?.remote,
isEnvSatisfied,
isConfigSatisfied,
});
const { emoji, homepage, required, missing, requirementsSatisfied, configChecks } =
requirementStatus;
evaluateEntryRequirementsForCurrentPlatform({
always,
entry,
hasLocalBin: hasBinary,
remote: eligibility?.remote,
isEnvSatisfied,
isConfigSatisfied,
});
const eligible = !disabled && !blockedByAllowlist && requirementsSatisfied;
return {

View File

@@ -5,6 +5,7 @@ import type { MemoryCitationsMode } from "../config/types.memory.js";
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
import type { ResolvedTimeFormat } from "./date-time.js";
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
import type { EmbeddedSandboxInfo } from "./pi-embedded-runner/types.js";
import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
/**
@@ -229,20 +230,7 @@ export function buildAgentSystemPrompt(params: {
repoRoot?: string;
};
messageToolHints?: string[];
sandboxInfo?: {
enabled: boolean;
workspaceDir?: string;
containerWorkspaceDir?: string;
workspaceAccess?: "none" | "ro" | "rw";
agentWorkspaceMount?: string;
browserBridgeUrl?: string;
browserNoVncUrl?: string;
hostBrowserAllowed?: boolean;
elevated?: {
allowed: boolean;
defaultLevel: "on" | "off" | "ask" | "full";
};
};
sandboxInfo?: EmbeddedSandboxInfo;
/** Reaction guidance for the agent (for Telegram minimal/extensive modes). */
reactionGuidance?: {
level: "minimal" | "extensive";