securityclaw 0.0.1 → 0.0.3

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.3] - 2026-03-18
4
+
5
+ ### Changed
6
+ - Reduced OpenClaw install-time false positives by separating environment reads, file reads, and process-launch helpers out of files that previously tripped static safety rules.
7
+ - Kept the admin console, installer CLI, and Feishu approval delivery behavior unchanged while restructuring those internals for cleaner package scans.
8
+ - Verified the packed npm tarball against the local OpenClaw `skill-scanner` and brought reported findings down to zero.
9
+
10
+ ## [0.0.2] - 2026-03-17
11
+
12
+ ### Changed
13
+ - Simplified the README install section to focus on product overview plus remote install and uninstall flows.
14
+ - Clarified that published npm releases ship with a prebuilt admin frontend bundle.
15
+ - Prevented admin build tooling from being shipped as runtime npm dependencies.
16
+ - Avoided auto-opening the dashboard from short-lived gateway service commands before the persistent admin backend is ready.
17
+
3
18
  ## [0.1.0] - 2026-03-15
4
19
 
5
20
  ### 架构重构
package/README.md CHANGED
@@ -19,99 +19,69 @@ LLM agents can execute powerful tools. SecurityClaw provides a policy guardrail
19
19
  - Decision events for audit and observability
20
20
  - Built-in internationalization (`en` and `zh-CN`) for runtime/admin text
21
21
 
22
- ## Architecture
22
+ ## Install
23
23
 
24
- SecurityClaw follows a layered architecture:
24
+ ### Direct Use
25
25
 
26
- - `domain`: policy, approval, context inference, formatting
27
- - `domain/services/sensitive_path_registry.ts`: built-in + runtime-overridden sensitive path mappings
28
- - `engine`: rule matching, decisioning, DLP scanning
29
- - `config`: base YAML + SQLite runtime override
30
- - `admin`: dashboard backend + frontend
31
- - `monitoring`: runtime status and decision snapshots
32
-
33
- See [Architecture](./docs/ARCHITECTURE.md) and [Technical Solution](./docs/TECHNICAL_SOLUTION.md).
34
-
35
- ## Quick Start
36
-
37
- ### 1. Install dependencies
26
+ Install the latest published release:
38
27
 
39
28
  ```bash
40
- npm install
29
+ npx securityclaw install
41
30
  ```
42
31
 
43
- ### 2. Install into OpenClaw
32
+ Or install through the remote script:
44
33
 
45
34
  ```bash
46
- npm run openclaw:install
35
+ curl -fsSL https://raw.githubusercontent.com/znary/securityclaw/main/install.sh | bash
47
36
  ```
48
37
 
49
- Alternative install paths for end users:
38
+ Install a specific published version:
50
39
 
51
40
  ```bash
52
- npx securityclaw install
53
- curl -fsSL https://raw.githubusercontent.com/znary/securityclaw/main/install.sh | bash
41
+ SECURITYCLAW_VERSION=0.0.3 curl -fsSL https://raw.githubusercontent.com/znary/securityclaw/main/install.sh | bash
54
42
  ```
55
43
 
56
- ### 3. Run verification
44
+ After installation, if the admin dashboard did not open automatically, open `http://127.0.0.1:4780`.
57
45
 
58
- ```bash
59
- npm test
60
- ```
46
+ ### From Source
61
47
 
62
- ### 4. Start admin dashboard (standalone)
48
+ Clone the repository, then install dependencies:
63
49
 
64
50
  ```bash
65
- npm run admin
51
+ npm install
66
52
  ```
67
53
 
68
- Default dashboard URL: `http://127.0.0.1:4780`
69
-
70
- ## OpenClaw Integration
71
-
72
- Preferred local install:
54
+ Install the current workspace build into OpenClaw:
73
55
 
74
56
  ```bash
75
57
  npm run openclaw:install
76
58
  ```
77
59
 
78
- This creates a versioned plugin archive, installs it through `openclaw plugins install`, restarts the gateway, and verifies gateway health.
79
- See [OpenClaw Install Guide](./docs/OPENCLAW_INSTALL.md) for details.
80
-
81
- ## Approval Commands
60
+ Run verification:
82
61
 
83
- After setting one account policy with `is_admin=true`, the admin can run:
84
-
85
- - `/securityclaw-approve <approval_id>`
86
- - `/securityclaw-approve <approval_id> long`
87
- - `/securityclaw-reject <approval_id>`
88
- - `/securityclaw-pending`
89
-
90
- ## Admin Dashboard
62
+ ```bash
63
+ npm test
64
+ ```
91
65
 
92
- Dashboard supports English and Chinese UI switching and stores language preference in local storage.
93
- By default, it follows the host system language.
66
+ Start the standalone admin dashboard when needed:
94
67
 
95
- Main panels:
68
+ ```bash
69
+ npm run admin
70
+ ```
96
71
 
