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,173 @@
1
+ import type {
2
+ NotificationPort,
3
+ NotificationTarget,
4
+ NotificationOptions,
5
+ NotificationResult,
6
+ ApprovalChannel
7
+ } from "../../domain/ports/notification_port.ts";
8
+ import type { OpenClawAdapter } from "../../domain/ports/openclaw_adapter.ts";
9
+
10
+ function normalizeThreadId(threadId: string | number | undefined): number | undefined {
11
+ if (typeof threadId === "number" && Number.isInteger(threadId)) {
12
+ return threadId;
13
+ }
14
+ if (typeof threadId === "string" && /^\d+$/.test(threadId.trim())) {
15
+ return Number(threadId.trim());
16
+ }
17
+ return undefined;
18
+ }
19
+
20
+ function nowIsoString(): string {
21
+ return new Date(Date.now()).toISOString();
22
+ }
23
+
24
+ abstract class BaseNotificationAdapter implements NotificationPort {
25
+ constructor(protected adapter: OpenClawAdapter) {}
26
+
27
+ abstract send(target: NotificationTarget, message: string, options?: NotificationOptions): Promise<NotificationResult>;
28
+
29
+ protected createBaseResult(target: NotificationTarget): NotificationResult {
30
+ const result: NotificationResult = {
31
+ channel: target.channel,
32
+ to: target.to,
33
+ sentAt: nowIsoString()
34
+ };
35
+ if (target.accountId) {
36
+ result.accountId = target.accountId;
37
+ }
38
+ const threadId = normalizeThreadId(target.threadId);
39
+ if (threadId !== undefined) {
40
+ result.threadId = threadId;
41
+ }
42
+ return result;
43
+ }
44
+ }
45
+
46
+ class TelegramNotificationAdapter extends BaseNotificationAdapter {
47
+ async send(target: NotificationTarget, message: string, options?: NotificationOptions): Promise<NotificationResult> {
48
+ const result = await this.adapter.sendTelegram(target.to, message, {
49
+ cfg: this.adapter.config,
50
+ ...(target.accountId ? { accountId: target.accountId } : {}),
51
+ ...(normalizeThreadId(target.threadId) !== undefined ? { messageThreadId: normalizeThreadId(target.threadId) } : {}),
52
+ ...(options?.buttons ? { buttons: options.buttons } : {})
53
+ });
54
+
55
+ const notification = this.createBaseResult(target);
56
+ if (result?.messageId) {
57
+ notification.messageId = result.messageId;
58
+ }
59
+ return notification;
60
+ }
61
+ }
62
+
63
+ class DiscordNotificationAdapter extends BaseNotificationAdapter {
64
+ async send(target: NotificationTarget, message: string, _options?: NotificationOptions): Promise<NotificationResult> {
65
+ const result = await this.adapter.sendDiscord(target.to, message, {
66
+ cfg: this.adapter.config,
67
+ ...(target.accountId ? { accountId: target.accountId } : {})
68
+ });
69
+
70
+ const notification = this.createBaseResult(target);
71
+ if (result?.messageId) {
72
+ notification.messageId = result.messageId;
73
+ }
74
+ return notification;
75
+ }
76
+ }
77
+
78
+ class SlackNotificationAdapter extends BaseNotificationAdapter {
79
+ async send(target: NotificationTarget, message: string, _options?: NotificationOptions): Promise<NotificationResult> {
80
+ const result = await this.adapter.sendSlack(target.to, message, {
81
+ cfg: this.adapter.config,
82
+ ...(target.accountId ? { accountId: target.accountId } : {})
83
+ });
84
+
85
+ const notification = this.createBaseResult(target);
86
+ if (result?.messageId) {
87
+ notification.messageId = result.messageId;
88
+ }
89
+ return notification;
90
+ }
91
+ }
92
+
93
+ class SignalNotificationAdapter extends BaseNotificationAdapter {
94
+ async send(target: NotificationTarget, message: string, _options?: NotificationOptions): Promise<NotificationResult> {
95
+ const result = await this.adapter.sendSignal(target.to, message, {
96
+ cfg: this.adapter.config,
97
+ ...(target.accountId ? { accountId: target.accountId } : {})
98
+ });
99
+
100
+ const notification = this.createBaseResult(target);
101
+ if (result?.messageId) {
102
+ notification.messageId = result.messageId;
103
+ }
104
+ return notification;
105
+ }
106
+ }
107
+
108
+ class IMessageNotificationAdapter extends BaseNotificationAdapter {
109
+ async send(target: NotificationTarget, message: string, _options?: NotificationOptions): Promise<NotificationResult> {
110
+ const result = await this.adapter.sendIMessage(target.to, message, {
111
+ cfg: this.adapter.config,
112
+ ...(target.accountId ? { accountId: target.accountId } : {})
113
+ });
114
+
115
+ const notification = this.createBaseResult(target);
116
+ if (result?.messageId) {
117
+ notification.messageId = result.messageId;
118
+ }
119
+ return notification;
120
+ }
121
+ }
122
+
123
+ class WhatsAppNotificationAdapter extends BaseNotificationAdapter {
124
+ async send(target: NotificationTarget, message: string, _options?: NotificationOptions): Promise<NotificationResult> {
125
+ const result = await this.adapter.sendWhatsApp(target.to, message, {
126
+ cfg: this.adapter.config
127
+ });
128
+
129
+ const notification = this.createBaseResult(target);
130
+ if (result?.messageId) {
131
+ notification.messageId = result.messageId;
132
+ }
133
+ return notification;
134
+ }
135
+ }
136
+
137
+ class LineNotificationAdapter extends BaseNotificationAdapter {
138
+ async send(target: NotificationTarget, message: string, _options?: NotificationOptions): Promise<NotificationResult> {
139
+ const result = await this.adapter.sendLine(target.to, message, {
140
+ cfg: this.adapter.config,
141
+ ...(target.accountId ? { accountId: target.accountId } : {})
142
+ });
143
+
144
+ const notification = this.createBaseResult(target);
145
+ if (result?.messageId) {
146
+ notification.messageId = result.messageId;
147
+ }
148
+ return notification;
149
+ }
150
+ }
151
+
152
+ export class NotificationAdapterFactory {
153
+ static create(channel: ApprovalChannel, adapter: OpenClawAdapter): NotificationPort {
154
+ switch (channel) {
155
+ case "telegram":
156
+ return new TelegramNotificationAdapter(adapter);
157
+ case "discord":
158
+ return new DiscordNotificationAdapter(adapter);
159
+ case "slack":
160
+ return new SlackNotificationAdapter(adapter);
161
+ case "signal":
162
+ return new SignalNotificationAdapter(adapter);
163
+ case "imessage":
164
+ return new IMessageNotificationAdapter(adapter);
165
+ case "whatsapp":
166
+ return new WhatsAppNotificationAdapter(adapter);
167
+ case "line":
168
+ return new LineNotificationAdapter(adapter);
169
+ default:
170
+ throw new Error(`Unsupported notification channel: ${channel}`);
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,59 @@
1
+ import type { OpenClawAdapter, OpenClawLogger, OpenClawConfig } from "../../domain/ports/openclaw_adapter.ts";
2
+
3
+ // This is a type from openclaw package that we need to reference
4
+ type OpenClawPluginApi = {
5
+ logger: OpenClawLogger;
6
+ config: OpenClawConfig;
7
+ pluginConfig?: unknown;
8
+ runtime: {
9
+ channel: {
10
+ telegram: { sendMessageTelegram: (to: string, text: string, opts?: Record<string, unknown>) => Promise<{ messageId?: string }> };
11
+ discord: { sendMessageDiscord: (to: string, text: string, opts?: Record<string, unknown>) => Promise<{ messageId?: string }> };
12
+ slack: { sendMessageSlack: (to: string, text: string, opts?: Record<string, unknown>) => Promise<{ messageId?: string }> };
13
+ signal: { sendMessageSignal: (to: string, text: string, opts?: Record<string, unknown>) => Promise<{ messageId?: string }> };
14
+ imessage: { sendMessageIMessage: (to: string, text: string, opts?: Record<string, unknown>) => Promise<{ messageId?: string }> };
15
+ whatsapp: { sendMessageWhatsApp: (to: string, text: string, opts?: Record<string, unknown>) => Promise<{ messageId?: string }> };
16
+ line: { pushMessageLine: (to: string, text: string, opts?: Record<string, unknown>) => Promise<{ messageId?: string }> };
17
+ };
18
+ };
19
+ };
20
+
21
+ export class OpenClawAdapterImpl implements OpenClawAdapter {
22
+ readonly logger: OpenClawLogger;
23
+ readonly config: OpenClawConfig;
24
+ private api: OpenClawPluginApi;
25
+
26
+ constructor(api: OpenClawPluginApi) {
27
+ this.api = api;
28
+ this.logger = api.logger;
29
+ this.config = api.config;
30
+ }
31
+
32
+ async sendTelegram(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }> {
33
+ return this.api.runtime.channel.telegram.sendMessageTelegram(to, text, opts);
34
+ }
35
+
36
+ async sendDiscord(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }> {
37
+ return this.api.runtime.channel.discord.sendMessageDiscord(to, text, opts);
38
+ }
39
+
40
+ async sendSlack(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }> {
41
+ return this.api.runtime.channel.slack.sendMessageSlack(to, text, opts);
42
+ }
43
+
44
+ async sendSignal(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }> {
45
+ return this.api.runtime.channel.signal.sendMessageSignal(to, text, opts);
46
+ }
47
+
48
+ async sendIMessage(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }> {
49
+ return this.api.runtime.channel.imessage.sendMessageIMessage(to, text, opts);
50
+ }
51
+
52
+ async sendWhatsApp(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }> {
53
+ return this.api.runtime.channel.whatsapp.sendMessageWhatsApp(to, text, opts);
54
+ }
55
+
56
+ async sendLine(to: string, text: string, opts?: Record<string, unknown>): Promise<{ messageId?: string }> {
57
+ return this.api.runtime.channel.line.pushMessageLine(to, text, opts);
58
+ }
59
+ }
@@ -0,0 +1,105 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ export interface SecurityClawPluginConfig {
5
+ configPath?: string;
6
+ overridePath?: string;
7
+ dbPath?: string;
8
+ webhookUrl?: string;
9
+ policyVersion?: string;
10
+ environment?: string;
11
+ approvalTtlSeconds?: number;
12
+ persistMode?: "strict" | "compat";
13
+ decisionLogMaxLength?: number;
14
+ statusPath?: string;
15
+ adminAutoStart?: boolean;
16
+ adminPort?: number;
17
+ }
18
+
19
+ export interface ResolvedPluginRuntime {
20
+ configPath: string;
21
+ dbPath: string;
22
+ legacyOverridePath: string;
23
+ statusPath: string;
24
+ protectedDataDir?: string;
25
+ protectedDbPaths: string[];
26
+ }
27
+
28
+ const SECURITYCLAW_EXTENSION_STATE_SEGMENTS = ["extensions", "securityclaw"] as const;
29
+ const DEFAULT_DB_FILE_NAME = "securityclaw.db";
30
+ const DEFAULT_STATUS_FILE_NAME = "securityclaw-status.json";
31
+ const SQLITE_ARTIFACT_SUFFIXES = ["", "-shm", "-wal"] as const;
32
+
33
+ function hasText(value: string | undefined): value is string {
34
+ return typeof value === "string" && value.trim().length > 0;
35
+ }
36
+
37
+ function resolveAbsoluteStoragePath(configuredPath: string | undefined): string | undefined {
38
+ return hasText(configuredPath) && path.isAbsolute(configuredPath) ? path.resolve(configuredPath) : undefined;
39
+ }
40
+
41
+ function sqliteArtifactPaths(dbPath: string): string[] {
42
+ return SQLITE_ARTIFACT_SUFFIXES.map((suffix) => `${dbPath}${suffix}`);
43
+ }
44
+
45
+ export function resolveDefaultOpenClawStateDir(env: NodeJS.ProcessEnv = process.env): string {
46
+ return hasText(env.OPENCLAW_HOME) ? path.resolve(env.OPENCLAW_HOME) : path.join(os.homedir(), ".openclaw");
47
+ }
48
+
49
+ export function resolveSecurityClawStateDir(stateDir: string): string {
50
+ const normalizedStateDir = path.resolve(stateDir);
51
+ const pluginSuffix = path.join(...SECURITYCLAW_EXTENSION_STATE_SEGMENTS);
52
+ if (
53
+ normalizedStateDir === pluginSuffix ||
54
+ normalizedStateDir.endsWith(`${path.sep}${pluginSuffix}`)
55
+ ) {
56
+ return normalizedStateDir;
57
+ }
58
+ return path.join(normalizedStateDir, ...SECURITYCLAW_EXTENSION_STATE_SEGMENTS);
59
+ }
60
+
61
+ export function resolveDefaultSecurityClawDbPath(stateDir: string): string {
62
+ return path.join(resolveSecurityClawStateDir(stateDir), "data", DEFAULT_DB_FILE_NAME);
63
+ }
64
+
65
+ export function resolveDefaultSecurityClawStatusPath(stateDir: string): string {
66
+ return path.join(resolveSecurityClawStateDir(stateDir), "runtime", DEFAULT_STATUS_FILE_NAME);
67
+ }
68
+
69
+ export class PluginConfigParser {
70
+ static resolve(
71
+ pluginRoot: string,
72
+ pluginConfig: SecurityClawPluginConfig,
73
+ stateDir = resolveDefaultOpenClawStateDir(),
74
+ ): ResolvedPluginRuntime {
75
+ const defaultDbPath = resolveDefaultSecurityClawDbPath(stateDir);
76
+ const defaultStatusPath = resolveDefaultSecurityClawStatusPath(stateDir);
77
+ const configuredDbPath = resolveAbsoluteStoragePath(pluginConfig.dbPath);
78
+ const configuredStatusPath = resolveAbsoluteStoragePath(pluginConfig.statusPath);
79
+ const usingDefaultDbPath = configuredDbPath === undefined;
80
+ const configPath = pluginConfig.configPath
81
+ ? path.isAbsolute(pluginConfig.configPath)
82
+ ? pluginConfig.configPath
83
+ : path.resolve(pluginRoot, pluginConfig.configPath)
84
+ : path.resolve(pluginRoot, "./config/policy.default.yaml");
85
+
86
+ const dbPath = configuredDbPath ?? defaultDbPath;
87
+
88
+ const legacyOverridePath = pluginConfig.overridePath
89
+ ? path.isAbsolute(pluginConfig.overridePath)
90
+ ? pluginConfig.overridePath
91
+ : path.resolve(pluginRoot, pluginConfig.overridePath)
92
+ : path.resolve(pluginRoot, "./config/policy.overrides.json");
93
+
94
+ const statusPath = configuredStatusPath ?? defaultStatusPath;
95
+
96
+ return {
97
+ configPath,
98
+ dbPath,
99
+ legacyOverridePath,
100
+ statusPath,
101
+ ...(usingDefaultDbPath ? { protectedDataDir: path.dirname(dbPath) } : {}),
102
+ protectedDbPaths: sqliteArtifactPaths(dbPath),
103
+ };
104
+ }
105
+ }