openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
337 lines (336 loc) • 18.8 kB
JavaScript
import { R as timestampMsToIsoString, w as parseStrictPositiveInteger } from "./number-coercion-CLj0HTDM.js";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { a as writeRuntimeJson, r as defaultRuntime } from "./runtime-CF2WjnNZ.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { n as sanitizeTerminalText } from "./safe-text-BGBqp1a4.js";
import { t as formatDocsLink } from "./links-ClIwBcy4.js";
import { r as theme } from "./theme-vjDs9tao.js";
import { o as callGateway } from "./call-BWR5pPgw.js";
import { f as findAuditActivityFilterConflict, i as AUDIT_ACTIVITY_STATUSES, n as AUDIT_ACTIVITY_KINDS, t as AUDIT_ACTIVITY_DIRECTIONS } from "./audit-activity-mkM4q0OU.js";
import { a as isExpectedCliError } from "./failure-output-Dc5ce9_F.js";
import { t as parseAbsoluteTimeMs } from "./parse-tlCSjrTr.js";
import { t as formatHumanList } from "./human-list-AC9kKekF.js";
import { n as runCommandWithRuntime } from "./cli-utils-D1DAWB8d.js";
import { t as parsePositiveAuditCursor } from "./audit-cursor-B-p0ImK5.js";
//#region src/commands/audit.ts
/** Operator CLI for bounded metadata-only activity audit pages. */
const DEFAULT_AUDIT_LIMIT = 100;
const MAX_AUDIT_LIMIT = 500;
const DEFAULT_AUDIT_DECISION_LIMIT = 50;
const MAX_AUDIT_DECISION_LIMIT = 100;
const MAX_AUDIT_EXECUTION_LIMIT = 50;
function parseAuditTimestamp(value, flag) {
const trimmed = value?.trim();
if (!trimmed) return;
if (/^\d+$/.test(trimmed)) {
const parsed = Number(trimmed);
if (Number.isSafeInteger(parsed)) return parsed;
}
const parsed = Date.parse(trimmed);
if (!Number.isNaN(parsed) && parseAbsoluteTimeMs(trimmed) !== null) return parsed;
throw new Error(`${flag} must be an ISO timestamp or Unix milliseconds.`);
}
function parseAuditLimit(value) {
if (!value) return DEFAULT_AUDIT_LIMIT;
const parsed = parseStrictPositiveInteger(value);
if (parsed === void 0 || parsed > MAX_AUDIT_LIMIT) throw new Error(`--limit must be between 1 and ${MAX_AUDIT_LIMIT}.`);
return parsed;
}
function parseAuditDecisionLimit(value) {
if (!value) return DEFAULT_AUDIT_DECISION_LIMIT;
const parsed = parseStrictPositiveInteger(value);
if (parsed === void 0 || parsed > MAX_AUDIT_DECISION_LIMIT) throw new Error(`--limit must be between 1 and ${String(MAX_AUDIT_DECISION_LIMIT)} with --explain.`);
return parsed;
}
function short(value, maxChars) {
if (!value) return "-";
const sanitized = sanitizeTerminalText(value);
if (!sanitized) return "-";
return sanitized.length <= maxChars ? sanitized : `${truncateUtf16Safe(sanitized, maxChars - 1)}…`;
}
function formatAuditRows(events) {
const rows = ["TIME KIND DIRECTION CHANNEL STATUS AGENT RUN ACTION"];
for (const event of events) rows.push([
timestampMsToIsoString(event.occurredAt) ?? String(event.occurredAt),
event.kind,
short(event.direction, 10),
short(event.channel, 18),
event.status,
short(event.agentId, 18),
short(event.runId, 18),
event.toolName ? `${event.action}:${short(event.toolName, 28)}` : event.action
].join(" "));
return rows;
}
function isUnsupportedActivityMethodError(value) {
return value instanceof Error && value.name === "GatewayClientRequestError" && value.gatewayCode === "INVALID_REQUEST" && (value.message === "unknown method: audit.activity.list" || value.message === "missing scope: operator.admin");
}
function isUnsupportedRunInspectMethodError(value) {
return value instanceof Error && value.name === "GatewayClientRequestError" && value.gatewayCode === "INVALID_REQUEST" && (value.message === "unknown method: audit.run.inspect" || value.message === "missing scope: operator.admin");
}
function hasMessageSpecificFilters(options) {
return options.kind === "message" || options.direction !== void 0 || options.channel !== void 0;
}
function validateAuditFilter(value, flag, allowed) {
if (value !== void 0 && !allowed.includes(value)) throw new Error(`${flag} must be ${formatHumanList(allowed)}.`);
}
const AUDIT_FILTER_FLAGS = {
sessionKey: "--session",
direction: "--direction",
channel: "--channel"
};
function validateAuditFilterCombination(options) {
const conflict = findAuditActivityFilterConflict(options);
if (!conflict) return;
const flag = AUDIT_FILTER_FLAGS[conflict.field];
if (conflict.type === "kind") throw new Error(`${flag} only applies to --kind ${formatHumanList(conflict.supportedKinds)}.`);
throw new Error(`${flag} cannot be combined with ${AUDIT_FILTER_FLAGS[conflict.conflictingField]}.`);
}
function formatAuditGatewayError(error) {
if (isExpectedCliError(error)) return error;
const message = formatErrorMessage(error);
return new Error(message === "invalid audit.activity.list range or cursor" ? "--cursor must be a continuation token returned by a previous audit result." : message);
}
function toLegacyAuditListParams(params) {
return {
...params.agentId ? { agentId: params.agentId } : {},
...params.sessionKey ? { sessionKey: params.sessionKey } : {},
...params.runId ? { runId: params.runId } : {},
...params.kind === "agent_run" || params.kind === "tool_action" ? { kind: params.kind } : {},
...params.status ? { status: params.status } : {},
...params.after !== void 0 ? { after: params.after } : {},
...params.before !== void 0 ? { before: params.before } : {},
...params.limit !== void 0 ? { limit: params.limit } : {},
...params.cursor ? { cursor: params.cursor } : {}
};
}
async function queryAuditActivity(params, options) {
try {
return await callGateway({
method: "audit.activity.list",
params
});
} catch (error) {
if (!isUnsupportedActivityMethodError(error)) throw formatAuditGatewayError(error);
if (hasMessageSpecificFilters(options)) throw new Error("The connected Gateway does not support message audit filters. Upgrade the Gateway to use --kind message, --direction, or --channel.", { cause: error });
return await callGateway({
method: "audit.list",
params: toLegacyAuditListParams(params)
});
}
}
function unsupportedRunInspection(selector) {
const missingEvidence = ["identity.context"];
return {
schemaVersion: 1,
run: {
...selector,
status: "unknown"
},
identity: {
state: "unsupported",
reasonCode: "gateway_upgrade_required",
missingEvidence,
remediation: [{
code: "upgrade_gateway",
text: "Upgrade the Gateway, run the agent again, and repeat this exact-run inspection."
}]
},
decisionDisplays: [],
coverage: {
state: "unsupported",
missingEvidence
}
};
}
async function queryAuditRunInspection(params) {
try {
return await callGateway({
method: "audit.run.inspect",
params
});
} catch (error) {
if (!isUnsupportedRunInspectMethodError(error)) throw formatAuditGatewayError(error);
return unsupportedRunInspection(typeof params.runId === "string" ? { runId: params.runId } : { executionId: params.executionId });
}
}
function safe(value) {
return sanitizeTerminalText(value ?? "") || "-";
}
function principalText(principal) {
return `${safe(principal.kind)} ${safe(principal.principalRef)} in ${safe(principal.domainRef)}`;
}
function fieldLine(label, state, value) {
return ` ${label} [${safe(state)}]${value ? `: ${safe(value)}` : ""}`;
}
const IDENTITY_FIELD_LABELS = [
"Trust domain",
"Invoker",
"Ingress",
"Agent principal",
"Agent definition",
"Runtime instance",
"Represented subject",
"Sponsor",
"Applicable grants",
"Assurance"
];
function contextIdentityLines(context) {
const grants = context.applicableGrants.map((grant) => `${grant.grantRef} [${grant.state}]`);
const assurance = context.assurance.map((item) => `${item.kind} ${item.evidenceRef} [${item.strength}]`);
return [
fieldLine("Trust domain", context.trustDomain.state, `${context.trustDomain.kind} ${context.trustDomain.domainRef}`),
fieldLine("Invoker", context.invoker.state, context.invoker.principal ? principalText(context.invoker.principal) : void 0),
fieldLine("Ingress", context.ingress.state, `${context.ingress.kind} at ${context.ingress.boundary}${context.ingress.sourceRef ? ` (${context.ingress.sourceRef})` : ""}`),
fieldLine("Agent principal", "present", principalText(context.agentPrincipal)),
fieldLine("Agent definition", context.agentDefinition.state, `${context.agentDefinition.definitionRef}${context.agentDefinition.revisionRef ? ` @ ${context.agentDefinition.revisionRef}` : ""}`),
fieldLine("Runtime instance", context.runtimeInstance.state, `${context.runtimeInstance.kind} ${context.runtimeInstance.runtimeRef}`),
fieldLine("Represented subject", context.representedSubject?.state ?? "absent", context.representedSubject ? principalText(context.representedSubject.principal) : void 0),
fieldLine("Sponsor", context.sponsor?.state ?? "absent", context.sponsor ? principalText(context.sponsor.principal) : void 0),
fieldLine("Applicable grants", grants.length > 0 ? "present" : "absent", grants.join(", ")),
fieldLine("Assurance", assurance.length > 0 ? "present" : "absent", assurance.join(", "))
];
}
function contextLineageLines(context) {
const lineage = context.lineage;
if (!lineage) return [fieldLine("Parent", "absent")];
return [
fieldLine("Parent context", lineage.parentContextId ? "present" : "unknown", lineage.parentContextId),
fieldLine("Parent execution", lineage.parentExecutionId ? "present" : "unknown", lineage.parentExecutionId),
fieldLine("Parent run", lineage.parentRunId ? "present" : "unknown", lineage.parentRunId),
fieldLine("Parent agent", lineage.parentAgentPrincipal ? "present" : "unknown", lineage.parentAgentPrincipal ? principalText(lineage.parentAgentPrincipal) : void 0),
fieldLine("Delegation", lineage.delegationRef ? "present" : "unknown", lineage.delegationRef),
fieldLine("Depth", "present", String(lineage.depth))
];
}
function unavailableIdentityLines(state) {
return IDENTITY_FIELD_LABELS.map((label) => fieldLine(label, state));
}
function decisionLines(receipt) {
const evidence = receipt.provenance.state === "unverified" ? "producer display contract unverified; receipt prose omitted" : receipt.provenance.producer === "run-admission" ? "admission provenance only; no enforcement decision" : receipt.enforcement.coverageState === "unknown" || receipt.enforcement.coverageState === "unsupported" ? "evidence unavailable or corrupt; do not infer authorization" : receipt.provenance.producer === "operator-approval" ? "authoritative owner-native SQLite record; retained 30 days" : receipt.enforcement.coverageState === "enforced" ? "validated immutable decision fact; retained 30 days" : "attribution record only; no enforcement decision";
const producer = receipt.provenance.state === "verified" ? receipt.provenance.producer : "unverified";
return [
` ${safe(receipt.action.family)}.${safe(receipt.action.operation)}: ${safe(receipt.decision.outcome)}`,
` Coverage: ${safe(receipt.enforcement.coverageState)}`,
` Reason: ${safe(receipt.decision.reasonCode)}`,
` Display producer: ${safe(producer)}`,
` Evidence: ${evidence}`,
` Policy refs: ${receipt.enforcement.policyCount}`,
` Grant refs: ${receipt.enforcement.grantCount}`,
` Context used: ${receipt.enforcement.contextFieldsUsed.length > 0 ? receipt.enforcement.contextFieldsUsed.map(safe).join(", ") : "none"}`,
...receipt.action.summary ? [` Summary: ${safe(receipt.action.summary)}`] : []
];
}
function formatAuditRunInspection(result) {
const decisionDisplays = result.decisionDisplays;
const lines = [
`${result.run.executionId ? `Execution ${safe(result.run.executionId)}${result.run.runId ? ` (run ${safe(result.run.runId)})` : ""}` : `Run ${safe(result.run.runId)}`}: ${safe(result.run.status)} (${safe(result.coverage.state)})`,
"",
"Identity"
];
if (result.identity.state === "present") {
const identityLines = contextIdentityLines(result.identity.context);
lines.push(` Context: ${safe(result.identity.context.contextId)}`, ` Created: ${timestampMsToIsoString(result.identity.context.createdAt) ?? String(result.identity.context.createdAt)}`, ...identityLines.slice(0, 8), "", "Authority", ...identityLines.slice(8), "", "Lineage", ...contextLineageLines(result.identity.context));
} else if (result.identity.state === "ambiguous") lines.push(` Reason: ${safe(result.identity.reasonCode)}`, ...result.identity.candidates.map((candidate) => ` Candidate: ${safe(candidate.executionId)} (context ${safe(candidate.contextId)}, ${timestampMsToIsoString(candidate.createdAt) ?? String(candidate.createdAt)})`), "", "Authority", fieldLine("Selection", "unknown"), "", "Lineage", fieldLine("Parent", "unknown"));
else lines.push(` Reason: ${safe(result.identity.reasonCode)}`, ...unavailableIdentityLines(result.identity.state).slice(0, 8), "", "Authority", ...unavailableIdentityLines(result.identity.state).slice(8), "", "Lineage", fieldLine("Parent", result.identity.state));
lines.push("", "Decisions");
if (decisionDisplays.length === 0) lines.push(" none [absent]");
else for (const receipt of decisionDisplays) lines.push(...decisionLines(receipt));
lines.push("", "Missing evidence");
lines.push(...result.coverage.missingEvidence.length > 0 ? result.coverage.missingEvidence.map((item) => ` - ${safe(item)}`) : [" none"]);
const remediation = [...result.identity.state === "present" ? [] : result.identity.remediation, ...decisionDisplays.flatMap((decision) => decision.remediation)];
lines.push("", "Next steps");
lines.push(...remediation.length > 0 ? [...new Map(remediation.map((item) => [item.code, item])).values()].map((item) => ` - ${safe(item.text)}`) : [" none"]);
if (result.nextDecisionCursor) lines.push(` More decisions: --cursor ${safe(result.nextDecisionCursor)}`);
if (result.nextExecutionCursor) lines.push(` More executions: --cursor ${safe(result.nextExecutionCursor)}`);
return lines;
}
function hasExplainIncompatibleFilters(options) {
return Boolean(options.agentId || options.sessionKey || options.kind || options.status || options.direction || options.channel || options.after || options.before);
}
/** Query one stable page. JSON output is a bounded export with its next cursor. */
async function auditListCommand(options, runtime) {
if (options.explain) {
const runId = options.runId?.trim();
const executionId = options.executionId?.trim();
if (Boolean(runId) === Boolean(executionId)) throw new Error("Pass exactly one of --run <id> or --execution <id> with --explain.");
if (hasExplainIncompatibleFilters(options)) throw new Error("--explain accepts only --run or --execution, plus --limit, --cursor, and --json; remove activity-list filters.");
const decisionLimit = parseAuditDecisionLimit(options.limit);
const cursor = options.cursor;
const numericCursor = parsePositiveAuditCursor(cursor);
const runExecutionCursor = numericCursor !== void 0 && numericCursor !== null ? cursor : void 0;
const decisionPage = {
decisionLimit,
...cursor ? { decisionCursor: cursor } : {}
};
const result = await queryAuditRunInspection(executionId ? {
executionId,
...decisionPage
} : {
runId,
executionLimit: Math.min(decisionLimit, MAX_AUDIT_EXECUTION_LIMIT),
...runExecutionCursor ? { executionCursor: runExecutionCursor } : {},
...decisionPage
});
if (options.json) {
writeRuntimeJson(runtime, result);
return;
}
for (const line of formatAuditRunInspection(result)) runtime.log(line);
return;
}
if (options.executionId) throw new Error("--execution requires --explain.");
validateAuditFilter(options.kind, "--kind", AUDIT_ACTIVITY_KINDS);
validateAuditFilter(options.status, "--status", AUDIT_ACTIVITY_STATUSES);
validateAuditFilter(options.direction, "--direction", AUDIT_ACTIVITY_DIRECTIONS);
validateAuditFilterCombination(options);
const after = parseAuditTimestamp(options.after, "--after");
const before = parseAuditTimestamp(options.before, "--before");
if (after !== void 0 && before !== void 0 && after > before) throw new Error("--after must not be later than --before.");
const result = await queryAuditActivity({
limit: parseAuditLimit(options.limit),
...options.agentId ? { agentId: options.agentId } : {},
...options.sessionKey ? { sessionKey: options.sessionKey } : {},
...options.runId ? { runId: options.runId } : {},
...options.kind ? { kind: options.kind } : {},
...options.status ? { status: options.status } : {},
...options.direction ? { direction: options.direction } : {},
...options.channel ? { channel: options.channel } : {},
...after !== void 0 ? { after } : {},
...before !== void 0 ? { before } : {},
...options.cursor ? { cursor: options.cursor } : {}
}, options);
if (options.json) {
writeRuntimeJson(runtime, result);
return;
}
for (const row of formatAuditRows(result.events)) runtime.log(row);
if (result.nextCursor) runtime.log(`More records: --cursor ${result.nextCursor}`);
}
//#endregion
//#region src/cli/program/register.audit.ts
/** Register the bounded operator audit query command. */
function registerAuditCommand(program) {
program.command("audit").description("Inspect activity records and exact-run identity context").option("--agent <id>", "Filter by agent id").option("--session <key>", "Filter by exact session key").option("--run <id>", "Filter by run id").option("--execution <id>", "Inspect one exact execution id").option("--kind <kind>", `Filter by kind (${formatHumanList(AUDIT_ACTIVITY_KINDS)})`).option("--status <status>", `Filter by status (${formatHumanList(AUDIT_ACTIVITY_STATUSES)})`).option("--direction <direction>", `Filter message direction (${formatHumanList(AUDIT_ACTIVITY_DIRECTIONS)})`).option("--channel <channel>", "Filter message channel").option("--after <timestamp>", "Include records at/after ISO time or Unix milliseconds").option("--before <timestamp>", "Include records at/before ISO time or Unix milliseconds").option("--cursor <sequence>", "Continue from a previous result cursor").option("--limit <count>", "Maximum records (1-500; decisions 1-100)").option("--explain", "Inspect execution identity and run-admission reasoning", false).option("--json", "Output a bounded JSON page", false).addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/audit", "docs.openclaw.ai/cli/audit")}\n`).action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await auditListCommand({
agentId: opts.agent,
sessionKey: opts.session,
runId: opts.run,
executionId: opts.execution,
kind: opts.kind,
status: opts.status,
direction: opts.direction,
channel: opts.channel,
after: opts.after,
before: opts.before,
cursor: opts.cursor,
limit: opts.limit,
explain: Boolean(opts.explain),
json: Boolean(opts.json)
}, defaultRuntime);
});
});
}
//#endregion
export { registerAuditCommand };