mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-05 20:22:53 +00:00
* refactor: remove stale file-backed shims * fix: harden sqlite state ci boundaries * refactor: store matrix idb snapshots in sqlite * fix: satisfy rebased CI guardrails * refactor: store current conversation bindings in sqlite table * refactor: store tui last sessions in sqlite table * refactor: reset sqlite schema history * refactor: drop unshipped sqlite table migration * refactor: remove plugin index file rollback * refactor: drop unshipped sqlite sidecar migrations * refactor: remove runtime commitments kv migration * refactor: preserve kysely sync result types * refactor: drop unshipped sqlite schema migration table * test: keep session usage coverage sqlite-backed * refactor: keep sqlite migration doctor-only * refactor: isolate device legacy imports * refactor: isolate push voicewake legacy imports * refactor: isolate remaining runtime legacy imports * refactor: tighten sqlite migration guardrails * test: cover sqlite persisted enum parsing * refactor: isolate legacy update and tui imports * refactor: tighten sqlite state ownership * refactor: move legacy imports behind doctor * refactor: remove legacy session row lookup * refactor: canonicalize memory transcript locators * refactor: drop transcript path scope fallbacks * refactor: drop runtime legacy session delivery pruning * refactor: store tts prefs only in sqlite * refactor: remove cron store path runtime * refactor: use cron sqlite store keys * refactor: rename telegram message cache scope * refactor: read memory dreaming status from sqlite * refactor: rename cron status store key * refactor: stop remembering transcript file paths * test: use sqlite locators in agent fixtures * refactor: remove file-shaped commitments and cron store surfaces * refactor: keep compaction transcript handles out of session rows * refactor: derive transcript handles from session identity * refactor: derive runtime transcript handles * refactor: remove gateway session locator reads * refactor: remove transcript locator from session rows * refactor: store raw stream diagnostics in sqlite * refactor: remove file-shaped transcript rotation * refactor: hide legacy trajectory paths from runtime * refactor: remove runtime transcript file bridges * refactor: repair database-first rebase fallout * refactor: align tests with database-first state * refactor: remove transcript file handoffs * refactor: sync post-compaction memory by transcript scope * refactor: run codex app-server sessions by id * refactor: bind codex runtime state by session id * refactor: pass memory transcripts by sqlite scope * refactor: remove transcript locator cleanup leftovers * test: remove stale transcript file fixtures * refactor: remove transcript locator test helper * test: make cron sqlite keys explicit * test: remove cron runtime store paths * test: remove stale session file fixtures * test: use sqlite cron keys in diagnostics * refactor: remove runtime delivery queue backfill * test: drop fake export session file mocks * refactor: rename acp session read failure flag * refactor: rename acp row session key * refactor: remove session store test seams * refactor: move legacy session parser tests to doctor * refactor: reindex managed memory in place * refactor: drop stale session store wording * refactor: rename session row helpers * refactor: rename sqlite session entry modules * refactor: remove transcript locator leftovers * refactor: trim file-era audit wording * refactor: clean managed media through sqlite * fix: prefer explicit agent for exports * fix: use prepared agent for session resets * fix: canonicalize legacy codex binding import * test: rename state cleanup helper * docs: align backup docs with sqlite state * refactor: drop legacy Pi usage auth fallback * refactor: move legacy auth profile imports to doctor * refactor: keep Pi model discovery auth in memory * refactor: remove MSTeams legacy learning key fallback * refactor: store model catalog config in sqlite * refactor: use sqlite model catalog at runtime * refactor: remove model json compatibility aliases * refactor: store auth profiles in sqlite * refactor: seed copied auth profiles in sqlite * refactor: make auth profile runtime sqlite-addressed * refactor: migrate hermes secrets into sqlite auth store * refactor: move plugin install config migration to doctor * refactor: rename plugin index audit checks * test: drop auth file assumptions * test: remove legacy transcript file assertions * refactor: drop legacy cli session aliases * refactor: store skill uploads in sqlite * refactor: keep subagent attachments in sqlite vfs * refactor: drop subagent attachment cleanup state * refactor: move legacy session aliases to doctor * refactor: require node 24 for sqlite state runtime * refactor: move provider caches into sqlite state * fix: harden virtual agent filesystem * refactor: enforce database-first runtime state * refactor: rename compaction transcript rotation setting * test: clean sqlite refactor test types * refactor: consolidate sqlite runtime state * refactor: model session conversations in sqlite * refactor: stop deriving cron delivery from session keys * refactor: stop classifying sessions from key shape * refactor: hydrate announce targets from typed delivery * refactor: route heartbeat delivery from typed sqlite context * refactor: tighten typed sqlite session routing * refactor: remove session origin routing shadow * refactor: drop session origin shadow fixtures * perf: query sqlite vfs paths by prefix * refactor: use typed conversation metadata for sessions * refactor: prefer typed session routing metadata * refactor: require typed session routing metadata * refactor: resolve group tool policy from typed sessions * refactor: delete dead session thread info bridge * Show Codex subscription reset times in channel errors (#80456) * feat(plugin-sdk): consolidate session workflow APIs * fix(agents): allow read-only agent mount reads * [codex] refresh plugin regression fixtures * fix(agents): restore compaction gateway logs * test: tighten gateway startup assertions * Redact persisted secret-shaped payloads [AI] (#79006) * test: tighten device pair notify assertions * test: tighten hermes secret assertions * test: assert matrix client error shapes * test: assert config compat warnings * fix(heartbeat): remap cron-run exec events to session keys (#80214) * fix(codex): route btw through native side threads * fix(auth): accept friendly OpenAI order for Codex profiles * fix(codex): rotate auth profiles inside harness * fix: keep browser status page probe within timeout * test: assert agents add outputs * test: pin cron read status * fix(agents): avoid Pi resource discovery stalls Co-authored-by: dataCenter430 <titan032000@gmail.com> * fix: retire timed-out codex app-server clients * test: tighten qa lab runtime assertions * test: check security fix outputs * test: verify extension runtime messages * feat(wake): expose typed sessionKey on wake protocol + system event CLI * fix(gateway): await session_end during shutdown drain and track channel + compaction lifecycle paths (#57790) * test: guard talk consult call helper * fix(codex): scale context engine projection (#80761) * fix(codex): scale context engine projection * fix: document Codex context projection scaling * fix: document Codex context projection scaling * fix: document Codex context projection scaling * fix: document Codex context projection scaling * chore: align Codex projection changelog * chore: realign Codex projection changelog * fix: isolate Codex projection patch --------- Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org> Co-authored-by: Josh Lehman <josh@martian.engineering> * refactor: move agent runtime state toward piless * refactor: remove cron session reaper * refactor: move session management to sqlite * refactor: finish database-first state migration * chore: refresh generated sqlite db types * refactor: remove stale file-backed shims * test: harden kysely type coverage # Conflicts: # .agents/skills/kysely-database-access/SKILL.md # src/infra/kysely-sync.types.test.ts # src/proxy-capture/store.sqlite.test.ts # src/state/openclaw-agent-db.test.ts # src/state/openclaw-state-db.test.ts * refactor: remove cron store path runtime * refactor: keep compaction transcript handles out of session rows * refactor: derive embedded transcripts from sqlite identity * refactor: remove embedded transcript locator handoff * refactor: remove runtime transcript file bridges * refactor: remove transcript file handoffs * refactor: remove MSTeams legacy learning key fallback * refactor: store model catalog config in sqlite * refactor: use sqlite model catalog at runtime # Conflicts: # docs/cli/secrets.md # docs/gateway/authentication.md # docs/gateway/secrets.md * fix: keep oauth sibling sync sqlite-local # Conflicts: # src/commands/onboard-auth.test.ts * refactor: remove task session store maintenance # Conflicts: # src/commands/tasks.ts * refactor: keep diagnostics in state sqlite * refactor: enforce database-first runtime state * refactor: consolidate sqlite runtime state * Show Codex subscription reset times in channel errors (#80456) * fix(codex): refresh subscription limit resets * fix(codex): format reset times for channels * Update CHANGELOG with latest changes and fixes Updated CHANGELOG with recent fixes and improvements. * fix(codex): keep command load failures on codex surface * fix(codex): format account rate limits as rows * fix(codex): summarize account limits as usage status * fix(codex): simplify account limit status * test: tighten subagent announce queue assertion * test: tighten session delete lifecycle assertions * test: tighten cron ops assertions * fix: track cron execution milestones * test: tighten hermes secret assertions * test: assert matrix sync store payloads * test: assert config compat warnings * fix(codex): align btw side thread semantics * fix(codex): honor codex fallback blocking * fix(agents): avoid Pi resource discovery stalls * test: tighten codex event assertions * test: tighten cron assertions * Fix Codex app-server OAuth harness auth * refactor: move agent runtime state toward piless * refactor: move device and push state to sqlite * refactor: move runtime json state imports to doctor * refactor: finish database-first state migration * chore: refresh generated sqlite db types * refactor: clarify cron sqlite store keys * refactor: remove stale file-backed shims * refactor: bind codex runtime state by session id * test: expect sqlite trajectory branch export * refactor: rename session row helpers * fix: keep legacy device identity import in doctor * refactor: enforce database-first runtime state * refactor: consolidate sqlite runtime state * build: align pi contract wrappers * chore: repair database-first rebase * refactor: remove session file test contracts * test: update gateway session expectations * refactor: stop routing from session compatibility shadows * refactor: stop persisting session route shadows * refactor: use typed delivery context in clients * refactor: stop echoing session route shadows * refactor: repair embedded runner rebase imports # Conflicts: # src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts * refactor: align pi contract imports * refactor: satisfy kysely sync helper guard * refactor: remove file transcript bridge remnants * refactor: remove session locator compatibility * refactor: remove session file test contracts * refactor: keep rebase database-first clean * refactor: remove session file assumptions from e2e * docs: clarify database-first goal state * test: remove legacy store markers from sqlite runtime tests * refactor: remove legacy store assumptions from runtime seams * refactor: align sqlite runtime helper seams * test: update memory recall sqlite audit mock * refactor: align database-first runtime type seams * test: clarify doctor cron legacy store names * fix: preserve sqlite session route projections * test: fix copilot token cache test syntax * docs: update database-first proof status * test: align database-first test fixtures * docs: update database-first proof status * refactor: clean extension database-first drift * test: align agent session route proof * test: clarify doctor legacy path fixtures * chore: clean database-first changed checks * chore: repair database-first rebase markers * build: allow baileys git subdependency * chore: repair exp-vfs rebase drift * chore: finish exp-vfs rebase cleanup * chore: satisfy rebase lint drift * chore: fix qqbot rebase type seam * chore: fix rebase drift leftovers * fix: keep auth profile oauth secrets out of sqlite * fix: repair rebase drift tests * test: stabilize pairing request ordering * test: use source manifests in plugin contract checks * fix: restore gateway session metadata after rebase * fix: repair database-first rebase drift * fix: clean up database-first rebase fallout * test: stabilize line quick reply receipt time * fix: repair extension rebase drift * test: keep transcript redaction tests sqlite-backed * fix: carry injected transcript redaction through sqlite * chore: clean database branch rebase residue * fix: repair database branch CI drift * fix: repair database branch CI guard drift * fix: stabilize oauth tls preflight test * test: align database branch fast guards * test: repair build artifact boundary guards * chore: clean changelog rebase markers --------- Co-authored-by: pashpashpash <nik@vault77.ai> Co-authored-by: Eva <eva@100yen.org> Co-authored-by: stainlu <stainlu@newtype-ai.org> Co-authored-by: Jason Zhou <jason.zhou.design@gmail.com> Co-authored-by: Ruben Cuevas <hi@rubencu.com> Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com> Co-authored-by: Shakker <shakkerdroid@gmail.com> Co-authored-by: Kaspre <36520309+Kaspre@users.noreply.github.com> Co-authored-by: dataCenter430 <titan032000@gmail.com> Co-authored-by: Kaspre <kaspre@gmail.com> Co-authored-by: pandadev66 <nova.full.stack@outlook.com> Co-authored-by: Eva <admin@100yen.org> Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org> Co-authored-by: Josh Lehman <josh@martian.engineering> Co-authored-by: jeffjhunter <support@aipersonamethod.com>
517 lines
17 KiB
TypeScript
517 lines
17 KiB
TypeScript
import crypto from "node:crypto";
|
|
import { formatErrorMessage } from "../infra/errors.js";
|
|
import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js";
|
|
import {
|
|
type ExecApprovalInitiatingSurfaceState,
|
|
resolveExecApprovalInitiatingSurfaceState,
|
|
} from "../infra/exec-approval-surface.js";
|
|
import {
|
|
minSecurity,
|
|
maxAsk,
|
|
resolveExecApprovalAllowedDecisions,
|
|
resolveExecApprovals,
|
|
type ExecAsk,
|
|
type ExecApprovalDecision,
|
|
type ExecSecurity,
|
|
} from "../infra/exec-approvals.js";
|
|
import { logWarn } from "../logger.js";
|
|
import { registerExecApprovalFollowupRuntimeHandoff } from "./bash-tools.exec-approval-followup-state.js";
|
|
import type { AgentToolResult } from "./agent-core-contract.js";
|
|
import { sendExecApprovalFollowup } from "./bash-tools.exec-approval-followup.js";
|
|
import {
|
|
type ExecApprovalRegistration,
|
|
resolveRegisteredExecApprovalDecision,
|
|
} from "./bash-tools.exec-approval-request.js";
|
|
import { buildApprovalPendingMessage } from "./bash-tools.exec-runtime.js";
|
|
import { DEFAULT_APPROVAL_TIMEOUT_MS } from "./bash-tools.exec-runtime.js";
|
|
import type { ExecElevatedDefaults, ExecToolDetails } from "./bash-tools.exec-types.js";
|
|
|
|
type ResolvedExecApprovals = ReturnType<typeof resolveExecApprovals>;
|
|
export const MAX_EXEC_APPROVAL_FOLLOWUP_FAILURE_LOG_KEYS = 256;
|
|
const loggedExecApprovalFollowupFailures = new Set<string>();
|
|
|
|
function rememberExecApprovalFollowupFailureKey(key: string): boolean {
|
|
if (loggedExecApprovalFollowupFailures.has(key)) {
|
|
return false;
|
|
}
|
|
loggedExecApprovalFollowupFailures.add(key);
|
|
// Bound memory growth for long-lived processes that see many unique approval failures.
|
|
if (loggedExecApprovalFollowupFailures.size > MAX_EXEC_APPROVAL_FOLLOWUP_FAILURE_LOG_KEYS) {
|
|
const oldestKey = loggedExecApprovalFollowupFailures.values().next().value;
|
|
if (typeof oldestKey === "string") {
|
|
loggedExecApprovalFollowupFailures.delete(oldestKey);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export type ExecHostApprovalContext = {
|
|
approvals: ResolvedExecApprovals;
|
|
hostSecurity: ExecSecurity;
|
|
hostAsk: ExecAsk;
|
|
askFallback: ResolvedExecApprovals["agent"]["askFallback"];
|
|
};
|
|
|
|
export type ExecApprovalPendingState = {
|
|
warningText: string;
|
|
expiresAtMs: number;
|
|
preResolvedDecision: string | null | undefined;
|
|
};
|
|
|
|
export type ExecApprovalRequestState = ExecApprovalPendingState & {
|
|
noticeSeconds: number;
|
|
};
|
|
|
|
export type ExecApprovalUnavailableReason =
|
|
| "no-approval-route"
|
|
| "initiating-platform-disabled"
|
|
| "initiating-platform-unsupported";
|
|
|
|
function isHeadlessExecTrigger(trigger?: string): boolean {
|
|
return trigger === "cron";
|
|
}
|
|
|
|
export type RegisteredExecApprovalRequestContext = {
|
|
approvalId: string;
|
|
approvalSlug: string;
|
|
warningText: string;
|
|
expiresAtMs: number;
|
|
preResolvedDecision: string | null | undefined;
|
|
initiatingSurface: ExecApprovalInitiatingSurfaceState;
|
|
sentApproverDms: boolean;
|
|
unavailableReason: ExecApprovalUnavailableReason | null;
|
|
};
|
|
|
|
export type ExecApprovalFollowupTarget = {
|
|
approvalId: string;
|
|
sessionKey?: string;
|
|
turnSourceChannel?: string;
|
|
turnSourceTo?: string;
|
|
turnSourceAccountId?: string;
|
|
turnSourceThreadId?: string | number;
|
|
direct?: boolean;
|
|
bashElevated?: ExecElevatedDefaults;
|
|
};
|
|
|
|
export type ExecApprovalFollowupResultDeps = {
|
|
sendExecApprovalFollowup?: typeof sendExecApprovalFollowup;
|
|
logWarn?: typeof logWarn;
|
|
};
|
|
|
|
export type DefaultExecApprovalRequestArgs = {
|
|
warnings: string[];
|
|
approvalRunningNoticeMs: number;
|
|
createApprovalSlug: (approvalId: string) => string;
|
|
turnSourceChannel?: string;
|
|
turnSourceAccountId?: string;
|
|
};
|
|
|
|
export function createExecApprovalPendingState(params: {
|
|
warnings: string[];
|
|
timeoutMs: number;
|
|
}): ExecApprovalPendingState {
|
|
return {
|
|
warningText: params.warnings.length ? `${params.warnings.join("\n")}\n\n` : "",
|
|
expiresAtMs: Date.now() + params.timeoutMs,
|
|
preResolvedDecision: undefined,
|
|
};
|
|
}
|
|
|
|
export function createExecApprovalRequestState(params: {
|
|
warnings: string[];
|
|
timeoutMs: number;
|
|
approvalRunningNoticeMs: number;
|
|
}): ExecApprovalRequestState {
|
|
const pendingState = createExecApprovalPendingState({
|
|
warnings: params.warnings,
|
|
timeoutMs: params.timeoutMs,
|
|
});
|
|
return {
|
|
...pendingState,
|
|
noticeSeconds: Math.max(1, Math.round(params.approvalRunningNoticeMs / 1000)),
|
|
};
|
|
}
|
|
|
|
export function createExecApprovalRequestContext(params: {
|
|
warnings: string[];
|
|
timeoutMs: number;
|
|
approvalRunningNoticeMs: number;
|
|
createApprovalSlug: (approvalId: string) => string;
|
|
}): ExecApprovalRequestState & {
|
|
approvalId: string;
|
|
approvalSlug: string;
|
|
contextKey: string;
|
|
} {
|
|
const approvalId = crypto.randomUUID();
|
|
const pendingState = createExecApprovalRequestState({
|
|
warnings: params.warnings,
|
|
timeoutMs: params.timeoutMs,
|
|
approvalRunningNoticeMs: params.approvalRunningNoticeMs,
|
|
});
|
|
return {
|
|
...pendingState,
|
|
approvalId,
|
|
approvalSlug: params.createApprovalSlug(approvalId),
|
|
contextKey: `exec:${approvalId}`,
|
|
};
|
|
}
|
|
|
|
export function createDefaultExecApprovalRequestContext(params: {
|
|
warnings: string[];
|
|
approvalRunningNoticeMs: number;
|
|
createApprovalSlug: (approvalId: string) => string;
|
|
}) {
|
|
return createExecApprovalRequestContext({
|
|
warnings: params.warnings,
|
|
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
approvalRunningNoticeMs: params.approvalRunningNoticeMs,
|
|
createApprovalSlug: params.createApprovalSlug,
|
|
});
|
|
}
|
|
|
|
export function resolveBaseExecApprovalDecision(params: {
|
|
decision: string | null;
|
|
askFallback: ResolvedExecApprovals["agent"]["askFallback"];
|
|
}): {
|
|
approvedByAsk: boolean;
|
|
deniedReason: string | null;
|
|
timedOut: boolean;
|
|
} {
|
|
if (params.decision === "deny") {
|
|
return { approvedByAsk: false, deniedReason: "user-denied", timedOut: false };
|
|
}
|
|
if (!params.decision) {
|
|
if (params.askFallback === "full") {
|
|
return { approvedByAsk: true, deniedReason: null, timedOut: true };
|
|
}
|
|
if (params.askFallback === "deny") {
|
|
return { approvedByAsk: false, deniedReason: "approval-timeout", timedOut: true };
|
|
}
|
|
return { approvedByAsk: false, deniedReason: null, timedOut: true };
|
|
}
|
|
return { approvedByAsk: false, deniedReason: null, timedOut: false };
|
|
}
|
|
|
|
export function resolveExecHostApprovalContext(params: {
|
|
agentId?: string;
|
|
security: ExecSecurity;
|
|
ask: ExecAsk;
|
|
host: "gateway" | "node";
|
|
}): ExecHostApprovalContext {
|
|
const approvals = resolveExecApprovals(params.agentId, {
|
|
security: params.security,
|
|
ask: params.ask,
|
|
});
|
|
// Session/config tool policy is the caller's requested contract. The host file
|
|
// may tighten that contract, but it must not silently broaden it.
|
|
const hostSecurity = minSecurity(params.security, approvals.agent.security);
|
|
const hostAsk = maxAsk(params.ask, approvals.agent.ask);
|
|
const askFallback = minSecurity(hostSecurity, approvals.agent.askFallback);
|
|
if (hostSecurity === "deny") {
|
|
throw new Error(`exec denied: host=${params.host} security=deny`);
|
|
}
|
|
return { approvals, hostSecurity, hostAsk, askFallback };
|
|
}
|
|
|
|
export async function resolveApprovalDecisionOrUndefined(params: {
|
|
approvalId: string;
|
|
preResolvedDecision: string | null | undefined;
|
|
onFailure: () => void;
|
|
}): Promise<string | null | undefined> {
|
|
try {
|
|
return await resolveRegisteredExecApprovalDecision({
|
|
approvalId: params.approvalId,
|
|
preResolvedDecision: params.preResolvedDecision,
|
|
});
|
|
} catch {
|
|
params.onFailure();
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function resolveExecApprovalUnavailableState(params: {
|
|
turnSourceChannel?: string;
|
|
turnSourceAccountId?: string;
|
|
preResolvedDecision: string | null | undefined;
|
|
}): {
|
|
initiatingSurface: ExecApprovalInitiatingSurfaceState;
|
|
sentApproverDms: boolean;
|
|
unavailableReason: ExecApprovalUnavailableReason | null;
|
|
} {
|
|
const initiatingSurface = resolveExecApprovalInitiatingSurfaceState({
|
|
channel: params.turnSourceChannel,
|
|
accountId: params.turnSourceAccountId,
|
|
});
|
|
// Native approval runtimes emit routed-elsewhere notices after actual delivery.
|
|
// Avoid claiming approver DMs were sent from config-only guesses here.
|
|
const sentApproverDms = false;
|
|
const unavailableReason =
|
|
params.preResolvedDecision === null
|
|
? "no-approval-route"
|
|
: initiatingSurface.kind === "disabled"
|
|
? "initiating-platform-disabled"
|
|
: initiatingSurface.kind === "unsupported"
|
|
? "initiating-platform-unsupported"
|
|
: null;
|
|
return {
|
|
initiatingSurface,
|
|
sentApproverDms,
|
|
unavailableReason,
|
|
};
|
|
}
|
|
|
|
export async function createAndRegisterDefaultExecApprovalRequest(params: {
|
|
warnings: string[];
|
|
approvalRunningNoticeMs: number;
|
|
createApprovalSlug: (approvalId: string) => string;
|
|
turnSourceChannel?: string;
|
|
turnSourceAccountId?: string;
|
|
register: (approvalId: string) => Promise<ExecApprovalRegistration>;
|
|
}): Promise<RegisteredExecApprovalRequestContext> {
|
|
const {
|
|
approvalId,
|
|
approvalSlug,
|
|
warningText,
|
|
expiresAtMs: defaultExpiresAtMs,
|
|
preResolvedDecision: defaultPreResolvedDecision,
|
|
} = createDefaultExecApprovalRequestContext({
|
|
warnings: params.warnings,
|
|
approvalRunningNoticeMs: params.approvalRunningNoticeMs,
|
|
createApprovalSlug: params.createApprovalSlug,
|
|
});
|
|
const registration = await params.register(approvalId);
|
|
const preResolvedDecision = registration.finalDecision;
|
|
const { initiatingSurface, sentApproverDms, unavailableReason } =
|
|
resolveExecApprovalUnavailableState({
|
|
turnSourceChannel: params.turnSourceChannel,
|
|
turnSourceAccountId: params.turnSourceAccountId,
|
|
preResolvedDecision,
|
|
});
|
|
|
|
return {
|
|
approvalId,
|
|
approvalSlug,
|
|
warningText,
|
|
expiresAtMs: registration.expiresAtMs ?? defaultExpiresAtMs,
|
|
preResolvedDecision:
|
|
registration.finalDecision === undefined
|
|
? defaultPreResolvedDecision
|
|
: registration.finalDecision,
|
|
initiatingSurface,
|
|
sentApproverDms,
|
|
unavailableReason,
|
|
};
|
|
}
|
|
|
|
export function buildDefaultExecApprovalRequestArgs(
|
|
params: DefaultExecApprovalRequestArgs,
|
|
): DefaultExecApprovalRequestArgs {
|
|
return {
|
|
warnings: params.warnings,
|
|
approvalRunningNoticeMs: params.approvalRunningNoticeMs,
|
|
createApprovalSlug: params.createApprovalSlug,
|
|
turnSourceChannel: params.turnSourceChannel,
|
|
turnSourceAccountId: params.turnSourceAccountId,
|
|
};
|
|
}
|
|
|
|
export function buildExecApprovalFollowupTarget(
|
|
params: ExecApprovalFollowupTarget,
|
|
): ExecApprovalFollowupTarget {
|
|
return {
|
|
approvalId: params.approvalId,
|
|
sessionKey: params.sessionKey,
|
|
turnSourceChannel: params.turnSourceChannel,
|
|
turnSourceTo: params.turnSourceTo,
|
|
turnSourceAccountId: params.turnSourceAccountId,
|
|
turnSourceThreadId: params.turnSourceThreadId,
|
|
direct: params.direct,
|
|
bashElevated: params.bashElevated,
|
|
};
|
|
}
|
|
|
|
export function createExecApprovalDecisionState(params: {
|
|
decision: string | null | undefined;
|
|
askFallback: ResolvedExecApprovals["agent"]["askFallback"];
|
|
}) {
|
|
const baseDecision = resolveBaseExecApprovalDecision({
|
|
decision: params.decision ?? null,
|
|
askFallback: params.askFallback,
|
|
});
|
|
return {
|
|
baseDecision,
|
|
approvedByAsk: baseDecision.approvedByAsk,
|
|
deniedReason: baseDecision.deniedReason,
|
|
};
|
|
}
|
|
|
|
export function enforceStrictInlineEvalApprovalBoundary(params: {
|
|
baseDecision: {
|
|
timedOut: boolean;
|
|
};
|
|
approvedByAsk: boolean;
|
|
deniedReason: string | null;
|
|
requiresInlineEvalApproval: boolean;
|
|
}): {
|
|
approvedByAsk: boolean;
|
|
deniedReason: string | null;
|
|
} {
|
|
if (
|
|
!params.baseDecision.timedOut ||
|
|
!params.requiresInlineEvalApproval ||
|
|
!params.approvedByAsk
|
|
) {
|
|
return {
|
|
approvedByAsk: params.approvedByAsk,
|
|
deniedReason: params.deniedReason,
|
|
};
|
|
}
|
|
return {
|
|
approvedByAsk: false,
|
|
deniedReason: params.deniedReason ?? "approval-timeout",
|
|
};
|
|
}
|
|
|
|
export function shouldResolveExecApprovalUnavailableInline(params: {
|
|
trigger?: string;
|
|
unavailableReason: ExecApprovalUnavailableReason | null;
|
|
preResolvedDecision: string | null | undefined;
|
|
}): boolean {
|
|
return (
|
|
isHeadlessExecTrigger(params.trigger) &&
|
|
params.unavailableReason === "no-approval-route" &&
|
|
params.preResolvedDecision === null
|
|
);
|
|
}
|
|
|
|
export function buildHeadlessExecApprovalDeniedMessage(params: {
|
|
trigger?: string;
|
|
host: "gateway" | "node";
|
|
security: ExecSecurity;
|
|
ask: ExecAsk;
|
|
askFallback: ResolvedExecApprovals["agent"]["askFallback"];
|
|
}): string {
|
|
const runLabel = params.trigger === "cron" ? "Cron runs" : "Headless runs";
|
|
return [
|
|
`exec denied: ${runLabel} cannot wait for interactive exec approval.`,
|
|
`Effective host exec policy: security=${params.security} ask=${params.ask} askFallback=${params.askFallback}`,
|
|
"Stricter values from tools.exec and SQLite exec approvals state both apply.",
|
|
"Fix one of these:",
|
|
'- align config and approvals state to security="full" and ask="off" for trusted local automation',
|
|
"- keep allowlist mode and add an explicit allowlist entry for this command",
|
|
"- enable Web UI, terminal UI, or chat exec approvals and rerun interactively",
|
|
'Tip: run "openclaw doctor" and "openclaw approvals get --gateway" to inspect the effective policy.',
|
|
].join("\n");
|
|
}
|
|
|
|
export async function sendExecApprovalFollowupResult(
|
|
target: ExecApprovalFollowupTarget,
|
|
resultText: string,
|
|
deps: ExecApprovalFollowupResultDeps = {},
|
|
): Promise<void> {
|
|
const send = deps.sendExecApprovalFollowup ?? sendExecApprovalFollowup;
|
|
const warn = deps.logWarn ?? logWarn;
|
|
const runtimeHandoff =
|
|
target.direct === true || !target.sessionKey
|
|
? undefined
|
|
: registerExecApprovalFollowupRuntimeHandoff({
|
|
approvalId: target.approvalId,
|
|
sessionKey: target.sessionKey,
|
|
bashElevated: target.bashElevated,
|
|
});
|
|
await send({
|
|
approvalId: target.approvalId,
|
|
sessionKey: target.sessionKey,
|
|
turnSourceChannel: target.turnSourceChannel,
|
|
turnSourceTo: target.turnSourceTo,
|
|
turnSourceAccountId: target.turnSourceAccountId,
|
|
turnSourceThreadId: target.turnSourceThreadId,
|
|
resultText,
|
|
direct: target.direct,
|
|
...(runtimeHandoff
|
|
? {
|
|
internalRuntimeHandoffId: runtimeHandoff.handoffId,
|
|
idempotencyKey: runtimeHandoff.idempotencyKey,
|
|
}
|
|
: {}),
|
|
}).catch((error) => {
|
|
const message = formatErrorMessage(error);
|
|
const key = `${target.approvalId}:${message}`;
|
|
if (!rememberExecApprovalFollowupFailureKey(key)) {
|
|
return;
|
|
}
|
|
warn(`exec approval followup dispatch failed (id=${target.approvalId}): ${message}`);
|
|
});
|
|
}
|
|
|
|
export function buildExecApprovalPendingToolResult(params: {
|
|
host: "gateway" | "node";
|
|
command: string;
|
|
cwd: string | undefined;
|
|
warningText: string;
|
|
approvalId: string;
|
|
approvalSlug: string;
|
|
expiresAtMs: number;
|
|
initiatingSurface: ExecApprovalInitiatingSurfaceState;
|
|
sentApproverDms: boolean;
|
|
unavailableReason: ExecApprovalUnavailableReason | null;
|
|
allowedDecisions?: readonly ExecApprovalDecision[];
|
|
nodeId?: string;
|
|
}): AgentToolResult<ExecToolDetails> {
|
|
const allowedDecisions = params.allowedDecisions ?? resolveExecApprovalAllowedDecisions();
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text:
|
|
params.unavailableReason !== null
|
|
? (buildExecApprovalUnavailableReplyPayload({
|
|
warningText: params.warningText,
|
|
reason: params.unavailableReason,
|
|
channel: params.initiatingSurface.channel,
|
|
channelLabel: params.initiatingSurface.channelLabel,
|
|
accountId: params.initiatingSurface.accountId,
|
|
sentApproverDms: params.sentApproverDms,
|
|
}).text ?? "")
|
|
: buildApprovalPendingMessage({
|
|
warningText: params.warningText,
|
|
approvalSlug: params.approvalSlug,
|
|
approvalId: params.approvalId,
|
|
allowedDecisions,
|
|
command: params.command,
|
|
cwd: params.cwd,
|
|
host: params.host,
|
|
nodeId: params.nodeId,
|
|
}),
|
|
},
|
|
],
|
|
details:
|
|
params.unavailableReason !== null
|
|
? ({
|
|
status: "approval-unavailable",
|
|
reason: params.unavailableReason,
|
|
channel: params.initiatingSurface.channel,
|
|
channelLabel: params.initiatingSurface.channelLabel,
|
|
accountId: params.initiatingSurface.accountId,
|
|
sentApproverDms: params.sentApproverDms,
|
|
host: params.host,
|
|
command: params.command,
|
|
cwd: params.cwd,
|
|
nodeId: params.nodeId,
|
|
warningText: params.warningText,
|
|
} satisfies ExecToolDetails)
|
|
: ({
|
|
status: "approval-pending",
|
|
approvalId: params.approvalId,
|
|
approvalSlug: params.approvalSlug,
|
|
expiresAtMs: params.expiresAtMs,
|
|
allowedDecisions,
|
|
host: params.host,
|
|
command: params.command,
|
|
cwd: params.cwd,
|
|
nodeId: params.nodeId,
|
|
warningText: params.warningText,
|
|
} satisfies ExecToolDetails),
|
|
};
|
|
}
|