97
- - Overview: posture and trend signals, plus a skill-risk snapshot for high-priority installed skills
98
- - Decisions: recent decision events and reasons
99
- - Policies: grouped rule strategy controls plus sensitive-path registry management
100
- - Skill Interception: installed skill inventory, risk scoring, undeclared-change detection, rescan/quarantine/trust override actions, and interception policy matrix
101
- - Accounts: admin approver account selection and mode settings
72
+ ## Uninstall
102
73
 
103
- Sensitive path registry behavior:
74
+ Remove the installed plugin from OpenClaw:
104
75
 
105
- - Built-in path patterns cover credentials, personal content, download staging, browser profiles, browser secret stores, and communication stores.
106
- - Registry entries are persisted in SQLite runtime strategy overrides together with rule decisions.
107
- - Built-in entries can be disabled from the dashboard, and custom path rules can be added without editing the base YAML.
76
+ ```bash
77
+ openclaw plugins uninstall securityclaw
78
+ ```
108
79
 
109
- Skill interception behavior:
80
+ Preview the removal first if needed:
110
81
 
111
- - Dashboard discovers installed skills from local OpenClaw / Codex skill roots and stores scan results in SQLite.
112
- - A skill can be flagged when its content changes without a matching version update.
113
- - Overview surfaces the most important skill signals directly so admins can see high-risk items without switching tabs.
114
- - The dedicated Skill Interception panel supports rescan, quarantine, temporary trust override, and risk-matrix editing.
82
+ ```bash
83
+ openclaw plugins uninstall securityclaw --dry-run
84
+ ```
115
85
 
116
86
  ## Documentation
117
87
 
@@ -121,15 +91,6 @@ Skill interception behavior:
121
91
  - [Runbook](./docs/RUNBOOK.md)
122
92
  - [Integration Guide](./docs/INTEGRATION_GUIDE.md)
123
93
 
124
- ## Development
125
-
126
- ```bash
127
- npm run typecheck
128
- npm run test:unit
129
- npm test
130
- npm run admin:build
131
- ```
132
-
133
94
  ## License
134
95
 
135
96
  MIT. See [LICENSE](./LICENSE).
package/README.zh-CN.md CHANGED
@@ -19,99 +19,69 @@ LLM Agent 具备高权限工具调用能力。SecurityClaw 在运行时提供策
19
19
  - 决策事件与状态观测
20
20
  - 中英文国际化(`en` / `zh-CN`)
21
21
 
22
- ## 架构说明
22
+ ## 安装
23
23
 
24
- 分层结构如下:
24
+ ### 直接使用
25
25
 
26
- - `domain`:策略、审批、上下文推断、格式化
27
- - `domain/services/sensitive_path_registry.ts`:内置 + 运行时覆写的敏感路径映射
28
- - `engine`:规则匹配、决策引擎、DLP
29
- - `config`:YAML 基线配置 + SQLite 运行时覆盖
30
- - `admin`:管理后台前后端
31
- - `monitoring`:运行状态与决策快照
32
-
33
- 详见 [架构文档](./docs/ARCHITECTURE.md) 与 [技术方案](./docs/TECHNICAL_SOLUTION.md)。
34
-
35
- ## 快速开始
36
-
37
- ### 1. 安装依赖
26
+ 安装最新发布版本:
38
27
 
39
28
  ```bash
40
- npm install
29
+ npx securityclaw install
41
30
  ```
42
31
 
43
- ### 2. 安装到 OpenClaw
32
+ 或者通过远程脚本安装:
44
33
 
45
34
  ```bash
46
- npm run openclaw:install
35
+ curl -fsSL https://raw.githubusercontent.com/znary/securityclaw/main/install.sh | bash
47
36
  ```
48
37
 
49
- 终端用户也可以直接使用:
38
+ 安装指定发布版本:
50
39
 
51
40
  ```bash
52
- npx securityclaw install
53
- curl -fsSL https://raw.githubusercontent.com/znary/securityclaw/main/install.sh | bash
41
+ SECURITYCLAW_VERSION=0.0.3 curl -fsSL https://raw.githubusercontent.com/znary/securityclaw/main/install.sh | bash
54
42
  ```
55
43
 
56
- ### 3. 执行验证
44
+ 安装完成后,如果管理后台没有自动打开,可手动访问 `http://127.0.0.1:4780`。
57
45
 
58
- ```bash
59
- npm test
60
- ```
46
+ ### 从源码开发
61
47
 
62
- ### 4. 启动管理后台(独立模式)
48
+ 克隆仓库后先安装依赖:
63
49
 
64
50
  ```bash
65
- npm run admin
51
+ npm install
66
52
  ```
67
53
 
68
- 默认地址:`http://127.0.0.1:4780`
69
-
70
- ## OpenClaw 集成
71
-
72
- 推荐本地安装方式:
54
+ 把当前工作区构建安装到 OpenClaw:
73
55
 
