@sayknow-cli/coding-agent 0.2.6 → 0.3.0
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 +32 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +71 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/defaults/skc-defaults.d.ts +13 -2
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/extensibility/plugins/index.d.ts +1 -0
- package/dist/types/extensibility/plugins/security-scanner.d.ts +31 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/state-writer.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/plugin-cli.ts +63 -6
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +61 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/skc/rules/ponytail.md +68 -0
- package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
- package/src/defaults/skc-defaults.ts +27 -2
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/plugins/index.ts +1 -0
- package/src/extensibility/plugins/manager.ts +50 -0
- package/src/extensibility/plugins/marketplace/manager.ts +42 -0
- package/src/extensibility/plugins/security-scanner.ts +477 -0
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +10 -8
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +700 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +113 -3
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skc-runtime/deep-interview-recorder.ts +2 -2
- package/src/skc-runtime/launch-tmux.ts +10 -2
- package/src/skc-runtime/state-runtime.ts +18 -10
- package/src/skc-runtime/state-writer.ts +8 -8
- package/src/skc-runtime/tmux-common.ts +8 -0
- package/src/skc-runtime/tmux-sessions.ts +8 -1
- package/src/skc-runtime/ultragoal-guard.ts +57 -2
- package/src/skc-runtime/ultragoal-runtime.ts +105 -19
- package/src/skc-runtime/workflow-manifest.generated.json +27 -2
- package/src/skc-runtime/workflow-manifest.ts +11 -1
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
// Regex-based, false-positive-prone — ADVISORY ONLY. Never hard-block by default.
|
|
2
|
+
|
|
3
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { basename, extname, join, relative } from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { DoctorCheck } from "./types";
|
|
7
|
+
|
|
8
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export type RiskLevel = "none" | "low" | "medium" | "high";
|
|
11
|
+
|
|
12
|
+
export type FindingCategory =
|
|
13
|
+
| "credential"
|
|
14
|
+
| "execution"
|
|
15
|
+
| "network"
|
|
16
|
+
| "obfuscation"
|
|
17
|
+
| "package_install"
|
|
18
|
+
| "persistence";
|
|
19
|
+
|
|
20
|
+
export interface SecurityFinding {
|
|
21
|
+
category: FindingCategory;
|
|
22
|
+
id: string;
|
|
23
|
+
label: string;
|
|
24
|
+
severity: "low" | "medium" | "high";
|
|
25
|
+
file: string;
|
|
26
|
+
line?: number;
|
|
27
|
+
snippet?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ScanReport {
|
|
31
|
+
findings: SecurityFinding[];
|
|
32
|
+
networkUrls: string[];
|
|
33
|
+
riskLevel: RiskLevel;
|
|
34
|
+
score: number;
|
|
35
|
+
reasoning: string;
|
|
36
|
+
recommendation: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Rule definitions ─────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
interface ScanRule {
|
|
42
|
+
id: string;
|
|
43
|
+
label: string;
|
|
44
|
+
pattern: RegExp;
|
|
45
|
+
category: FindingCategory;
|
|
46
|
+
severity: "low" | "medium" | "high";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CREDENTIAL_RULES: ScanRule[] = [
|
|
50
|
+
{
|
|
51
|
+
id: "credential_keyword",
|
|
52
|
+
label: "Hardcoded credential keyword (API key/token/secret)",
|
|
53
|
+
pattern: /\b(api[_-]?key|secret|token|private[_-]?key|access[_-]?key|auth[_-]?token|bearer)\b/i,
|
|
54
|
+
category: "credential",
|
|
55
|
+
severity: "medium",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "credential_access",
|
|
59
|
+
label: "Environment variable access (process.env / os.environ)",
|
|
60
|
+
pattern: /\b(os\.environ|getenv|process\.env|dotenv|load_dotenv)\b/i,
|
|
61
|
+
category: "credential",
|
|
62
|
+
severity: "low",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "crypto_wallet",
|
|
66
|
+
label: "Cryptocurrency wallet reference (wallet/seed/mnemonic)",
|
|
67
|
+
pattern: /\b(wallet\.dat|keystore|mnemonic|seed phrase|private key|ledger|trezor|metamask)\b/i,
|
|
68
|
+
category: "credential",
|
|
69
|
+
severity: "high",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "ssh_access",
|
|
73
|
+
label: "SSH key / config access",
|
|
74
|
+
pattern: /(~\/.ssh|\\.ssh|id_rsa|id_ed25519|authorized_keys)/i,
|
|
75
|
+
category: "credential",
|
|
76
|
+
severity: "medium",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "cloud_credentials",
|
|
80
|
+
label: "Cloud credential file reference (AWS / GCloud / Azure)",
|
|
81
|
+
pattern: /(\.aws\/credentials|\.aws\/config|\.config\/gcloud|\.azure\/)/i,
|
|
82
|
+
category: "credential",
|
|
83
|
+
severity: "medium",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "browser_data",
|
|
87
|
+
label: "Browser sensitive data access (cookies/passwords)",
|
|
88
|
+
pattern: /(Login Data|Chrome\/User Data|Firefox\/Profiles|Brave\/User Data)/i,
|
|
89
|
+
category: "credential",
|
|
90
|
+
severity: "medium",
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const EXEC_RULES: ScanRule[] = [
|
|
95
|
+
{
|
|
96
|
+
id: "download_exec",
|
|
97
|
+
label: "Download-then-execute pattern (curl|wget piped to shell)",
|
|
98
|
+
pattern: /(curl\s+.*\|\s*(bash|sh)|wget\s+.*\|\s*(bash|sh)|powershell\s+.*-c|Invoke-Expression)/i,
|
|
99
|
+
category: "execution",
|
|
100
|
+
severity: "high",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "shell_exec",
|
|
104
|
+
label: "Arbitrary system command execution (shell/subprocess)",
|
|
105
|
+
pattern: /\b(subprocess\.Popen|os\.system|popen|child_process\.exec|Runtime\.getRuntime\(\)\.exec)\b/i,
|
|
106
|
+
category: "execution",
|
|
107
|
+
severity: "medium",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "dynamic_exec",
|
|
111
|
+
label: "Dynamic code loading (eval / import)",
|
|
112
|
+
pattern: /\b(eval\(|__import__|importlib|dlopen)\b/i,
|
|
113
|
+
category: "execution",
|
|
114
|
+
severity: "medium",
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const PERSISTENCE_RULES: ScanRule[] = [
|
|
119
|
+
{
|
|
120
|
+
id: "persistence",
|
|
121
|
+
label: "Background persistence / scheduled job (cron/systemd/launchd)",
|
|
122
|
+
pattern: /\b(cron|crontab|@reboot|systemd|launchd\.plist|schtasks)\b/i,
|
|
123
|
+
category: "persistence",
|
|
124
|
+
severity: "high",
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const OBFUSCATION_RULES: ScanRule[] = [
|
|
129
|
+
{
|
|
130
|
+
id: "obfuscation_encoding",
|
|
131
|
+
label: "Obfuscation / encoding (base64 / hex / XOR)",
|
|
132
|
+
pattern: /\b(base64|atob|btoa|fromCharCode|rot13|xor|unescape|eval\(atob)\b/i,
|
|
133
|
+
category: "obfuscation",
|
|
134
|
+
severity: "medium",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "geo_evasion",
|
|
138
|
+
label: "Locale/timezone evasion pattern",
|
|
139
|
+
pattern: /(timezone|Intl\.DateTimeFormat|locale|LANG=)\b/i,
|
|
140
|
+
category: "obfuscation",
|
|
141
|
+
severity: "low",
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const PACKAGE_INSTALL_RULES: ScanRule[] = [
|
|
146
|
+
{
|
|
147
|
+
id: "npm_install",
|
|
148
|
+
label: "npm install invocation",
|
|
149
|
+
pattern: /\b(npm\s+install|npm\s+i\s|npx\s)/i,
|
|
150
|
+
category: "package_install",
|
|
151
|
+
severity: "low",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "pip_install",
|
|
155
|
+
label: "pip install invocation",
|
|
156
|
+
pattern: /\b(pip\s+install|pip3\s+install|python\s+-m\s+pip)\b/i,
|
|
157
|
+
category: "package_install",
|
|
158
|
+
severity: "low",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "apt_install",
|
|
162
|
+
label: "apt-get install invocation",
|
|
163
|
+
pattern: /\b(apt-get\s+install|apt\s+install)\b/i,
|
|
164
|
+
category: "package_install",
|
|
165
|
+
severity: "low",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "brew_install",
|
|
169
|
+
label: "brew install invocation",
|
|
170
|
+
pattern: /\b(brew\s+install)\b/i,
|
|
171
|
+
category: "package_install",
|
|
172
|
+
severity: "low",
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const ALL_RULES: ScanRule[] = [
|
|
177
|
+
...CREDENTIAL_RULES,
|
|
178
|
+
...EXEC_RULES,
|
|
179
|
+
...PERSISTENCE_RULES,
|
|
180
|
+
...OBFUSCATION_RULES,
|
|
181
|
+
...PACKAGE_INSTALL_RULES,
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const URL_REGEX = /https?:\/\/[^\s)\]"'<>]+/gi;
|
|
185
|
+
|
|
186
|
+
const TEXT_EXTS = new Set([
|
|
187
|
+
".md",
|
|
188
|
+
".py",
|
|
189
|
+
".js",
|
|
190
|
+
".ts",
|
|
191
|
+
".tsx",
|
|
192
|
+
".sh",
|
|
193
|
+
".json",
|
|
194
|
+
".yaml",
|
|
195
|
+
".yml",
|
|
196
|
+
".toml",
|
|
197
|
+
".txt",
|
|
198
|
+
".env",
|
|
199
|
+
".ini",
|
|
200
|
+
".conf",
|
|
201
|
+
".rb",
|
|
202
|
+
".go",
|
|
203
|
+
".java",
|
|
204
|
+
".html",
|
|
205
|
+
".css",
|
|
206
|
+
".xml",
|
|
207
|
+
".jsx",
|
|
208
|
+
".mjs",
|
|
209
|
+
".cjs",
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
const SKIP_DIRS = new Set([
|
|
213
|
+
".git",
|
|
214
|
+
"node_modules",
|
|
215
|
+
"dist",
|
|
216
|
+
"build",
|
|
217
|
+
"__pycache__",
|
|
218
|
+
".venv",
|
|
219
|
+
"venv",
|
|
220
|
+
".idea",
|
|
221
|
+
".vscode",
|
|
222
|
+
"coverage",
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
const ENV_FILENAMES = new Set([
|
|
226
|
+
".env",
|
|
227
|
+
".env.local",
|
|
228
|
+
".env.production",
|
|
229
|
+
".env.development",
|
|
230
|
+
".env.test",
|
|
231
|
+
".env.example",
|
|
232
|
+
"config.json",
|
|
233
|
+
"secrets.json",
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
const MAX_FILE_SIZE = 2_000_000; // 2 MB
|
|
237
|
+
|
|
238
|
+
const SNIPPET_MAX_LEN = 120;
|
|
239
|
+
|
|
240
|
+
// ─── Score deduction per unique category ─────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
function deductionForCategory(category: FindingCategory): number {
|
|
243
|
+
switch (category) {
|
|
244
|
+
case "execution":
|
|
245
|
+
return 25;
|
|
246
|
+
case "persistence":
|
|
247
|
+
return 20;
|
|
248
|
+
case "credential":
|
|
249
|
+
return 15;
|
|
250
|
+
case "obfuscation":
|
|
251
|
+
return 10;
|
|
252
|
+
case "network":
|
|
253
|
+
return 5;
|
|
254
|
+
case "package_install":
|
|
255
|
+
return 5;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── Risk thresholds ──────────────────────────────────────────────────────────
|
|
260
|
+
//
|
|
261
|
+
// Flags with inherently high risk elevate the level regardless of score.
|
|
262
|
+
// Otherwise: score ≤ 50 → high, ≤ 75 → medium, ≤ 90 → low, 91-100 → none.
|
|
263
|
+
|
|
264
|
+
const HIGH_FLAGS = new Set(["download_exec", "persistence", "crypto_wallet"]);
|
|
265
|
+
const MEDIUM_FLAGS = new Set([
|
|
266
|
+
"shell_exec",
|
|
267
|
+
"dynamic_exec",
|
|
268
|
+
"ssh_access",
|
|
269
|
+
"cloud_credentials",
|
|
270
|
+
"browser_data",
|
|
271
|
+
"obfuscation_encoding",
|
|
272
|
+
"geo_evasion",
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
function determineRiskLevel(flagIds: Set<string>, score: number, networkCount: number): RiskLevel {
|
|
276
|
+
for (const f of HIGH_FLAGS) {
|
|
277
|
+
if (flagIds.has(f)) return "high";
|
|
278
|
+
}
|
|
279
|
+
for (const f of MEDIUM_FLAGS) {
|
|
280
|
+
if (flagIds.has(f)) return "medium";
|
|
281
|
+
}
|
|
282
|
+
if (networkCount > 0) return "medium";
|
|
283
|
+
if (flagIds.size === 0) return "none";
|
|
284
|
+
if (score <= 50) return "high";
|
|
285
|
+
if (score <= 75) return "medium";
|
|
286
|
+
if (score <= 90) return "low";
|
|
287
|
+
// Findings are present here (the flagIds.size === 0 case returned "none" above), so a
|
|
288
|
+
// single low-weight pattern (e.g. a package-install / supply-chain marker scoring 95)
|
|
289
|
+
// still surfaces as an advisory instead of being silently suppressed.
|
|
290
|
+
return "low";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function buildReasoning(flagIds: Set<string>, networkCount: number): string {
|
|
294
|
+
const reasons: string[] = [];
|
|
295
|
+
if (flagIds.has("download_exec")) reasons.push("download-then-execute pattern detected");
|
|
296
|
+
if (flagIds.has("persistence")) reasons.push("background persistence / scheduled job");
|
|
297
|
+
if (flagIds.has("crypto_wallet")) reasons.push("cryptocurrency wallet / private key access");
|
|
298
|
+
if (flagIds.has("shell_exec")) reasons.push("arbitrary system command execution");
|
|
299
|
+
if (flagIds.has("dynamic_exec")) reasons.push("dynamic code execution (eval/import)");
|
|
300
|
+
if (flagIds.has("ssh_access") || flagIds.has("cloud_credentials"))
|
|
301
|
+
reasons.push("SSH key or cloud credential access");
|
|
302
|
+
if (flagIds.has("browser_data")) reasons.push("browser sensitive data access");
|
|
303
|
+
if (flagIds.has("obfuscation_encoding")) reasons.push("code obfuscation / encoding signs");
|
|
304
|
+
if (networkCount > 2) reasons.push(`multiple external connections (${networkCount})`);
|
|
305
|
+
else if (networkCount > 0) reasons.push("external network URLs present");
|
|
306
|
+
return reasons.length > 0 ? reasons.join("; ") : "";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function buildRecommendation(riskLevel: RiskLevel): string {
|
|
310
|
+
switch (riskLevel) {
|
|
311
|
+
case "high":
|
|
312
|
+
return "HIGH RISK: Do not install in environments with real credentials. Test only in an isolated sandbox.";
|
|
313
|
+
case "medium":
|
|
314
|
+
return "SUSPICIOUS: Test in an isolated environment and verify network traffic before activating.";
|
|
315
|
+
case "low":
|
|
316
|
+
return "LOW RISK: No clearly malicious patterns detected. Follow least-privilege principles.";
|
|
317
|
+
case "none":
|
|
318
|
+
return "No security concerns detected.";
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ─── Walker ───────────────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
async function walkDir(
|
|
325
|
+
dir: string,
|
|
326
|
+
rootDir: string,
|
|
327
|
+
callback: (relPath: string, content: string, absPath: string) => void,
|
|
328
|
+
): Promise<void> {
|
|
329
|
+
let entries: import("node:fs").Dirent[];
|
|
330
|
+
try {
|
|
331
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
332
|
+
} catch {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const entry of entries) {
|
|
337
|
+
const fullPath = join(dir, entry.name);
|
|
338
|
+
|
|
339
|
+
if (entry.isDirectory()) {
|
|
340
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
341
|
+
await walkDir(fullPath, rootDir, callback);
|
|
342
|
+
} else if (entry.isFile()) {
|
|
343
|
+
const ext = extname(entry.name).toLowerCase();
|
|
344
|
+
const isText = TEXT_EXTS.has(ext) || ENV_FILENAMES.has(entry.name);
|
|
345
|
+
if (!isText) continue;
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const info = await stat(fullPath);
|
|
349
|
+
if (info.size > MAX_FILE_SIZE) continue;
|
|
350
|
+
} catch {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const content = await readFile(fullPath, "utf-8");
|
|
356
|
+
const relPath = relative(rootDir, fullPath);
|
|
357
|
+
callback(relPath, content, fullPath);
|
|
358
|
+
} catch {
|
|
359
|
+
// ignore unreadable files
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Recursively scan `dir` for risky patterns.
|
|
369
|
+
* Never throws — on any top-level error returns an empty safe report.
|
|
370
|
+
*/
|
|
371
|
+
export async function scanPluginDir(dir: string): Promise<ScanReport> {
|
|
372
|
+
try {
|
|
373
|
+
const findings: SecurityFinding[] = [];
|
|
374
|
+
const networkUrls: string[] = [];
|
|
375
|
+
const flagIds = new Set<string>();
|
|
376
|
+
const seenFlagFiles = new Map<string, Set<string>>(); // flagId -> set of relPaths already recorded
|
|
377
|
+
|
|
378
|
+
await walkDir(dir, dir, (relPath, content) => {
|
|
379
|
+
// Sensitive file detection
|
|
380
|
+
if (ENV_FILENAMES.has(basename(relPath))) {
|
|
381
|
+
findings.push({
|
|
382
|
+
category: "credential",
|
|
383
|
+
id: "sensitive_file",
|
|
384
|
+
label: `Sensitive config file present: ${relPath}`,
|
|
385
|
+
severity: "medium",
|
|
386
|
+
file: relPath,
|
|
387
|
+
});
|
|
388
|
+
flagIds.add("sensitive_file");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Per-line rule matching
|
|
392
|
+
const lines = content.split("\n");
|
|
393
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
394
|
+
const line = lines[lineIdx];
|
|
395
|
+
for (const rule of ALL_RULES) {
|
|
396
|
+
if (!rule.pattern.test(line)) continue;
|
|
397
|
+
// Reset lastIndex for global regexes
|
|
398
|
+
rule.pattern.lastIndex = 0;
|
|
399
|
+
|
|
400
|
+
const fileSet = seenFlagFiles.get(rule.id) ?? new Set<string>();
|
|
401
|
+
seenFlagFiles.set(rule.id, fileSet);
|
|
402
|
+
// One finding per (rule.id, file) pair to avoid noise
|
|
403
|
+
if (fileSet.has(relPath)) continue;
|
|
404
|
+
fileSet.add(relPath);
|
|
405
|
+
|
|
406
|
+
flagIds.add(rule.id);
|
|
407
|
+
const snippet = line.trim().slice(0, SNIPPET_MAX_LEN);
|
|
408
|
+
findings.push({
|
|
409
|
+
category: rule.category,
|
|
410
|
+
id: rule.id,
|
|
411
|
+
label: rule.label,
|
|
412
|
+
severity: rule.severity,
|
|
413
|
+
file: relPath,
|
|
414
|
+
line: lineIdx + 1,
|
|
415
|
+
snippet,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
// Reset all global regexes after each line
|
|
419
|
+
for (const rule of ALL_RULES) rule.pattern.lastIndex = 0;
|
|
420
|
+
|
|
421
|
+
// URL extraction (not per-rule deduplicated)
|
|
422
|
+
URL_REGEX.lastIndex = 0;
|
|
423
|
+
for (const urlMatch of line.matchAll(URL_REGEX)) {
|
|
424
|
+
if (!networkUrls.includes(urlMatch[0])) networkUrls.push(urlMatch[0]);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Calculate score
|
|
430
|
+
const seenCategories = new Set<FindingCategory>();
|
|
431
|
+
let deduction = 0;
|
|
432
|
+
for (const f of findings) {
|
|
433
|
+
if (!seenCategories.has(f.category)) {
|
|
434
|
+
seenCategories.add(f.category);
|
|
435
|
+
deduction += deductionForCategory(f.category);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const score = Math.max(0, Math.min(100, 100 - deduction));
|
|
439
|
+
const riskLevel = determineRiskLevel(flagIds, score, networkUrls.length);
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
findings,
|
|
443
|
+
networkUrls,
|
|
444
|
+
riskLevel,
|
|
445
|
+
score,
|
|
446
|
+
reasoning: buildReasoning(flagIds, networkUrls.length),
|
|
447
|
+
recommendation: buildRecommendation(riskLevel),
|
|
448
|
+
};
|
|
449
|
+
} catch {
|
|
450
|
+
return { findings: [], networkUrls: [], riskLevel: "none", score: 0, reasoning: "", recommendation: "" };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Convert a ScanReport into DoctorCheck entries for the plugin health check.
|
|
456
|
+
* Returns [] when riskLevel is "none".
|
|
457
|
+
* NEVER emits status:"error" — advisory only.
|
|
458
|
+
*/
|
|
459
|
+
export function toDoctorChecks(pluginName: string, report: ScanReport): DoctorCheck[] {
|
|
460
|
+
if (report.riskLevel === "none") return [];
|
|
461
|
+
|
|
462
|
+
const topFindings = report.findings.slice(0, 5);
|
|
463
|
+
const topLines = topFindings.map(f => `${f.id}: ${f.file}${f.line !== undefined ? `:${f.line}` : ""}`).join(", ");
|
|
464
|
+
|
|
465
|
+
const message =
|
|
466
|
+
`risk=${report.riskLevel} score=${report.score}` +
|
|
467
|
+
(topLines ? ` | ${topLines}` : "") +
|
|
468
|
+
(report.findings.length > 5 ? ` (+${report.findings.length - 5} more)` : "");
|
|
469
|
+
|
|
470
|
+
return [
|
|
471
|
+
{
|
|
472
|
+
name: `plugin:${pluginName}:security`,
|
|
473
|
+
status: "warning",
|
|
474
|
+
message,
|
|
475
|
+
},
|
|
476
|
+
];
|
|
477
|
+
}
|
|
@@ -6,7 +6,7 @@ import * as z from "zod/v4";
|
|
|
6
6
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
7
7
|
import type { Theme, ThemeColor } from "../../modes/theme/theme";
|
|
8
8
|
import goalDescription from "../../prompts/tools/goal.md" with { type: "text" };
|
|
9
|
-
import { assertCanCompleteCurrentGoal } from "../../skc-runtime/ultragoal-guard";
|
|
9
|
+
import { assertCanCompleteCurrentGoal, assertUltragoalPauseAllowed } from "../../skc-runtime/ultragoal-guard";
|
|
10
10
|
import { formatDuration } from "../../slash-commands/helpers/format";
|
|
11
11
|
import type { ToolSession } from "../../tools";
|
|
12
12
|
import { formatErrorMessage, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
@@ -94,6 +94,11 @@ async function executeGoalOperation(session: ToolSession, params: GoalToolInput)
|
|
|
94
94
|
return buildGoalToolResponse(resumed.goal);
|
|
95
95
|
}
|
|
96
96
|
if (params.op === "pause") {
|
|
97
|
+
try {
|
|
98
|
+
await assertUltragoalPauseAllowed(session.cwd);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
101
|
+
}
|
|
97
102
|
const paused = await runtime.pauseGoal();
|
|
98
103
|
return buildGoalToolResponse(paused?.goal ?? null);
|
|
99
104
|
}
|
|
@@ -102,7 +107,11 @@ async function executeGoalOperation(session: ToolSession, params: GoalToolInput)
|
|
|
102
107
|
return buildGoalToolResponse(dropped ?? null);
|
|
103
108
|
}
|
|
104
109
|
try {
|
|
105
|
-
await assertCanCompleteCurrentGoal({
|
|
110
|
+
await assertCanCompleteCurrentGoal({
|
|
111
|
+
cwd: session.cwd,
|
|
112
|
+
currentGoal: session.getGoalModeState?.()?.goal ?? null,
|
|
113
|
+
sessionId: session.getSessionId?.(),
|
|
114
|
+
});
|
|
106
115
|
} catch (error) {
|
|
107
116
|
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
108
117
|
}
|
package/src/hashline/hash.ts
CHANGED
|
@@ -9,7 +9,7 @@ import bigrams from "./bigrams.json" with { type: "json" };
|
|
|
9
9
|
// module evaluation so this core module (and its re-exported helpers) stays
|
|
10
10
|
// usable and falls back to the TS loop if the native addon is unavailable.
|
|
11
11
|
let formatHashLinesNative: ((text: string, startLine?: number) => string) | undefined;
|
|
12
|
-
void import("
|
|
12
|
+
void import("@sayknow-cli/natives")
|
|
13
13
|
.then(mod => {
|
|
14
14
|
if (typeof mod.h06FormatHashLines === "function") {
|
|
15
15
|
formatHashLinesNative = mod.h06FormatHashLines;
|