securityclaw 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +135 -0
  4. package/README.zh-CN.md +135 -0
  5. package/admin/public/app.js +148 -0
  6. package/admin/public/favicon.svg +21 -0
  7. package/admin/public/index.html +31 -0
  8. package/admin/public/styles.css +2715 -0
  9. package/admin/server.ts +1053 -0
  10. package/bin/install-lib.mjs +88 -0
  11. package/bin/securityclaw.mjs +66 -0
  12. package/config/policy.default.yaml +520 -0
  13. package/index.ts +2662 -0
  14. package/install.sh +22 -0
  15. package/openclaw.plugin.json +60 -0
  16. package/package.json +69 -0
  17. package/src/admin/build.ts +113 -0
  18. package/src/admin/console_notice.ts +195 -0
  19. package/src/admin/dashboard_url_state.ts +80 -0
  20. package/src/admin/openclaw_session_catalog.ts +137 -0
  21. package/src/admin/runtime_guard.ts +51 -0
  22. package/src/admin/skill_interception_store.ts +1606 -0
  23. package/src/application/commands/approval_commands.ts +189 -0
  24. package/src/approvals/chat_approval_store.ts +433 -0
  25. package/src/config/live_config.ts +144 -0
  26. package/src/config/loader.ts +168 -0
  27. package/src/config/runtime_override.ts +66 -0
  28. package/src/config/strategy_store.ts +121 -0
  29. package/src/config/validator.ts +222 -0
  30. package/src/domain/models/resource_context.ts +31 -0
  31. package/src/domain/ports/approval_repository.ts +40 -0
  32. package/src/domain/ports/notification_port.ts +29 -0
  33. package/src/domain/ports/openclaw_adapter.ts +22 -0
  34. package/src/domain/services/account_policy_engine.ts +163 -0
  35. package/src/domain/services/approval_service.ts +336 -0
  36. package/src/domain/services/approval_subject_resolver.ts +37 -0
  37. package/src/domain/services/context_inference_service.ts +502 -0
  38. package/src/domain/services/file_rule_registry.ts +171 -0
  39. package/src/domain/services/formatting_service.ts +101 -0
  40. package/src/domain/services/path_candidate_inference.ts +111 -0
  41. package/src/domain/services/sensitive_path_registry.ts +288 -0
  42. package/src/domain/services/sensitivity_label_inference.ts +161 -0
  43. package/src/domain/services/shell_filesystem_inference.ts +360 -0
  44. package/src/engine/approval_fsm.ts +104 -0
  45. package/src/engine/decision_engine.ts +39 -0
  46. package/src/engine/dlp_engine.ts +91 -0
  47. package/src/engine/rule_engine.ts +208 -0
  48. package/src/events/emitter.ts +86 -0
  49. package/src/events/schema.ts +27 -0
  50. package/src/hooks/context_guard.ts +36 -0
  51. package/src/hooks/output_guard.ts +66 -0
  52. package/src/hooks/persist_guard.ts +69 -0
  53. package/src/hooks/policy_guard.ts +222 -0
  54. package/src/hooks/result_guard.ts +88 -0
  55. package/src/i18n/locale.ts +36 -0
  56. package/src/index.ts +255 -0
  57. package/src/infrastructure/adapters/notification_adapter.ts +173 -0
  58. package/src/infrastructure/adapters/openclaw_adapter_impl.ts +59 -0
  59. package/src/infrastructure/config/plugin_config_parser.ts +105 -0
  60. package/src/monitoring/status_store.ts +612 -0
  61. package/src/types.ts +409 -0
  62. package/src/utils.ts +97 -0
