import { getActivePluginRegistry } from "../plugins/runtime.js"; import { resolveReservedGatewayMethodScope } from "../shared/gateway-method-policy.js"; import { ADMIN_SCOPE, APPROVALS_SCOPE, PAIRING_SCOPE, READ_SCOPE, TALK_SECRETS_SCOPE, WRITE_SCOPE, type OperatorScope, } from "./operator-scopes.js"; export { ADMIN_SCOPE, APPROVALS_SCOPE, PAIRING_SCOPE, READ_SCOPE, TALK_SECRETS_SCOPE, WRITE_SCOPE, type OperatorScope, }; export const CLI_DEFAULT_OPERATOR_SCOPES: OperatorScope[] = [ ADMIN_SCOPE, READ_SCOPE, WRITE_SCOPE, APPROVALS_SCOPE, PAIRING_SCOPE, TALK_SECRETS_SCOPE, ]; const NODE_ROLE_METHODS = new Set([ "node.invoke.result", "node.event", "node.pending.drain", "node.canvas.capability.refresh", "node.pending.pull", "node.pending.ack", "skills.bins", ]); const METHOD_SCOPE_GROUPS: Record = { [APPROVALS_SCOPE]: [ "exec.approval.get", "exec.approval.list", "exec.approval.request", "exec.approval.waitDecision", "exec.approval.resolve", "plugin.approval.list", "plugin.approval.request", "plugin.approval.waitDecision", "plugin.approval.resolve", ], [PAIRING_SCOPE]: [ "node.pair.request", "node.pair.list", "node.pair.reject", "node.pair.verify", "node.pair.approve", "device.pair.list", "device.pair.approve", "device.pair.reject", "device.pair.remove", "device.token.rotate", "device.token.revoke", "node.rename", ], [READ_SCOPE]: [ "health", "doctor.memory.status", "doctor.memory.dreamDiary", "logs.tail", "channels.status", "status", "usage.status", "usage.cost", "tts.status", "tts.providers", "models.list", "tools.catalog", "tools.effective", "agents.list", "agent.identity.get", "skills.status", "skills.search", "skills.detail", "voicewake.get", "sessions.list", "sessions.get", "sessions.preview", "sessions.resolve", "sessions.compaction.list", "sessions.compaction.get", "sessions.subscribe", "sessions.unsubscribe", "sessions.messages.subscribe", "sessions.messages.unsubscribe", "sessions.usage", "sessions.usage.timeseries", "sessions.usage.logs", "cron.list", "cron.status", "cron.runs", "gateway.identity.get", "system-presence", "last-heartbeat", "node.list", "node.describe", "chat.history", "config.get", "config.schema.lookup", "talk.config", "agents.files.list", "agents.files.get", ], [WRITE_SCOPE]: [ "send", "poll", "agent", "agent.wait", "wake", "talk.mode", "talk.speak", "tts.enable", "tts.disable", "tts.convert", "tts.setProvider", "voicewake.set", "node.invoke", "chat.send", "chat.abort", "sessions.create", "sessions.send", "sessions.steer", "sessions.abort", "sessions.compaction.branch", "push.test", "node.pending.enqueue", ], [ADMIN_SCOPE]: [ "channels.logout", "agents.create", "agents.update", "agents.delete", "skills.install", "skills.update", "secrets.reload", "secrets.resolve", "cron.add", "cron.update", "cron.remove", "cron.run", "sessions.patch", "sessions.reset", "sessions.delete", "sessions.compact", "sessions.compaction.restore", "connect", "chat.inject", "web.login.start", "web.login.wait", "set-heartbeats", "system-event", "agents.files.set", ], [TALK_SECRETS_SCOPE]: [], }; const METHOD_SCOPE_BY_NAME = new Map( Object.entries(METHOD_SCOPE_GROUPS).flatMap(([scope, methods]) => methods.map((method) => [method, scope as OperatorScope]), ), ); function resolveScopedMethod(method: string): OperatorScope | undefined { const explicitScope = METHOD_SCOPE_BY_NAME.get(method); if (explicitScope) { return explicitScope; } const reservedScope = resolveReservedGatewayMethodScope(method); if (reservedScope) { return reservedScope; } const pluginScope = getActivePluginRegistry()?.gatewayMethodScopes?.[method]; if (pluginScope) { return pluginScope; } return undefined; } export function isApprovalMethod(method: string): boolean { return resolveScopedMethod(method) === APPROVALS_SCOPE; } export function isPairingMethod(method: string): boolean { return resolveScopedMethod(method) === PAIRING_SCOPE; } export function isReadMethod(method: string): boolean { return resolveScopedMethod(method) === READ_SCOPE; } export function isWriteMethod(method: string): boolean { return resolveScopedMethod(method) === WRITE_SCOPE; } export function isNodeRoleMethod(method: string): boolean { return NODE_ROLE_METHODS.has(method); } export function isAdminOnlyMethod(method: string): boolean { return resolveScopedMethod(method) === ADMIN_SCOPE; } export function resolveRequiredOperatorScopeForMethod(method: string): OperatorScope | undefined { return resolveScopedMethod(method); } export function resolveLeastPrivilegeOperatorScopesForMethod(method: string): OperatorScope[] { const requiredScope = resolveRequiredOperatorScopeForMethod(method); if (requiredScope) { return [requiredScope]; } // Default-deny for unclassified methods. return []; } export function authorizeOperatorScopesForMethod( method: string, scopes: readonly string[], ): { allowed: true } | { allowed: false; missingScope: OperatorScope } { if (scopes.includes(ADMIN_SCOPE)) { return { allowed: true }; } const requiredScope = resolveRequiredOperatorScopeForMethod(method) ?? ADMIN_SCOPE; if (requiredScope === READ_SCOPE) { if (scopes.includes(READ_SCOPE) || scopes.includes(WRITE_SCOPE)) { return { allowed: true }; } return { allowed: false, missingScope: READ_SCOPE }; } if (scopes.includes(requiredScope)) { return { allowed: true }; } return { allowed: false, missingScope: requiredScope }; } export function isGatewayMethodClassified(method: string): boolean { if (isNodeRoleMethod(method)) { return true; } return resolveRequiredOperatorScopeForMethod(method) !== undefined; }