@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.
Files changed (145) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/types/async/job-manager.d.ts +3 -1
  3. package/dist/types/cli/daemon-cli.d.ts +25 -0
  4. package/dist/types/cli/notify-cli.d.ts +23 -0
  5. package/dist/types/cli/setup-cli.d.ts +20 -1
  6. package/dist/types/commands/daemon.d.ts +41 -0
  7. package/dist/types/commands/notify.d.ts +41 -0
  8. package/dist/types/config/model-profile-activation.d.ts +12 -0
  9. package/dist/types/config/model-profiles.d.ts +2 -1
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +71 -0
  13. package/dist/types/coordinator/contract.d.ts +1 -1
  14. package/dist/types/daemon/builtin.d.ts +20 -0
  15. package/dist/types/daemon/control-types.d.ts +57 -0
  16. package/dist/types/daemon/runtime.d.ts +25 -0
  17. package/dist/types/defaults/skc-defaults.d.ts +13 -2
  18. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  19. package/dist/types/extensibility/plugins/index.d.ts +1 -0
  20. package/dist/types/extensibility/plugins/security-scanner.d.ts +31 -0
  21. package/dist/types/modes/components/oauth-selector.d.ts +2 -0
  22. package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
  23. package/dist/types/modes/interactive-mode.d.ts +1 -1
  24. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  25. package/dist/types/modes/types.d.ts +7 -1
  26. package/dist/types/notifications/config-commands.d.ts +26 -0
  27. package/dist/types/notifications/config.d.ts +61 -0
  28. package/dist/types/notifications/helpers.d.ts +55 -0
  29. package/dist/types/notifications/html-format.d.ts +62 -0
  30. package/dist/types/notifications/index.d.ts +28 -0
  31. package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
  32. package/dist/types/notifications/telegram-cli.d.ts +19 -0
  33. package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
  34. package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
  35. package/dist/types/notifications/telegram-daemon.d.ts +276 -0
  36. package/dist/types/notifications/telegram-reference.d.ts +111 -0
  37. package/dist/types/notifications/threaded-inbound.d.ts +58 -0
  38. package/dist/types/notifications/threaded-render.d.ts +66 -0
  39. package/dist/types/notifications/topic-registry.d.ts +67 -0
  40. package/dist/types/rlm/index.d.ts +12 -0
  41. package/dist/types/session/agent-session.d.ts +39 -2
  42. package/dist/types/session/auth-storage.d.ts +1 -1
  43. package/dist/types/setup/credential-auto-import.d.ts +63 -0
  44. package/dist/types/setup/credential-import.d.ts +3 -0
  45. package/dist/types/setup/host-plugin-setup.d.ts +39 -0
  46. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  47. package/dist/types/skc-runtime/state-writer.d.ts +2 -0
  48. package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
  49. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  50. package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
  51. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -0
  52. package/dist/types/tools/ask-answer-registry.d.ts +13 -0
  53. package/dist/types/tools/index.d.ts +18 -0
  54. package/dist/types/tools/subagent.d.ts +3 -0
  55. package/package.json +7 -7
  56. package/scripts/build-binary.ts +3 -0
  57. package/src/async/job-manager.ts +5 -1
  58. package/src/cli/daemon-cli.ts +122 -0
  59. package/src/cli/notify-cli.ts +274 -0
  60. package/src/cli/plugin-cli.ts +63 -6
  61. package/src/cli/setup-cli.ts +173 -84
  62. package/src/cli.ts +3 -3
  63. package/src/commands/daemon.ts +47 -0
  64. package/src/commands/notify.ts +61 -0
  65. package/src/commands/setup.ts +11 -1
  66. package/src/config/model-profile-activation.ts +74 -5
  67. package/src/config/model-profiles.ts +7 -4
  68. package/src/config/model-registry.ts +6 -3
  69. package/src/config/models-config-schema.ts +1 -1
  70. package/src/config/settings-schema.ts +61 -0
  71. package/src/coordinator/contract.ts +3 -0
  72. package/src/coordinator-mcp/server.ts +270 -1
  73. package/src/daemon/builtin.ts +46 -0
  74. package/src/daemon/control-types.ts +65 -0
  75. package/src/daemon/runtime.ts +51 -0
  76. package/src/defaults/skc/rules/ponytail.md +68 -0
  77. package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
  78. package/src/defaults/skc-defaults.ts +27 -2
  79. package/src/edit/modes/replace.ts +1 -1
  80. package/src/extensibility/extensions/runner.ts +4 -0
  81. package/src/extensibility/extensions/types.ts +8 -0
  82. package/src/extensibility/plugins/index.ts +1 -0
  83. package/src/extensibility/plugins/manager.ts +50 -0
  84. package/src/extensibility/plugins/marketplace/manager.ts +42 -0
  85. package/src/extensibility/plugins/security-scanner.ts +477 -0
  86. package/src/goals/tools/goal-tool.ts +11 -2
  87. package/src/hashline/hash.ts +1 -1
  88. package/src/internal-urls/docs-index.generated.ts +10 -8
  89. package/src/main.ts +30 -0
  90. package/src/modes/acp/acp-event-mapper.ts +1 -0
  91. package/src/modes/components/hook-editor.ts +7 -2
  92. package/src/modes/components/oauth-selector.ts +19 -0
  93. package/src/modes/controllers/event-controller.ts +20 -0
  94. package/src/modes/controllers/selector-controller.ts +80 -17
  95. package/src/modes/interactive-mode.ts +6 -2
  96. package/src/modes/runtime-init.ts +1 -0
  97. package/src/modes/shared/agent-wire/event-contract.ts +1 -0
  98. package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
  99. package/src/modes/shared/agent-wire/event-observation.ts +16 -0
  100. package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
  101. package/src/modes/types.ts +7 -1
  102. package/src/modes/utils/ui-helpers.ts +23 -0
  103. package/src/notifications/config-commands.ts +50 -0
  104. package/src/notifications/config.ts +107 -0
  105. package/src/notifications/helpers.ts +135 -0
  106. package/src/notifications/html-format.ts +389 -0
  107. package/src/notifications/index.ts +700 -0
  108. package/src/notifications/rate-limit-pool.ts +179 -0
  109. package/src/notifications/telegram-cli.ts +194 -0
  110. package/src/notifications/telegram-daemon-cli.ts +74 -0
  111. package/src/notifications/telegram-daemon-control.ts +370 -0
  112. package/src/notifications/telegram-daemon.ts +1370 -0
  113. package/src/notifications/telegram-reference.ts +335 -0
  114. package/src/notifications/threaded-inbound.ts +80 -0
  115. package/src/notifications/threaded-render.ts +155 -0
  116. package/src/notifications/topic-registry.ts +133 -0
  117. package/src/rlm/index.ts +19 -0
  118. package/src/sdk.ts +16 -0
  119. package/src/session/agent-session.ts +113 -3
  120. package/src/session/auth-storage.ts +3 -0
  121. package/src/session/session-dump-format.ts +43 -2
  122. package/src/session/session-manager.ts +39 -5
  123. package/src/setup/credential-auto-import.ts +258 -0
  124. package/src/setup/credential-import.ts +17 -0
  125. package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
  126. package/src/setup/host-plugin-setup.ts +142 -0
  127. package/src/skc-runtime/deep-interview-recorder.ts +2 -2
  128. package/src/skc-runtime/launch-tmux.ts +10 -2
  129. package/src/skc-runtime/state-runtime.ts +18 -10
  130. package/src/skc-runtime/state-writer.ts +8 -8
  131. package/src/skc-runtime/tmux-common.ts +8 -0
  132. package/src/skc-runtime/tmux-sessions.ts +8 -1
  133. package/src/skc-runtime/ultragoal-guard.ts +57 -2
  134. package/src/skc-runtime/ultragoal-runtime.ts +105 -19
  135. package/src/skc-runtime/workflow-manifest.generated.json +27 -2
  136. package/src/skc-runtime/workflow-manifest.ts +11 -1
  137. package/src/slash-commands/builtin-registry.ts +4 -1
  138. package/src/task/executor.ts +5 -1
  139. package/src/tools/ask-answer-registry.ts +25 -0
  140. package/src/tools/ask.ts +77 -6
  141. package/src/tools/image-gen.ts +5 -8
  142. package/src/tools/index.ts +19 -0
  143. package/src/tools/inspect-image.ts +16 -11
  144. package/src/tools/subagent-render.ts +7 -0
  145. 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({ cwd: session.cwd, currentGoal: session.getGoalModeState?.()?.goal ?? null });
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
  }
@@ -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("../../../natives/native/index.js")
12
+ void import("@sayknow-cli/natives")
13
13
  .then(mod => {
14
14
  if (typeof mod.h06FormatHashLines === "function") {
15
15
  formatHashLinesNative = mod.h06FormatHashLines;