refactor(gateway): centralize system.run approval context and errors
This commit is contained in:
29
src/gateway/node-invoke-system-run-approval-errors.ts
Normal file
29
src/gateway/node-invoke-system-run-approval-errors.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export type SystemRunApprovalGuardError = {
|
||||
ok: false;
|
||||
message: string;
|
||||
details: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export function systemRunApprovalGuardError(params: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
}): SystemRunApprovalGuardError {
|
||||
const details = params.details ? { ...params.details } : {};
|
||||
return {
|
||||
ok: false,
|
||||
message: params.message,
|
||||
details: {
|
||||
code: params.code,
|
||||
...details,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function systemRunApprovalRequired(runId: string): SystemRunApprovalGuardError {
|
||||
return systemRunApprovalGuardError({
|
||||
code: "APPROVAL_REQUIRED",
|
||||
message: "approval required",
|
||||
details: { runId },
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { normalizeSystemRunApprovalPlanV2 } from "../infra/system-run-approval-binding.js";
|
||||
import { resolveSystemRunApprovalRuntimeContext } from "../infra/system-run-approval-context.js";
|
||||
import { resolveSystemRunCommand } from "../infra/system-run-command.js";
|
||||
import type { ExecApprovalRecord } from "./exec-approval-manager.js";
|
||||
import {
|
||||
systemRunApprovalGuardError,
|
||||
systemRunApprovalRequired,
|
||||
} from "./node-invoke-system-run-approval-errors.js";
|
||||
import {
|
||||
evaluateSystemRunApprovalMatch,
|
||||
toSystemRunApprovalMismatchError,
|
||||
@@ -125,62 +129,60 @@ export function sanitizeSystemRunParamsForForwarding(opts: {
|
||||
|
||||
const runId = normalizeString(p.runId);
|
||||
if (!runId) {
|
||||
return {
|
||||
ok: false,
|
||||
return systemRunApprovalGuardError({
|
||||
code: "MISSING_RUN_ID",
|
||||
message: "approval override requires params.runId",
|
||||
details: { code: "MISSING_RUN_ID" },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const manager = opts.execApprovalManager;
|
||||
if (!manager) {
|
||||
return {
|
||||
ok: false,
|
||||
return systemRunApprovalGuardError({
|
||||
code: "APPROVALS_UNAVAILABLE",
|
||||
message: "exec approvals unavailable",
|
||||
details: { code: "APPROVALS_UNAVAILABLE" },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const snapshot = manager.getSnapshot(runId);
|
||||
if (!snapshot) {
|
||||
return {
|
||||
ok: false,
|
||||
return systemRunApprovalGuardError({
|
||||
code: "UNKNOWN_APPROVAL_ID",
|
||||
message: "unknown or expired approval id",
|
||||
details: { code: "UNKNOWN_APPROVAL_ID", runId },
|
||||
};
|
||||
details: { runId },
|
||||
});
|
||||
}
|
||||
|
||||
const nowMs = typeof opts.nowMs === "number" ? opts.nowMs : Date.now();
|
||||
if (nowMs > snapshot.expiresAtMs) {
|
||||
return {
|
||||
ok: false,
|
||||
return systemRunApprovalGuardError({
|
||||
code: "APPROVAL_EXPIRED",
|
||||
message: "approval expired",
|
||||
details: { code: "APPROVAL_EXPIRED", runId },
|
||||
};
|
||||
details: { runId },
|
||||
});
|
||||
}
|
||||
|
||||
const targetNodeId = normalizeString(opts.nodeId);
|
||||
if (!targetNodeId) {
|
||||
return {
|
||||
ok: false,
|
||||
return systemRunApprovalGuardError({
|
||||
code: "MISSING_NODE_ID",
|
||||
message: "node.invoke requires nodeId",
|
||||
details: { code: "MISSING_NODE_ID", runId },
|
||||
};
|
||||
details: { runId },
|
||||
});
|
||||
}
|
||||
const approvalNodeId = normalizeString(snapshot.request.nodeId);
|
||||
if (!approvalNodeId) {
|
||||
return {
|
||||
ok: false,
|
||||
return systemRunApprovalGuardError({
|
||||
code: "APPROVAL_NODE_BINDING_MISSING",
|
||||
message: "approval id missing node binding",
|
||||
details: { code: "APPROVAL_NODE_BINDING_MISSING", runId },
|
||||
};
|
||||
details: { runId },
|
||||
});
|
||||
}
|
||||
if (approvalNodeId !== targetNodeId) {
|
||||
return {
|
||||
ok: false,
|
||||
return systemRunApprovalGuardError({
|
||||
code: "APPROVAL_NODE_MISMATCH",
|
||||
message: "approval id not valid for this node",
|
||||
details: { code: "APPROVAL_NODE_MISMATCH", runId },
|
||||
};
|
||||
details: { runId },
|
||||
});
|
||||
}
|
||||
|
||||
// Prefer binding by device identity (stable across reconnects / per-call clients like callGateway()).
|
||||
@@ -189,79 +191,69 @@ export function sanitizeSystemRunParamsForForwarding(opts: {
|
||||
const clientDeviceId = opts.client?.connect?.device?.id ?? null;
|
||||
if (snapshotDeviceId) {
|
||||
if (snapshotDeviceId !== clientDeviceId) {
|
||||
return {
|
||||
ok: false,
|
||||
return systemRunApprovalGuardError({
|
||||
code: "APPROVAL_DEVICE_MISMATCH",
|
||||
message: "approval id not valid for this device",
|
||||
details: { code: "APPROVAL_DEVICE_MISMATCH", runId },
|
||||
};
|
||||
details: { runId },
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
snapshot.requestedByConnId &&
|
||||
snapshot.requestedByConnId !== (opts.client?.connId ?? null)
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
return systemRunApprovalGuardError({
|
||||
code: "APPROVAL_CLIENT_MISMATCH",
|
||||
message: "approval id not valid for this client",
|
||||
details: { code: "APPROVAL_CLIENT_MISMATCH", runId },
|
||||
};
|
||||
details: { runId },
|
||||
});
|
||||
}
|
||||
|
||||
const planV2 = normalizeSystemRunApprovalPlanV2(snapshot.request.systemRunPlanV2 ?? null);
|
||||
let approvalArgv: string[];
|
||||
let approvalCwd: string | null;
|
||||
let approvalAgentId: string | null;
|
||||
let approvalSessionKey: string | null;
|
||||
if (planV2) {
|
||||
approvalArgv = [...planV2.argv];
|
||||
approvalCwd = planV2.cwd;
|
||||
approvalAgentId = planV2.agentId;
|
||||
approvalSessionKey = planV2.sessionKey;
|
||||
next.command = [...planV2.argv];
|
||||
if (planV2.rawCommand) {
|
||||
next.rawCommand = planV2.rawCommand;
|
||||
const runtimeContext = resolveSystemRunApprovalRuntimeContext({
|
||||
planV2: snapshot.request.systemRunPlanV2 ?? null,
|
||||
command: p.command,
|
||||
rawCommand: p.rawCommand,
|
||||
cwd: p.cwd,
|
||||
agentId: p.agentId,
|
||||
sessionKey: p.sessionKey,
|
||||
});
|
||||
if (!runtimeContext.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
message: runtimeContext.message,
|
||||
details: runtimeContext.details,
|
||||
};
|
||||
}
|
||||
if (runtimeContext.planV2) {
|
||||
next.command = [...runtimeContext.planV2.argv];
|
||||
if (runtimeContext.rawCommand) {
|
||||
next.rawCommand = runtimeContext.rawCommand;
|
||||
} else {
|
||||
delete next.rawCommand;
|
||||
}
|
||||
if (planV2.cwd) {
|
||||
next.cwd = planV2.cwd;
|
||||
if (runtimeContext.cwd) {
|
||||
next.cwd = runtimeContext.cwd;
|
||||
} else {
|
||||
delete next.cwd;
|
||||
}
|
||||
if (planV2.agentId) {
|
||||
next.agentId = planV2.agentId;
|
||||
if (runtimeContext.agentId) {
|
||||
next.agentId = runtimeContext.agentId;
|
||||
} else {
|
||||
delete next.agentId;
|
||||
}
|
||||
if (planV2.sessionKey) {
|
||||
next.sessionKey = planV2.sessionKey;
|
||||
if (runtimeContext.sessionKey) {
|
||||
next.sessionKey = runtimeContext.sessionKey;
|
||||
} else {
|
||||
delete next.sessionKey;
|
||||
}
|
||||
} else {
|
||||
const cmdTextResolution = resolveSystemRunCommand({
|
||||
command: p.command,
|
||||
rawCommand: p.rawCommand,
|
||||
});
|
||||
if (!cmdTextResolution.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
message: cmdTextResolution.message,
|
||||
details: cmdTextResolution.details,
|
||||
};
|
||||
}
|
||||
approvalArgv = cmdTextResolution.argv;
|
||||
approvalCwd = normalizeString(p.cwd) ?? null;
|
||||
approvalAgentId = normalizeString(p.agentId) ?? null;
|
||||
approvalSessionKey = normalizeString(p.sessionKey) ?? null;
|
||||
}
|
||||
|
||||
const approvalMatch = evaluateSystemRunApprovalMatch({
|
||||
argv: approvalArgv,
|
||||
argv: runtimeContext.argv,
|
||||
request: snapshot.request,
|
||||
binding: {
|
||||
cwd: approvalCwd,
|
||||
agentId: approvalAgentId,
|
||||
sessionKey: approvalSessionKey,
|
||||
cwd: runtimeContext.cwd,
|
||||
agentId: runtimeContext.agentId,
|
||||
sessionKey: runtimeContext.sessionKey,
|
||||
env: p.env,
|
||||
},
|
||||
});
|
||||
@@ -272,11 +264,7 @@ export function sanitizeSystemRunParamsForForwarding(opts: {
|
||||
// Normal path: enforce the decision recorded by the gateway.
|
||||
if (snapshot.decision === "allow-once") {
|
||||
if (typeof manager.consumeAllowOnce !== "function" || !manager.consumeAllowOnce(runId)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "approval required",
|
||||
details: { code: "APPROVAL_REQUIRED", runId },
|
||||
};
|
||||
return systemRunApprovalRequired(runId);
|
||||
}
|
||||
next.approved = true;
|
||||
next.approvalDecision = "allow-once";
|
||||
@@ -306,9 +294,5 @@ export function sanitizeSystemRunParamsForForwarding(opts: {
|
||||
return { ok: true, params: next };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
message: "approval required",
|
||||
details: { code: "APPROVAL_REQUIRED", runId },
|
||||
};
|
||||
return systemRunApprovalRequired(runId);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,8 @@ import {
|
||||
DEFAULT_EXEC_APPROVAL_TIMEOUT_MS,
|
||||
type ExecApprovalDecision,
|
||||
} from "../../infra/exec-approvals.js";
|
||||
import {
|
||||
buildSystemRunApprovalBindingV1,
|
||||
normalizeSystemRunApprovalPlanV2,
|
||||
} from "../../infra/system-run-approval-binding.js";
|
||||
import { formatExecCommand } from "../../infra/system-run-command.js";
|
||||
import { buildSystemRunApprovalBindingV1 } from "../../infra/system-run-approval-binding.js";
|
||||
import { resolveSystemRunApprovalRequestContext } from "../../infra/system-run-approval-context.js";
|
||||
import type { ExecApprovalManager } from "../exec-approval-manager.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
@@ -72,21 +69,20 @@ export function createExecApprovalHandlers(
|
||||
const explicitId = typeof p.id === "string" && p.id.trim().length > 0 ? p.id.trim() : null;
|
||||
const host = typeof p.host === "string" ? p.host.trim() : "";
|
||||
const nodeId = typeof p.nodeId === "string" ? p.nodeId.trim() : "";
|
||||
const commandArgv = Array.isArray(p.commandArgv)
|
||||
? p.commandArgv.map((entry) => String(entry))
|
||||
: undefined;
|
||||
const systemRunPlanV2 =
|
||||
host === "node" ? normalizeSystemRunApprovalPlanV2(p.systemRunPlanV2) : null;
|
||||
const effectiveCommandArgv = systemRunPlanV2?.argv ?? commandArgv;
|
||||
const effectiveCwd = systemRunPlanV2?.cwd ?? p.cwd;
|
||||
const effectiveAgentId = systemRunPlanV2?.agentId ?? p.agentId;
|
||||
const effectiveSessionKey = systemRunPlanV2?.sessionKey ?? p.sessionKey;
|
||||
const effectiveCommandText = (() => {
|
||||
if (!systemRunPlanV2) {
|
||||
return p.command;
|
||||
}
|
||||
return systemRunPlanV2.rawCommand ?? formatExecCommand(systemRunPlanV2.argv);
|
||||
})();
|
||||
const approvalContext = resolveSystemRunApprovalRequestContext({
|
||||
host,
|
||||
command: p.command,
|
||||
commandArgv: p.commandArgv,
|
||||
systemRunPlanV2: p.systemRunPlanV2,
|
||||
cwd: p.cwd,
|
||||
agentId: p.agentId,
|
||||
sessionKey: p.sessionKey,
|
||||
});
|
||||
const effectiveCommandArgv = approvalContext.commandArgv;
|
||||
const effectiveCwd = approvalContext.cwd;
|
||||
const effectiveAgentId = approvalContext.agentId;
|
||||
const effectiveSessionKey = approvalContext.sessionKey;
|
||||
const effectiveCommandText = approvalContext.commandText;
|
||||
if (host === "node" && !nodeId) {
|
||||
respond(
|
||||
false,
|
||||
@@ -129,7 +125,7 @@ export function createExecApprovalHandlers(
|
||||
commandArgv: effectiveCommandArgv,
|
||||
envKeys: systemRunBindingV1?.envKeys?.length ? systemRunBindingV1.envKeys : undefined,
|
||||
systemRunBindingV1: systemRunBindingV1?.binding ?? null,
|
||||
systemRunPlanV2: systemRunPlanV2,
|
||||
systemRunPlanV2: approvalContext.planV2,
|
||||
cwd: effectiveCwd ?? null,
|
||||
nodeId: host === "node" ? nodeId : null,
|
||||
host: host || null,
|
||||
|
||||
123
src/infra/system-run-approval-context.ts
Normal file
123
src/infra/system-run-approval-context.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { SystemRunApprovalPlanV2 } from "./exec-approvals.js";
|
||||
import { normalizeSystemRunApprovalPlanV2 } from "./system-run-approval-binding.js";
|
||||
import { formatExecCommand, resolveSystemRunCommand } from "./system-run-command.js";
|
||||
|
||||
type PreparedRunPayload = {
|
||||
cmdText: string;
|
||||
plan: SystemRunApprovalPlanV2;
|
||||
};
|
||||
|
||||
type SystemRunApprovalRequestContext = {
|
||||
planV2: SystemRunApprovalPlanV2 | null;
|
||||
commandArgv: string[] | undefined;
|
||||
commandText: string;
|
||||
cwd: string | null;
|
||||
agentId: string | null;
|
||||
sessionKey: string | null;
|
||||
};
|
||||
|
||||
type SystemRunApprovalRuntimeContext =
|
||||
| {
|
||||
ok: true;
|
||||
planV2: SystemRunApprovalPlanV2 | null;
|
||||
argv: string[];
|
||||
cwd: string | null;
|
||||
agentId: string | null;
|
||||
sessionKey: string | null;
|
||||
rawCommand: string | null;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function normalizeString(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.map((entry) => String(entry)) : [];
|
||||
}
|
||||
|
||||
function normalizeCommandText(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
export function parsePreparedSystemRunPayload(payload: unknown): PreparedRunPayload | null {
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||||
return null;
|
||||
}
|
||||
const raw = payload as { cmdText?: unknown; plan?: unknown };
|
||||
const cmdText = normalizeString(raw.cmdText);
|
||||
const plan = normalizeSystemRunApprovalPlanV2(raw.plan);
|
||||
if (!cmdText || !plan) {
|
||||
return null;
|
||||
}
|
||||
return { cmdText, plan };
|
||||
}
|
||||
|
||||
export function resolveSystemRunApprovalRequestContext(params: {
|
||||
host?: unknown;
|
||||
command?: unknown;
|
||||
commandArgv?: unknown;
|
||||
systemRunPlanV2?: unknown;
|
||||
cwd?: unknown;
|
||||
agentId?: unknown;
|
||||
sessionKey?: unknown;
|
||||
}): SystemRunApprovalRequestContext {
|
||||
const host = normalizeString(params.host) ?? "";
|
||||
const planV2 = host === "node" ? normalizeSystemRunApprovalPlanV2(params.systemRunPlanV2) : null;
|
||||
const fallbackArgv = normalizeStringArray(params.commandArgv);
|
||||
const fallbackCommand = normalizeCommandText(params.command);
|
||||
return {
|
||||
planV2,
|
||||
commandArgv: planV2?.argv ?? (fallbackArgv.length > 0 ? fallbackArgv : undefined),
|
||||
commandText: planV2 ? (planV2.rawCommand ?? formatExecCommand(planV2.argv)) : fallbackCommand,
|
||||
cwd: planV2?.cwd ?? normalizeString(params.cwd),
|
||||
agentId: planV2?.agentId ?? normalizeString(params.agentId),
|
||||
sessionKey: planV2?.sessionKey ?? normalizeString(params.sessionKey),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSystemRunApprovalRuntimeContext(params: {
|
||||
planV2?: unknown;
|
||||
command?: unknown;
|
||||
rawCommand?: unknown;
|
||||
cwd?: unknown;
|
||||
agentId?: unknown;
|
||||
sessionKey?: unknown;
|
||||
}): SystemRunApprovalRuntimeContext {
|
||||
const normalizedPlan = normalizeSystemRunApprovalPlanV2(params.planV2 ?? null);
|
||||
if (normalizedPlan) {
|
||||
return {
|
||||
ok: true,
|
||||
planV2: normalizedPlan,
|
||||
argv: [...normalizedPlan.argv],
|
||||
cwd: normalizedPlan.cwd,
|
||||
agentId: normalizedPlan.agentId,
|
||||
sessionKey: normalizedPlan.sessionKey,
|
||||
rawCommand: normalizedPlan.rawCommand,
|
||||
};
|
||||
}
|
||||
const command = resolveSystemRunCommand({
|
||||
command: params.command,
|
||||
rawCommand: params.rawCommand,
|
||||
});
|
||||
if (!command.ok) {
|
||||
return { ok: false, message: command.message, details: command.details };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
planV2: null,
|
||||
argv: command.argv,
|
||||
cwd: normalizeString(params.cwd),
|
||||
agentId: normalizeString(params.agentId),
|
||||
sessionKey: normalizeString(params.sessionKey),
|
||||
rawCommand: normalizeString(params.rawCommand),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user