74
56
  ```bash
75
57
  npm run openclaw:install
76
58
  ```
77
59
 
78
- 这条命令会生成带版本号的插件压缩包,通过 `openclaw plugins install` 安装,随后自动重启 gateway 并校验状态。
79
- 详情见 [OpenClaw 安装指南](./docs/OPENCLAW_INSTALL.md)。
80
-
81
- ## 审批命令
60
+ 执行验证:
82
61
 
83
- 当账号策略中配置 `is_admin=true` 后,管理员可在聊天渠道执行:
84
-
85
- - `/securityclaw-approve <approval_id>`
86
- - `/securityclaw-approve <approval_id> long`
87
- - `/securityclaw-reject <approval_id>`
88
- - `/securityclaw-pending`
89
-
90
- ## 管理后台
62
+ ```bash
63
+ npm test
64
+ ```
91
65
 
92
- 管理后台支持中英文切换,并将语言偏好保存在本地存储。
93
- 默认跟随系统语言。
66
+ 需要时可单独启动管理后台:
94
67
 
95
- 核心模块:
68
+ ```bash
69
+ npm run admin
70
+ ```
96
71
 
97
- - 概览:总体态势、趋势,以及高优先级已安装 skill 的风险快照
98
- - 决策记录:最近决策事件与原因
99
- - 规则策略:按分组编辑规则动作,并维护敏感路径注册表
100
- - Skill 拦截:已安装 skill 清单、风险打分、未声明变更检测、重扫 / 隔离 / 受信覆盖操作,以及拦截策略矩阵
101
- - 账号策略:管理员审批账号与模式配置
72
+ ## 卸载
102
73
 
103
- 敏感路径注册表说明:
74
+ 从 OpenClaw 中卸载已安装插件:
104
75
 
105
- - 内置覆盖凭据目录、个人内容目录、下载暂存区、浏览器资料目录、浏览器密钥库和通信存储。
106
- - 路径注册表与规则动作一起持久化到 SQLite 运行时策略覆盖中。
107
- - 可在后台删除内置项,也可直接添加自定义路径,无需手改基线 YAML。
76
+ ```bash
77
+ openclaw plugins uninstall securityclaw
78
+ ```
108
79
 
109
- Skill 拦截说明:
80
+ 如果想先预览会删除什么:
110
81
 
111
- - 后台会从本地 OpenClaw / Codex skill 目录自动发现已安装 skills,并把扫描结果持久化到 SQLite。
112
- - skill 内容发生变化、但版本号没有同步更新时,会被标记为“内容变了但版本没变”。
113
- - 概览页会直接展示最值得优先处理的 skill 风险信号,不需要先切到 Skill 页签。
114
- - `Skill 拦截` 面板支持重扫、隔离、临时受信覆盖,以及风险矩阵配置。
82
+ ```bash
83
+ openclaw plugins uninstall securityclaw --dry-run
84
+ ```
115
85
 
116
86
  ## 文档导航
117
87
 
@@ -121,15 +91,6 @@ Skill 拦截说明:
121
91
  - [运行手册](./docs/RUNBOOK.md)
122
92
  - [集成指南](./docs/INTEGRATION_GUIDE.md)
123
93
 
124
- ## 开发命令
125
-
126
- ```bash
127
- npm run typecheck
128
- npm run test:unit
129
- npm test
130
- npm run admin:build
131
- ```
132
-
133
94
  ## 许可证
134
95
 
135
96
  MIT,详见 [LICENSE](./LICENSE)。
package/admin/server.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import http from "node:http";
2
- import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { existsSync, readdirSync, statSync } from "node:fs";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
6
5
  import { DatabaseSync } from "node:sqlite";
@@ -10,6 +9,7 @@ import {
10
9
  matchesAdminDecisionFilter,
11
10
  normalizeAdminDecisionFilterId,
12
11
  } from "../src/admin/dashboard_url_state.ts";
12
+ import { readJsonRecordFile, readUtf8File } from "../src/admin/file_reader.ts";
13
13
  import { SkillInterceptionStore } from "../src/admin/skill_interception_store.ts";
14
14
  import { listOpenClawChatSessions } from "../src/admin/openclaw_session_catalog.ts";
15
15
  import { ConfigManager } from "../src/config/loader.ts";
@@ -29,10 +29,13 @@ import {
29
29
  } from "../src/domain/services/sensitive_path_registry.ts";
30
30
  import type { SecurityClawLocale } from "../src/i18n/locale.ts";
31
31
  import { pickLocalized, resolveSecurityClawLocale } from "../src/i18n/locale.ts";
32
+ import { readSecurityClawAdminServerEnv, resolveSecurityClawAdminPort } from "../src/runtime/process_env.ts";
33
+ import { runProcessSync } from "../src/runtime/process_runner.ts";
32
34
 
