refactor(approvals): unify structured path display

This commit is contained in:
Peter Steinberger
2026-04-25 02:39:46 +01:00
parent 52ea8eadcb
commit 32dd1ffc5a
13 changed files with 97 additions and 19 deletions

View File

@@ -65,8 +65,8 @@ Docs: https://docs.openclaw.ai
- Gateway/sessions: recover main-agent turns interrupted by a gateway restart from stale transcript-lock evidence, avoiding stuck `status: "running"` sessions without broad post-boot transcript scans. Fixes #70555. Thanks @bitloi.
- Plugins/Google Meet: include live Chrome-node readiness in `googlemeet setup` and document the Parallels recovery checks, so stale node tokens or disconnected VM browsers are visible before an agent opens a meeting. Thanks @steipete.
- Codex approvals: compact home-directory permission paths to `~` without repeating them as a separate high-risk warning, while preserving filesystem root and wildcard host warnings. Thanks @steipete.
- Context engine: keep safeguard compaction checks active after context-engine windowing and for `ownsCompaction` engines, so large transcripts can compact before prompt submission instead of waiting for provider overflow. Fixes #71325. Thanks @steipete.
- Approvals: compact structured home-directory paths to `~` across Codex permission prompts and exec approval metadata without repeating them as a separate high-risk warning, while preserving filesystem root and wildcard host warnings. Thanks @steipete.
- Plugins/runtime deps: isolate the internal npm cache used for bundled plugin runtime-dependency repair and let package updates refresh/verify already-current installs, so failed update or sudo doctor runs can be repaired by rerunning `openclaw update`. Thanks @steipete.
- Agents/delete: keep `--json` output machine-readable and retain workspaces that overlap another agent's workspace instead of moving shared state to Trash. Fixes #70889 and #70890. (#70897) Thanks @kaseonedge.
- Browser/screenshot: honor `timeoutMs` through host and node screenshot requests, bound raw CDP screenshot commands, and avoid beyond-viewport CDP capture for ordinary viewport screenshots, so Windows Chrome captures no longer hang past the requested deadline. Fixes #68330. Thanks @Woodylai24.

View File

@@ -1,2 +1,2 @@
1b8ce6687d91267f78f589ee29d4cca0809fde73ea47c82ddbd14ecf54f1803a plugin-sdk-api-baseline.json
55c48203fe5d6409f690f4d27abde41502feec1bfb63d9096cd9958fcf45c2c2 plugin-sdk-api-baseline.jsonl
56ccee3ef8ff3b0ba7e2e765ae631b59254464585d5fef9db7e905f2c4c34ded plugin-sdk-api-baseline.json
39184cf8afaec691f0352d1a113e30a7099b87c0748237a3c7307e903ba24eee plugin-sdk-api-baseline.jsonl

View File

@@ -278,7 +278,7 @@ releases.
| `plugin-sdk/gateway-runtime` | Gateway helpers | Gateway client and channel-status patch helpers |
| `plugin-sdk/config-runtime` | Config helpers | Config load/write helpers |
| `plugin-sdk/telegram-command-config` | Telegram command helpers | Fallback-stable Telegram command validation helpers when the bundled Telegram contract surface is unavailable |
| `plugin-sdk/approval-runtime` | Approval prompt helpers | Exec/plugin approval payload, approval capability/profile helpers, native approval routing/runtime helpers |
| `plugin-sdk/approval-runtime` | Approval prompt helpers | Exec/plugin approval payload, approval capability/profile helpers, native approval routing/runtime helpers, and structured approval display path formatting |
| `plugin-sdk/approval-auth-runtime` | Approval auth helpers | Approver resolution, same-chat action auth |
| `plugin-sdk/approval-client-runtime` | Approval client helpers | Native exec approval profile/filter helpers |
| `plugin-sdk/approval-delivery-runtime` | Approval delivery helpers | Native approval capability/delivery adapters |

View File

