@gendigital/sage-opencode 0.9.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 (59) hide show
  1. package/README.md +102 -0
  2. package/dist/bundled-dirs.d.ts +12 -0
  3. package/dist/bundled-dirs.d.ts.map +1 -0
  4. package/dist/bundled-dirs.js +29 -0
  5. package/dist/bundled-dirs.js.map +1 -0
  6. package/dist/error.d.ts +7 -0
  7. package/dist/error.d.ts.map +1 -0
  8. package/dist/error.js +12 -0
  9. package/dist/error.js.map +1 -0
  10. package/dist/extractors.d.ts +6 -0
  11. package/dist/extractors.d.ts.map +1 -0
  12. package/dist/extractors.js +111 -0
  13. package/dist/extractors.js.map +1 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +128 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/logger-adaptor.d.ts +15 -0
  19. package/dist/logger-adaptor.d.ts.map +1 -0
  20. package/dist/logger-adaptor.js +32 -0
  21. package/dist/logger-adaptor.js.map +1 -0
  22. package/dist/plugin-discovery.d.ts +12 -0
  23. package/dist/plugin-discovery.d.ts.map +1 -0
  24. package/dist/plugin-discovery.js +138 -0
  25. package/dist/plugin-discovery.js.map +1 -0
  26. package/dist/startup-scan.d.ts +7 -0
  27. package/dist/startup-scan.d.ts.map +1 -0
  28. package/dist/startup-scan.js +41 -0
  29. package/dist/startup-scan.js.map +1 -0
  30. package/dist/tool-handler.d.ts +14 -0
  31. package/dist/tool-handler.d.ts.map +1 -0
  32. package/dist/tool-handler.js +93 -0
  33. package/dist/tool-handler.js.map +1 -0
  34. package/package.json +60 -0
  35. package/resources/allowlists/trusted-installer-domains.yaml +55 -0
  36. package/resources/threats/LICENSE +45 -0
  37. package/resources/threats/commands.yaml +351 -0
  38. package/resources/threats/credentials.yaml +105 -0
  39. package/resources/threats/dummy.yaml +195 -0
  40. package/resources/threats/files.yaml +239 -0
  41. package/resources/threats/mac-commands.yaml +355 -0
  42. package/resources/threats/mac-credentials.yaml +99 -0
  43. package/resources/threats/mac-files.yaml +124 -0
  44. package/resources/threats/mac-obfuscation.yaml +92 -0
  45. package/resources/threats/mac-persistence.yaml +117 -0
  46. package/resources/threats/mac-supply-chain.yaml +40 -0
  47. package/resources/threats/mitre.yaml +810 -0
  48. package/resources/threats/obfuscation.yaml +98 -0
  49. package/resources/threats/persistence.yaml +87 -0
  50. package/resources/threats/prompt-injection.yaml +334 -0
  51. package/resources/threats/self-defense.yaml +55 -0
  52. package/resources/threats/supply_chain.yaml +15 -0
  53. package/resources/threats/urls.yaml +66 -0
  54. package/resources/threats/win-commands.yaml +785 -0
  55. package/resources/threats/win-credentials.yaml +128 -0
  56. package/resources/threats/win-files.yaml +124 -0
  57. package/resources/threats/win-obfuscation.yaml +205 -0
  58. package/resources/threats/win-persistence.yaml +100 -0
  59. package/resources/threats/win-supply-chain.yaml +15 -0
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # Sage for OpenCode
2
+
3
+ Sage integrates with OpenCode as a plugin and evaluates tool calls before they run.
4
+
5
+ ## What it protects
6
+
7
+ - `bash` commands
8
+ - `webfetch` URLs
9
+ - File operations (`read`, `write`, `edit`, `ls`, `glob`, `grep`)
10
+
11
+ Unmapped tools pass through unchanged.
12
+
13
+ ## Install
14
+
15
+ Clone the repo, build the OpenCode package, then point OpenCode to this package path:
16
+
17
+ ```bash
18
+ git clone https://github.com/gendigitalinc/sage
19
+ cd sage
20
+ pnpm install
21
+ pnpm --filter @gendigital/sage-opencode run build
22
+ ```
23
+
24
+ Global config (`~/.config/opencode/opencode.json`):
25
+
26
+ ```json
27
+ {
28
+ "plugin": ["/absolute/path/to/sage/packages/opencode"]
29
+ }
30
+ ```
31
+
32
+ ## Behavior
33
+
34
+ - **deny** verdicts: blocked immediately
35
+ - **ask** verdicts: blocked with an explicit `sage_approve` action ID
36
+ - **allow** verdicts: pass through
37
+
38
+ ## Approval Flow
39
+
40
+ OpenCode does not expose a plugin-spawned permission dialog, so Sage uses tool-based approval:
41
+
42
+ 1. Sage blocks an `ask` verdict and returns an `actionId`
43
+ 2. Ask the user for explicit confirmation in chat
44
+ 3. If approved, call:
45
+
46
+ ```ts
47
+ sage_approve({ actionId: "...", approved: true })
48
+ ```
49
+
50
+ 4. Retry the original tool call
51
+
52
+ ## Session Startup Scanning
53
+
54
+ Sage automatically scans all installed OpenCode plugins when a new session starts.
55
+
56
+ ### What's Scanned
57
+
58
+ - **NPM plugins**: Packages listed in `opencode.json` config (global + project)
59
+ - **Local plugins**: Files in `~/.config/opencode/plugins/` (global)
60
+ - **Project plugins**: Files in `.opencode/plugins/` (project-specific)
61
+
62
+ ### When Scanning Runs
63
+
64
+ - **Trigger**: Once per session on `session.updated` event (deduplicated by session ID)
65
+ - **Performance**: Results are cached for fast subsequent sessions
66
+
67
+ ### How Findings Are Shown
68
+
69
+ 1. **System Prompt Injection**: Findings appear as first message in agent's context
70
+ 2. **One-Shot**: Only injected once per session (not repeated)
71
+ 3. **Agent Notification**: Agent sees threats and can inform user
72
+ 4. **Console Logging**: Findings also logged to OpenCode console
73
+
74
+ ### What Gets Scanned
75
+
76
+ - Plugin source code (JS/TS/PY files)
77
+ - Package metadata (package.json)
78
+ - Embedded URLs (checked against threat intelligence)
79
+ - Suspicious patterns (credentials, obfuscation, persistence)
80
+
81
+ ### Self-Protection
82
+
83
+ - Sage excludes itself from scanning (`@gendigital/sage-opencode`)
84
+ - Fail-open philosophy: Errors don't block OpenCode
85
+
86
+ ### Cache Location
87
+
88
+ Scan cache stored at `~/.sage/plugin_scan_cache.json` for performance.
89
+
90
+ ## Build
91
+
92
+ ```bash
93
+ pnpm --filter @gendigital/sage-opencode run build
94
+ ```
95
+
96
+ This copies `threats/` and `allowlists/` into `packages/opencode/resources/`.
97
+
98
+ ## Test
99
+
100
+ ```bash
101
+ pnpm test -- packages/opencode/src/__tests__/integration.test.ts
102
+ ```
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Bundled directory and version helpers for OpenCode plugin.
3
+ * Centralizes package path resolution for reuse across modules.
4
+ */
5
+ export declare function getBundledDataDirs(): {
6
+ pluginDir: string;
7
+ packageRoot: string;
8
+ threatsDir: string;
9
+ allowlistsDir: string;
10
+ };
11
+ export declare function getSageVersion(): string;
12
+ //# sourceMappingURL=bundled-dirs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled-dirs.d.ts","sourceRoot":"","sources":["../src/bundled-dirs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,wBAAgB,kBAAkB,IAAI;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACtB,CASA;AAED,wBAAgB,cAAc,IAAI,MAAM,CASvC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Bundled directory and version helpers for OpenCode plugin.
3
+ * Centralizes package path resolution for reuse across modules.
4
+ */
5
+ import { readFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ export function getBundledDataDirs() {
9
+ const pluginDir = dirname(fileURLToPath(import.meta.url));
10
+ const packageRoot = join(pluginDir, "..");
11
+ return {
12
+ pluginDir,
13
+ packageRoot,
14
+ threatsDir: join(packageRoot, "resources", "threats"),
15
+ allowlistsDir: join(packageRoot, "resources", "allowlists"),
16
+ };
17
+ }
18
+ export function getSageVersion() {
19
+ try {
20
+ const { packageRoot } = getBundledDataDirs();
21
+ const pkgPath = join(packageRoot, "package.json");
22
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
23
+ return pkg.version ?? "0.0.0";
24
+ }
25
+ catch {
26
+ return "0.0.0";
27
+ }
28
+ }
29
+ //# sourceMappingURL=bundled-dirs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled-dirs.js","sourceRoot":"","sources":["../src/bundled-dirs.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,kBAAkB;IAMjC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC1C,OAAO;QACN,SAAS;QACT,WAAW;QACX,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,SAAS,CAAC;QACrD,aAAa,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,EAAE,YAAY,CAAC;KAC3D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc;IAC7B,IAAI,CAAC;QACJ,MAAM,EAAE,WAAW,EAAE,GAAG,kBAAkB,EAAE,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;QAClD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAA4B,CAAC;QAClF,OAAQ,GAAG,CAAC,OAAkB,IAAI,OAAO,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,OAAO,CAAC;IAChB,CAAC;AACF,CAAC"}
@@ -0,0 +1,7 @@
1
+ export declare abstract class SageVerdictError extends Error {
2
+ constructor(message: string, name?: string);
3
+ }
4
+ export declare class SageVerdictBlockError extends SageVerdictError {
5
+ constructor(message: string);
6
+ }
7
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,8BAAsB,gBAAiB,SAAQ,KAAK;gBACvC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;CAI1C;AAED,qBAAa,qBAAsB,SAAQ,gBAAgB;gBAC9C,OAAO,EAAE,MAAM;CAG3B"}
package/dist/error.js ADDED
@@ -0,0 +1,12 @@
1
+ export class SageVerdictError extends Error {
2
+ constructor(message, name) {
3
+ super(message);
4
+ this.name = name ?? "SageVerdictError";
5
+ }
6
+ }
7
+ export class SageVerdictBlockError extends SageVerdictError {
8
+ constructor(message) {
9
+ super(message, "SageVerdictBlockError");
10
+ }
11
+ }
12
+ //# sourceMappingURL=error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.js","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,MAAM,OAAgB,gBAAiB,SAAQ,KAAK;IACnD,YAAY,OAAe,EAAE,IAAa;QACzC,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,kBAAkB,CAAC;IACxC,CAAC;CACD;AAED,MAAM,OAAO,qBAAsB,SAAQ,gBAAgB;IAC1D,YAAY,OAAe;QAC1B,KAAK,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;IACzC,CAAC;CACD"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * OpenCode tool input extraction mapped onto @gendigital/sage-core artifact extractors.
3
+ */
4
+ import { type Artifact } from "@gendigital/sage-core";
5
+ export declare function extractFromOpenCodeTool(toolName: string, args: Record<string, unknown>): Artifact[] | null;
6
+ //# sourceMappingURL=extractors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractors.d.ts","sourceRoot":"","sources":["../src/extractors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,KAAK,QAAQ,EAKb,MAAM,uBAAuB,CAAC;AAU/B,wBAAgB,uBAAuB,CACtC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,QAAQ,EAAE,GAAG,IAAI,CA2FnB"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * OpenCode tool input extraction mapped onto @gendigital/sage-core artifact extractors.
3
+ */
4
+ import { extractFromBash, extractFromEdit, extractFromWebFetch, extractFromWrite, } from "@gendigital/sage-core";
5
+ function asString(value) {
6
+ return typeof value === "string" ? value : undefined;
7
+ }
8
+ function asNumber(value) {
9
+ return typeof value === "number" ? value : undefined;
10
+ }
11
+ export function extractFromOpenCodeTool(toolName, args) {
12
+ switch (toolName) {
13
+ case "bash": {
14
+ const command = asString(args.command) ?? "";
15
+ return command ? extractFromBash(command) : null;
16
+ }
17
+ case "webfetch": {
18
+ const url = asString(args.url) ?? "";
19
+ return url ? extractFromWebFetch({ url }) : null;
20
+ }
21
+ case "write": {
22
+ const filePath = asString(args.filePath) ?? "";
23
+ if (!filePath)
24
+ return null;
25
+ return extractFromWrite({
26
+ file_path: filePath,
27
+ content: asString(args.content) ?? "",
28
+ });
29
+ }
30
+ case "edit": {
31
+ const filePath = asString(args.filePath) ?? "";
32
+ if (!filePath)
33
+ return null;
34
+ return extractFromEdit({
35
+ file_path: filePath,
36
+ old_string: asString(args.oldString) ?? "",
37
+ new_string: asString(args.newString) ?? "",
38
+ });
39
+ }
40
+ case "read": {
41
+ const filePath = asString(args.filePath) ?? "";
42
+ if (!filePath)
43
+ return null;
44
+ return [{ type: "file_path", value: filePath, context: "read" }];
45
+ }
46
+ case "glob": {
47
+ const pattern = asString(args.pattern) ?? "";
48
+ if (!pattern)
49
+ return null;
50
+ return [{ type: "file_path", value: pattern, context: "glob_pattern" }];
51
+ }
52
+ case "grep": {
53
+ const pattern = asString(args.pattern) ?? "";
54
+ if (!pattern)
55
+ return null;
56
+ const artifacts = [{ type: "content", value: pattern, context: "grep_pattern" }];
57
+ const include = asString(args.include);
58
+ if (include) {
59
+ artifacts.push({ type: "file_path", value: include, context: "grep_include" });
60
+ }
61
+ return artifacts;
62
+ }
63
+ case "ls": {
64
+ const dir = asString(args.path) ?? asString(args.directoryPath) ?? "";
65
+ if (!dir)
66
+ return null;
67
+ return [{ type: "file_path", value: dir, context: "list" }];
68
+ }
69
+ case "codesearch": {
70
+ const query = asString(args.query) ?? "";
71
+ return query ? [{ type: "content", value: query, context: "codesearch_query" }] : null;
72
+ }
73
+ case "websearch": {
74
+ const query = asString(args.query) ?? "";
75
+ return query ? [{ type: "content", value: query, context: "websearch_query" }] : null;
76
+ }
77
+ case "question": {
78
+ const selected = Array.isArray(args.selected) ? args.selected : [];
79
+ const joined = selected
80
+ .map((item) => (typeof item === "string" ? item : null))
81
+ .filter((item) => item !== null)
82
+ .join("\n");
83
+ if (!joined)
84
+ return null;
85
+ return [{ type: "content", value: joined, context: "question_options" }];
86
+ }
87
+ case "task": {
88
+ const prompt = asString(args.prompt) ?? "";
89
+ const desc = asString(args.description) ?? "";
90
+ const payload = [desc, prompt].filter(Boolean).join("\n\n");
91
+ return payload ? [{ type: "content", value: payload, context: "task" }] : null;
92
+ }
93
+ case "read_lines": {
94
+ const filePath = asString(args.filePath) ?? "";
95
+ if (!filePath)
96
+ return null;
97
+ const offset = asNumber(args.offset);
98
+ const limit = asNumber(args.limit);
99
+ return [
100
+ {
101
+ type: "file_path",
102
+ value: `${filePath}:${offset ?? 1}:${limit ?? 0}`,
103
+ context: "read_lines",
104
+ },
105
+ ];
106
+ }
107
+ default:
108
+ return null;
109
+ }
110
+ }
111
+ //# sourceMappingURL=extractors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractors.js","sourceRoot":"","sources":["../src/extractors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Sage OpenCode plugin.
3
+ * Intercepts tool calls and uses @gendigital/sage-core to enforce security verdicts.
4
+ */
5
+ import type { Plugin } from "@opencode-ai/plugin";
6
+ export declare const SagePlugin: Plugin;
7
+ export default SagePlugin;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AASlD,eAAO,MAAM,UAAU,EAAE,MAqIxB,CAAC;AAEF,eAAe,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Sage OpenCode plugin.
3
+ * Intercepts tool calls and uses @gendigital/sage-core to enforce security verdicts.
4
+ */
5
+ import { ApprovalStore, approveAction, loadConfig, resolveBranding } from "@gendigital/sage-core";
6
+ import { tool } from "@opencode-ai/plugin/tool";
7
+ import { getBundledDataDirs } from "./bundled-dirs.js";
8
+ import { OpencodeLogger } from "./logger-adaptor.js";
9
+ import { createSessionScanHandler } from "./startup-scan.js";
10
+ import { createToolHandlers } from "./tool-handler.js";
11
+ const APPROVAL_STORE_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
12
+ export const SagePlugin = async ({ client, directory }) => {
13
+ const logger = new OpencodeLogger(client);
14
+ const config = await loadConfig(undefined, logger);
15
+ const branding = resolveBranding(config.brand_key, logger);
16
+ const { threatsDir, allowlistsDir } = getBundledDataDirs();
17
+ const approvalStore = new ApprovalStore();
18
+ // State: track findings per session for one-shot injection
19
+ const pendingFindingsBySession = new Map();
20
+ // Track scanned sessions to avoid re-triggering on repeated session.updated events
21
+ const scannedSessions = new Set();
22
+ // Set up the cron job that cleans up the approval store.
23
+ const interval = setInterval(() => {
24
+ approvalStore.cleanup();
25
+ }, APPROVAL_STORE_CLEANUP_INTERVAL_MS);
26
+ interval.unref?.();
27
+ const toolHandlers = createToolHandlers(logger, approvalStore, threatsDir, allowlistsDir, {
28
+ showToast: (msg, variant) => {
29
+ client.tui
30
+ .showToast({
31
+ body: { title: branding.name, message: msg, variant, duration: 5000 },
32
+ })
33
+ .catch(() => { });
34
+ },
35
+ }, branding);
36
+ return {
37
+ "tool.execute.before": toolHandlers["tool.execute.before"],
38
+ /**
39
+ * Inject plugin scan findings into first user message as <system-reminder>.
40
+ * This approach leverages OpenCode's existing pattern of appending system
41
+ * reminders to user messages (e.g., plan mode constraints), enabling the
42
+ * agent to reason about security findings before tool execution. Appending
43
+ * to assistant messages or system prompts was less effective.
44
+ */
45
+ "experimental.chat.messages.transform": async (_input, output) => {
46
+ if (pendingFindingsBySession.size === 0)
47
+ return;
48
+ const userMessage = output.messages.filter((m) => m.info.role === "user");
49
+ const message = userMessage[0];
50
+ if (!message)
51
+ return;
52
+ const sessionID = message.info.sessionID;
53
+ const findings = pendingFindingsBySession.get(sessionID);
54
+ if (!findings)
55
+ return;
56
+ const textPart = {
57
+ id: crypto.randomUUID(),
58
+ sessionID,
59
+ messageID: message.info.id,
60
+ type: "text",
61
+ text: [
62
+ `<system-reminder>`,
63
+ findings,
64
+ "",
65
+ "Inform the user about these security findings.",
66
+ `</system-reminder>`,
67
+ ].join("\n"),
68
+ synthetic: true,
69
+ };
70
+ message.parts.push(textPart);
71
+ logger.info(`Injected sage plugin scan findings to user message`, {
72
+ findings,
73
+ });
74
+ pendingFindingsBySession.delete(sessionID);
75
+ },
76
+ // Trigger session scan on first session.updated event per session.
77
+ // OpenCode >=1.3 replaced session.created with session.updated events.
78
+ event: async ({ event }) => {
79
+ if (event.type !== "session.updated")
80
+ return;
81
+ // biome-ignore lint/suspicious/noExplicitAny: Event types from SDK not fully typed
82
+ const props = event.properties;
83
+ const sessionID = props?.sessionID ?? props?.info?.id ?? "unknown";
84
+ if (sessionID !== "unknown" && scannedSessions.has(sessionID))
85
+ return;
86
+ try {
87
+ logger.debug(`${branding.name}: starting session scan`, { sessionID });
88
+ const scanHandler = createSessionScanHandler(logger, directory, (msg) => {
89
+ pendingFindingsBySession.set(sessionID, msg);
90
+ }, branding);
91
+ await scanHandler();
92
+ if (sessionID !== "unknown") {
93
+ scannedSessions.add(sessionID);
94
+ }
95
+ }
96
+ catch (error) {
97
+ logger.error(`${branding.name} session scan failed (fail-open)`, {
98
+ sessionID,
99
+ error: String(error),
100
+ });
101
+ }
102
+ },
103
+ tool: {
104
+ // TODO: After the following PR merged to support client V2 in Opencode Plugin, gitleaks:allow
105
+ // use QuestionTools.ask to replace sage_approve tool
106
+ // PR: https://github.com/anomalyco/opencode/pull/12046
107
+ // Discussion: https://github.com/avast/sage/pull/21#discussion_r2873812399
108
+ sage_approve: tool({
109
+ description: `Approve or reject a ${branding.name}-flagged tool call. IMPORTANT: you MUST ask the user for explicit confirmation in the conversation BEFORE calling this tool. Never auto-approve - always present the flagged action and wait for the user to response.`,
110
+ args: {
111
+ actionId: tool.schema
112
+ .string()
113
+ .describe(`Action ID from ${branding.name} blocked message`),
114
+ approved: tool.schema.boolean().describe("true to approve, false to reject"),
115
+ },
116
+ async execute(args, _context) {
117
+ if (!args.approved) {
118
+ approvalStore.deletePending(args.actionId);
119
+ return "Rejected by user.";
120
+ }
121
+ return approveAction(approvalStore, args.actionId, branding);
122
+ },
123
+ }),
124
+ },
125
+ };
126
+ };
127
+ export default SagePlugin;
128
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Bridges OpenCode's client logger to Sage's Logger interface.
3
+ */
4
+ import type { Logger } from "@gendigital/sage-core";
5
+ import type { PluginInput } from "@opencode-ai/plugin";
6
+ export declare class OpencodeLogger implements Logger {
7
+ private client;
8
+ constructor(client: PluginInput["client"]);
9
+ debug(msg: string, data?: Record<string, unknown>): Promise<void>;
10
+ info(msg: string, data?: Record<string, unknown>): Promise<void>;
11
+ warn(msg: string, data?: Record<string, unknown>): Promise<void>;
12
+ error(msg: string, data?: Record<string, unknown>): Promise<void>;
13
+ private write;
14
+ }
15
+ //# sourceMappingURL=logger-adaptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger-adaptor.d.ts","sourceRoot":"","sources":["../src/logger-adaptor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,qBAAa,cAAe,YAAW,MAAM;IAC5C,OAAO,CAAC,MAAM,CAAwB;gBAE1B,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC;IAI5B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAIjD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAIhD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAIhD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;YAIhD,KAAK;CAUnB"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Bridges OpenCode's client logger to Sage's Logger interface.
3
+ */
4
+ export class OpencodeLogger {
5
+ client;
6
+ constructor(client) {
7
+ this.client = client;
8
+ }
9
+ async debug(msg, data) {
10
+ await this.write("debug", msg, data).catch(() => { });
11
+ }
12
+ async info(msg, data) {
13
+ await this.write("info", msg, data).catch(() => { });
14
+ }
15
+ async warn(msg, data) {
16
+ await this.write("warn", msg, data).catch(() => { });
17
+ }
18
+ async error(msg, data) {
19
+ await this.write("error", msg, data).catch(() => { });
20
+ }
21
+ async write(level, msg, data) {
22
+ await this.client.app.log({
23
+ body: {
24
+ service: "sage-opencode",
25
+ level,
26
+ message: msg,
27
+ extra: data && typeof data === "object" ? data : undefined,
28
+ },
29
+ });
30
+ }
31
+ }
32
+ //# sourceMappingURL=logger-adaptor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger-adaptor.js","sourceRoot":"","sources":["../src/logger-adaptor.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,MAAM,OAAO,cAAc;IAClB,MAAM,CAAwB;IAEtC,YAAY,MAA6B;QACxC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;IAEM,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,IAA8B;QAC7D,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACtD,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,IAA8B;QAC5D,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACrD,CAAC;IAEM,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,IAA8B;QAC5D,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACrD,CAAC;IAEM,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,IAA8B;QAC7D,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACtD,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,KAA0C,EAAE,GAAW,EAAE,IAAc;QAC1F,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;YACzB,IAAI,EAAE;gBACL,OAAO,EAAE,eAAe;gBACxB,KAAK;gBACL,OAAO,EAAE,GAAG;gBACZ,KAAK,EAAE,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAgC,CAAC,CAAC,CAAC,SAAS;aACvF;SACD,CAAC,CAAC;IACJ,CAAC;CACD"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Discovers installed OpenCode plugins for plugin scanning.
3
+ * Scans NPM packages from config files and local plugin files.
4
+ */
5
+ import type { Branding, Logger, PluginInfo } from "@gendigital/sage-core";
6
+ /**
7
+ * Discover OpenCode plugins from all sources:
8
+ * 1. NPM packages from config files
9
+ * 2. Local plugin files (global + project)
10
+ */
11
+ export declare function discoverOpenCodePlugins(logger: Logger, projectDir?: string, branding?: Branding): Promise<PluginInfo[]>;
12
+ //# sourceMappingURL=plugin-discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-discovery.d.ts","sourceRoot":"","sources":["../src/plugin-discovery.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAc1E;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC5C,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,GAAE,QAA0B,GAClC,OAAO,CAAC,UAAU,EAAE,CAAC,CAuBvB"}