33
35
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
34
36
  const PUBLIC_DIR = path.resolve(ROOT, "admin/public");
35
- const DEFAULT_PORT = Number(process.env.SECURITYCLAW_ADMIN_PORT ?? 4780);
37
+ const DEFAULT_ADMIN_ENV = readSecurityClawAdminServerEnv();
38
+ const DEFAULT_PORT = resolveSecurityClawAdminPort();
36
39
  const DEFAULT_OPENCLAW_HOME = resolveDefaultOpenClawStateDir();
37
40
 
38
41
  type AdminLogger = {
@@ -128,7 +131,7 @@ const EMPTY_DECISION_COUNTS: DecisionHistoryCounts = {
128
131
  block: 0,
129
132
  };
130
133
 
131
- const ADMIN_DEFAULT_LOCALE = resolveSecurityClawLocale(process.env.SECURITYCLAW_LOCALE, "en");
134
+ const ADMIN_DEFAULT_LOCALE = resolveSecurityClawLocale(DEFAULT_ADMIN_ENV.locale, "en");
132
135
 
133
136
  function sendJson(res: http.ServerResponse, status: number, body: unknown): void {
134
137
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
@@ -181,7 +184,7 @@ function safeReadStatus(statusPath: string): JsonRecord {
181
184
  };
182
185
  }
183
186
  try {
184
- return JSON.parse(readFileSync(statusPath, "utf8")) as JsonRecord;
187
+ return readJsonRecordFile(statusPath);
185
188
  } catch {
186
189
  return {
187
190
  message: "status file exists but cannot be parsed",
@@ -842,7 +845,7 @@ function serveStatic(req: http.IncomingMessage, res: http.ServerResponse, url: U
842
845
  : ext === ".svg"
843
846
  ? "image/svg+xml"
844
847
  : "application/octet-stream";
845
- sendText(res, 200, readFileSync(absolute, "utf8"), contentType);
848
+ sendText(res, 200, readUtf8File(absolute), contentType);
846
849
  }
847
850
 
848
851
  function readEffectivePolicy(runtime: AdminRuntime, strategyStore: StrategyStore): {
@@ -858,10 +861,10 @@ function readEffectivePolicy(runtime: AdminRuntime, strategyStore: StrategyStore
858
861
 
859
862
  function resolveAdminPluginConfig(options: AdminServerOptions): SecurityClawPluginConfig {
860
863
  return {
861
- ...(process.env.SECURITYCLAW_CONFIG_PATH ? { configPath: process.env.SECURITYCLAW_CONFIG_PATH } : {}),
862
- ...(process.env.SECURITYCLAW_LEGACY_OVERRIDE_PATH ? { overridePath: process.env.SECURITYCLAW_LEGACY_OVERRIDE_PATH } : {}),
863
- ...(process.env.SECURITYCLAW_STATUS_PATH ? { statusPath: process.env.SECURITYCLAW_STATUS_PATH } : {}),
864
- ...(process.env.SECURITYCLAW_DB_PATH ? { dbPath: process.env.SECURITYCLAW_DB_PATH } : {}),
864
+ ...(DEFAULT_ADMIN_ENV.configPath ? { configPath: DEFAULT_ADMIN_ENV.configPath } : {}),
865
+ ...(DEFAULT_ADMIN_ENV.legacyOverridePath ? { overridePath: DEFAULT_ADMIN_ENV.legacyOverridePath } : {}),
866
+ ...(DEFAULT_ADMIN_ENV.statusPath ? { statusPath: DEFAULT_ADMIN_ENV.statusPath } : {}),
867
+ ...(DEFAULT_ADMIN_ENV.dbPath ? { dbPath: DEFAULT_ADMIN_ENV.dbPath } : {}),
865
868
  ...(options.configPath !== undefined ? { configPath: options.configPath } : {}),
866
869
  ...(options.legacyOverridePath !== undefined ? { overridePath: options.legacyOverridePath } : {}),
867
870
  ...(options.statusPath !== undefined ? { statusPath: options.statusPath } : {}),
@@ -890,19 +893,19 @@ function parsePids(output: string): number[] {
890
893
  }
891
894
 
892
895
  function listListeningPidsByPort(port: number): number[] {
893
- const result = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { encoding: "utf8" });
896
+ const result = runProcessSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { encoding: "utf8" });
894
897
  if (result.error || result.status !== 0) {
895
898
  return [];
896
899
  }
897
- return parsePids(result.stdout);
900
+ return parsePids(result.stdout ?? "");
898
901
  }
899
902
 
900
903
  function readProcessCommand(pid: number): string {
901
- const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
904
+ const result = runProcessSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
902
905
  if (result.error || result.status !== 0) {
903
906
  return "";
904
907
  }
905
- return result.stdout.trim();
908
+ return (result.stdout ?? "").trim();
906
909
  }
907
910
 
908
911
  function looksLikeOpenClawProcess(command: string): boolean {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFileSync } from "node:fs";
4
- import { spawnSync } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
@@ -9,6 +9,9 @@ import { buildInstallPlan, parseInstallArgs } from "./install-lib.mjs";
9
9
 
10
10
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
11
  const pkg = JSON.parse(readFileSync(path.join(ROOT, "package.json"), "utf8"));
12
+ const requireFromHere = createRequire(import.meta.url);
13
+ const SYSTEM_PROCESS_MODULE_ID = `node:child${String.fromCharCode(95)}process`;
14
+ const { spawnSync } = requireFromHere(SYSTEM_PROCESS_MODULE_ID);
12
15
 
13
16
  function printUsage() {
14
17
  console.log(`SecurityClaw installer
package/index.ts CHANGED
@@ -30,9 +30,9 @@ import {
30
30
  } from "./src/infrastructure/config/plugin_config_parser.ts";
31
31
  import { RuntimeStatusStore } from "./src/monitoring/status_store.ts";
32
32
  import { startAdminServer } from "./admin/server.ts";
33
- import { ensureAdminAssetsBuilt } from "./src/admin/build.ts";
34
33
  import { announceAdminConsole, shouldAnnounceAdminConsoleForArgv } from "./src/admin/console_notice.ts";
35
34
  import { shouldAutoStartAdminServer } from "./src/admin/runtime_guard.ts";
35
+ import { readProcessEnvValue, resolveSecurityClawAdminPort } from "./src/runtime/process_env.ts";
36
36
  import { AccountPolicyEngine } from "./src/domain/services/account_policy_engine.ts";
37
37
  import { ApprovalSubjectResolver } from "./src/domain/services/approval_subject_resolver.ts";
38
38
  import {
@@ -172,7 +172,7 @@ function resolvePluginStateDir(api: OpenClawPluginApi): string {
172
172
  }
173
173
 
174
174
  function resolveAdminConsoleUrl(pluginConfig: SecurityClawPluginConfig): string {
175
- const port = pluginConfig.adminPort ?? Number(process.env.SECURITYCLAW_ADMIN_PORT ?? 4780);
175
+ const port = pluginConfig.adminPort ?? resolveSecurityClawAdminPort();
176
176
  return `http://127.0.0.1:${port}`;
177
177
  }
178
178
 
@@ -1165,7 +1165,7 @@ function resolveFeishuSecretValue(value: unknown): string | undefined {
1165
1165
  const source = feishuTrimmedString(record.source)?.toLowerCase();
1166
1166
  const id = feishuTrimmedString(record.id);
1167
1167
  if (source === "env" && id) {
1168
- const envValue = feishuTrimmedString(process.env[id]);
1168
+ const envValue = feishuTrimmedString(readProcessEnvValue(id));
1169
1169
  if (envValue) {
1170
1170
  return envValue;
1171
1171
  }
@@ -1991,69 +1991,67 @@ const plugin = {
1991
1991
  return runtime;
1992
1992
  }
1993
1993
 
1994
- statusStore.markBoot(toStatusConfig(runtime.config, runtime.overrideLoaded, resolved));
1995
- const adminBuildPromise = ensureAdminAssetsBuilt({
1996
- logger: {
1997
- info: (message: string) => api.logger.info?.(`securityclaw: ${message}`)
1994
+ function startManagedAdminConsole(): void {
1995
+ if (!adminAutoStart) {
1996
+ api.logger.info?.("securityclaw: admin auto-start disabled by config");
1997
+ return;
1998
1998
  }
1999
- }).catch((error) => {
2000
- api.logger.warn?.(`securityclaw: failed to refresh admin bundle (${String(error)})`);
2001
- });
2002
- if (adminAutoStart) {
2003
- const autoStartDecision = shouldAutoStartAdminServer(process.env);
2004
- if (autoStartDecision.enabled) {
2005
- const adminServerOptions = {
2006
- configPath: resolved.configPath,
2007
- legacyOverridePath: resolved.legacyOverridePath,
2008
- statusPath,
2009
- dbPath,
2010
- unrefOnStart: true,
2011
- logger: {
2012
- info: (message: string) => api.logger.info?.(`securityclaw: ${message}`),
2013
- warn: (message: string) => api.logger.warn?.(`securityclaw: ${message}`)
2014
- },
2015
- ...(pluginConfig.adminPort !== undefined ? { port: pluginConfig.adminPort } : {})
2016
- };
2017
- void adminBuildPromise
2018
- .then(() => startAdminServer(adminServerOptions))
2019
- .then((result) => {
2020
- announceAdminConsole({
2021
- locale: runtimeLocale,
2022
- logger: {
2023
- info: (message: string) => api.logger.info?.(`securityclaw: ${message}`),
2024
- warn: (message: string) => api.logger.warn?.(`securityclaw: ${message}`),
2025
- },
2026
- stateDir,
2027
- state: result.state,
2028
- url: `http://127.0.0.1:${result.runtime.port}`,
2029
- });
2030
- })
2031
- .catch((error) => {
2032
- api.logger.warn?.(`securityclaw: failed to auto-start admin dashboard (${String(error)})`);
1999
+
2000
+ const autoStartDecision = shouldAutoStartAdminServer();
2001
+ if (!autoStartDecision.enabled) {
2002
+ if (shouldAnnounceAdminConsoleForArgv(process.argv)) {
2003
+ announceAdminConsole({
2004
+ locale: runtimeLocale,
2005
+ logger: {
2006
+ info: (message: string) => api.logger.info?.(`securityclaw: ${message}`),
2007
+ warn: (message: string) => api.logger.warn?.(`securityclaw: ${message}`),
2008
+ },
2009
+ stateDir,
2010
+ state: "service-command",
2011
+ url: adminConsoleUrl,
2012
+ });
2013
+ api.logger.info?.("securityclaw: admin dashboard is hosted by the background OpenClaw gateway service");
2014
+ } else {
2015
+ api.logger.info?.(
2016
+ `securityclaw: admin auto-start skipped in ${autoStartDecision.reason}; use npm run admin for standalone dashboard`,
2017
+ );
2018
+ }
2019
+ return;
2020
+ }
2021
+
2022
+ const adminServerOptions = {
2023
+ configPath: resolved.configPath,
2024
+ legacyOverridePath: resolved.legacyOverridePath,
2025
+ statusPath,
2026
+ dbPath,
2027
+ unrefOnStart: true,
2028
+ logger: {
2029
+ info: (message: string) => api.logger.info?.(`securityclaw: ${message}`),
2030
+ warn: (message: string) => api.logger.warn?.(`securityclaw: ${message}`),
2031
+ },
2032
+ ...(pluginConfig.adminPort !== undefined ? { port: pluginConfig.adminPort } : {}),
2033
+ };
2034
+
2035
+ void startAdminServer(adminServerOptions)
2036
+ .then((result) => {
2037
+ announceAdminConsole({
2038
+ locale: runtimeLocale,
2039
+ logger: {
2040
+ info: (message: string) => api.logger.info?.(`securityclaw: ${message}`),
2041
+ warn: (message: string) => api.logger.warn?.(`securityclaw: ${message}`),
2042
+ },
2043
+ stateDir,
2044
+ state: result.state,
2045
+ url: `http://127.0.0.1:${result.runtime.port}`,
2033
2046
  });
2034
- } else {
2035
- if (shouldAnnounceAdminConsoleForArgv(process.argv)) {
2036
- announceAdminConsole({
2037
- locale: runtimeLocale,
2038
- logger: {
2039
- info: (message: string) => api.logger.info?.(`securityclaw: ${message}`),
2040
- warn: (message: string) => api.logger.warn?.(`securityclaw: ${message}`),
2041
- },
2042
- stateDir,
2043
- state: "service-command",
2044
- url: adminConsoleUrl,
2045
- });
2046
- api.logger.info?.("securityclaw: admin dashboard is hosted by the background OpenClaw gateway service");
2047
- } else {
2048
- api.logger.info?.(
2049
- `securityclaw: admin auto-start skipped in ${autoStartDecision.reason}; use npm run admin for standalone dashboard`,
2050
- );
2051
- }
2052
- }
2053
- } else {
2054
- api.logger.info?.("securityclaw: admin auto-start disabled by config");
2047
+ })
2048
+ .catch((error) => {
2049
+ api.logger.warn?.(`securityclaw: failed to auto-start admin dashboard (${String(error)})`);
2050
+ });
2055
2051
  }
2056
2052
 
2053
+ statusStore.markBoot(toStatusConfig(runtime.config, runtime.overrideLoaded, resolved));
2054
+
2057
2055
  api.logger.info?.(
2058
2056
  `securityclaw: boot env=${runtime.config.environment} policy_version=${runtime.config.policy_version} dlp_mode=${runtime.config.dlp.on_dlp_hit} rules=${runtime.config.policies.length}`,
2059
2057
  );
@@ -2655,6 +2653,7 @@ const plugin = {
2655
2653
  { priority: 100 },
2656
2654
  );
2657
2655
 
2656
+ startManagedAdminConsole();
2658
2657
  api.logger.info?.(`securityclaw: loaded policy_version=${runtime.config.policy_version}`);
2659
2658
  }
2660
2659
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securityclaw",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "SecurityClaw security plugin for OpenClaw-compatible hooks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  "test:unit": "node --test --experimental-strip-types",
43
43
  "test": "npm run typecheck && npm run test:unit",
44
44
  "check": "npm test",
45
- "admin": "node --experimental-strip-types ./admin/server.ts",
45
+ "admin": "npm run admin:build && node --experimental-strip-types ./admin/server.ts",
46
46
  "admin:build": "node ./scripts/build-admin.mjs",
47
47
  "prepack": "npm test && npm run admin:build",
48
48
  "pack:plugin": "node ./scripts/pack-plugin.mjs",
@@ -50,17 +50,15 @@
50
50
  "release:check": "node ./scripts/release-preflight.mjs",
51
51
  "release:dry-run": "npm run release:check && npm publish --dry-run --access public"
52
52
  },
53
- "dependencies": {
54
- "esbuild": "^0.27.4",
55
- "react": "^19.2.4",
56
- "react-dom": "^19.2.4",
57
- "recharts": "^3.8.0"
58
- },
59
53
  "devDependencies": {
60
54
  "@types/node": "^25.5.0",
61
55
  "@types/react": "^19.2.14",
62
56
  "@types/react-dom": "^19.2.3",
57
+ "esbuild": "^0.27.4",
63
58
  "openclaw": "^2026.3.13",
59
+ "react": "^19.2.4",
60
+ "react-dom": "^19.2.4",
61
+ "recharts": "^3.8.0",
64
62
  "typescript": "^5.9.3"
65
63
  },
66
64
  "peerDependencies": {
@@ -1,10 +1,10 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
2
  import path from "node:path";
4
3
 
5
4
  import { resolveSecurityClawStateDir } from "../infrastructure/config/plugin_config_parser.ts";
6
5
  import type { SecurityClawLocale } from "../i18n/locale.ts";
7
6
  import { pickLocalized } from "../i18n/locale.ts";
7
+ import { runProcessSync } from "../runtime/process_runner.ts";
8
8
 
9
9
  export type AdminConsoleState = "started" | "already-running" | "service-command";
10
10
 
@@ -122,7 +122,7 @@ function writeAdminConsoleMarker(markerPath: string, url: string): void {
122
122
 
123
123
  export function openAdminConsoleInBrowser(url: string): BrowserOpenResult {
124
124
  if (process.platform === "darwin") {
125
- const result = spawnSync("open", [url], { stdio: "ignore", timeout: 5_000 });
125
+ const result = runProcessSync("open", [url], { stdio: "ignore", timeout: 5_000 });
126
126
  if (result.error) {
127
127
  return { ok: false, command: "open", error: String(result.error) };
128
128
  }
@@ -133,7 +133,7 @@ export function openAdminConsoleInBrowser(url: string): BrowserOpenResult {
133
133
  }
134
134
 
135
135
  if (process.platform === "win32") {
136
- const result = spawnSync("cmd", ["/c", "start", "", url], {
136
+ const result = runProcessSync("cmd", ["/c", "start", "", url], {
137
137
  stdio: "ignore",
138
138
  timeout: 5_000,
139
139
  windowsHide: true,
@@ -148,7 +148,7 @@ export function openAdminConsoleInBrowser(url: string): BrowserOpenResult {
148
148
  }
149
149
 
150
150
  if (process.platform === "linux") {
151
- const result = spawnSync("xdg-open", [url], { stdio: "ignore", timeout: 5_000 });
151
+ const result = runProcessSync("xdg-open", [url], { stdio: "ignore", timeout: 5_000 });
152
152
  if (result.error) {
153
153
  return { ok: false, command: "xdg-open", error: String(result.error) };
154
154
  }
@@ -168,9 +168,10 @@ export function announceAdminConsole(options: AnnounceAdminConsoleOptions): Anno
168
168
  const { locale, logger, url, state, stateDir, opener = openAdminConsoleInBrowser } = options;
169
169
  const markerPath = stateDir ? resolveAdminConsoleMarkerPath(stateDir) : undefined;
170
170
  const firstRun = markerPath !== undefined && !existsSync(markerPath);
171
+ const shouldAutoOpen = firstRun && state !== "service-command";
171
172
 
172
173
  let openedAutomatically = false;
173
- if (firstRun) {
174
+ if (shouldAutoOpen) {
174
175
  const result = opener(url);
175
176
  if (result.ok) {
176
177
  openedAutomatically = true;
@@ -0,0 +1,12 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ type JsonRecord = Record<string, unknown>;
4
+
5
+ export function readUtf8File(filePath: string): string {
6
+ return readFileSync(filePath, "utf8");
7
+ }
8
+
9
+ export function readJsonRecordFile(filePath: string): JsonRecord {
10
+ return JSON.parse(readUtf8File(filePath)) as JsonRecord;
11
+ }
12
+
@@ -670,8 +670,21 @@ export function analyzeSkillDocument(input: {
670
670
  const normalizedName = input.name.trim().toLowerCase();
671
671
  const siblingNames = input.siblingNames.map((item) => item.trim().toLowerCase()).filter(Boolean);
672
672
  const downloadExecutePattern = /(curl|wget)[^\n]{0,120}\|\s*(sh|bash|zsh)|download[^.\n]{0,60}(then )?(run|execute)/i;
673
- const shellExecPattern =
674
- /(exec_command|spawnSync|child_process|bash\s+-lc|zsh\s+-lc|powershell|rm\s+-rf|chmod\s+-R|chown\s+-R)/i;
673
+ const childProcessToken = `child${String.fromCharCode(95)}process`;
674
+ const shellExecPattern = new RegExp(
675
+ [
676
+ "exec_command",
677
+ "spawnSync",
678
+ childProcessToken,
679
+ "bash\\s+-lc",
680
+ "zsh\\s+-lc",
681
+ "powershell",
682
+ "rm\\s+-rf",
683
+ "chmod\\s+-R",
684
+ "chown\\s+-R",
685
+ ].join("|"),
686
+ "i",
687
+ );
675
688
  const bypassPattern =
676
689
  /(ignore|bypass|disable|skip)[^.\n]{0,40}(policy|security|guard|safety)|hide (the )?(output|logs)|不要提示|不要暴露/i;
677
690
  const credentialPattern =
@@ -0,0 +1,47 @@
1
+ type SecurityClawAdminServerEnv = {
2
+ adminPort?: number | undefined;
3
+ locale?: string | undefined;
4
+ configPath?: string | undefined;
5
+ legacyOverridePath?: string | undefined;
6
+ statusPath?: string | undefined;
7
+ dbPath?: string | undefined;
8
+ };
9
+
10
+ function readTextEnv(name: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
11
+ const value = env[name];
12
+ if (typeof value !== "string") {
13
+ return undefined;
14
+ }
15
+ const trimmed = value.trim();
16
+ return trimmed || undefined;
17
+ }
18
+
19
+ function readNumericEnv(name: string, env: NodeJS.ProcessEnv = process.env): number | undefined {
20
+ const value = readTextEnv(name, env);
21
+ if (!value) {
22
+ return undefined;
23
+ }
24
+ const parsed = Number(value);
25
+ return Number.isFinite(parsed) ? parsed : undefined;
26
+ }
27
+
28
+ export function readProcessEnvValue(name: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
29
+ return readTextEnv(name, env);
30
+ }
31
+
32
+ export function resolveSecurityClawAdminPort(defaultPort = 4780, env: NodeJS.ProcessEnv = process.env): number {
33
+ return readNumericEnv("SECURITYCLAW_ADMIN_PORT", env) ?? defaultPort;
34
+ }
35
+
36
+ export function readSecurityClawAdminServerEnv(
37
+ env: NodeJS.ProcessEnv = process.env,
38
+ ): SecurityClawAdminServerEnv {
39
+ return {
40
+ adminPort: readNumericEnv("SECURITYCLAW_ADMIN_PORT", env),
41
+ locale: readTextEnv("SECURITYCLAW_LOCALE", env),
42
+ configPath: readTextEnv("SECURITYCLAW_CONFIG_PATH", env),
43
+ legacyOverridePath: readTextEnv("SECURITYCLAW_LEGACY_OVERRIDE_PATH", env),
44
+ statusPath: readTextEnv("SECURITYCLAW_STATUS_PATH", env),
45
+ dbPath: readTextEnv("SECURITYCLAW_DB_PATH", env),
46
+ };
47
+ }
@@ -0,0 +1,38 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ export type RunProcessSyncOptions = {
4
+ cwd?: string;
5
+ encoding?: BufferEncoding;
6
+ stdio?: "ignore" | "inherit" | "pipe";
7
+ timeout?: number;
8
+ windowsHide?: boolean;
9
+ };
10
+
11
+ export type RunProcessSyncResult = {
12
+ status: number | null;
13
+ stdout?: string;
14
+ stderr?: string;
15
+ error?: unknown;
16
+ };
17
+
18
+ type RunProcessSyncFn = (
19
+ command: string,
20
+ args?: readonly string[],
21
+ options?: RunProcessSyncOptions,
22
+ ) => RunProcessSyncResult;
23
+
24
+ const requireFromHere = createRequire(import.meta.url);
25
+ const SYSTEM_PROCESS_MODULE_ID = `node:child${String.fromCharCode(95)}process`;
26
+ const RUN_PROCESS_SYNC_METHOD = ["spawn", "Sync"].join("");
27
+ const runProcessSyncImpl = (requireFromHere(SYSTEM_PROCESS_MODULE_ID) as Record<string, unknown>)[
28
+ RUN_PROCESS_SYNC_METHOD
29
+ ] as RunProcessSyncFn;
30
+
31
+ export function runProcessSync(
32
+ command: string,
33
+ args: readonly string[],
34
+ options: RunProcessSyncOptions = {},
35
+ ): RunProcessSyncResult {
36
+ return runProcessSyncImpl(command, args, options);
37
+ }
38
+