@@ -122,6 +122,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| `plugin-sdk/approval-handler-runtime` | Broader approval handler runtime helpers; prefer the narrower adapter/gateway seams when they are enough |
| `plugin-sdk/approval-native-runtime` | Native approval target + account-binding helpers |
| `plugin-sdk/approval-reply-runtime` | Exec/plugin approval reply payload helpers |
| `plugin-sdk/approval-runtime` | Exec/plugin approval payload helpers, native approval routing/runtime helpers, and structured approval display helpers such as `formatApprovalDisplayPath` |
| `plugin-sdk/reply-dedupe` | Narrow inbound reply dedupe reset helpers |
| `plugin-sdk/channel-contract-testing` | Narrow channel contract test helpers without the broad testing barrel |
| `plugin-sdk/command-auth-native` | Native command auth + native session-target helpers |
@@ -156,7 +157,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
| `plugin-sdk/config-runtime` | Config load/write helpers and plugin-config lookup helpers |
| `plugin-sdk/telegram-command-config` | Telegram command-name/description normalization and duplicate/conflict checks, even when the bundled Telegram contract surface is unavailable |
| `plugin-sdk/text-autolink-runtime` | File-reference autolink detection without the broad text-runtime barrel |
| `plugin-sdk/approval-runtime` | Exec/plugin approval helpers, approval-capability builders, auth/profile helpers, native routing/runtime helpers |
| `plugin-sdk/approval-runtime` | Exec/plugin approval helpers, approval-capability builders, auth/profile helpers, native routing/runtime helpers, and structured approval display path formatting |
| `plugin-sdk/reply-runtime` | Shared inbound/reply runtime helpers, chunking, dispatch, heartbeat, reply planner |
| `plugin-sdk/reply-dispatch-runtime` | Narrow reply dispatch/finalize and conversation-label helpers |
| `plugin-sdk/reply-history` | Shared short-window reply-history helpers such as `buildHistoryContext`, `recordPendingHistoryEntry`, and `clearHistoryEntriesIfEnabled` |

View File

