@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
|
@@ -107,6 +107,21 @@ Loop until `skc ultragoal status` reports all goals complete:
|
|
|
107
107
|
`skc ultragoal checkpoint --goal-id <id> --status blocked --evidence "<completed legacy SKC goal blocks goal create in this thread>" --skc-goal-json <goal-get-json-or-path>`
|
|
108
108
|
11. Resume failed goals with `skc ultragoal complete-goals --retry-failed`.
|
|
109
109
|
|
|
110
|
+
## Blocker triage and pause discipline
|
|
111
|
+
|
|
112
|
+
An active Ultragoal run must not give up on a blocker by pausing the goal and asking the user. Classify every blocker before deciding what to do, and default to `resolvable` when unsure:
|
|
113
|
+
|
|
114
|
+
- **`resolvable`** — anything the agent can act on: failing tests, missing implementation, a dependency to install, an ambiguous-but-inferable detail, investigation. **Never pause.** Exhaust autonomous resolution first: investigate, `skc ultragoal steer --kind add_subgoal --title "Investigate blocker" --objective "..." --evidence "..." --rationale "..."`, delegate an `executor`, or preserve the blocker durably with `skc ultragoal checkpoint --status blocked` / `skc ultragoal record-review-blockers` and keep scheduling the next goal.
|
|
115
|
+
- **`human_blocked`** — only the user can act: credentials/secrets, a manual or physical step, an external approval/decision, access the agent lacks. Pause is the last resort and is gated.
|
|
116
|
+
|
|
117
|
+
`goal({"op":"pause"})` is **blocked at runtime** while an Ultragoal run is active unless the latest durable ledger event classifies the current blocker as `human_blocked`. To pause, record the classification immediately before pausing and cite the human-only dependency as evidence:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
skc ultragoal classify-blocker --classification human_blocked --evidence "<the specific human-only dependency>" [--goal-id <id>]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Recording `--classification resolvable` is an audit note only; it never authorizes a pause. The `ask` tool stays blocked during active runs regardless of classification — record unresolved decisions as durable blockers instead of prompting.
|
|
124
|
+
|
|
110
125
|
## Dynamic steering
|
|
111
126
|
|
|
112
127
|
Use `skc ultragoal steer` when real findings or blockers prove the current story decomposition should change while the aggregate objective and constraints stay fixed. Steering is explicit-only and evidence-backed; broad natural-language requests are rejected instead of guessed.
|
|
@@ -199,6 +214,7 @@ An ultragoal story cannot be checkpointed `complete` until the active agent has
|
|
|
199
214
|
- CLI surfaces require runtime argv replay: `replaySafe: true`, an allowlisted argv `command`, and replayed normalized stdout matching `recordedStdout`; unsafe commands require audited `replayExempt` metadata with exact fields `reasonCode`, `reason`, `approvedBy`, and `fallbackArtifactRefs` plus a structurally valid fallback artifact. Allowed `reasonCode` values are exactly `unsafe_side_effect`, `requires_credentials`, `requires_network`, `non_deterministic_external`, `destructive`, `interactive_only`, and `platform_unavailable`.
|
|
200
215
|
- Native/desktop/tui surfaces require a structurally valid screenshot, PTY capture with terminal control codes, or app-automation transcript.
|
|
201
216
|
- API/package/algorithm/math surfaces require a real artifact file or typed receipt. Bare `inlineEvidence` text alone is not sufficient for any surface.
|
|
217
|
+
- The mandatory **computer-use** red-team suite (`kill-switch-bypass`, `suspended-enforcement`, `permission-revoked`, …) is conditional, not universal: require it only when computer/desktop control is genuinely part of the product surface being dogfooded. For every other product type, prove the change through the matching live surface instead — browser-use automation for web/GUI, bash/CLI live invocation or argv replay for CLI, and real artifacts or typed receipts for API/package/algorithm/math. Editing docs, prompts, or skills that merely mention computer-use does not by itself make the computer-use suite applicable; pick the red-team surface that matches what the change actually ships.
|
|
202
218
|
7. The executor QA/red-team lane must report a matrix using `executorQa.contractCoverage`, `executorQa.surfaceEvidence`, `executorQa.adversarialCases`, and `executorQa.artifactRefs`. Not-applicable rows are allowed only in `contractCoverage` and `surfaceEvidence`; each `status: "not_applicable"` row requires `contractRef` plus `reason`. `adversarialCases` rows cannot be not-applicable.
|
|
203
219
|
8. Run a final code review pass and fold it into the strict quality gate. Clean means `architectReview.architectureStatus`, `architectReview.productStatus`, and `architectReview.codeStatus` are all `"CLEAR"`, `architectReview.recommendation` is `"APPROVE"`, executor QA statuses are `"passed"`, iteration is `"passed"` with `fullRerun: true`, every evidence field is non-empty, every required matrix row is present, and every blockers array is empty. `COMMENT`, `WATCH`, `REQUEST CHANGES`, `BLOCK`, missing evidence, missing or shallow matrix rows, plan/code mismatches, or non-empty blockers are non-clean.
|
|
204
220
|
9. If any lane finds an issue, do **not** checkpoint `complete` and do **not** call `goal({"op":"complete"})`. Record durable blocker work instead:
|
|
@@ -10,10 +10,11 @@ import ralplanSkill from "./skc/skills/ralplan/SKILL.md" with { type: "text" };
|
|
|
10
10
|
import teamSkill from "./skc/skills/team/SKILL.md" with { type: "text" };
|
|
11
11
|
import aiSlopCleanerFragment from "./skc/skills/ultragoal/ai-slop-cleaner.md" with { type: "text" };
|
|
12
12
|
import ultragoalSkill from "./skc/skills/ultragoal/SKILL.md" with { type: "text" };
|
|
13
|
+
import ponytailRule from "./skc/rules/ponytail.md" with { type: "text" };
|
|
13
14
|
|
|
14
15
|
export const DEFAULT_SKC_DEFINITION_NAMES = ["deep-interview", "ralplan", "team", "ultragoal"] as const;
|
|
15
16
|
export type DefaultSkcDefinitionName = (typeof DEFAULT_SKC_DEFINITION_NAMES)[number];
|
|
16
|
-
export type DefaultSkcDefinitionKind = "skill" | "skill-fragment";
|
|
17
|
+
export type DefaultSkcDefinitionKind = "skill" | "skill-fragment" | "rule";
|
|
17
18
|
export type EmbeddedDefaultSkcSkill = {
|
|
18
19
|
name: DefaultSkcDefinitionName;
|
|
19
20
|
description: string;
|
|
@@ -39,7 +40,17 @@ export interface DefaultSkcSkillFragmentDefinition {
|
|
|
39
40
|
content: string;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
export
|
|
43
|
+
export interface DefaultSkcRuleDefinition {
|
|
44
|
+
kind: "rule";
|
|
45
|
+
name: string;
|
|
46
|
+
relativePath: string;
|
|
47
|
+
content: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type DefaultSkcDefinition =
|
|
51
|
+
| DefaultSkcSkillDefinition
|
|
52
|
+
| DefaultSkcSkillFragmentDefinition
|
|
53
|
+
| DefaultSkcRuleDefinition;
|
|
43
54
|
|
|
44
55
|
export interface InstallDefaultSkcDefinitionsOptions {
|
|
45
56
|
check?: boolean;
|
|
@@ -59,6 +70,12 @@ export type DefaultSkcDefinitionInstallFile =
|
|
|
59
70
|
parentSkillName: DefaultSkcDefinitionName;
|
|
60
71
|
path: string;
|
|
61
72
|
status: DefaultSkcInstallStatus;
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
kind: "rule";
|
|
76
|
+
name: string;
|
|
77
|
+
path: string;
|
|
78
|
+
status: DefaultSkcInstallStatus;
|
|
62
79
|
};
|
|
63
80
|
|
|
64
81
|
export interface DefaultSkcDefinitionInstallResult {
|
|
@@ -106,6 +123,7 @@ const DEFAULT_SKC_DEFINITIONS: readonly DefaultSkcDefinition[] = [
|
|
|
106
123
|
relativePath: "skill-fragments/ultragoal/ai-slop-cleaner.md",
|
|
107
124
|
content: aiSlopCleanerFragment,
|
|
108
125
|
},
|
|
126
|
+
{ kind: "rule", name: "ponytail", relativePath: "rules/ponytail.md", content: ponytailRule },
|
|
109
127
|
];
|
|
110
128
|
|
|
111
129
|
export function getDefaultSkcDefinitions(): readonly DefaultSkcDefinition[] {
|
|
@@ -174,6 +192,13 @@ export async function installDefaultSkcDefinitions(
|
|
|
174
192
|
path: destination,
|
|
175
193
|
status,
|
|
176
194
|
});
|
|
195
|
+
} else if (definition.kind === "rule") {
|
|
196
|
+
files.push({
|
|
197
|
+
kind: definition.kind,
|
|
198
|
+
name: definition.name,
|
|
199
|
+
path: destination,
|
|
200
|
+
status,
|
|
201
|
+
});
|
|
177
202
|
} else {
|
|
178
203
|
files.push({
|
|
179
204
|
kind: definition.kind,
|
|
@@ -42,7 +42,7 @@ let scoreSequenceFuzzyNative:
|
|
|
42
42
|
let findBestFuzzyMatchNative:
|
|
43
43
|
| ((content: string, target: string, threshold: number) => NativeBestFuzzyMatchResult)
|
|
44
44
|
| undefined;
|
|
45
|
-
void import("
|
|
45
|
+
void import("@sayknow-cli/natives")
|
|
46
46
|
.then(mod => {
|
|
47
47
|
if (typeof mod.h02ScoreSequenceFuzzy === "function") {
|
|
48
48
|
scoreSequenceFuzzyNative = mod.h02ScoreSequenceFuzzy;
|
|
@@ -6,6 +6,7 @@ import type { CredentialDisabledEvent, ImageContent, Model, ProviderResponseMeta
|
|
|
6
6
|
import type { KeyId } from "@sayknow-cli/tui";
|
|
7
7
|
import { logger } from "@sayknow-cli/utils";
|
|
8
8
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
9
|
+
import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/unattended-session";
|
|
9
10
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
10
11
|
import type { SessionManager } from "../../session/session-manager";
|
|
11
12
|
import type {
|
|
@@ -180,6 +181,7 @@ export class ExtensionRunner {
|
|
|
180
181
|
#getContextUsageFn: () => ContextUsage | undefined = () => undefined;
|
|
181
182
|
#compactFn: (instructionsOrOptions?: string | CompactOptions) => Promise<void> = async () => {};
|
|
182
183
|
#getSystemPromptFn: () => string[] = () => [];
|
|
184
|
+
#getWorkflowGateFn: () => WorkflowGateEmitter | undefined = () => undefined;
|
|
183
185
|
#newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
|
184
186
|
#branchHandler: BranchHandler = async () => ({ cancelled: false });
|
|
185
187
|
#navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
|
@@ -234,6 +236,7 @@ export class ExtensionRunner {
|
|
|
234
236
|
this.#hasPendingMessagesFn = contextActions.hasPendingMessages;
|
|
235
237
|
this.#shutdownHandler = contextActions.shutdown;
|
|
236
238
|
this.#getSystemPromptFn = contextActions.getSystemPrompt;
|
|
239
|
+
this.#getWorkflowGateFn = contextActions.getWorkflowGate ?? (() => undefined);
|
|
237
240
|
|
|
238
241
|
// Command context actions (optional, only for interactive mode)
|
|
239
242
|
if (commandContextActions) {
|
|
@@ -463,6 +466,7 @@ export class ExtensionRunner {
|
|
|
463
466
|
shutdown: () => this.#shutdownHandler(),
|
|
464
467
|
getSystemPrompt: () => this.#getSystemPromptFn(),
|
|
465
468
|
hasQueuedMessages: () => this.#hasPendingMessagesFn(), // deprecated alias
|
|
469
|
+
workflowGate: this.#getWorkflowGateFn(),
|
|
466
470
|
};
|
|
467
471
|
}
|
|
468
472
|
|
|
@@ -32,6 +32,7 @@ import type { PythonResult } from "../../eval/py/executor";
|
|
|
32
32
|
import type { BashResult } from "../../exec/bash-executor";
|
|
33
33
|
import type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
34
34
|
import type { CustomEditor } from "../../modes/components/custom-editor";
|
|
35
|
+
import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/unattended-session";
|
|
35
36
|
import type { Theme } from "../../modes/theme/theme";
|
|
36
37
|
import type { CustomMessage } from "../../session/messages";
|
|
37
38
|
import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
|
|
@@ -310,6 +311,11 @@ export interface ExtensionContext {
|
|
|
310
311
|
getSystemPrompt(): string[];
|
|
311
312
|
/** @deprecated Use hasPendingMessages() instead */
|
|
312
313
|
hasQueuedMessages(): boolean;
|
|
314
|
+
/**
|
|
315
|
+
* Unattended workflow-gate bridge. Present only when the session runs in
|
|
316
|
+
* unattended/RPC mode; `undefined` in interactive/TUI mode (notify-only).
|
|
317
|
+
*/
|
|
318
|
+
workflowGate?: WorkflowGateEmitter;
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
/**
|
|
@@ -1234,6 +1240,8 @@ export interface ExtensionContextActions {
|
|
|
1234
1240
|
getContextUsage: () => ContextUsage | undefined;
|
|
1235
1241
|
compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
|
|
1236
1242
|
getSystemPrompt: () => string[];
|
|
1243
|
+
/** Unattended workflow-gate bridge (present only in unattended/RPC mode). */
|
|
1244
|
+
getWorkflowGate?: () => WorkflowGateEmitter | undefined;
|
|
1237
1245
|
}
|
|
1238
1246
|
|
|
1239
1247
|
/** Actions for ExtensionCommandContext (ctx.* in command handlers). */
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
isEnoent,
|
|
11
11
|
logger,
|
|
12
12
|
} from "@sayknow-cli/utils";
|
|
13
|
+
import { settings } from "../../config/settings";
|
|
13
14
|
import { extractPackageName, parsePluginSpec } from "./parser";
|
|
15
|
+
import { scanPluginDir, toDoctorChecks } from "./security-scanner";
|
|
14
16
|
import type {
|
|
15
17
|
DoctorCheck,
|
|
16
18
|
DoctorOptions,
|
|
@@ -211,6 +213,9 @@ export class PluginManager {
|
|
|
211
213
|
}
|
|
212
214
|
// null = use defaults
|
|
213
215
|
|
|
216
|
+
// Security scan — advisory hook, runs after package is on disk but before config is saved.
|
|
217
|
+
await this.#runSecurityScan(pkg.name, path.join(getPluginsNodeModules(), actualName));
|
|
218
|
+
|
|
214
219
|
// Update runtime config
|
|
215
220
|
const config = await this.#ensureConfigLoaded();
|
|
216
221
|
config.plugins[pkg.name] = {
|
|
@@ -614,6 +619,14 @@ export class PluginManager {
|
|
|
614
619
|
}
|
|
615
620
|
}
|
|
616
621
|
}
|
|
622
|
+
|
|
623
|
+
// Security scan for installed plugin
|
|
624
|
+
try {
|
|
625
|
+
const secReport = await scanPluginDir(pluginPath);
|
|
626
|
+
checks.push(...toDoctorChecks(name, secReport));
|
|
627
|
+
} catch {
|
|
628
|
+
// advisory — never fail doctor
|
|
629
|
+
}
|
|
617
630
|
}
|
|
618
631
|
|
|
619
632
|
// Check for orphaned runtime config entries
|
|
@@ -632,6 +645,43 @@ export class PluginManager {
|
|
|
632
645
|
return checks;
|
|
633
646
|
}
|
|
634
647
|
|
|
648
|
+
async #runSecurityScan(pluginName: string, dir: string): Promise<void> {
|
|
649
|
+
// Resolve the scan mode defensively — this can run from CLI paths where Settings
|
|
650
|
+
// is not initialized, and an advisory scan must NEVER abort an install.
|
|
651
|
+
let mode = "warn";
|
|
652
|
+
try {
|
|
653
|
+
mode = settings.get("plugins.security.scanMode");
|
|
654
|
+
} catch {
|
|
655
|
+
mode = "warn";
|
|
656
|
+
}
|
|
657
|
+
if (mode === "off") return;
|
|
658
|
+
try {
|
|
659
|
+
const report = await scanPluginDir(dir);
|
|
660
|
+
if (report.riskLevel === "none") return;
|
|
661
|
+
logger.warn("Plugin security scan flagged risky patterns", {
|
|
662
|
+
plugin: pluginName,
|
|
663
|
+
riskLevel: report.riskLevel,
|
|
664
|
+
score: report.score,
|
|
665
|
+
findings: report.findings,
|
|
666
|
+
});
|
|
667
|
+
// score: 100 = safe, lower = riskier — block when it drops to/under the threshold.
|
|
668
|
+
let threshold = 60;
|
|
669
|
+
try {
|
|
670
|
+
threshold = settings.get("plugins.security.riskThreshold");
|
|
671
|
+
} catch {
|
|
672
|
+
threshold = 60;
|
|
673
|
+
}
|
|
674
|
+
if (mode === "block" && report.score <= threshold) {
|
|
675
|
+
throw new Error(
|
|
676
|
+
`Plugin "${pluginName}" blocked by security policy: ${report.riskLevel} risk (score ${report.score}). Set plugins.security.scanMode=warn to override.`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
} catch (err) {
|
|
680
|
+
if (mode === "block") throw err;
|
|
681
|
+
logger.debug("Security scan skipped due to error", { plugin: pluginName, error: err });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
635
685
|
async #fixMissingPlugin(): Promise<boolean> {
|
|
636
686
|
try {
|
|
637
687
|
const proc = Bun.spawn(["bun", "install"], {
|
|
@@ -11,6 +11,8 @@ import * as os from "node:os";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
|
|
13
13
|
import { isEnoent, logger, pathIsWithin } from "@sayknow-cli/utils";
|
|
14
|
+
import { settings } from "../../../config/settings";
|
|
15
|
+
import { scanPluginDir } from "../security-scanner";
|
|
14
16
|
|
|
15
17
|
import { cachePlugin } from "./cache";
|
|
16
18
|
import { classifySource, fetchMarketplace, parseMarketplaceCatalog, promoteCloneToCache } from "./fetcher";
|
|
@@ -298,6 +300,9 @@ export class MarketplaceManager {
|
|
|
298
300
|
}
|
|
299
301
|
}
|
|
300
302
|
|
|
303
|
+
// Security scan — advisory hook, runs after plugin is on disk but before registry write.
|
|
304
|
+
await this.#runSecurityScan(pluginId, cachePath);
|
|
305
|
+
|
|
301
306
|
// Only now clean up old entries — new cache succeeded, so it is safe to remove old ones.
|
|
302
307
|
if (existing && existing.length > 0) {
|
|
303
308
|
// Remove from scope-appropriate registry first, then cross-check refs before disk deletion.
|
|
@@ -745,6 +750,43 @@ export class MarketplaceManager {
|
|
|
745
750
|
}
|
|
746
751
|
}
|
|
747
752
|
|
|
753
|
+
async #runSecurityScan(pluginName: string, dir: string): Promise<void> {
|
|
754
|
+
// Resolve the scan mode defensively — this can run from CLI paths where Settings
|
|
755
|
+
// is not initialized, and an advisory scan must NEVER abort an install.
|
|
756
|
+
let mode = "warn";
|
|
757
|
+
try {
|
|
758
|
+
mode = settings.get("plugins.security.scanMode");
|
|
759
|
+
} catch {
|
|
760
|
+
mode = "warn";
|
|
761
|
+
}
|
|
762
|
+
if (mode === "off") return;
|
|
763
|
+
try {
|
|
764
|
+
const report = await scanPluginDir(dir);
|
|
765
|
+
if (report.riskLevel === "none") return;
|
|
766
|
+
logger.warn("Plugin security scan flagged risky patterns", {
|
|
767
|
+
plugin: pluginName,
|
|
768
|
+
riskLevel: report.riskLevel,
|
|
769
|
+
score: report.score,
|
|
770
|
+
findings: report.findings,
|
|
771
|
+
});
|
|
772
|
+
// score: 100 = safe, lower = riskier — block when it drops to/under the threshold.
|
|
773
|
+
let threshold = 60;
|
|
774
|
+
try {
|
|
775
|
+
threshold = settings.get("plugins.security.riskThreshold");
|
|
776
|
+
} catch {
|
|
777
|
+
threshold = 60;
|
|
778
|
+
}
|
|
779
|
+
if (mode === "block" && report.score <= threshold) {
|
|
780
|
+
throw new Error(
|
|
781
|
+
`Plugin "${pluginName}" blocked by security policy: ${report.riskLevel} risk (score ${report.score}). Set plugins.security.scanMode=warn to override.`,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
} catch (err) {
|
|
785
|
+
if (mode === "block") throw err;
|
|
786
|
+
logger.debug("Security scan skipped due to error", { plugin: pluginName, error: err });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
748
790
|
/**
|
|
749
791
|
* Compute the marketplace root directory for source resolution.
|
|
750
792
|
*
|