@@ -0,0 +1,222 @@
1
+ import type {
2
+ ApprovalRequirements,
3
+ ChallengeConfig,
4
+ Decision,
5
+ HookControls,
6
+ HookName,
7
+ PolicyMatch,
8
+ PolicyRule,
9
+ SecurityClawConfig,
10
+ Severity,
11
+ } from "../types.ts";
12
+ import { normalizeFileRules } from "../domain/services/file_rule_registry.ts";
13
+ import { normalizeSensitivePathRules } from "../domain/services/sensitive_path_registry.ts";
14
+
15
+ const DEFAULT_HOOKS: Record<HookName, HookControls> = {
16
+ before_prompt_build: { enabled: true, timeout_ms: 50, fail_mode: "open" },
17
+ before_tool_call: { enabled: true, timeout_ms: 50, fail_mode: "close" },
18
+ after_tool_call: { enabled: true, timeout_ms: 50, fail_mode: "open" },
19
+ tool_result_persist: { enabled: true, timeout_ms: 50, fail_mode: "close" },
20
+ message_sending: { enabled: true, timeout_ms: 50, fail_mode: "open" }
21
+ };
22
+
23
+ const DECISIONS: Decision[] = ["allow", "warn", "challenge", "block"];
24
+ const DEFAULT_GROUP = "general";
25
+ const DEFAULT_CONTROL_DOMAIN = "execution_control";
26
+ const SEVERITIES: Severity[] = ["low", "medium", "high", "critical"];
27
+
28
+ function normalizeString(value: unknown): string | undefined {
29
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
30
+ }
31
+
32
+ function normalizeStringArray(value: unknown): string[] | undefined {
33
+ if (!Array.isArray(value)) {
34
+ return undefined;
35
+ }
36
+ const normalized = value
37
+ .map((entry) => normalizeString(entry))
38
+ .filter((entry): entry is string => Boolean(entry));
39
+ return normalized.length > 0 ? normalized : undefined;
40
+ }
41
+
42
+ function normalizeNumber(value: unknown): number | undefined {
43
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
44
+ }
45
+
46
+ function normalizeSeverity(value: unknown): Severity | undefined {
47
+ return typeof value === "string" && SEVERITIES.includes(value as Severity)
48
+ ? (value as Severity)
49
+ : undefined;
50
+ }
51
+
52
+ function sanitizeDecision(value: unknown): Decision | undefined {
53
+ if (typeof value !== "string") {
54
+ return undefined;
55
+ }
56
+ return DECISIONS.includes(value as Decision) ? (value as Decision) : undefined;
57
+ }
58
+
59
+ function sanitizePolicyMatch(match: PolicyRule["match"]): PolicyMatch {
60
+ const normalized: PolicyMatch = {};
61
+ const identity = normalizeStringArray(match?.identity);
62
+ const scope = normalizeStringArray(match?.scope);
63
+ const tool = normalizeStringArray(match?.tool);
64
+ const toolGroup = normalizeStringArray(match?.tool_group);
65
+ const operation = normalizeStringArray(match?.operation);
66
+ const tags = normalizeStringArray(match?.tags);
67
+ const resourceScope = normalizeStringArray(match?.resource_scope) as PolicyMatch["resource_scope"] | undefined;
68
+ const pathPrefix = normalizeStringArray(match?.path_prefix);
69
+ const pathGlob = normalizeStringArray(match?.path_glob);
70
+ const pathRegex = normalizeStringArray(match?.path_regex);
71
+ const fileType = normalizeStringArray(match?.file_type);
72
+ const assetLabels = normalizeStringArray(match?.asset_labels);
73
+ const dataLabels = normalizeStringArray(match?.data_labels);
74
+ const trustLevel = normalizeStringArray(match?.trust_level) as PolicyMatch["trust_level"] | undefined;
75
+ const destinationType = normalizeStringArray(match?.destination_type) as PolicyMatch["destination_type"] | undefined;
76
+ const destDomain = normalizeStringArray(match?.dest_domain);
77
+ const destIpClass = normalizeStringArray(match?.dest_ip_class) as PolicyMatch["dest_ip_class"] | undefined;
78
+ const toolArgsSummary = normalizeStringArray(match?.tool_args_summary);
79
+ const toolArgsRegex = normalizeStringArray(match?.tool_args_regex);
80
+ const minFileCount = normalizeNumber(match?.min_file_count);
81
+ const minBytes = normalizeNumber(match?.min_bytes);
82
+ const minRecordCount = normalizeNumber(match?.min_record_count);
83
+
84
+ if (identity) normalized.identity = identity;
85
+ if (scope) normalized.scope = scope;
86
+ if (tool) normalized.tool = tool;
87
+ if (toolGroup) normalized.tool_group = toolGroup;
88
+ if (operation) normalized.operation = operation;
89
+ if (tags) normalized.tags = tags;
90
+ if (resourceScope) normalized.resource_scope = resourceScope;
91
+ if (pathPrefix) normalized.path_prefix = pathPrefix;
92
+ if (pathGlob) normalized.path_glob = pathGlob;
93
+ if (pathRegex) normalized.path_regex = pathRegex;
94
+ if (fileType) normalized.file_type = fileType;
95
+ if (assetLabels) normalized.asset_labels = assetLabels;
96
+ if (dataLabels) normalized.data_labels = dataLabels;
97
+ if (trustLevel) normalized.trust_level = trustLevel;
98
+ if (destinationType) normalized.destination_type = destinationType;
99
+ if (destDomain) normalized.dest_domain = destDomain;
100
+ if (destIpClass) normalized.dest_ip_class = destIpClass;
101
+ if (toolArgsSummary) normalized.tool_args_summary = toolArgsSummary;
102
+ if (toolArgsRegex) normalized.tool_args_regex = toolArgsRegex;
103
+ if (minFileCount !== undefined) normalized.min_file_count = minFileCount;
104
+ if (minBytes !== undefined) normalized.min_bytes = minBytes;
105
+ if (minRecordCount !== undefined) normalized.min_record_count = minRecordCount;
106
+
107
+ return normalized;
108
+ }
109
+
110
+ function sanitizeChallenge(value: PolicyRule["challenge"]): ChallengeConfig | undefined {
111
+ const ttlSeconds = normalizeNumber(value?.ttl_seconds);
112
+ return ttlSeconds !== undefined ? { ttl_seconds: ttlSeconds } : undefined;
113
+ }
114
+
115
+ function sanitizeApprovalRequirements(value: PolicyRule["approval_requirements"]): ApprovalRequirements | undefined {
116
+ if (!value || typeof value !== "object") {
117
+ return undefined;
118
+ }
119
+ const approverRoles = normalizeStringArray(value.approver_roles);
120
+ const ttlSeconds = normalizeNumber(value.ttl_seconds);
121
+ const traceBinding = value.trace_binding === "trace" ? "trace" : value.trace_binding === "none" ? "none" : undefined;
122
+ const normalized: ApprovalRequirements = {
123
+ ...(value.ticket_required === true ? { ticket_required: true } : {}),
124
+ ...(approverRoles ? { approver_roles: approverRoles } : {}),
125
+ ...(value.single_use === true ? { single_use: true } : {}),
126
+ ...(traceBinding ? { trace_binding: traceBinding } : {}),
127
+ ...(ttlSeconds !== undefined ? { ttl_seconds: ttlSeconds } : {}),
128
+ };
129
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
130
+ }
131
+
132
+ function sanitizePolicyRule(rule: PolicyRule): PolicyRule {
133
+ const decision = sanitizeDecision(rule.decision);
134
+ if (!decision) {
135
+ throw new Error(`Policy ${rule.rule_id || "<unknown>"} must define a valid decision.`);
136
+ }
137
+ const controlDomain =
138
+ normalizeString(rule.control_domain) ??
139
+ normalizeString(rule.group) ??
140
+ DEFAULT_CONTROL_DOMAIN;
141
+ const title = normalizeString(rule.title);
142
+ const description = normalizeString(rule.description);
143
+ const severity = normalizeSeverity(rule.severity);
144
+ const owner = normalizeString(rule.owner);
145
+ const playbookUrl = normalizeString(rule.playbook_url);
146
+ const challenge = sanitizeChallenge(rule.challenge);
147
+ const approvalRequirements = sanitizeApprovalRequirements(rule.approval_requirements);
148
+ return {
149
+ ...rule,
150
+ group: normalizeString(rule.group) ?? controlDomain ?? DEFAULT_GROUP,
151
+ ...(title ? { title } : {}),
152
+ ...(description ? { description } : {}),
153
+ ...(severity ? { severity } : {}),
154
+ control_domain: controlDomain,
155
+ ...(owner ? { owner } : {}),
156
+ ...(playbookUrl ? { playbook_url: playbookUrl } : {}),
157
+ enabled: rule.enabled !== false,
158
+ decision,
159
+ reason_codes: Array.isArray(rule.reason_codes) ? rule.reason_codes : [],
160
+ match: sanitizePolicyMatch(rule.match ?? {}),
161
+ ...(challenge ? { challenge } : {}),
162
+ ...(approvalRequirements ? { approval_requirements: approvalRequirements } : {})
163
+ };
164
+ }
165
+
166
+ export function validateConfig(raw: Record<string, unknown>): SecurityClawConfig {
167
+ if (!raw || typeof raw !== "object") {
168
+ throw new Error("Config must be an object.");
169
+ }
170
+
171
+ const config = raw as Partial<SecurityClawConfig>;
172
+ if (!config.version || !config.policy_version || !config.environment) {
173
+ throw new Error("Config must define version, policy_version, and environment.");
174
+ }
175
+
176
+ const hooks = { ...DEFAULT_HOOKS, ...(config.hooks ?? {}) };
177
+
178
+ const policies = Array.isArray(config.policies) ? config.policies : [];
179
+ const normalizedPolicies: PolicyRule[] = [];
180
+ for (const rule of policies) {
181
+ if (!rule.rule_id || typeof rule.priority !== "number") {
182
+ throw new Error("Every policy must define rule_id and priority.");
183
+ }
184
+ normalizedPolicies.push(sanitizePolicyRule(rule));
185
+ }
186
+
187
+ const dlp = config.dlp;
188
+ if (!dlp || !Array.isArray(dlp.patterns) || !dlp.on_dlp_hit) {
189
+ throw new Error("DLP config must define patterns and on_dlp_hit.");
190
+ }
191
+ const webhookUrl = config.event_sink?.webhook_url;
192
+ const rawSensitivity =
193
+ raw.sensitivity && typeof raw.sensitivity === "object" && !Array.isArray(raw.sensitivity)
194
+ ? (raw.sensitivity as Record<string, unknown>)
195
+ : undefined;
196
+
197
+ return {
198
+ version: config.version,
199
+ policy_version: config.policy_version,
200
+ environment: config.environment,
201
+ defaults: {
202
+ approval_ttl_seconds: config.defaults?.approval_ttl_seconds ?? 900,
203
+ persist_mode: config.defaults?.persist_mode ?? "compat"
204
+ },
205
+ hooks,
206
+ policies: normalizedPolicies,
207
+ sensitivity: {
208
+ path_rules: normalizeSensitivePathRules(rawSensitivity?.path_rules, "builtin")
209
+ },
210
+ file_rules: normalizeFileRules(raw.file_rules),
211
+ dlp: {
212
+ on_dlp_hit: dlp.on_dlp_hit,
213
+ patterns: dlp.patterns
214
+ },
215
+ event_sink: {
216
+ timeout_ms: config.event_sink?.timeout_ms ?? 3000,
217
+ max_buffer: config.event_sink?.max_buffer ?? 100,
218
+ retry_limit: config.event_sink?.retry_limit ?? 3,
219
+ ...(webhookUrl !== undefined ? { webhook_url: webhookUrl } : {})
220
+ }
221
+ };
222
+ }
@@ -0,0 +1,31 @@
1
+ import type { ResourceScope } from "../../types.ts";
2
+
3
+ export interface ResourceContext {
4
+ resourceScope: ResourceScope;
5
+ resourcePaths: string[];
6
+ }
7
+
8
+ export interface ToolContext {
9
+ toolGroup?: string;
10
+ operation?: string;
11
+ resourceScope: ResourceScope;
12
+ resourcePaths: string[];
13
+ tags: string[];
14
+ }
15
+
16
+ export interface DestinationContext {
17
+ destinationType?: "internal" | "public" | "unknown" | "personal_storage" | "paste_service";
18
+ destDomain?: string;
19
+ destIpClass?: "loopback" | "private" | "public" | "unknown";
20
+ }
21
+
22
+ export interface LabelContext {
23
+ assetLabels: string[];
24
+ dataLabels: string[];
25
+ }
26
+
27
+ export interface VolumeContext {
28
+ fileCount?: number;
29
+ bytes?: number;
30
+ recordCount?: number;
31
+ }
@@ -0,0 +1,40 @@
1
+ export interface StoredApprovalNotification {
2
+ channel: string;
3
+ to: string;
4
+ account_id?: string;
5
+ thread_id?: number;
6
+ message_id?: string;
7
+ sent_at?: string;
8
+ }
9
+
10
+ export interface StoredApprovalRecord {
11
+ approval_id: string;
12
+ request_key: string;
13
+ session_scope: string;
14
+ status: "pending" | "approved" | "rejected" | "expired";
15
+ requested_at: string;
16
+ expires_at: string;
17
+ policy_version: string;
18
+ actor_id: string;
19
+ scope: string;
20
+ tool_name: string;
21
+ resource_scope: string;
22
+ resource_paths: string[];
23
+ reason_codes: string[];
24
+ rule_ids: string[];
25
+ args_summary?: string;
26
+ approver?: string;
27
+ decided_at?: string;
28
+ notifications: StoredApprovalNotification[];
29
+ }
30
+
31
+ export interface ApprovalRepository {
32
+ create(record: Omit<StoredApprovalRecord, "notifications">): StoredApprovalRecord;
33
+ getById(approvalId: string): StoredApprovalRecord | undefined;
34
+ findApproved(sessionScope: string, requestKey: string): StoredApprovalRecord | undefined;
35
+ findPending(sessionScope: string, requestKey: string): StoredApprovalRecord | undefined;
36
+ resolve(approvalId: string, approver: string, decision: "approved" | "rejected", metadata?: { expires_at?: string }): StoredApprovalRecord | undefined;
37
+ updateNotifications(approvalId: string, notifications: StoredApprovalNotification[]): StoredApprovalRecord | undefined;
38
+ listPending(limit: number): StoredApprovalRecord[];
39
+ close(): void;
40
+ }
@@ -0,0 +1,29 @@
1
+ export type ApprovalChannel = "telegram" | "discord" | "slack" | "signal" | "imessage" | "whatsapp" | "line";
2
+
3
+ export interface NotificationTarget {
4
+ channel: ApprovalChannel;
5
+ to: string;
6
+ accountId?: string;
7
+ threadId?: string | number;
8
+ }
9
+
10
+ export interface NotificationOptions {
11
+ buttons?: Array<Array<{
12
+ text: string;
13
+ callback_data: string;
14
+ style?: string;
15
+ }>>;
16
+ }
17
+
18
+ export interface NotificationResult {
19
+ channel: ApprovalChannel;
20
+ to: string;
21
+ accountId?: string;
22
+ threadId?: number;
23
+ messageId?: string;
24
+ sentAt: string;
25
+ }
26
+
27
+ export interface NotificationPort {
28
+ send(target: NotificationTarget, message: string, options?: NotificationOptions): Promise<NotificationResult>;
29
+ }
@@ -0,0 +1,22 @@
1
+ export interface OpenClawLogger {
2
+ info?: (message: string) => void;
3
+ warn?: (message: string) => void;
4
+ error?: (message: string) => void;
5
+ }
6
+
7
+ export interface OpenClawConfig {
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ export interface OpenClawAdapter {
12
+ readonly logger: OpenClawLogger;
13
+ readonly config: OpenClawConfig;
14
+
15
+ sendTelegram(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }>;
16
+ sendDiscord(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }>;
17
+ sendSlack(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }>;
18
+ sendSignal(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }>;
19
+ sendIMessage(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }>;
20
+ sendWhatsApp(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }>;
21
+ sendLine(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }>;
22
+ }
@@ -0,0 +1,163 @@
1
+ import type { AccountPolicyMode, AccountPolicyRecord } from "../../types.ts";
2
+
3
+ export type AccountDecisionOverride = {
4
+ decision: "allow";
5
+ decision_source: "account";
6
+ reason_codes: string[];
7
+ policy: AccountPolicyRecord;
8
+ };
9
+
10
+ function normalizeString(value: unknown): string | undefined {
11
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
12
+ }
13
+
14
+ function normalizeMode(value: unknown): AccountPolicyMode {
15
+ return value === "default_allow" ? "default_allow" : "apply_rules";
16
+ }
17
+
18
+ function enforceSingleAdmin(policies: AccountPolicyRecord[]): AccountPolicyRecord[] {
19
+ let selectedAdminSubject: string | undefined;
20
+ for (const policy of policies) {
21
+ if (policy.is_admin) {
22
+ selectedAdminSubject = policy.subject;
23
+ }
24
+ }
25
+ if (!selectedAdminSubject) {
26
+ return policies;
27
+ }
28
+ return policies.map((policy) =>
29
+ policy.is_admin && policy.subject !== selectedAdminSubject
30
+ ? {
31
+ ...policy,
32
+ is_admin: false,
33
+ }
34
+ : policy,
35
+ );
36
+ }
37
+
38
+ function normalizePolicyEntry(value: unknown): AccountPolicyRecord | undefined {
39
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
40
+ return undefined;
41
+ }
42
+
43
+ const subject = normalizeString((value as { subject?: unknown }).subject);
44
+ if (!subject) {
45
+ return undefined;
46
+ }
47
+
48
+ const record: AccountPolicyRecord = {
49
+ subject,
50
+ mode: normalizeMode((value as { mode?: unknown }).mode),
51
+ is_admin: (value as { is_admin?: unknown }).is_admin === true,
52
+ };
53
+
54
+ const optionalFields = [
55
+ "label",
56
+ "session_key",
57
+ "session_id",
58
+ "agent_id",
59
+ "channel",
60
+ "chat_type",
61
+ "updated_at",
62
+ ] as const;
63
+
64
+ for (const field of optionalFields) {
65
+ const normalized = normalizeString((value as Record<string, unknown>)[field]);
66
+ if (normalized) {
67
+ record[field] = normalized;
68
+ }
69
+ }
70
+
71
+ return record;
72
+ }
73
+
74
+ export function sanitizeAccountPolicies(input: unknown): AccountPolicyRecord[] {
75
+ if (!Array.isArray(input)) {
76
+ return [];
77
+ }
78
+
79
+ const deduped = new Map<string, AccountPolicyRecord>();
80
+ for (const entry of input) {
81
+ const normalized = normalizePolicyEntry(entry);
82
+ if (!normalized) {
83
+ continue;
84
+ }
85
+ deduped.set(normalized.subject, normalized);
86
+ }
87
+ return enforceSingleAdmin(Array.from(deduped.values()));
88
+ }
89
+
90
+ export function canonicalizeAccountPolicies(input: unknown): AccountPolicyRecord[] {
91
+ return sanitizeAccountPolicies(input)
92
+ .map((policy) => {
93
+ const canonical: AccountPolicyRecord = {
94
+ subject: policy.subject,
95
+ mode: policy.mode,
96
+ is_admin: policy.is_admin,
97
+ };
98
+
99
+ const optionalFields = [
100
+ "label",
101
+ "session_key",
102
+ "session_id",
103
+ "agent_id",
104
+ "channel",
105
+ "chat_type",
106
+ "updated_at",
107
+ ] as const;
108
+
109
+ for (const field of optionalFields) {
110
+ if (policy[field]) {
111
+ canonical[field] = policy[field];
112
+ }
113
+ }
114
+
115
+ return canonical;
116
+ })
117
+ .sort((left, right) => left.subject.localeCompare(right.subject));
118
+ }
119
+
120
+ export class AccountPolicyEngine {
121
+ #policiesBySubject: Map<string, AccountPolicyRecord>;
122
+
123
+ constructor(policies: unknown) {
124
+ this.#policiesBySubject = new Map(
125
+ sanitizeAccountPolicies(policies).map((policy) => [policy.subject, policy]),
126
+ );
127
+ }
128
+
129
+ getPolicy(subject: string | undefined): AccountPolicyRecord | undefined {
130
+ if (!subject) {
131
+ return undefined;
132
+ }
133
+ return this.#policiesBySubject.get(subject);
134
+ }
135
+
136
+ listPolicies(): AccountPolicyRecord[] {
137
+ return Array.from(this.#policiesBySubject.values());
138
+ }
139
+
140
+ evaluate(subject: string | undefined): AccountDecisionOverride | undefined {
141
+ const policy = this.getPolicy(subject);
142
+ if (!policy) {
143
+ return undefined;
144
+ }
145
+ if (policy.mode === "default_allow") {
146
+ return {
147
+ decision: "allow",
148
+ decision_source: "account",
149
+ reason_codes: ["ACCOUNT_DEFAULT_ALLOW"],
150
+ policy
151
+ };
152
+ }
153
+ return undefined;
154
+ }
155
+
156
+ static sanitize(input: unknown): AccountPolicyRecord[] {
157
+ return sanitizeAccountPolicies(input);
158
+ }
159
+
160
+ static canonicalize(input: unknown): AccountPolicyRecord[] {
161
+ return canonicalizeAccountPolicies(input);
162
+ }
163
+ }