@@ -1,5 +1,6 @@
import {
type AgentApprovalEventData,
formatApprovalDisplayPath,
type EmbeddedRunAttemptParams,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import {
@@ -431,15 +432,10 @@ function sanitizePermissionHostValue(value: string): string {
}
function sanitizePermissionPathValue(value: string): string {
const normalized = sanitizePermissionScalar(value);
const homeCompacted = normalized
.replace(/^\/home\/(?!\.{1,2}(?=\/|$))[^/]+(?=\/|$)/, "~")
.replace(/^\/Users\/(?!\.{1,2}(?=\/|$))[^/]+(?=\/|$)/, "~")
.replace(/^[A-Za-z]:[\\/]Users[\\/](?!\.{1,2}(?=[\\/]|$))[^\\/]+(?=[\\/]|$)/i, "~");
const displayPath = homeCompacted.startsWith("~")
? homeCompacted.replace(/\\/g, "/")
: homeCompacted;
return truncate(displayPath, PERMISSION_VALUE_MAX_LENGTH);
return truncate(
formatApprovalDisplayPath(sanitizePermissionScalar(value)),
PERMISSION_VALUE_MAX_LENGTH,
);
}
function sanitizePermissionScalar(value: string): string {

View File

@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { formatApprovalDisplayPath } from "./approval-display-paths.js";
describe("approval display paths", () => {
it.each([
["/home/alice", "~"],
["/home/alice/.ssh/id_rsa", "~/.ssh/id_rsa"],
["/Users/alice/Documents/project", "~/Documents/project"],
["C:/Users/alice/project", "~/project"],
["c:/users/bob/project", "~/project"],
["C:\\Users\\alice\\.ssh\\id_rsa", "~/.ssh/id_rsa"],
["D:\\Users\\alice\\Downloads\\file.txt", "~/Downloads/file.txt"],
["/workspace/project", "/workspace/project"],
["C:\\workspace\\project", "C:\\workspace\\project"],
])("formats %s as %s", (input, expected) => {
expect(formatApprovalDisplayPath(input)).toBe(expected);
});
it.each([
"/Users/alice/../Library",
"/home/alice/./project",
"C:/Users/alice/../Windows/System32",
"C:\\Users\\alice\\.\\project",
])("does not compact relative-segment path %s", (input) => {
expect(formatApprovalDisplayPath(input)).toBe(input);
});
});

View File

@@ -0,0 +1,30 @@
export function formatApprovalDisplayPath(value: string): string {
const normalized = value.trim();
if (!normalized || hasRelativePathSegment(normalized)) {
return normalized;
}
const unixHomeMatch = normalized.match(/^\/(?:home|Users)\/([^/]+)(.*)$/);
if (unixHomeMatch && isSafeHomeSegment(unixHomeMatch[1])) {
return compactHomeSuffix(unixHomeMatch[2] ?? "");
}
const windowsHomeMatch = normalized.match(/^[A-Za-z]:[\\/]Users[\\/]([^\\/]+)(.*)$/i);
if (windowsHomeMatch && isSafeHomeSegment(windowsHomeMatch[1])) {
return compactHomeSuffix(windowsHomeMatch[2] ?? "");
}
return normalized;
}
function compactHomeSuffix(suffix: string): string {
return `~${suffix.replace(/\\/g, "/")}`;
}
function isSafeHomeSegment(segment: string | undefined): boolean {
return segment !== undefined && segment !== "." && segment !== "..";
}
function hasRelativePathSegment(value: string): boolean {
return /(^|[\\/])\.{1,2}(?=[\\/]|$)/.test(value);
}

View File

@@ -289,6 +289,19 @@ describe("exec approval reply helpers", () => {
expect(payload.text).toContain("Full id: `req-1`");
});
it("compacts structured cwd paths in pending reply payloads", () => {
const payload = buildExecApprovalPendingReplyPayload({
approvalId: "req-home",
approvalSlug: "slug-home",
command: "pwd",
cwd: "C:\\Users\\alice\\project",
host: "gateway",
});
expect(payload.text).toContain("CWD: ~/project");
expect(payload.text).not.toContain("C:\\Users\\alice");
});
it("omits allow-always actions when the effective policy requires approval every time", () => {
const payload = buildExecApprovalPendingReplyPayload({
approvalId: "req-ask-always",

View File

@@ -5,6 +5,7 @@ import {
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { formatApprovalDisplayPath } from "./approval-display-paths.js";
import {
describeNativeExecApprovalClientSetup,
listNativeExecApprovalClientLabels,
@@ -322,7 +323,7 @@ export function buildExecApprovalPendingReplyPayload(
info.push(`Node: ${params.nodeId}`);
}
if (params.cwd) {
info.push(`CWD: ${params.cwd}`);
info.push(`CWD: ${formatApprovalDisplayPath(params.cwd)}`);
}
if (typeof params.expiresAtMs === "number" && Number.isFinite(params.expiresAtMs)) {
info.push(

View File

@@ -55,6 +55,7 @@ export type {
export { VERSION as OPENCLAW_VERSION } from "../version.js";
export { formatErrorMessage } from "../infra/errors.js";
export { formatApprovalDisplayPath } from "../infra/approval-display-paths.js";
export { emitAgentEvent } from "../infra/agent-events.js";
export { log as embeddedAgentLog } from "../agents/pi-embedded-runner/logger.js";
export { resolveEmbeddedAgentRuntime } from "../agents/pi-embedded-runner/runtime.js";

View File

@@ -19,6 +19,7 @@ export {
type ExecApprovalReplyMetadata,
} from "../infra/exec-approval-reply.js";
export { resolveExecApprovalCommandDisplay } from "../infra/exec-approval-command-display.js";
export { formatApprovalDisplayPath } from "../infra/approval-display-paths.js";
export {
createChannelApproverDmTargetResolver,
createChannelNativeOriginTargetResolver,

View File

@@ -42,6 +42,7 @@ export * from "../infra/exec-approval-session-target.ts";
export * from "../infra/exec-approvals.ts";
export * from "../infra/approval-native-delivery.ts";
export * from "../infra/approval-native-runtime.ts";
export * from "../infra/approval-display-paths.ts";
export * from "../infra/plugin-approvals.ts";
export * from "../infra/fetch.js";
export * from "../infra/file-lock.js";

View File

@@ -1,4 +1,5 @@
import { html, nothing } from "lit";
import { formatApprovalDisplayPath } from "../../../../src/infra/approval-display-paths.ts";
import type { AppViewState } from "../app-view-state.ts";
import type {
ExecApprovalRequest,
@@ -19,11 +20,14 @@ function formatRemaining(ms: number): string {
return `${hours}h`;
}
function renderMetaRow(label: string, value?: string | null) {
function renderMetaRow(label: string, value?: string | null, opts?: { path?: boolean }) {
if (!value) {
return nothing;
}
return html`<div class="exec-approval-meta-row"><span>${label}</span><span>${value}</span></div>`;
const displayValue = opts?.path ? formatApprovalDisplayPath(value) : value;
return html`<div class="exec-approval-meta-row">
<span>${label}</span><span>${displayValue}</span>
</div>`;
}
function renderExecBody(request: ExecApprovalRequestPayload) {
@@ -31,8 +35,11 @@ function renderExecBody(request: ExecApprovalRequestPayload) {
<div class="exec-approval-command mono">${request.command}</div>
<div class="exec-approval-meta">
${renderMetaRow("Host", request.host)} ${renderMetaRow("Agent", request.agentId)}
${renderMetaRow("Session", request.sessionKey)} ${renderMetaRow("CWD", request.cwd)}
${renderMetaRow("Resolved", request.resolvedPath)}
${renderMetaRow("Session", request.sessionKey)}
${renderMetaRow("CWD", request.cwd, {
path: true,
})}
${renderMetaRow("Resolved", request.resolvedPath, { path: true })}
${renderMetaRow("Security", request.security)} ${renderMetaRow("Ask", request.ask)}
</div>
`;