refactor(policy): split policy state evidence (#108210)

This commit is contained in:
Peter Steinberger
2026-07-15 02:56:52 -07:00
committed by GitHub
parent 61bd901da9
commit b2a336a2b9
14 changed files with 3130 additions and 2998 deletions
-1
View File
@@ -237,7 +237,6 @@ extensions/policy/src/doctor/register.gateway-data-and-approvals.test-utils.ts
extensions/policy/src/doctor/register.ingress-and-secrets.test-utils.ts
extensions/policy/src/doctor/register.models-and-mcp.test-utils.ts
extensions/policy/src/doctor/register.sandbox-and-tools.test-utils.ts
extensions/policy/src/policy-state.ts
extensions/qa-lab/src/cli.runtime.test.ts
extensions/qa-lab/src/cli.runtime.ts
extensions/qa-lab/src/cli.ts
@@ -0,0 +1,76 @@
// Policy plugin attestation hashing.
import { createHash } from "node:crypto";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { PolicyAttestation, PolicyEvidence } from "./policy-state-types.js";
export function policyDocumentHash(policy: unknown): string {
return sha256(stableJson(policy));
}
function policyWorkspaceHash(evidence: PolicyEvidence): string {
return sha256(stableJson(evidence));
}
function policyFindingsHash(findings: readonly unknown[]): string {
return sha256(stableJson(findings));
}
function policyAttestationHash(input: {
readonly ok: boolean;
readonly policyHash?: string;
readonly workspaceHash: string;
readonly findingsHash: string;
}): string {
return sha256(stableJson(input));
}
export function createPolicyAttestation(input: {
readonly ok: boolean;
readonly checkedAt: string;
readonly policyPath: string;
readonly policyHash?: string;
readonly evidence: PolicyEvidence;
readonly findings: readonly unknown[];
}): PolicyAttestation {
const workspaceHash = policyWorkspaceHash(input.evidence);
const findingsHash = policyFindingsHash(input.findings);
return {
checkedAt: input.checkedAt,
...(input.policyHash === undefined
? {}
: {
policy: {
path: input.policyPath,
hash: input.policyHash,
},
}),
workspace: {
scope: "policy",
hash: workspaceHash,
},
findingsHash,
attestationHash: policyAttestationHash({
ok: input.ok,
policyHash: input.policyHash,
workspaceHash,
findingsHash,
}),
};
}
function sha256(value: string): string {
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
}
function stableJson(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map(stableJson).join(",")}]`;
}
if (isRecord(value)) {
return `{${Object.entries(value)
.toSorted(([a], [b]) => a.localeCompare(b))
.map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
+289
View File
@@ -0,0 +1,289 @@
// Policy plugin channel, model, MCP, and network evidence.
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { ocPathSegment, readBooleanPath } from "./policy-state-helpers.js";
import { RESERVED_CHANNEL_CONFIG_KEYS } from "./policy-state-types.js";
import type {
PolicyChannelEvidence,
PolicyMcpServerEvidence,
PolicyModelProviderEvidence,
PolicyModelRefEvidence,
PolicyNetworkEvidence,
} from "./policy-state-types.js";
export function scanPolicyChannels(cfg: Record<string, unknown>): readonly PolicyChannelEvidence[] {
return Object.entries(configuredChannels(cfg))
.filter(([id]) => !RESERVED_CHANNEL_CONFIG_KEYS.has(id))
.toSorted(([a], [b]) => a.localeCompare(b))
.map(([id, value]) => {
const entry: {
id: string;
provider: string;
source: string;
enabled?: boolean;
} = {
id,
provider: id,
source: `oc://openclaw.config/channels/${id}`,
};
if (isRecord(value) && typeof value.enabled === "boolean") {
entry.enabled = value.enabled;
}
return entry;
});
}
export function scanPolicyMcpServers(
cfg: Record<string, unknown>,
): readonly PolicyMcpServerEvidence[] {
return Object.entries(configuredMcpServers(cfg))
.toSorted(([a], [b]) => a.localeCompare(b))
.map(([id, value]) => {
const entry: {
id: string;
transport: "stdio" | "sse" | "streamable-http" | "unknown";
source: string;
command?: string;
url?: string;
} = {
id,
transport: mcpServerTransport(value),
source: `oc://openclaw.config/mcp/servers/${ocPathSegment(id)}`,
};
if (isRecord(value)) {
if (typeof value.command === "string") {
entry.command = value.command;
}
if (typeof value.url === "string") {
entry.url = redactMcpUrlForEvidence(value.url);
}
}
return entry;
});
}
export function scanPolicyModelProviders(
cfg: Record<string, unknown>,
): readonly PolicyModelProviderEvidence[] {
return Object.keys(configuredModelProviders(cfg))
.toSorted((a, b) => a.localeCompare(b))
.map((id) => ({
id: normalizeProviderId(id),
source: `oc://openclaw.config/models/providers/${id}`,
}));
}
export function scanPolicyModelRefs(
cfg: Record<string, unknown>,
): readonly PolicyModelRefEvidence[] {
const refs: PolicyModelRefEvidence[] = [];
if (isRecord(cfg.agents)) {
collectModelRefsFromRecord(refs, cfg.agents, "oc://openclaw.config/agents");
collectModelRefsFromAgentAllowlist(refs, cfg.agents);
}
return refs.toSorted(
(a, b) => a.provider.localeCompare(b.provider) || a.model.localeCompare(b.model),
);
}
export function scanPolicyNetwork(cfg: Record<string, unknown>): readonly PolicyNetworkEvidence[] {
return [
networkBooleanEvidence(
cfg,
"browser-private-network",
["browser", "ssrfPolicy", "dangerouslyAllowPrivateNetwork"],
"oc://openclaw.config/browser/ssrfPolicy/dangerouslyAllowPrivateNetwork",
),
networkBooleanEvidence(
cfg,
"browser-private-network-legacy",
["browser", "ssrfPolicy", "allowPrivateNetwork"],
"oc://openclaw.config/browser/ssrfPolicy/allowPrivateNetwork",
),
networkBooleanEvidence(
cfg,
"web-fetch-private-network",
["tools", "web", "fetch", "ssrfPolicy", "dangerouslyAllowPrivateNetwork"],
"oc://openclaw.config/tools/web/fetch/ssrfPolicy/dangerouslyAllowPrivateNetwork",
),
networkBooleanEvidence(
cfg,
"web-fetch-private-network-legacy",
["tools", "web", "fetch", "ssrfPolicy", "allowPrivateNetwork"],
"oc://openclaw.config/tools/web/fetch/ssrfPolicy/allowPrivateNetwork",
),
networkBooleanEvidence(
cfg,
"web-fetch-rfc2544-benchmark-range",
["tools", "web", "fetch", "ssrfPolicy", "allowRfc2544BenchmarkRange"],
"oc://openclaw.config/tools/web/fetch/ssrfPolicy/allowRfc2544BenchmarkRange",
),
networkBooleanEvidence(
cfg,
"web-fetch-ipv6-unique-local-range",
["tools", "web", "fetch", "ssrfPolicy", "allowIpv6UniqueLocalRange"],
"oc://openclaw.config/tools/web/fetch/ssrfPolicy/allowIpv6UniqueLocalRange",
),
].filter((entry): entry is PolicyNetworkEvidence => entry !== undefined);
}
export function configuredChannels(cfg: Record<string, unknown>): Record<string, unknown> {
return isRecord(cfg.channels) ? cfg.channels : {};
}
function configuredMcpServers(cfg: Record<string, unknown>): Record<string, unknown> {
return isRecord(cfg.mcp) && isRecord(cfg.mcp.servers) ? cfg.mcp.servers : {};
}
function mcpServerTransport(value: unknown): PolicyMcpServerEvidence["transport"] {
if (!isRecord(value)) {
return "unknown";
}
if (typeof value.command === "string") {
return "stdio";
}
if (value.transport === "sse" || value.transport === "streamable-http") {
return value.transport;
}
if (typeof value.url === "string") {
return "streamable-http";
}
return "unknown";
}
function redactMcpUrlForEvidence(raw: string): string {
try {
const url = new URL(raw);
return `${url.protocol}//${url.host}`;
} catch {
return "[redacted-url]";
}
}
function configuredModelProviders(cfg: Record<string, unknown>): Record<string, unknown> {
return isRecord(cfg.models) && isRecord(cfg.models.providers) ? cfg.models.providers : {};
}
function networkBooleanEvidence(
cfg: Record<string, unknown>,
id: string,
path: readonly string[],
source: string,
): PolicyNetworkEvidence | undefined {
const value = readBooleanPath(cfg, path);
return value === undefined ? undefined : { id, source, value };
}
function collectModelRefsFromValue(
refs: PolicyModelRefEvidence[],
value: unknown,
source: string,
): void {
if (typeof value === "string") {
pushModelRef(refs, value, source);
return;
}
if (!isRecord(value)) {
return;
}
if (typeof value.primary === "string") {
pushModelRef(refs, value.primary, `${source}/primary`);
}
if (Array.isArray(value.fallbacks)) {
for (const [index, fallback] of value.fallbacks.entries()) {
if (typeof fallback === "string") {
pushModelRef(refs, fallback, `${source}/fallbacks/#${index}`);
}
}
}
}
function collectModelRefsFromRecord(
refs: PolicyModelRefEvidence[],
value: Record<string, unknown>,
source: string,
): void {
for (const [key, child] of Object.entries(value)) {
const childPath = `${source}/${key}`;
if (isModelSettingKey(key)) {
collectModelRefsFromValue(refs, child, childPath);
continue;
}
if (Array.isArray(child)) {
for (const [index, item] of child.entries()) {
if (isRecord(item)) {
collectModelRefsFromRecord(refs, item, `${childPath}/#${index}`);
}
}
continue;
}
if (isRecord(child)) {
collectModelRefsFromRecord(refs, child, childPath);
}
}
}
function collectModelRefsFromAgentAllowlist(
refs: PolicyModelRefEvidence[],
agents: Record<string, unknown>,
): void {
const defaults = agents.defaults;
if (isRecord(defaults) && isRecord(defaults.models)) {
collectModelRefsFromModelMap(
refs,
defaults.models,
"oc://openclaw.config/agents/defaults/models",
);
}
const list = agents.list;
if (!Array.isArray(list)) {
return;
}
for (const [index, agent] of list.entries()) {
if (!isRecord(agent) || !isRecord(agent.models)) {
continue;
}
collectModelRefsFromModelMap(
refs,
agent.models,
`oc://openclaw.config/agents/list/#${index}/models`,
);
}
}
function collectModelRefsFromModelMap(
refs: PolicyModelRefEvidence[],
models: Record<string, unknown>,
source: string,
): void {
for (const ref of Object.keys(models)) {
pushModelRef(refs, ref, `${source}/${ocPathSegment(ref)}`);
}
}
function isModelSettingKey(key: string): boolean {
return key === "model" || key.endsWith("Model");
}
function pushModelRef(refs: PolicyModelRefEvidence[], ref: string, source: string): void {
const parsed = parseModelRef(ref);
if (parsed === undefined) {
return;
}
refs.push({ ref, provider: parsed.provider, model: parsed.model, source });
}
function parseModelRef(
ref: string,
): { readonly provider: string; readonly model: string } | undefined {
const trimmed = ref.trim();
const slash = trimmed.indexOf("/");
if (slash <= 0 || slash >= trimmed.length - 1) {
return undefined;
}
return {
provider: normalizeProviderId(trimmed.slice(0, slash)),
model: trimmed.slice(slash + 1),
};
}
+498
View File
@@ -0,0 +1,498 @@
// Policy plugin data, secret, and auth evidence.
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import { coerceSecretRef } from "openclaw/plugin-sdk/secret-input";
import {
isRecord,
asBoolean as readBoolean,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { ocPathSegment } from "./policy-state-helpers.js";
import type {
PolicyAuthProfileEvidence,
PolicyDataHandlingEvidence,
PolicySecretEvidence,
SecretRefDefaults,
SecretRefEvidence,
} from "./policy-state-types.js";
export function scanPolicySecrets(cfg: Record<string, unknown>): readonly PolicySecretEvidence[] {
return [...scanPolicySecretProviders(cfg), ...scanPolicySecretInputs(cfg)].toSorted((a, b) =>
a.source.localeCompare(b.source),
);
}
export function scanPolicyAuthProfiles(
cfg: Record<string, unknown>,
): readonly PolicyAuthProfileEvidence[] {
const auth = isRecord(cfg.auth) ? cfg.auth : {};
const profiles = isRecord(auth.profiles) ? auth.profiles : {};
return Object.entries(profiles)
.toSorted(([a], [b]) => a.localeCompare(b))
.map(([id, value]) => {
const entry: {
id: string;
source: string;
validMetadata: boolean;
provider?: string;
mode?: string;
} = {
id,
source: `oc://openclaw.config/auth/profiles/${ocPathSegment(id)}`,
validMetadata: isValidAuthProfileMetadata(value),
};
if (isRecord(value)) {
if (typeof value.provider === "string") {
entry.provider = value.provider;
}
if (typeof value.mode === "string") {
entry.mode = value.mode;
}
}
return entry;
});
}
export function scanPolicyDataHandling(
cfg: Record<string, unknown>,
): readonly PolicyDataHandlingEvidence[] {
const entries: PolicyDataHandlingEvidence[] = [];
const logging = isRecord(cfg.logging) ? cfg.logging : {};
entries.push({
id: "logging-redaction",
kind: "sensitiveLoggingRedaction",
source: "oc://openclaw.config/logging/redactSensitive",
scope: "global",
value: logging.redactSensitive !== "off",
explicit: logging.redactSensitive !== undefined,
});
const diagnostics = isRecord(cfg.diagnostics) ? cfg.diagnostics : {};
const otel = isRecord(diagnostics.otel) ? diagnostics.otel : {};
const otelEnabled = diagnostics.enabled !== false && otel.enabled === true;
const tracesEnabled = otelEnabled && otel.traces !== false;
const logsEnabled = otelEnabled && otel.logs === true;
const captureContent =
otelEnabled &&
telemetryContentCaptureEnabled(otel.captureContent, {
tracesEnabled,
logsEnabled,
});
entries.push({
id: "diagnostics-otel-content-capture",
kind: "telemetryContentCapture",
source: "oc://openclaw.config/diagnostics/otel/captureContent",
scope: "global",
value: captureContent,
explicit: otel.captureContent !== undefined,
});
const session = isRecord(cfg.session) ? cfg.session : {};
const maintenance = isRecord(session.maintenance) ? session.maintenance : {};
const retentionMode = typeof maintenance.mode === "string" ? maintenance.mode : "enforce";
entries.push({
id: "session-maintenance-mode",
kind: "sessionRetentionMode",
source: "oc://openclaw.config/session/maintenance/mode",
scope: "global",
value: retentionMode,
explicit: maintenance.mode !== undefined,
});
pushMemorySessionTranscriptIndexing(entries, cfg);
return entries.toSorted((a, b) => a.source.localeCompare(b.source));
}
function telemetryContentCaptureEnabled(
value: unknown,
signals: { readonly tracesEnabled: boolean; readonly logsEnabled: boolean },
): boolean {
if (value === true) {
return signals.tracesEnabled || signals.logsEnabled;
}
if (!isRecord(value)) {
return false;
}
if (!signals.tracesEnabled) {
return false;
}
if (value.enabled !== true) {
return false;
}
return (
value.inputMessages === true ||
value.outputMessages === true ||
value.toolInputs === true ||
value.toolOutputs === true ||
value.systemPrompt === true ||
value.toolDefinitions === true
);
}
function pushMemorySessionTranscriptIndexing(
entries: PolicyDataHandlingEvidence[],
cfg: Record<string, unknown>,
): void {
const memory = isRecord(cfg.memory) ? cfg.memory : {};
const qmd = isRecord(memory.qmd) ? memory.qmd : {};
const qmdSessions = isRecord(qmd.sessions) ? qmd.sessions : {};
if (qmdSessions.enabled !== undefined) {
entries.push({
id: "memory-qmd-session-transcripts",
kind: "memorySessionTranscriptIndexing",
source: "oc://openclaw.config/memory/qmd/sessions/enabled",
scope: "global",
value: memory.backend === "qmd" && readBoolean(qmdSessions.enabled) === true,
explicit: true,
});
}
const agents = isRecord(cfg.agents) ? cfg.agents : {};
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
const defaultsMemorySearch = isRecord(defaults.memorySearch) ? defaults.memorySearch : {};
const defaultSessionMemory = memorySearchSessionTranscriptIndexing(defaultsMemorySearch);
if (defaultSessionMemory !== undefined) {
entries.push({
id: "agents-defaults-memory-session-transcripts",
kind: "memorySessionTranscriptIndexing",
source: "oc://openclaw.config/agents/defaults/memorySearch/experimental/sessionMemory",
scope: "global",
value: defaultSessionMemory,
explicit: true,
});
}
if (!Array.isArray(agents.list)) {
return;
}
agents.list.forEach((rawAgent, index) => {
if (!isRecord(rawAgent)) {
return;
}
const agentId =
readString(rawAgent.id) ??
readString(rawAgent.name) ??
readString(rawAgent.slug) ??
`agent-${index}`;
const memorySearch = isRecord(rawAgent.memorySearch) ? rawAgent.memorySearch : undefined;
const agentSessionMemory =
memorySearch === undefined
? defaultSessionMemory
: memorySearchSessionTranscriptIndexing(memorySearch, defaultsMemorySearch);
if (agentSessionMemory === undefined) {
return;
}
const explicit = memorySearchSessionTranscriptIndexingHasLocalConfig(memorySearch);
entries.push({
id: `${agentId}-memory-session-transcripts`,
kind: "memorySessionTranscriptIndexing",
source: explicit
? `oc://openclaw.config/agents/list/#${index}/memorySearch/experimental/sessionMemory`
: "oc://openclaw.config/agents/defaults/memorySearch/experimental/sessionMemory",
scope: "agent",
agentId: normalizeAgentId(agentId),
value: agentSessionMemory,
explicit,
});
});
}
function memorySearchSessionTranscriptIndexing(
memorySearch: unknown,
inheritedMemorySearch?: unknown,
): boolean | undefined {
if (!isRecord(memorySearch)) {
return undefined;
}
const experimental = isRecord(memorySearch.experimental) ? memorySearch.experimental : {};
const inherited = isRecord(inheritedMemorySearch) ? inheritedMemorySearch : {};
const inheritedExperimental = isRecord(inherited.experimental) ? inherited.experimental : {};
const enabled = readBoolean(memorySearch.enabled) ?? readBoolean(inherited.enabled) ?? true;
const sessionMemory =
readBoolean(experimental.sessionMemory) ?? readBoolean(inheritedExperimental.sessionMemory);
const sourcesIncludeSessions =
memorySearchSourcesIncludeSessions(memorySearch) ??
memorySearchSourcesIncludeSessions(inherited) ??
false;
if (
sessionMemory === undefined &&
memorySearchSourcesIncludeSessions(memorySearch) === undefined &&
readBoolean(memorySearch.enabled) === undefined
) {
return undefined;
}
if (!enabled) {
return false;
}
return sessionMemory === true && sourcesIncludeSessions;
}
function memorySearchSessionTranscriptIndexingHasLocalConfig(memorySearch: unknown): boolean {
if (!isRecord(memorySearch)) {
return false;
}
const experimental = isRecord(memorySearch.experimental) ? memorySearch.experimental : {};
return (
readBoolean(memorySearch.enabled) !== undefined ||
readBoolean(experimental.sessionMemory) !== undefined ||
memorySearchSourcesIncludeSessions(memorySearch) !== undefined
);
}
function memorySearchSourcesIncludeSessions(memorySearch: unknown): boolean | undefined {
if (!isRecord(memorySearch) || memorySearch.sources === undefined) {
return undefined;
}
if (!Array.isArray(memorySearch.sources)) {
return false;
}
return memorySearch.sources.includes("sessions");
}
function scanPolicySecretProviders(cfg: Record<string, unknown>): readonly PolicySecretEvidence[] {
const secrets = isRecord(cfg.secrets) ? cfg.secrets : {};
const providers = isRecord(secrets.providers) ? secrets.providers : {};
return Object.entries(providers).map(([id, value]) => {
const insecure = secretProviderInsecureFlags(value);
const entry: {
id: string;
kind: "provider";
source: string;
providerSource?: string;
insecure?: readonly string[];
} = {
id,
kind: "provider",
source: `oc://openclaw.config/secrets/providers/${ocPathSegment(id)}`,
};
if (isRecord(value) && typeof value.source === "string") {
entry.providerSource = value.source;
}
if (insecure.length > 0) {
entry.insecure = insecure;
}
return entry;
});
}
function scanPolicySecretInputs(cfg: Record<string, unknown>): readonly PolicySecretEvidence[] {
const entries: PolicySecretEvidence[] = [];
const secrets = isRecord(cfg.secrets) ? cfg.secrets : {};
collectSecretInputs(entries, cfg, [], secretRefDefaults(secrets.defaults));
return entries;
}
function collectSecretInputs(
entries: PolicySecretEvidence[],
value: unknown,
path: readonly string[],
defaults: SecretRefDefaults | undefined,
): void {
if (Array.isArray(value)) {
value.forEach((item, index) =>
collectSecretInputs(entries, item, [...path, `#${index}`], defaults),
);
return;
}
if (!isRecord(value)) {
return;
}
for (const [key, child] of Object.entries(value)) {
const childPath = [...path, key];
const source = configPathSource(childPath);
const secretInputPath = isSecretInputPath(childPath);
const ref = secretInputPath ? secretRefEvidence(child, defaults) : undefined;
if (ref !== undefined) {
entries.push({
id: source,
kind: "input",
source,
provenance: "secretRef",
refSource: ref.source,
refProvider: ref.provider,
});
continue;
}
collectSecretInputs(entries, child, childPath, defaults);
}
}
function configPathSource(path: readonly string[]): string {
return `oc://openclaw.config/${path.map(ocPathSegment).join("/")}`;
}
function isSecretInputPath(path: readonly string[]): boolean {
const key = path.at(-1);
if (key === undefined) {
return false;
}
if (
matchesConfigPath(path, ["plugins", "entries", "acpx", "config", "mcpServers", "*", "env", "*"])
) {
return true;
}
if (isRawEnvMapValuePath(path)) {
return false;
}
if (isSecretInputKey(key)) {
return true;
}
return (
matchesConfigPath(path, ["models", "providers", "*", "headers", "*"]) ||
isConfiguredProviderRequestSecretPath(path, ["models", "providers", "*"]) ||
isMediaConfiguredProviderRequestSecretPath(path) ||
matchesConfigPath(path, ["agents", "defaults", "memorySearch", "remote", "headers", "*"]) ||
matchesConfigPath(path, ["diagnostics", "otel", "headers", "*"])
);
}
function isRawEnvMapValuePath(path: readonly string[]): boolean {
return path.length >= 2 && path.at(-2) === "env";
}
function isMediaConfiguredProviderRequestSecretPath(path: readonly string[]): boolean {
return (
isConfiguredProviderRequestSecretPath(path, ["tools", "media", "models", "#"]) ||
isConfiguredProviderRequestSecretPath(path, ["tools", "media", "audio"]) ||
isConfiguredProviderRequestSecretPath(path, ["tools", "media", "audio", "models", "#"]) ||
isConfiguredProviderRequestSecretPath(path, ["tools", "media", "image"]) ||
isConfiguredProviderRequestSecretPath(path, ["tools", "media", "image", "models", "#"]) ||
isConfiguredProviderRequestSecretPath(path, ["tools", "media", "video"]) ||
isConfiguredProviderRequestSecretPath(path, ["tools", "media", "video", "models", "#"])
);
}
function isConfiguredProviderRequestSecretPath(
path: readonly string[],
prefix: readonly string[],
): boolean {
if (path.length < prefix.length + 3) {
return false;
}
if (!matchesConfigPathPrefix(path, prefix)) {
return false;
}
const requestIndex = prefix.length;
if (path[requestIndex] !== "request") {
return false;
}
const suffix = path.slice(requestIndex + 1);
if (suffix.length === 2 && suffix[0] === "headers") {
return true;
}
if (suffix.length === 2 && suffix[0] === "auth" && isConfiguredProviderAuthSecretKey(suffix[1])) {
return true;
}
if (suffix.length === 2 && suffix[0] === "tls" && isConfiguredProviderTlsSecretKey(suffix[1])) {
return true;
}
return (
suffix.length === 3 &&
suffix[0] === "proxy" &&
suffix[1] === "tls" &&
isConfiguredProviderTlsSecretKey(suffix[2])
);
}
function matchesConfigPathPrefix(path: readonly string[], prefix: readonly string[]): boolean {
if (path.length < prefix.length) {
return false;
}
return prefix.every((segment, index) => {
const value = path[index];
if (segment === "*") {
return value !== undefined && value !== "";
}
if (segment === "#") {
return value?.startsWith("#") ?? false;
}
return value === segment;
});
}
function matchesConfigPath(path: readonly string[], pattern: readonly string[]): boolean {
return path.length === pattern.length && matchesConfigPathPrefix(path, pattern);
}
function isConfiguredProviderTlsSecretKey(key: string | undefined): boolean {
return key === "ca" || key === "cert" || key === "key" || key === "passphrase";
}
function isConfiguredProviderAuthSecretKey(key: string | undefined): boolean {
return key === "token" || key === "value";
}
function isSecretInputKey(key: string): boolean {
const normalized = key.toLowerCase();
return (
normalized === "apikey" ||
normalized === "keyref" ||
normalized === "token" ||
normalized === "tokenref" ||
normalized === "password" ||
normalized === "secret" ||
normalized === "encryptkey" ||
normalized === "webhooksecret" ||
normalized === "serviceaccount" ||
normalized === "serviceaccountref" ||
normalized === "privatekey" ||
normalized === "certificate" ||
normalized === "certificatedata" ||
normalized === "identitydata" ||
normalized === "knownhosts" ||
normalized === "knownhostsdata" ||
normalized.endsWith("apikey") ||
normalized.endsWith("token") ||
normalized.endsWith("secret") ||
normalized.endsWith("password")
);
}
function secretRefDefaults(value: unknown): SecretRefDefaults | undefined {
if (!isRecord(value)) {
return undefined;
}
const defaults: SecretRefDefaults = {};
if (typeof value.env === "string") {
defaults.env = value.env;
}
if (typeof value.file === "string") {
defaults.file = value.file;
}
if (typeof value.exec === "string") {
defaults.exec = value.exec;
}
return defaults;
}
function secretRefEvidence(
value: unknown,
defaults: SecretRefDefaults | undefined,
): SecretRefEvidence | undefined {
const ref = coerceSecretRef(value, defaults);
return ref === null ? undefined : { source: ref.source, provider: ref.provider, id: ref.id };
}
function secretProviderInsecureFlags(value: unknown): readonly string[] {
if (!isRecord(value)) {
return [];
}
return [
...(value.allowInsecurePath === true ? ["allowInsecurePath"] : []),
...(value.allowSymlinkCommand === true ? ["allowSymlinkCommand"] : []),
];
}
function isValidAuthProfileMetadata(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
return (
typeof value.provider === "string" &&
value.provider.trim() !== "" &&
isAuthProfileMode(value.mode)
);
}
function isAuthProfileMode(value: unknown): boolean {
return value === "api_key" || value === "aws-sdk" || value === "oauth" || value === "token";
}
@@ -0,0 +1,282 @@
// Policy plugin exec approval evidence.
import {
isRecord,
asBoolean as readBoolean,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { ocPathSegment } from "./policy-state-helpers.js";
import type { PolicyExecApprovalEvidence } from "./policy-state-types.js";
const DEFAULT_EXEC_APPROVAL_AGENT_ID = "main";
export function scanPolicyExecApprovals(raw: string): readonly PolicyExecApprovalEvidence[] {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return [];
}
if (!isRecord(parsed) || parsed.version !== 1) {
return [];
}
const evidence: PolicyExecApprovalEvidence[] = [];
const defaults = isRecord(parsed.defaults) ? parsed.defaults : {};
evidence.push(
execApprovalPostureEvidence(
"defaults",
"defaults",
defaults,
"oc://exec-approvals.json/defaults",
),
);
for (const agent of normalizedExecApprovalAgents(parsed.agents)) {
const agentSource = `oc://exec-approvals.json/agents/${ocPathSegment(agent.sourceAgentId)}`;
evidence.push(
execApprovalPostureEvidence(
`agent:${agent.agentId}`,
"agent",
agent.value,
agentSource,
agent.agentId,
),
);
for (const [index, entry] of agent.allowlistEntries.entries()) {
const allowlistSource = `oc://exec-approvals.json/agents/${ocPathSegment(
entry.sourceAgentId,
)}/allowlist/#${entry.index}`;
evidence.push({
id: `agent:${agent.agentId}:allowlist:${index}`,
kind: "allowlist",
source: allowlistSource,
agentId: agent.agentId,
pattern: entry.pattern,
...(entry.argPattern === undefined ? {} : { argPattern: entry.argPattern }),
...(entry.entrySource === undefined ? {} : { entrySource: entry.entrySource }),
});
}
}
return evidence;
}
function execApprovalPostureEvidence(
id: string,
kind: "agent" | "defaults",
value: Record<string, unknown>,
source: string,
agentId?: string,
): PolicyExecApprovalEvidence {
const security = readExecApprovalSecurity(value.security);
const ask = readExecApprovalAsk(value.ask);
const askFallback = readExecApprovalSecurity(value.askFallback);
const autoAllowSkills = readBoolean(value.autoAllowSkills);
return {
id,
kind,
source,
...(agentId === undefined ? {} : { agentId }),
...(value.security == null ? {} : { securityConfigured: true }),
...(security === undefined ? {} : { security }),
...(ask === undefined ? {} : { ask }),
...(askFallback === undefined ? {} : { askFallback }),
...(autoAllowSkills === undefined ? {} : { autoAllowSkills }),
};
}
function readExecApprovalSecurity(value: unknown): string | undefined {
const normalized = readString(value);
return normalized === "deny" || normalized === "allowlist" || normalized === "full"
? normalized
: undefined;
}
function readExecApprovalAsk(value: unknown): string | undefined {
const normalized = readString(value);
return normalized === "off" || normalized === "on-miss" || normalized === "always"
? normalized
: undefined;
}
type NormalizedExecApprovalAllowlistEntry = ReturnType<
typeof execApprovalAllowlistEntries
>[number] & {
readonly sourceAgentId: string;
};
type NormalizedExecApprovalAgent = {
readonly agentId: string;
readonly sourceAgentId: string;
readonly value: Record<string, unknown>;
readonly allowlistEntries: readonly NormalizedExecApprovalAllowlistEntry[];
};
function normalizedExecApprovalAgents(rawAgents: unknown): readonly NormalizedExecApprovalAgent[] {
if (!isRecord(rawAgents)) {
return [];
}
const agents = Object.entries(rawAgents).filter(
(entry): entry is [string, Record<string, unknown>] => isRecord(entry[1]),
);
const legacyDefault = agents.find(([agentId]) => agentId === "default")?.[1];
const normalized = agents
.filter(([agentId]) => agentId !== "default")
.map(([agentId, value]): NormalizedExecApprovalAgent => {
if (agentId === DEFAULT_EXEC_APPROVAL_AGENT_ID && legacyDefault !== undefined) {
return {
agentId,
sourceAgentId: agentId,
value: mergeLegacyExecApprovalAgent(value, legacyDefault),
allowlistEntries: mergedExecApprovalAllowlistEntries(
value.allowlist,
legacyDefault.allowlist,
),
};
}
return execApprovalAgentFromParts(agentId, agentId, value);
});
if (
legacyDefault !== undefined &&
!agents.some(([agentId]) => agentId === DEFAULT_EXEC_APPROVAL_AGENT_ID)
) {
normalized.push(
execApprovalAgentFromParts(DEFAULT_EXEC_APPROVAL_AGENT_ID, "default", legacyDefault),
);
}
return normalized.toSorted((a, b) => a.agentId.localeCompare(b.agentId));
}
function execApprovalAgentFromParts(
agentId: string,
sourceAgentId: string,
value: Record<string, unknown>,
): NormalizedExecApprovalAgent {
const allowlistEntries = execApprovalAllowlistEntries(value.allowlist).map(
(entry): NormalizedExecApprovalAllowlistEntry => ({
index: entry.index,
pattern: entry.pattern,
argPattern: entry.argPattern,
entrySource: entry.entrySource,
sourceAgentId,
}),
);
return {
agentId,
sourceAgentId,
value,
allowlistEntries,
};
}
function mergeLegacyExecApprovalAgent(
current: Record<string, unknown>,
legacy: Record<string, unknown>,
): Record<string, unknown> {
return {
...legacy,
...current,
security: current.security ?? legacy.security,
ask: current.ask ?? legacy.ask,
askFallback: current.askFallback ?? legacy.askFallback,
autoAllowSkills: current.autoAllowSkills ?? legacy.autoAllowSkills,
allowlist: mergedExecApprovalAllowlist(current.allowlist, legacy.allowlist),
};
}
function mergedExecApprovalAllowlist(
current: unknown,
legacy: unknown,
): readonly unknown[] | undefined {
const entries = mergedExecApprovalAllowlistEntries(current, legacy).map((entry) => {
const allowlistEntry: Record<string, unknown> = { pattern: entry.pattern };
if (entry.argPattern !== undefined) {
allowlistEntry.argPattern = entry.argPattern;
}
if (entry.entrySource !== undefined) {
allowlistEntry.source = entry.entrySource;
}
return allowlistEntry;
});
return entries.length === 0 ? undefined : entries;
}
function mergedExecApprovalAllowlistEntries(
current: unknown,
legacy: unknown,
): readonly NormalizedExecApprovalAllowlistEntry[] {
const entries: NormalizedExecApprovalAllowlistEntry[] = [];
const seen = new Set<string>();
const appendEntries = (sourceEntries: readonly NormalizedExecApprovalAllowlistEntry[]) => {
for (const sourceEntry of sourceEntries) {
const key = `${sourceEntry.pattern.toLowerCase()}\x00${sourceEntry.argPattern ?? ""}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
entries.push(sourceEntry);
}
};
appendEntries(withExecApprovalAllowlistSource(current, DEFAULT_EXEC_APPROVAL_AGENT_ID));
appendEntries(withExecApprovalAllowlistSource(legacy, "default"));
return entries;
}
function withExecApprovalAllowlistSource(
value: unknown,
sourceAgentId: string,
): readonly NormalizedExecApprovalAllowlistEntry[] {
return execApprovalAllowlistEntries(value).map(
(entry): NormalizedExecApprovalAllowlistEntry => ({
index: entry.index,
pattern: entry.pattern,
argPattern: entry.argPattern,
entrySource: entry.entrySource,
sourceAgentId,
}),
);
}
function readExecApprovalAllowlistEntrySource(value: unknown): "allow-always" | undefined {
return readString(value) === "allow-always" ? "allow-always" : undefined;
}
function execApprovalAllowlistEntries(value: unknown): readonly {
readonly index: number;
readonly pattern: string;
readonly argPattern?: string;
readonly entrySource?: string;
}[] {
if (!Array.isArray(value)) {
return [];
}
const entries: {
readonly index: number;
readonly pattern: string;
readonly argPattern?: string;
readonly entrySource?: string;
}[] = [];
for (const [index, entry] of value.entries()) {
if (typeof entry === "string") {
const pattern = entry.trim();
if (pattern !== "") {
entries.push({ index, pattern });
}
continue;
}
if (!isRecord(entry)) {
continue;
}
const pattern = readString(entry.pattern);
if (pattern === undefined) {
continue;
}
const argPattern = readString(entry.argPattern);
const entrySource = readExecApprovalAllowlistEntrySource(entry.source);
entries.push({
index,
pattern,
...(argPattern === undefined ? {} : { argPattern }),
...(entrySource === undefined ? {} : { entrySource }),
});
}
return entries;
}
@@ -0,0 +1,271 @@
// Policy plugin gateway exposure evidence.
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { ocPathSegment } from "./policy-state-helpers.js";
import type { PolicyGatewayExposureEvidence } from "./policy-state-types.js";
export function scanPolicyGatewayExposure(
cfg: Record<string, unknown>,
): readonly PolicyGatewayExposureEvidence[] {
const gateway = isRecord(cfg.gateway) ? cfg.gateway : {};
const entries: PolicyGatewayExposureEvidence[] = [];
const bind = typeof gateway.bind === "string" ? gateway.bind : undefined;
const customBindHost =
typeof gateway.customBindHost === "string" ? gateway.customBindHost : undefined;
const hasCustomBindHost = customBindHost !== undefined && customBindHost.trim() !== "";
const tailscale = isRecord(gateway.tailscale) ? gateway.tailscale : {};
const tailscaleForcesLoopback = tailscale.mode === "serve" || tailscale.mode === "funnel";
entries.push({
id: bind === undefined ? "gateway-bind-default" : "gateway-bind",
kind: "bind",
source: "oc://openclaw.config/gateway/bind",
value: bind ?? (tailscaleForcesLoopback ? "loopback" : "runtime-default"),
nonLoopback:
bind === undefined
? !tailscaleForcesLoopback
: bind === "custom"
? false
: isGatewayNonLoopbackBind(bind),
explicit: bind !== undefined,
});
if (bind === "custom" && hasCustomBindHost) {
entries.push({
id: "gateway-custom-bind-host",
kind: "bind",
source: "oc://openclaw.config/gateway/customBindHost",
value: customBindHost,
nonLoopback: isRuntimeNonLoopbackCustomBindHost(customBindHost),
});
}
const auth = isRecord(gateway.auth) ? gateway.auth : {};
entries.push({
id: "gateway-auth-mode",
kind: "auth",
source: "oc://openclaw.config/gateway/auth/mode",
value: typeof auth.mode === "string" ? auth.mode : "token",
explicit: typeof auth.mode === "string",
});
entries.push({
id: "gateway-auth-rate-limit",
kind: "authRateLimit",
source: "oc://openclaw.config/gateway/auth/rateLimit",
value: isRecord(auth.rateLimit),
explicit: isRecord(auth.rateLimit),
});
const controlUi = isRecord(gateway.controlUi) ? gateway.controlUi : {};
pushGatewayBooleanEvidence(
entries,
"gateway-control-ui-enabled",
"controlUi",
controlUi.enabled,
"oc://openclaw.config/gateway/controlUi/enabled",
);
pushGatewayBooleanEvidence(
entries,
"gateway-control-ui-insecure-auth",
"controlUi",
controlUi.allowInsecureAuth,
"oc://openclaw.config/gateway/controlUi/allowInsecureAuth",
);
pushGatewayBooleanEvidence(
entries,
"gateway-control-ui-device-auth-disabled",
"controlUi",
controlUi.dangerouslyDisableDeviceAuth,
"oc://openclaw.config/gateway/controlUi/dangerouslyDisableDeviceAuth",
);
pushGatewayBooleanEvidence(
entries,
"gateway-control-ui-host-origin-fallback",
"controlUi",
controlUi.dangerouslyAllowHostHeaderOriginFallback,
"oc://openclaw.config/gateway/controlUi/dangerouslyAllowHostHeaderOriginFallback",
);
if (typeof tailscale.mode === "string") {
entries.push({
id: "gateway-tailscale-mode",
kind: "tailscale",
source: "oc://openclaw.config/gateway/tailscale/mode",
value: tailscale.mode,
});
}
if (tailscale.mode === "serve" && tailscale.preserveFunnel === true) {
entries.push({
id: "gateway-tailscale-preserve-funnel",
kind: "tailscale",
source: "oc://openclaw.config/gateway/tailscale/preserveFunnel",
value: "funnel",
});
}
const remote = isRecord(gateway.remote) ? gateway.remote : {};
if (gateway.mode === "remote") {
entries.push({
id: "gateway-mode-remote",
kind: "remote",
source: "oc://openclaw.config/gateway/mode",
value: "remote",
});
if (typeof remote.url === "string" && remote.url.trim() !== "") {
entries.push({
id: "gateway-remote-url",
kind: "remote",
source: "oc://openclaw.config/gateway/remote/url",
value: true,
});
}
}
const http = isRecord(gateway.http) ? gateway.http : {};
const endpoints = isRecord(http.endpoints) ? http.endpoints : {};
pushGatewayHttpEndpointEvidence(entries, endpoints, "chatCompletions");
pushGatewayHttpEndpointEvidence(entries, endpoints, "responses");
const nodes = isRecord(gateway.nodes) ? gateway.nodes : {};
pushGatewayNodeCommandEvidence(entries, nodes);
return entries.toSorted((a, b) => a.source.localeCompare(b.source));
}
function pushGatewayBooleanEvidence(
entries: PolicyGatewayExposureEvidence[],
id: string,
kind: PolicyGatewayExposureEvidence["kind"],
value: unknown,
source: string,
): void {
if (typeof value !== "boolean") {
return;
}
entries.push({ id, kind, source, value });
}
function pushGatewayHttpEndpointEvidence(
entries: PolicyGatewayExposureEvidence[],
endpoints: Record<string, unknown>,
endpoint: "chatCompletions" | "responses",
): void {
const config = endpoints[endpoint];
if (!isRecord(config)) {
return;
}
const source = `oc://openclaw.config/gateway/http/endpoints/${endpoint}`;
const enabled = config.enabled === true;
if (enabled) {
entries.push({
id: `gateway-http-${endpoint}`,
kind: "httpEndpoint",
source: `${source}/enabled`,
value: true,
endpoint,
});
}
if (!enabled) {
return;
}
if (endpoint === "chatCompletions") {
pushGatewayHttpUrlFetchEvidence(entries, source, endpoint, ["images"], config.images);
return;
}
pushGatewayHttpUrlFetchEvidence(entries, source, endpoint, ["files"], config.files);
pushGatewayHttpUrlFetchEvidence(entries, source, endpoint, ["images"], config.images);
}
function pushGatewayHttpUrlFetchEvidence(
entries: PolicyGatewayExposureEvidence[],
endpointSource: string,
endpoint: string,
path: readonly string[],
value: unknown,
): void {
const allowUrl = isRecord(value) ? value.allowUrl : undefined;
if (allowUrl === false || (allowUrl !== true && endpoint !== "responses")) {
return;
}
const allowlist = isRecord(value) ? value.urlAllowlist : undefined;
const hasEffectiveAllowlist =
Array.isArray(allowlist) &&
allowlist.some((entry) => isEffectiveGatewayUrlAllowlistEntry(entry));
entries.push({
id: `gateway-http-${endpoint}-${path.join("-")}-url-fetch`,
kind: "httpUrlFetch",
source: `${endpointSource}/${path.map(ocPathSegment).join("/")}/allowUrl`,
value: true,
endpoint,
explicit: allowUrl === true,
hasAllowlist: hasEffectiveAllowlist,
});
}
function pushGatewayNodeCommandEvidence(
entries: PolicyGatewayExposureEvidence[],
nodes: Record<string, unknown>,
): void {
const deniedCommands = new Set(
Array.isArray(nodes.denyCommands)
? nodes.denyCommands
.filter((command): command is string => typeof command === "string")
.map((command) => command.trim())
: [],
);
if (Array.isArray(nodes.denyCommands)) {
nodes.denyCommands.forEach((command, index) => {
if (typeof command !== "string") {
return;
}
const normalized = command.trim();
if (normalized === "") {
return;
}
entries.push({
id: `gateway-node-deny-command-${normalized}`,
kind: "nodeDenyCommand",
source: `oc://openclaw.config/gateway/nodes/denyCommands/#${index}`,
value: normalized,
command: normalized,
});
});
}
if (!Array.isArray(nodes.allowCommands)) {
return;
}
nodes.allowCommands.forEach((command, index) => {
if (typeof command !== "string") {
return;
}
const normalized = command.trim();
if (normalized === "" || deniedCommands.has(normalized)) {
return;
}
entries.push({
id: `gateway-node-command-${normalized}`,
kind: "nodeCommand",
source: `oc://openclaw.config/gateway/nodes/allowCommands/#${index}`,
value: normalized,
command: normalized,
});
});
}
function isEffectiveGatewayUrlAllowlistEntry(value: unknown): boolean {
if (typeof value !== "string") {
return false;
}
const normalized = value.trim().toLowerCase();
return normalized !== "" && normalized !== "*" && normalized !== "*.";
}
function isGatewayNonLoopbackBind(value: string): boolean {
return value === "auto" || value === "lan" || value === "custom" || value === "tailnet";
}
function isRuntimeNonLoopbackCustomBindHost(value: string): boolean {
const normalized = value.trim().toLowerCase();
return isCanonicalDottedDecimalIPv4(normalized) && !normalized.startsWith("127.");
}
function isCanonicalDottedDecimalIPv4(value: string): boolean {
return /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/.test(
value,
);
}
@@ -0,0 +1,23 @@
// Shared policy evidence path and value helpers.
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
export function ocPathSegment(value: string): string {
if (/^(?:[A-Za-z0-9_-]+|#\d+)$/.test(value)) {
return value;
}
if (value.includes('"') || value.includes("\\")) {
return value;
}
return `"${value}"`;
}
export function readBooleanPath(value: unknown, path: readonly string[]): boolean | undefined {
let current = value;
for (const part of path) {
if (!isRecord(current)) {
return undefined;
}
current = current[part];
}
return typeof current === "boolean" ? current : undefined;
}
@@ -0,0 +1,428 @@
// Policy plugin ingress evidence.
import {
isRecord,
asBoolean as readBoolean,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { configuredChannels } from "./policy-state-core.js";
import { ocPathSegment } from "./policy-state-helpers.js";
import { IMPLICIT_DEFAULT_ACCOUNT_FIELDS } from "./policy-state-tool-posture.js";
import { RESERVED_CHANNEL_CONFIG_KEYS } from "./policy-state-types.js";
import type { PolicyIngressEvidence } from "./policy-state-types.js";
const ALLOWLIST_DEFAULT_INGRESS_GROUP_POLICY_CHANNELS = new Set([
"googlechat",
"irc",
"line",
"mattermost",
"matrix",
"msteams",
"nextcloud-talk",
"signal",
]);
const OPEN_GROUPS_DEFAULT_TO_NO_MENTION_CHANNELS = new Set(["feishu", "qa-channel"]);
export function scanPolicyIngress(cfg: Record<string, unknown>): readonly PolicyIngressEvidence[] {
const channels = configuredChannels(cfg);
const channelDefaults = isRecord(channels.defaults) ? channels.defaults : {};
const inheritedChannelDefaults = pickSupportedIngressDefaults(channelDefaults);
const channelDefaultsSource = "oc://openclaw.config/channels/defaults";
const entries: PolicyIngressEvidence[] = [];
const session = isRecord(cfg.session) ? cfg.session : {};
const dmScope = readString(session.dmScope)?.toLowerCase();
entries.push({
id: "session-dm-scope",
kind: "sessionDmScope",
source: "oc://openclaw.config/session/dmScope",
value: dmScope ?? "main",
explicit: dmScope !== undefined,
});
for (const [channel, value] of Object.entries(channels)) {
if (RESERVED_CHANNEL_CONFIG_KEYS.has(channel) || !isRecord(value) || value.enabled === false) {
continue;
}
const channelSource = `oc://openclaw.config/channels/${ocPathSegment(channel)}`;
const accounts = isRecord(value.accounts) ? value.accounts : {};
const configuredAccounts = Object.entries(accounts).filter(
(entry): entry is [string, Record<string, unknown>] => isRecord(entry[1]),
);
const activeAccounts = configuredAccounts.filter(([, account]) => account.enabled !== false);
if (configuredAccounts.length === 0 || hasImplicitDefaultAccountConfig(channel, value)) {
pushChannelIngress(entries, {
channel,
config: value,
inheritedConfig: inheritedChannelDefaults,
sourceBase: channelSource,
inheritedSourceBase: channelDefaultsSource,
fallbackSourceBase: channelSource,
});
}
for (const [accountId, account] of activeAccounts) {
const inheritsNestedContainers = channel !== "telegram" || configuredAccounts.length <= 1;
pushChannelIngress(entries, {
channel,
accountId,
config: account,
inheritedConfig: value,
inheritNestedContainers: inheritsNestedContainers,
sourceBase: `${channelSource}/accounts/${ocPathSegment(accountId)}`,
inheritedSourceBase: channelSource,
fallbackConfig: inheritedChannelDefaults,
fallbackSourceBase: channelDefaultsSource,
});
}
}
return entries.toSorted((a, b) => a.source.localeCompare(b.source) || a.id.localeCompare(b.id));
}
function pickSupportedIngressDefaults(config: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (config.groupPolicy !== undefined) {
result.groupPolicy = config.groupPolicy;
}
return result;
}
function hasImplicitDefaultAccountConfig(
channel: string,
config: Record<string, unknown>,
): boolean {
switch (channel) {
case "clickclack":
return (
hasConfiguredAccountValue(config.baseUrl) &&
hasConfiguredAccountValue(config.workspace) &&
hasConfiguredAccountValue(config.token)
);
case "feishu":
return hasConfiguredAccountValue(config.appId) && hasConfiguredAccountValue(config.appSecret);
case "irc":
return hasConfiguredAccountValue(config.host) && hasConfiguredAccountValue(config.nick);
case "line":
return (
hasConfiguredAccountValue(config.channelAccessToken) ||
hasConfiguredAccountValue(config.tokenFile)
);
case "matrix":
return (
hasConfiguredAccountValue(config.homeserver) &&
(hasConfiguredAccountValue(config.accessToken) ||
(hasConfiguredAccountValue(config.userId) && hasConfiguredAccountValue(config.password)))
);
case "mattermost":
return (
hasConfiguredAccountValue(config.baseUrl) && hasConfiguredAccountValue(config.botToken)
);
case "nextcloud-talk":
return (
hasConfiguredAccountValue(config.baseUrl) &&
(hasConfiguredAccountValue(config.botSecret) ||
hasConfiguredAccountValue(config.botSecretFile))
);
default:
return (IMPLICIT_DEFAULT_ACCOUNT_FIELDS[channel] ?? []).some((field) =>
hasConfiguredAccountValue(config[field]),
);
}
}
function hasConfiguredAccountValue(value: unknown): boolean {
return typeof value === "string"
? value.trim().length > 0
: value !== undefined && value !== null;
}
type ChannelIngressParams = {
readonly channel: string;
readonly accountId?: string;
readonly config: Record<string, unknown>;
readonly inheritedConfig: Record<string, unknown>;
readonly inheritNestedContainers?: boolean;
readonly sourceBase: string;
readonly inheritedSourceBase: string;
readonly fallbackConfig?: Record<string, unknown>;
readonly fallbackSourceBase: string;
};
function pushChannelIngress(entries: PolicyIngressEvidence[], params: ChannelIngressParams): void {
const localDmPolicy = channelDmPolicy(params.config);
const inheritedDmPolicy = channelDmPolicy(params.inheritedConfig);
const fallbackDmPolicy = channelDmPolicy(params.fallbackConfig ?? {});
const effectiveDmPolicy =
localDmPolicy.disabledByEnabled === true
? localDmPolicy
: localDmPolicy.value !== undefined
? localDmPolicy
: inheritedDmPolicy.disabledByEnabled === true
? inheritedDmPolicy
: inheritedDmPolicy.value !== undefined
? inheritedDmPolicy
: fallbackDmPolicy.disabledByEnabled === true || fallbackDmPolicy.value !== undefined
? fallbackDmPolicy
: undefined;
const dmPolicySource =
effectiveDmPolicy?.sourceSuffix === undefined
? `${params.fallbackSourceBase}/dmPolicy`
: effectiveDmPolicy === localDmPolicy
? `${params.sourceBase}/${effectiveDmPolicy.sourceSuffix}`
: effectiveDmPolicy === inheritedDmPolicy
? `${params.inheritedSourceBase}/${effectiveDmPolicy.sourceSuffix}`
: `${params.fallbackSourceBase}/${effectiveDmPolicy.sourceSuffix}`;
entries.push({
id: channelIngressId(params, "dm-policy"),
kind: "channelDmPolicy",
source: dmPolicySource,
channel: params.channel,
...(params.accountId === undefined ? {} : { accountId: params.accountId }),
value: effectiveDmPolicy?.value ?? "pairing",
explicit: effectiveDmPolicy !== undefined,
});
const localGroupPolicy = readString(params.config.groupPolicy);
const inheritedGroupPolicy = readString(params.inheritedConfig.groupPolicy);
const fallbackGroupPolicy = readString(params.fallbackConfig?.groupPolicy);
const implicitGroupPolicy = channelImplicitGroupPolicy(params);
entries.push({
id: channelIngressId(params, "group-policy"),
kind: "channelGroupPolicy",
source:
localGroupPolicy !== undefined
? `${params.sourceBase}/groupPolicy`
: inheritedGroupPolicy !== undefined
? `${params.inheritedSourceBase}/groupPolicy`
: fallbackGroupPolicy !== undefined
? `${params.fallbackSourceBase}/groupPolicy`
: implicitGroupPolicy.source,
channel: params.channel,
...(params.accountId === undefined ? {} : { accountId: params.accountId }),
value:
localGroupPolicy ?? inheritedGroupPolicy ?? fallbackGroupPolicy ?? implicitGroupPolicy.value,
explicit:
localGroupPolicy !== undefined ||
inheritedGroupPolicy !== undefined ||
fallbackGroupPolicy !== undefined,
});
pushChannelRequireMentionIngress(entries, params);
}
function channelImplicitGroupPolicy(params: ChannelIngressParams): {
readonly source: string;
readonly value: "allowlist" | "open";
} {
for (const [config, sourceBase] of [
[params.config, params.sourceBase],
...(params.inheritNestedContainers === true
? ([[params.inheritedConfig, params.inheritedSourceBase]] as const)
: []),
[params.fallbackConfig, params.fallbackSourceBase],
] as const) {
if (config === undefined || sourceBase === undefined) {
continue;
}
for (const key of ["groups"] as const) {
const container = isRecord(config[key]) ? config[key] : undefined;
if (container !== undefined && Object.keys(container).length > 0) {
return { source: `${sourceBase}/${key}`, value: "allowlist" };
}
}
}
return {
source: `${params.sourceBase}/groupPolicy`,
value: ALLOWLIST_DEFAULT_INGRESS_GROUP_POLICY_CHANNELS.has(params.channel)
? "allowlist"
: "open",
};
}
function pushChannelRequireMentionIngress(
entries: PolicyIngressEvidence[],
params: ChannelIngressParams,
): void {
const localRequireMention = readBoolean(params.config.requireMention);
const inheritedRequireMention = readBoolean(params.inheritedConfig.requireMention);
const fallbackRequireMention = readBoolean(params.fallbackConfig?.requireMention);
const wildcardRequireMention = channelWildcardRequireMention(params);
const defaultRequireMention = channelDefaultRequireMention(params);
entries.push({
id: channelIngressId(params, "require-mention"),
kind: "channelRequireMention",
source:
wildcardRequireMention !== undefined
? wildcardRequireMention.source
: localRequireMention !== undefined
? `${params.sourceBase}/requireMention`
: inheritedRequireMention !== undefined
? `${params.inheritedSourceBase}/requireMention`
: fallbackRequireMention !== undefined
? `${params.fallbackSourceBase}/requireMention`
: `${params.sourceBase}/requireMention`,
channel: params.channel,
...(params.accountId === undefined ? {} : { accountId: params.accountId }),
value:
wildcardRequireMention?.value ??
localRequireMention ??
inheritedRequireMention ??
fallbackRequireMention ??
defaultRequireMention,
explicit:
wildcardRequireMention !== undefined ||
localRequireMention !== undefined ||
inheritedRequireMention !== undefined ||
fallbackRequireMention !== undefined,
});
const containers = nestedIngressContainers(params);
for (const { containerKey, container, sourceBase } of containers) {
for (const [groupId, groupConfig] of Object.entries(container)) {
if (!isRecord(groupConfig)) {
continue;
}
pushNestedRequireMentionIngress(
entries,
params,
containerKey,
groupId,
groupConfig,
sourceBase,
);
}
}
}
function channelDefaultRequireMention(params: ChannelIngressParams): boolean {
const groupPolicy =
readString(params.config.groupPolicy) ??
readString(params.inheritedConfig.groupPolicy) ??
readString(params.fallbackConfig?.groupPolicy) ??
channelImplicitGroupPolicy(params).value;
return !(
groupPolicy === "open" && OPEN_GROUPS_DEFAULT_TO_NO_MENTION_CHANNELS.has(params.channel)
);
}
function channelWildcardRequireMention(
params: ChannelIngressParams,
): { readonly source: string; readonly value: boolean } | undefined {
for (const [config, sourceBase] of [
[params.config, params.sourceBase],
[params.inheritedConfig, params.inheritedSourceBase],
[params.fallbackConfig, params.fallbackSourceBase],
] as const) {
if (config === undefined || sourceBase === undefined) {
continue;
}
for (const key of ["groups", "guilds", "channels", "rooms", "teams"] as const) {
const container = isRecord(config[key]) ? config[key] : undefined;
const wildcard = isRecord(container?.["*"]) ? container["*"] : undefined;
const requireMention = readBoolean(wildcard?.requireMention);
if (wildcard?.enabled !== false && requireMention !== undefined) {
return {
source: `${sourceBase}/${key}/${ocPathSegment("*")}/requireMention`,
value: requireMention,
};
}
}
}
return undefined;
}
function nestedIngressContainers(params: ChannelIngressParams): readonly {
readonly containerKey: string;
readonly container: Record<string, unknown>;
readonly sourceBase: string;
}[] {
const containers: {
readonly containerKey: string;
readonly container: Record<string, unknown>;
readonly sourceBase: string;
}[] = [];
for (const key of ["groups", "guilds", "channels", "rooms", "teams"] as const) {
const local = isRecord(params.config[key]) ? params.config[key] : undefined;
const inherited = isRecord(params.inheritedConfig[key])
? params.inheritedConfig[key]
: undefined;
if (local !== undefined) {
if (Object.keys(local).length > 0) {
containers.push({ containerKey: key, container: local, sourceBase: params.sourceBase });
}
} else if (params.inheritNestedContainers === true && inherited !== undefined) {
containers.push({
containerKey: key,
container: inherited,
sourceBase: params.inheritedSourceBase,
});
}
}
return containers;
}
function pushNestedRequireMentionIngress(
entries: PolicyIngressEvidence[],
params: ChannelIngressParams,
containerKey: string,
groupId: string,
config: Record<string, unknown>,
parentSourceBase: string,
): void {
if (config.enabled === false) {
return;
}
const sourceBase = `${parentSourceBase}/${containerKey}/${ocPathSegment(groupId)}`;
const requireMention = readBoolean(config.requireMention);
if (requireMention !== undefined) {
entries.push({
id: `${channelIngressId(params, `${containerKey}-${ocPathSegment(groupId)}`)}-require-mention`,
kind: "channelRequireMention",
source: `${sourceBase}/requireMention`,
channel: params.channel,
...(params.accountId === undefined ? {} : { accountId: params.accountId }),
groupId,
value: requireMention ?? true,
explicit: requireMention !== undefined,
});
}
for (const nestedKey of ["channels", "topics"] as const) {
const nested = config[nestedKey];
if (!isRecord(nested)) {
continue;
}
for (const [nestedId, nestedConfig] of Object.entries(nested)) {
if (isRecord(nestedConfig)) {
pushNestedRequireMentionIngress(
entries,
params,
`${containerKey}/${ocPathSegment(groupId)}/${nestedKey}`,
nestedId,
nestedConfig,
parentSourceBase,
);
}
}
}
}
function channelDmPolicy(config: Record<string, unknown>): {
readonly value?: string;
readonly sourceSuffix?: string;
readonly disabledByEnabled?: boolean;
} {
const dm = isRecord(config.dm) ? config.dm : {};
if (dm.enabled === false) {
return { value: "disabled", sourceSuffix: "dm/enabled", disabledByEnabled: true };
}
const direct = readString(config.dmPolicy);
if (direct !== undefined) {
return { value: direct, sourceSuffix: "dmPolicy" };
}
const legacy = readString(dm.policy);
return legacy === undefined ? {} : { value: legacy, sourceSuffix: "dm/policy" };
}
function channelIngressId(params: ChannelIngressParams, suffix: string): string {
return params.accountId === undefined
? `${params.channel}-${suffix}`
: `${params.channel}-${params.accountId}-${suffix}`;
}
@@ -0,0 +1,336 @@
// Policy plugin sandbox posture evidence.
import {
isRecord,
asBoolean as readBoolean,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { readStringArray } from "./policy-state-tool-posture.js";
import type { PolicySandboxPostureEvidence } from "./policy-state-types.js";
// Mirrors the sandbox browser config default without importing core internals into the policy plugin.
const DEFAULT_POLICY_SANDBOX_BROWSER_NETWORK = "openclaw-sandbox-browser";
export function scanPolicySandboxPosture(
cfg: Record<string, unknown>,
): readonly PolicySandboxPostureEvidence[] {
const agents = isRecord(cfg.agents) ? cfg.agents : {};
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
const defaultSandbox = isRecord(defaults.sandbox) ? defaults.sandbox : {};
const entries: PolicySandboxPostureEvidence[] = [];
pushSandboxPostureEvidence(entries, {
id: "agents-defaults",
scope: "defaults",
sandbox: defaultSandbox,
inheritedSandbox: {},
sourceBase: "oc://openclaw.config/agents/defaults/sandbox",
inheritedSourceBase: "oc://openclaw.config/agents/defaults/sandbox",
});
const list = Array.isArray(agents.list) ? agents.list : [];
list.forEach((agent, index) => {
if (!isRecord(agent)) {
return;
}
const agentId =
typeof agent.id === "string" && agent.id.trim() !== "" ? agent.id.trim() : undefined;
const sandbox = isRecord(agent.sandbox) ? agent.sandbox : {};
pushSandboxPostureEvidence(entries, {
id: agentId ?? `agent-${index}`,
scope: "agent",
agentId,
sandbox,
inheritedSandbox: defaultSandbox,
sharedSandboxScope: sandboxScopeIsShared(sandbox, defaultSandbox),
sourceBase: `oc://openclaw.config/agents/list/#${index}/sandbox`,
inheritedSourceBase: "oc://openclaw.config/agents/defaults/sandbox",
});
});
return entries.toSorted((a, b) => a.source.localeCompare(b.source) || a.id.localeCompare(b.id));
}
type SandboxPostureParams = {
readonly id: string;
readonly scope: "defaults" | "agent";
readonly agentId?: string;
readonly effectiveBackend?: string;
readonly sandbox: Record<string, unknown>;
readonly inheritedSandbox: Record<string, unknown>;
readonly sharedSandboxScope?: boolean;
readonly sourceBase: string;
readonly inheritedSourceBase: string;
};
function pushSandboxPostureEvidence(
entries: PolicySandboxPostureEvidence[],
params: SandboxPostureParams,
): void {
const localMode = readString(params.sandbox.mode);
const inheritedMode = readString(params.inheritedSandbox.mode);
pushSandboxPostureValue(entries, params, {
suffix: "mode",
kind: "mode",
value: localMode ?? inheritedMode ?? "off",
explicit: localMode !== undefined || inheritedMode !== undefined,
inherited: localMode === undefined && inheritedMode !== undefined,
});
const localBackend = readString(params.sandbox.backend);
const inheritedBackend = readString(params.inheritedSandbox.backend);
const effectiveBackend = (localBackend ?? inheritedBackend ?? "docker").toLowerCase();
const effectiveParams = { ...params, effectiveBackend };
pushSandboxPostureValue(entries, params, {
suffix: "backend",
kind: "backend",
value: effectiveBackend,
explicit: localBackend !== undefined || inheritedBackend !== undefined,
inherited: localBackend === undefined && inheritedBackend !== undefined,
});
if (effectiveBackend === "docker") {
pushSandboxDockerPosture(entries, effectiveParams);
}
pushSandboxBrowserPosture(entries, effectiveParams);
}
function pushSandboxDockerPosture(
entries: PolicySandboxPostureEvidence[],
params: SandboxPostureParams,
): void {
const localDocker =
!params.sharedSandboxScope && isRecord(params.sandbox.docker) ? params.sandbox.docker : {};
const inheritedDocker = isRecord(params.inheritedSandbox.docker)
? params.inheritedSandbox.docker
: {};
const localNetwork = readString(localDocker.network);
const inheritedNetwork = readString(inheritedDocker.network);
pushSandboxPostureValue(entries, params, {
suffix: "docker/network",
kind: "containerNetwork",
value: localNetwork ?? inheritedNetwork ?? "none",
networkSurface: "docker",
explicit: localNetwork !== undefined || inheritedNetwork !== undefined,
inherited: localNetwork === undefined && inheritedNetwork !== undefined,
});
pushSandboxDockerProfilePosture(entries, params, localDocker, inheritedDocker, "seccomp");
pushSandboxDockerProfilePosture(entries, params, localDocker, inheritedDocker, "apparmor");
pushSandboxBindPosture(entries, params, {
inheritedBinds: readStringArray(inheritedDocker.binds),
localBinds: readStringArray(localDocker.binds),
sourceSuffix: "docker/binds",
surface: "docker",
});
}
function pushSandboxBindPosture(
entries: PolicySandboxPostureEvidence[],
params: SandboxPostureParams,
bindParams: {
readonly inheritedBinds: readonly string[];
readonly localBinds: readonly string[];
readonly sourceSuffix: string;
readonly surface: "browser" | "docker";
},
): void {
const { inheritedBinds, localBinds } = bindParams;
for (const [index, bind] of [...inheritedBinds, ...localBinds].entries()) {
const inherited = index < inheritedBinds.length;
const parsed = splitPolicyBindSpec(bind);
entries.push({
id: `${params.id}-${bindParams.surface}-bind-${index}`,
kind: "containerMount",
source: `${inherited ? params.inheritedSourceBase : params.sourceBase}/${bindParams.sourceSuffix}/#${
inherited ? index : index - inheritedBinds.length
}`,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
bind,
bindHost: parsed?.host,
bindMode: parsed?.mode ?? "rw",
bindSurface: bindParams.surface,
explicit: true,
});
}
}
function pushSandboxDockerProfilePosture(
entries: PolicySandboxPostureEvidence[],
params: SandboxPostureParams,
localDocker: Record<string, unknown>,
inheritedDocker: Record<string, unknown>,
profile: "apparmor" | "seccomp",
): void {
const key = profile === "apparmor" ? "apparmorProfile" : "seccompProfile";
const localValue = readString(localDocker[key]);
const inheritedValue = readString(inheritedDocker[key]);
const inherited = localValue === undefined && inheritedValue !== undefined;
const value = localValue ?? inheritedValue;
entries.push({
id: `${params.id}-docker-${profile}-profile`,
kind: "containerSecurityProfile",
source: `${inherited ? params.inheritedSourceBase : params.sourceBase}/docker/${key}`,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
profile,
...(value === undefined ? {} : { value }),
explicit: value !== undefined,
});
}
function pushSandboxBrowserPosture(
entries: PolicySandboxPostureEvidence[],
params: SandboxPostureParams,
): void {
const localBrowser =
!params.sharedSandboxScope && isRecord(params.sandbox.browser) ? params.sandbox.browser : {};
const inheritedBrowser = isRecord(params.inheritedSandbox.browser)
? params.inheritedSandbox.browser
: {};
const localEnabled = readBoolean(localBrowser.enabled);
const inheritedEnabled = readBoolean(inheritedBrowser.enabled);
const enabled = localEnabled ?? inheritedEnabled ?? false;
if (!enabled) {
const disabledInherited = localEnabled === undefined && inheritedEnabled !== undefined;
if (localEnabled !== undefined || inheritedEnabled !== undefined) {
entries.push({
id: `${params.id}-browser-cdp-source-range`,
kind: "browserCdpSourceRange",
source: `${disabledInherited ? params.inheritedSourceBase : params.sourceBase}/browser/enabled`,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
value: false,
explicit: true,
});
}
return;
}
const hasLocalRange = Object.hasOwn(localBrowser, "cdpSourceRange");
const localRange = readString(localBrowser.cdpSourceRange);
const inheritedRange = readString(inheritedBrowser.cdpSourceRange);
const inherited = !hasLocalRange && inheritedRange !== undefined;
const value = hasLocalRange ? localRange : inheritedRange;
entries.push({
id: `${params.id}-browser-cdp-source-range`,
kind: "browserCdpSourceRange",
source: `${inherited ? params.inheritedSourceBase : params.sourceBase}/browser/cdpSourceRange`,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
...(value === undefined ? {} : { value }),
explicit: value !== undefined,
});
const localNetwork = readString(localBrowser.network);
const inheritedNetwork = readString(inheritedBrowser.network);
pushSandboxPostureValue(entries, params, {
suffix: "browser/network",
kind: "containerNetwork",
value: localNetwork ?? inheritedNetwork ?? DEFAULT_POLICY_SANDBOX_BROWSER_NETWORK,
networkSurface: "browser",
explicit: localNetwork !== undefined || inheritedNetwork !== undefined,
inherited: localNetwork === undefined && inheritedNetwork !== undefined,
});
const browserBindsConfigured =
inheritedBrowser.binds !== undefined || localBrowser.binds !== undefined;
if (browserBindsConfigured) {
pushSandboxBindPosture(entries, params, {
inheritedBinds: readStringArray(inheritedBrowser.binds),
localBinds: readStringArray(localBrowser.binds),
sourceSuffix: "browser/binds",
surface: "browser",
});
} else if (params.effectiveBackend !== "docker") {
const localDocker =
!params.sharedSandboxScope && isRecord(params.sandbox.docker) ? params.sandbox.docker : {};
const inheritedDocker = isRecord(params.inheritedSandbox.docker)
? params.inheritedSandbox.docker
: {};
pushSandboxBindPosture(entries, params, {
inheritedBinds: readStringArray(inheritedDocker.binds),
localBinds: readStringArray(localDocker.binds),
sourceSuffix: "docker/binds",
surface: "browser",
});
}
}
function sandboxScopeIsShared(
sandbox: Record<string, unknown>,
inheritedSandbox: Record<string, unknown>,
): boolean {
const localScope = readString(sandbox.scope);
const inheritedScope = readString(inheritedSandbox.scope);
const configuredScope = localScope ?? inheritedScope;
if (configuredScope !== undefined) {
return configuredScope === "shared";
}
const localPerSession = readBoolean(sandbox.perSession);
const inheritedPerSession = readBoolean(inheritedSandbox.perSession);
return (localPerSession ?? inheritedPerSession) === false;
}
function pushSandboxPostureValue(
entries: PolicySandboxPostureEvidence[],
params: SandboxPostureParams,
entry: {
readonly suffix: string;
readonly kind: PolicySandboxPostureEvidence["kind"];
readonly value: string | undefined;
readonly networkSurface?: "browser" | "docker";
readonly explicit: boolean;
readonly inherited: boolean;
},
): void {
entries.push({
id: `${params.id}-${entry.suffix.replaceAll("/", "-")}`,
kind: entry.kind,
source: `${entry.inherited ? params.inheritedSourceBase : params.sourceBase}/${entry.suffix}`,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
...(entry.value === undefined ? {} : { value: entry.value }),
...(entry.networkSurface === undefined ? {} : { networkSurface: entry.networkSurface }),
explicit: entry.explicit,
});
}
function splitPolicyBindSpec(
value: string,
): { readonly host: string; readonly mode: string } | undefined {
const separator = policyBindSeparatorIndex(value);
if (separator < 0) {
return undefined;
}
const host = value.slice(0, separator);
const rest = value.slice(separator + 1);
const optionsStart = policyBindOptionsSeparatorIndex(rest);
const options = optionsStart < 0 ? "" : rest.slice(optionsStart + 1);
const mode = options
.split(",")
.map((entry) => entry.trim().toLowerCase())
.includes("ro")
? "ro"
: "rw";
return { host, mode };
}
function policyBindSeparatorIndex(value: string): number {
const hasDriveLetterPrefix = /^[A-Za-z]:[\\/]/.test(value);
for (let index = hasDriveLetterPrefix ? 2 : 0; index < value.length; index += 1) {
if (value[index] === ":") {
return index;
}
}
return -1;
}
function policyBindOptionsSeparatorIndex(value: string): number {
const hasDriveLetterPrefix = /^[A-Za-z]:[\\/]/.test(value);
for (let index = hasDriveLetterPrefix ? 2 : 0; index < value.length; index += 1) {
if (value[index] === ":") {
return index;
}
}
return -1;
}
@@ -0,0 +1,340 @@
import {
isRecord,
asBoolean as readBoolean,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { ocPathSegment } from "./policy-state-helpers.js";
import type { PolicyToolPostureEvidence } from "./policy-state-types.js";
// Policy plugin tool posture evidence.
import { POLICY_TOOL_GROUPS } from "./tool-policy-conformance.js";
export function scanPolicyToolPosture(
cfg: Record<string, unknown>,
): readonly PolicyToolPostureEvidence[] {
const globalTools = isRecord(cfg.tools) ? cfg.tools : {};
const agents = isRecord(cfg.agents) ? cfg.agents : {};
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
const defaultSandbox = isRecord(defaults.sandbox) ? defaults.sandbox : {};
const entries: PolicyToolPostureEvidence[] = [];
pushToolPostureEvidence(entries, {
id: "tools",
scope: "global",
tools: globalTools,
inheritedTools: {},
sandbox: defaultSandbox,
inheritedSandbox: {},
sourceBase: "oc://openclaw.config/tools",
inheritedSourceBase: "oc://openclaw.config/tools",
});
const list = Array.isArray(agents.list) ? agents.list : [];
list.forEach((agent, index) => {
if (!isRecord(agent)) {
return;
}
const agentId =
typeof agent.id === "string" && agent.id.trim() !== "" ? agent.id.trim() : undefined;
pushToolPostureEvidence(entries, {
id: agentId ?? `agent-${index}`,
scope: "agent",
agentId,
tools: isRecord(agent.tools) ? agent.tools : {},
inheritedTools: globalTools,
sandbox: isRecord(agent.sandbox) ? agent.sandbox : {},
inheritedSandbox: defaultSandbox,
sourceBase: `oc://openclaw.config/agents/list/#${index}/tools`,
inheritedSourceBase: "oc://openclaw.config/tools",
});
});
return entries.toSorted((a, b) => a.source.localeCompare(b.source) || a.id.localeCompare(b.id));
}
function pushToolPostureEvidence(
entries: PolicyToolPostureEvidence[],
params: {
readonly id: string;
readonly scope: "global" | "agent";
readonly agentId?: string;
readonly tools: Record<string, unknown>;
readonly inheritedTools: Record<string, unknown>;
readonly sandbox: Record<string, unknown>;
readonly inheritedSandbox: Record<string, unknown>;
readonly sourceBase: string;
readonly inheritedSourceBase: string;
},
): void {
const localProfile = readString(params.tools.profile);
const inheritedProfile = readString(params.inheritedTools.profile);
pushToolPostureValue(entries, params, {
suffix: "profile",
kind: "profile",
value: localProfile ?? inheritedProfile ?? "full",
explicit: localProfile !== undefined || inheritedProfile !== undefined,
inherited: localProfile === undefined && inheritedProfile !== undefined,
});
pushToolPostureList(entries, params, "allow");
pushToolAlsoAllowPostureList(entries, params);
pushToolPostureList(entries, params, "deny");
pushToolFsPosture(entries, params);
pushToolExecPosture(entries, params);
pushToolElevatedPosture(entries, params);
}
function pushToolFsPosture(entries: PolicyToolPostureEvidence[], params: ToolPostureParams): void {
const localFs = isRecord(params.tools.fs) ? params.tools.fs : {};
const inheritedFs = isRecord(params.inheritedTools.fs) ? params.inheritedTools.fs : {};
const localWorkspaceOnly = readBoolean(localFs.workspaceOnly);
const inheritedWorkspaceOnly = readBoolean(inheritedFs.workspaceOnly);
pushToolPostureValue(entries, params, {
suffix: "fs/workspaceOnly",
kind: "fsWorkspaceOnly",
value: localWorkspaceOnly ?? inheritedWorkspaceOnly ?? false,
explicit: localWorkspaceOnly !== undefined || inheritedWorkspaceOnly !== undefined,
inherited: localWorkspaceOnly === undefined && inheritedWorkspaceOnly !== undefined,
});
}
function pushToolExecPosture(
entries: PolicyToolPostureEvidence[],
params: ToolPostureParams,
): void {
const localExec = isRecord(params.tools.exec) ? params.tools.exec : {};
const inheritedExec = isRecord(params.inheritedTools.exec) ? params.inheritedTools.exec : {};
const localHost = readString(localExec.host);
const inheritedHost = readString(inheritedExec.host);
const host = localHost ?? inheritedHost ?? "auto";
pushToolPostureValue(entries, params, {
suffix: "exec/host",
kind: "execHost",
value: host,
explicit: localHost !== undefined || inheritedHost !== undefined,
inherited: localHost === undefined && inheritedHost !== undefined,
});
const localSecurity = readString(localExec.security);
const inheritedSecurity = readString(inheritedExec.security);
// Config conformance intentionally ignores exec-approvals.json runtime/operator state.
const sandboxMode = readString(params.sandbox.mode) ?? readString(params.inheritedSandbox.mode);
const sandboxCanApply = sandboxMode === "all";
pushToolPostureValue(entries, params, {
suffix: "exec/security",
kind: "execSecurity",
value:
localSecurity ??
inheritedSecurity ??
(host === "sandbox" || (host === "auto" && sandboxCanApply) ? "deny" : "full"),
explicit: localSecurity !== undefined || inheritedSecurity !== undefined,
inherited: localSecurity === undefined && inheritedSecurity !== undefined,
});
const localAsk = readString(localExec.ask);
const inheritedAsk = readString(inheritedExec.ask);
pushToolPostureValue(entries, params, {
suffix: "exec/ask",
kind: "execAsk",
value: localAsk ?? inheritedAsk ?? "off",
explicit: localAsk !== undefined || inheritedAsk !== undefined,
inherited: localAsk === undefined && inheritedAsk !== undefined,
});
}
function pushToolElevatedPosture(
entries: PolicyToolPostureEvidence[],
params: ToolPostureParams,
): void {
const localElevated = isRecord(params.tools.elevated) ? params.tools.elevated : {};
const inheritedElevated = isRecord(params.inheritedTools.elevated)
? params.inheritedTools.elevated
: {};
const localEnabled = readBoolean(localElevated.enabled);
const inheritedEnabled = readBoolean(inheritedElevated.enabled);
const effectiveEnabled =
inheritedEnabled === false ? false : (localEnabled ?? inheritedEnabled ?? true);
pushToolPostureValue(entries, params, {
suffix: "elevated/enabled",
kind: "elevatedEnabled",
value: effectiveEnabled,
explicit: localEnabled !== undefined || inheritedEnabled !== undefined,
inherited:
(inheritedEnabled === false && localEnabled !== false) ||
(localEnabled === undefined && inheritedEnabled !== undefined),
});
const localAllowFrom = isRecord(localElevated.allowFrom) ? localElevated.allowFrom : {};
const inheritedAllowFrom = isRecord(inheritedElevated.allowFrom)
? inheritedElevated.allowFrom
: {};
const providers = [
...new Set([...Object.keys(inheritedAllowFrom), ...Object.keys(localAllowFrom)]),
].toSorted((a, b) => a.localeCompare(b));
for (const provider of providers) {
const localEntries = readStringOrNumberArray(localAllowFrom[provider]);
const inheritedEntries = readStringOrNumberArray(inheritedAllowFrom[provider]);
const inherited = localEntries.length === 0 && inheritedEntries.length > 0;
entries.push({
id: `${params.id}-elevated-allow-from-${ocPathSegment(provider)}`,
kind: "elevatedAllowFrom",
source: `${inherited ? params.inheritedSourceBase : params.sourceBase}/elevated/allowFrom/${ocPathSegment(provider)}`,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
entries: localEntries.length > 0 ? localEntries : inheritedEntries,
explicit: localEntries.length > 0 || inheritedEntries.length > 0,
});
}
}
type ToolPostureParams = {
readonly id: string;
readonly scope: "global" | "agent";
readonly agentId?: string;
readonly tools: Record<string, unknown>;
readonly inheritedTools: Record<string, unknown>;
readonly sandbox: Record<string, unknown>;
readonly inheritedSandbox: Record<string, unknown>;
readonly sourceBase: string;
readonly inheritedSourceBase: string;
};
function pushToolPostureValue(
entries: PolicyToolPostureEvidence[],
params: ToolPostureParams,
entry: {
readonly suffix: string;
readonly kind: PolicyToolPostureEvidence["kind"];
readonly value: boolean | string | undefined;
readonly explicit: boolean;
readonly inherited: boolean;
},
): void {
entries.push({
id: `${params.id}-${entry.suffix.replaceAll("/", "-")}`,
kind: entry.kind,
source: `${entry.inherited ? params.inheritedSourceBase : params.sourceBase}/${entry.suffix}`,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
...(entry.value === undefined ? {} : { value: entry.value }),
explicit: entry.explicit,
});
}
function pushToolPostureList(
entries: PolicyToolPostureEvidence[],
params: ToolPostureParams,
key: "allow" | "deny",
): void {
const localEntries = readStringArray(params.tools[key]);
const inheritedEntries = readStringArray(params.inheritedTools[key]);
const inherited = localEntries.length === 0 && inheritedEntries.length > 0;
entries.push({
id: `${params.id}-${key}`,
kind: key,
source: `${inherited ? params.inheritedSourceBase : params.sourceBase}/${key}`,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
entries: [...inheritedEntries, ...localEntries],
explicit: localEntries.length > 0 || inheritedEntries.length > 0,
});
}
function pushToolAlsoAllowPostureList(
entries: PolicyToolPostureEvidence[],
params: ToolPostureParams,
): void {
const localValue = params.tools.alsoAllow;
const inheritedValue = params.inheritedTools.alsoAllow;
const localConfigured = Array.isArray(localValue);
const inheritedConfigured = Array.isArray(inheritedValue);
const localEntries = localConfigured ? readStringArray(localValue) : [];
const inheritedEntries = inheritedConfigured ? readStringArray(inheritedValue) : [];
const inherited = !localConfigured && inheritedConfigured;
entries.push({
id: `${params.id}-alsoAllow`,
kind: "alsoAllow",
source: `${inherited ? params.inheritedSourceBase : params.sourceBase}/alsoAllow`,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
entries: inherited ? inheritedEntries : localEntries,
explicit: localConfigured || inheritedConfigured,
});
}
export const AGENT_WORKSPACE_POLICY_TOOLS = [
"exec",
"process",
"write",
"edit",
"apply_patch",
] as const;
export const IMPLICIT_DEFAULT_ACCOUNT_FIELDS: Readonly<Record<string, readonly string[]>> = {
discord: ["token"],
googlechat: ["serviceAccount", "serviceAccountRef", "serviceAccountFile"],
imessage: ["cliPath", "dbPath"],
"qa-channel": ["baseUrl"],
qqbot: ["appId", "clientSecret", "clientSecretFile"],
signal: ["account"],
slack: ["appToken", "botToken", "signingSecret"],
"synology-chat": ["token"],
telegram: ["botToken", "tokenFile"],
tlon: ["ship"],
twitch: ["username"],
whatsapp: ["authDir"],
zalo: ["botToken", "tokenFile"],
zalouser: ["profile"],
};
export function readStringArray(value: unknown): readonly string[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((entry): entry is string => typeof entry === "string" && entry.trim() !== "");
}
function readStringOrNumberArray(value: unknown): readonly string[] {
if (!Array.isArray(value)) {
return [];
}
const entries: string[] = [];
for (const entry of value) {
if (typeof entry === "string" && entry.trim() !== "") {
entries.push(entry.trim());
} else if (typeof entry === "number" && Number.isFinite(entry)) {
entries.push(String(entry));
}
}
return entries;
}
function normalizePolicyToolName(value: string): string {
const normalized = value.trim().toLowerCase();
if (normalized === "bash") {
return "exec";
}
if (normalized === "apply-patch") {
return "apply_patch";
}
return normalized;
}
function policyToolGlobMatches(tool: string, pattern: string): boolean {
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`).test(tool);
}
export function toolListCoversTool(list: readonly string[], tool: string): boolean {
for (const entry of list) {
const normalized = normalizePolicyToolName(entry);
if (normalized === "*" || normalized === tool) {
return true;
}
if (POLICY_TOOL_GROUPS[normalized]?.includes(tool)) {
return true;
}
if (normalized.includes("*") && policyToolGlobMatches(tool, normalized)) {
return true;
}
}
return false;
}
+149
View File
@@ -0,0 +1,149 @@
// Policy plugin TOOLS.md evidence.
import { COLLAPSE_HYPHENS, NON_SLUG_CHARS, TRIM_HYPHENS } from "./policy-state-types.js";
import type { PolicyToolEvidence } from "./policy-state-types.js";
export function scanPolicyTools(raw: string): Promise<readonly PolicyToolEvidence[]> {
return Promise.resolve(scanPolicyToolHeaders(raw));
}
function scanPolicyToolHeaders(raw: string): readonly PolicyToolEvidence[] {
const section = markdownSectionLines(raw, "tools");
if (section.length === 0) {
return [];
}
const tools: PolicyToolEvidence[] = [];
for (let index = 0; index < section.length; index += 1) {
const line = section[index]?.text ?? "";
const heading = /^###\s+([^\s#]+)(.*)$/.exec(line);
const bullet = /^[-*+]\s+([^:\s][^:]*?)\s*:(.*)$/.exec(line);
const match = heading ?? bullet;
const toolName = match?.[1];
if (!toolName) {
continue;
}
const id = slugify(toolName);
if (!id) {
continue;
}
const entry: {
id: string;
source: string;
line: number;
risk?: string;
sensitivity?: string;
owner?: string;
capabilities?: readonly string[];
} = {
id,
source: `oc://TOOLS.md/tools/${id}`,
line: section[index]?.line ?? index + 1,
};
const metaLines = [match[2] ?? ""];
for (let metaIndex = index + 1; metaIndex < section.length; metaIndex += 1) {
const metaLine = section[metaIndex]?.text ?? "";
if (/^###\s+\S+/.test(metaLine.trim()) || /^[-*+]\s+[^:\s][^:]*?\s*:/.test(metaLine)) {
break;
}
metaLines.push(metaLine);
}
const meta = metaLines.join("\n");
const risk = riskFromMeta(meta);
const sensitivity = /\bsensitivity\s*:\s*([a-z0-9_-]+)\b/i.exec(meta)?.[1]?.toLowerCase();
const owner = /\bowner\s*:\s*([^\s#]+)\b/i.exec(meta)?.[1];
const capabilities = capabilityTokensFromMetaLines(metaLines);
if (risk !== undefined) {
entry.risk = risk;
}
if (sensitivity !== undefined) {
entry.sensitivity = sensitivity;
}
if (owner !== undefined) {
entry.owner = owner;
}
if (capabilities.length > 0) {
entry.capabilities = capabilities;
}
tools.push(entry);
}
return tools;
}
function markdownSectionLines(
raw: string,
sectionSlug: string,
): readonly { readonly line: number; readonly text: string }[] {
const lines = raw.split(/\r?\n/);
let sectionDepth: number | undefined;
const section: { line: number; text: string }[] = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index] ?? "";
const heading = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
if (heading !== null) {
const depth = heading[1]?.length ?? 0;
const slug = slugify(heading[2] ?? "");
if (sectionDepth !== undefined && depth <= sectionDepth) {
break;
}
if (sectionDepth !== undefined) {
section.push({ line: index + 1, text: line });
continue;
}
if (sectionDepth === undefined && slug === sectionSlug) {
sectionDepth = depth;
}
continue;
}
if (sectionDepth !== undefined) {
section.push({ line: index + 1, text: line });
}
}
return section;
}
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/_/g, "-")
.replace(NON_SLUG_CHARS, "-")
.replace(COLLAPSE_HYPHENS, "-")
.replace(TRIM_HYPHENS, "");
}
function riskFromMeta(meta: string): string | undefined {
const namedRisk = /\brisk\s*:\s*([a-z0-9_-]+)\b/i.exec(meta)?.[1];
if (namedRisk !== undefined) {
return namedRisk.toLowerCase();
}
const alias = /\bR([0-5])\b/.exec(meta)?.[1];
switch (alias) {
case "0":
case "1":
return "low";
case "2":
case "3":
return "medium";
case "4":
return "high";
case "5":
return "critical";
default:
return undefined;
}
}
function capabilityTokensFromMetaLines(lines: readonly string[]): readonly string[] {
return lines.flatMap((line, index): string[] => {
const trimmed = line.trim();
if (trimmed.length === 0) {
return [];
}
const tokens = trimmed.match(/\b[A-Z][A-Z0-9_]{2,}\b/g) ?? [];
if (index === 0 || /\bcapabilities\s*:/i.test(trimmed)) {
return tokens;
}
const withoutTokens = tokens.reduce((remaining, token) => {
return remaining.replace(token, "");
}, trimmed);
return /^[\s,;:[\](){}#*_-]*$/.test(withoutTokens) ? tokens : [];
});
}
+236
View File
@@ -0,0 +1,236 @@
// Policy plugin evidence types.
import type { coerceSecretRef } from "openclaw/plugin-sdk/secret-input";
export type PolicyAttestation = {
readonly checkedAt: string;
readonly policy?: {
readonly path: string;
readonly hash: string;
};
readonly workspace: {
readonly scope: "policy";
readonly hash: string;
};
readonly findingsHash?: string;
readonly attestationHash?: string;
};
export type PolicyEvidence = {
readonly channels: readonly PolicyChannelEvidence[];
readonly tools?: readonly PolicyToolEvidence[];
readonly toolPosture?: readonly PolicyToolPostureEvidence[];
readonly sandboxPosture?: readonly PolicySandboxPostureEvidence[];
readonly mcpServers: readonly PolicyMcpServerEvidence[];
readonly modelProviders: readonly PolicyModelProviderEvidence[];
readonly modelRefs: readonly PolicyModelRefEvidence[];
readonly network: readonly PolicyNetworkEvidence[];
readonly ingress?: readonly PolicyIngressEvidence[];
readonly gatewayExposure?: readonly PolicyGatewayExposureEvidence[];
readonly agentWorkspace?: readonly PolicyAgentWorkspaceEvidence[];
readonly dataHandling?: readonly PolicyDataHandlingEvidence[];
readonly secrets?: readonly PolicySecretEvidence[];
readonly authProfiles?: readonly PolicyAuthProfileEvidence[];
readonly execApprovals?: readonly PolicyExecApprovalEvidence[];
};
export type PolicyChannelEvidence = {
readonly id: string;
readonly provider: string;
readonly source: string;
readonly enabled?: boolean;
};
export type PolicyMcpServerEvidence = {
readonly id: string;
readonly transport: "stdio" | "sse" | "streamable-http" | "unknown";
readonly source: string;
readonly command?: string;
readonly url?: string;
};
export type PolicyToolEvidence = {
readonly id: string;
readonly source: string;
readonly line: number;
readonly risk?: string;
readonly sensitivity?: string;
readonly owner?: string;
readonly capabilities?: readonly string[];
};
export type PolicyToolPostureEvidence = {
readonly id: string;
readonly kind:
| "allow"
| "alsoAllow"
| "deny"
| "elevatedAllowFrom"
| "elevatedEnabled"
| "execAsk"
| "execHost"
| "execSecurity"
| "fsWorkspaceOnly"
| "profile";
readonly source: string;
readonly scope: "global" | "agent";
readonly agentId?: string;
readonly value?: boolean | string;
readonly entries?: readonly string[];
readonly explicit?: boolean;
};
export type PolicySandboxPostureEvidence = {
readonly id: string;
readonly kind:
| "backend"
| "browserCdpSourceRange"
| "containerMount"
| "containerNetwork"
| "containerSecurityProfile"
| "mode";
readonly source: string;
readonly scope: "defaults" | "agent";
readonly agentId?: string;
readonly value?: boolean | string;
readonly bind?: string;
readonly bindMode?: string;
readonly bindHost?: string;
readonly bindSurface?: "browser" | "docker";
readonly networkSurface?: "browser" | "docker";
readonly profile?: "apparmor" | "seccomp";
readonly explicit?: boolean;
};
export type PolicyModelProviderEvidence = {
readonly id: string;
readonly source: string;
};
export type PolicyModelRefEvidence = {
readonly ref: string;
readonly provider: string;
readonly model: string;
readonly source: string;
};
export type PolicyNetworkEvidence = {
readonly id: string;
readonly source: string;
readonly value: boolean;
};
export type PolicyIngressEvidence = {
readonly id: string;
readonly kind:
| "channelDmPolicy"
| "channelGroupPolicy"
| "channelRequireMention"
| "sessionDmScope";
readonly source: string;
readonly channel?: string;
readonly accountId?: string;
readonly groupId?: string;
readonly value?: boolean | string;
readonly explicit?: boolean;
};
export type PolicyGatewayExposureEvidence = {
readonly id: string;
readonly kind:
| "auth"
| "authRateLimit"
| "bind"
| "controlUi"
| "httpEndpoint"
| "httpUrlFetch"
| "nodeCommand"
| "nodeDenyCommand"
| "remote"
| "tailscale";
readonly source: string;
readonly value?: boolean | string;
readonly nonLoopback?: boolean;
readonly explicit?: boolean;
readonly endpoint?: string;
readonly hasAllowlist?: boolean;
readonly command?: string;
};
export type PolicyAgentWorkspaceEvidence = {
readonly id: string;
readonly kind: "workspaceAccess" | "toolDeny";
readonly source: string;
readonly scope: "defaults" | "agent";
readonly agentId?: string;
readonly value?: string;
readonly sandboxMode?: string;
readonly sandboxModeSource?: string;
readonly sandboxEnabled?: boolean;
readonly tool?: string;
readonly denied?: boolean;
readonly explicit?: boolean;
};
export type PolicySecretEvidence = {
readonly id: string;
readonly kind: "input" | "provider";
readonly source: string;
readonly provenance?: "secretRef";
readonly refSource?: "env" | "file" | "exec";
readonly refProvider?: string;
readonly providerSource?: string;
readonly insecure?: readonly string[];
};
export type PolicyAuthProfileEvidence = {
readonly id: string;
readonly source: string;
readonly validMetadata: boolean;
readonly provider?: string;
readonly mode?: string;
};
export type PolicyExecApprovalEvidence = {
readonly id: string;
readonly kind: "agent" | "allowlist" | "defaults";
readonly source: string;
readonly agentId?: string;
readonly security?: string;
readonly securityConfigured?: boolean;
readonly ask?: string;
readonly askFallback?: string;
readonly autoAllowSkills?: boolean;
readonly pattern?: string;
readonly argPattern?: string;
readonly entrySource?: string;
};
export type PolicyDataHandlingEvidence = {
readonly id: string;
readonly kind:
| "memorySessionTranscriptIndexing"
| "sensitiveLoggingRedaction"
| "sessionRetentionMode"
| "telemetryContentCapture";
readonly source: string;
readonly scope: "global" | "agent";
readonly agentId?: string;
readonly value?: boolean | string;
readonly explicit?: boolean;
};
export type SecretRefEvidence = {
readonly source: "env" | "file" | "exec";
readonly provider: string;
readonly id: string;
};
export type SecretRefDefaults = NonNullable<Parameters<typeof coerceSecretRef>[1]>;
export const RESERVED_CHANNEL_CONFIG_KEYS = new Set(["defaults", "modelByChannel"]);
export const NON_SLUG_CHARS = /[^a-z0-9-]+/g;
export const COLLAPSE_HYPHENS = /-+/g;
export const TRIM_HYPHENS = /^-+|-+$/g;
@@ -0,0 +1,169 @@
// Policy plugin agent workspace evidence.
import {
isRecord,
normalizeOptionalString as readString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import {
AGENT_WORKSPACE_POLICY_TOOLS,
readStringArray,
toolListCoversTool,
} from "./policy-state-tool-posture.js";
import type { PolicyAgentWorkspaceEvidence } from "./policy-state-types.js";
export function scanPolicyAgentWorkspace(
cfg: Record<string, unknown>,
): readonly PolicyAgentWorkspaceEvidence[] {
const agents = isRecord(cfg.agents) ? cfg.agents : {};
const defaults = isRecord(agents.defaults) ? agents.defaults : {};
const defaultSandbox = isRecord(defaults.sandbox) ? defaults.sandbox : {};
const defaultTools = isRecord(cfg.tools) ? cfg.tools : {};
const entries: PolicyAgentWorkspaceEvidence[] = [];
pushAgentWorkspaceEvidence(entries, {
id: "agents-defaults",
scope: "defaults",
sandbox: defaultSandbox,
inheritedSandbox: {},
tools: defaultTools,
inheritedTools: {},
workspaceSourceBase: "oc://openclaw.config/agents/defaults",
inheritedWorkspaceSourceBase: "oc://openclaw.config/agents/defaults",
toolsSourceBase: "oc://openclaw.config/tools",
inheritedToolsSourceBase: "oc://openclaw.config/tools",
});
const list = Array.isArray(agents.list) ? agents.list : [];
list.forEach((agent, index) => {
if (!isRecord(agent)) {
return;
}
const agentId =
typeof agent.id === "string" && agent.id.trim() !== "" ? agent.id.trim() : undefined;
const sandbox = isRecord(agent.sandbox) ? agent.sandbox : {};
const tools = isRecord(agent.tools) ? agent.tools : {};
pushAgentWorkspaceEvidence(entries, {
id: agentId ?? `agent-${index}`,
scope: "agent",
agentId,
sandbox,
inheritedSandbox: defaultSandbox,
tools,
inheritedTools: defaultTools,
workspaceSourceBase: `oc://openclaw.config/agents/list/#${index}`,
inheritedWorkspaceSourceBase: "oc://openclaw.config/agents/defaults",
toolsSourceBase: `oc://openclaw.config/agents/list/#${index}/tools`,
inheritedToolsSourceBase: "oc://openclaw.config/tools",
});
});
return entries.toSorted((a, b) => a.source.localeCompare(b.source) || a.id.localeCompare(b.id));
}
function pushAgentWorkspaceEvidence(
entries: PolicyAgentWorkspaceEvidence[],
params: {
readonly id: string;
readonly scope: "defaults" | "agent";
readonly agentId?: string;
readonly sandbox: Record<string, unknown>;
readonly inheritedSandbox: Record<string, unknown>;
readonly tools: Record<string, unknown>;
readonly inheritedTools: Record<string, unknown>;
readonly workspaceSourceBase: string;
readonly inheritedWorkspaceSourceBase: string;
readonly toolsSourceBase: string;
readonly inheritedToolsSourceBase: string;
},
): void {
const explicitSandboxMode = readString(params.sandbox.mode);
const inheritedSandboxMode = readString(params.inheritedSandbox.mode);
const sandboxMode = explicitSandboxMode ?? inheritedSandboxMode ?? "off";
const sandboxModeCoversAgentMain = sandboxMode === "all";
const sandboxModeSource =
explicitSandboxMode !== undefined
? `${params.workspaceSourceBase}/sandbox/mode`
: inheritedSandboxMode !== undefined
? `${params.inheritedWorkspaceSourceBase}/sandbox/mode`
: "oc://openclaw.config/agents/defaults/sandbox/mode";
const explicitWorkspaceAccess = readString(params.sandbox.workspaceAccess);
const inheritedWorkspaceAccess = readString(params.inheritedSandbox.workspaceAccess);
entries.push({
id: `${params.id}-workspace-access`,
kind: "workspaceAccess",
source:
explicitWorkspaceAccess !== undefined
? `${params.workspaceSourceBase}/sandbox/workspaceAccess`
: inheritedWorkspaceAccess !== undefined
? `${params.inheritedWorkspaceSourceBase}/sandbox/workspaceAccess`
: "oc://openclaw.config/agents/defaults/sandbox/workspaceAccess",
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
value: explicitWorkspaceAccess ?? inheritedWorkspaceAccess ?? "none",
sandboxMode,
sandboxModeSource,
sandboxEnabled: sandboxModeCoversAgentMain,
explicit: explicitWorkspaceAccess !== undefined,
});
for (const tool of AGENT_WORKSPACE_POLICY_TOOLS) {
const denyEvidence = agentWorkspaceToolDenyEvidence(params, tool, sandboxModeCoversAgentMain);
entries.push({
id: `${params.id}-tool-${tool}`,
kind: "toolDeny",
source: denyEvidence.source,
scope: params.scope,
...(params.agentId === undefined ? {} : { agentId: params.agentId }),
tool,
denied: denyEvidence.denied,
explicit: denyEvidence.denied,
});
}
}
function agentWorkspaceToolDenyEvidence(
params: {
readonly tools: Record<string, unknown>;
readonly inheritedTools: Record<string, unknown>;
readonly toolsSourceBase: string;
readonly inheritedToolsSourceBase: string;
},
tool: string,
sandboxModeCoversAgentMain: boolean,
): { readonly denied: boolean; readonly source: string } {
const localSandboxToolDeny = configuredSandboxToolDenyEntries(params.tools);
const inheritedSandboxToolDeny = configuredSandboxToolDenyEntries(params.inheritedTools);
const sources = [
{
entries: readStringArray(params.tools.deny),
source: `${params.toolsSourceBase}/deny`,
},
{
entries: readStringArray(params.inheritedTools.deny),
source: `${params.inheritedToolsSourceBase}/deny`,
},
...(sandboxModeCoversAgentMain
? [
localSandboxToolDeny !== undefined
? {
entries: localSandboxToolDeny,
source: `${params.toolsSourceBase}/sandbox/tools/deny`,
}
: {
entries: inheritedSandboxToolDeny ?? [],
source: `${params.inheritedToolsSourceBase}/sandbox/tools/deny`,
},
]
: []),
];
const match = sources.find((entry) => toolListCoversTool(entry.entries, tool));
if (match !== undefined) {
return { denied: true, source: match.source };
}
return { denied: false, source: `${params.toolsSourceBase}/deny` };
}
function configuredSandboxToolDenyEntries(
tools: Record<string, unknown>,
): readonly string[] | undefined {
const sandbox = isRecord(tools.sandbox) ? tools.sandbox : {};
const sandboxTools = isRecord(sandbox.tools) ? sandbox.tools : {};
return Array.isArray(sandboxTools.deny) ? readStringArray(sandboxTools.deny) : undefined;
}
File diff suppressed because it is too large Load Diff