@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
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Discovers installed OpenCode plugins for plugin scanning.
3
+ * Scans NPM packages from config files and local plugin files.
4
+ */
5
+ import { readdir, stat } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { basename, join } from "node:path";
8
+ import { defaultBranding, getFileContent } from "@gendigital/sage-core";
9
+ /** Resolve XDG base directories with fallbacks to homedir defaults */
10
+ function getConfigHome() {
11
+ return process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
12
+ }
13
+ function getCacheHome() {
14
+ return process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
15
+ }
16
+ const PROJECT_CONFIG_NAME = "opencode.json";
17
+ /**
18
+ * Discover OpenCode plugins from all sources:
19
+ * 1. NPM packages from config files
20
+ * 2. Local plugin files (global + project)
21
+ */
22
+ export async function discoverOpenCodePlugins(logger, projectDir, branding = defaultBranding) {
23
+ logger.debug(`${branding.name} plugin discovery: scanning OpenCode plugins`);
24
+ const plugins = [];
25
+ // 1. Discover NPM plugins from config files
26
+ const npmPlugins = await discoverNpmPlugins(logger, projectDir);
27
+ plugins.push(...npmPlugins);
28
+ // 2. Discover local plugin files (global)
29
+ const globalPluginsDir = join(getConfigHome(), "opencode", "plugins");
30
+ const globalPlugins = await discoverLocalPlugins(globalPluginsDir, "global", logger);
31
+ plugins.push(...globalPlugins);
32
+ // 3. Discover local plugin files (project)
33
+ if (projectDir) {
34
+ const projectPluginsDir = join(projectDir, ".opencode", "plugins");
35
+ const projectPlugins = await discoverLocalPlugins(projectPluginsDir, "project", logger);
36
+ plugins.push(...projectPlugins);
37
+ }
38
+ logger.info(`${branding.name} plugin discovery: found ${plugins.length} plugin(s)`);
39
+ return plugins;
40
+ }
41
+ /**
42
+ * Discover NPM plugins listed in config files
43
+ */
44
+ async function discoverNpmPlugins(logger, projectDir) {
45
+ const plugins = [];
46
+ const pluginNames = new Set();
47
+ // Read global config
48
+ try {
49
+ const globalConfigPath = join(getConfigHome(), "opencode", "opencode.json");
50
+ const globalConfig = JSON.parse(await getFileContent(globalConfigPath));
51
+ const globalPlugins = (globalConfig.plugin ?? []);
52
+ for (const name of globalPlugins) {
53
+ pluginNames.add(name);
54
+ }
55
+ }
56
+ catch {
57
+ logger.debug("Global OpenCode config not found or invalid");
58
+ }
59
+ // Read project config (overrides global)
60
+ if (projectDir) {
61
+ try {
62
+ const projectConfigPath = join(projectDir, PROJECT_CONFIG_NAME);
63
+ const projectConfig = JSON.parse(await getFileContent(projectConfigPath));
64
+ const projectPlugins = (projectConfig.plugin ?? []);
65
+ for (const name of projectPlugins) {
66
+ pluginNames.add(name);
67
+ }
68
+ }
69
+ catch {
70
+ logger.debug("Project OpenCode config not found or invalid");
71
+ }
72
+ }
73
+ // For each plugin name, find it in node_modules
74
+ const npmCacheDir = join(getCacheHome(), "opencode", "node_modules");
75
+ for (const pluginName of pluginNames) {
76
+ try {
77
+ const installPath = join(npmCacheDir, pluginName);
78
+ const pkgJsonPath = join(installPath, "package.json");
79
+ const pkgJson = JSON.parse(await getFileContent(pkgJsonPath));
80
+ const stats = await stat(installPath);
81
+ const version = pkgJson.version ?? "unknown";
82
+ const key = `${pluginName}@${version}`;
83
+ plugins.push({
84
+ key,
85
+ installPath,
86
+ version,
87
+ lastUpdated: stats.mtime.toISOString(),
88
+ });
89
+ logger.debug(`Discovered NPM plugin: ${key}`, { installPath });
90
+ }
91
+ catch {
92
+ logger.warn(`NPM plugin listed in config but not found: ${pluginName}`);
93
+ }
94
+ }
95
+ return plugins;
96
+ }
97
+ /**
98
+ * Discover local plugin files (.js/.ts) in a directory
99
+ */
100
+ async function discoverLocalPlugins(pluginsDir, context, logger) {
101
+ const plugins = [];
102
+ let entries;
103
+ try {
104
+ entries = await readdir(pluginsDir);
105
+ }
106
+ catch {
107
+ logger.debug(`${context} plugins directory not found: ${pluginsDir}`);
108
+ return [];
109
+ }
110
+ for (const entry of entries) {
111
+ const fullPath = join(pluginsDir, entry);
112
+ // Only scan .js and .ts files
113
+ if (!(entry.endsWith(".js") || entry.endsWith(".ts"))) {
114
+ continue;
115
+ }
116
+ try {
117
+ const stats = await stat(fullPath);
118
+ if (!stats.isFile())
119
+ continue;
120
+ // Use filename as plugin name
121
+ const name = basename(entry, entry.endsWith(".ts") ? ".ts" : ".js");
122
+ const version = `local-${stats.mtime.getTime()}`; // Synthetic version from mtime
123
+ const key = `${context}/${name}@${version}`;
124
+ plugins.push({
125
+ key,
126
+ installPath: fullPath,
127
+ version,
128
+ lastUpdated: stats.mtime.toISOString(),
129
+ });
130
+ logger.debug(`Discovered local plugin: ${key}`, { path: fullPath });
131
+ }
132
+ catch {
133
+ logger.warn(`Failed to read local plugin: ${entry}`);
134
+ }
135
+ }
136
+ return plugins;
137
+ }
138
+ //# sourceMappingURL=plugin-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-discovery.js","sourceRoot":"","sources":["../src/plugin-discovery.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAExE,sEAAsE;AACtE,SAAS,aAAa;IACrB,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;AAClE,CAAC;AAED,SAAS,YAAY;IACpB,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,mBAAmB,GAAG,eAAe,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC5C,MAAc,EACd,UAAmB,EACnB,WAAqB,eAAe;IAEpC,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,IAAI,8CAA8C,CAAC,CAAC;IAE7E,MAAM,OAAO,GAAiB,EAAE,CAAC;IAEjC,4CAA4C;IAC5C,MAAM,UAAU,GAAG,MAAM,kBAAkB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;IAE5B,0CAA0C;IAC1C,MAAM,gBAAgB,GAAG,IAAI,CAAC,aAAa,EAAE,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;IACtE,MAAM,aAAa,GAAG,MAAM,oBAAoB,CAAC,gBAAgB,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACrF,OAAO,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC;IAE/B,2CAA2C;IAC3C,IAAI,UAAU,EAAE,CAAC;QAChB,MAAM,iBAAiB,GAAG,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;QACnE,MAAM,cAAc,GAAG,MAAM,oBAAoB,CAAC,iBAAiB,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QACxF,OAAO,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,CAAC;IACjC,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,4BAA4B,OAAO,CAAC,MAAM,YAAY,CAAC,CAAC;IACpF,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,UAAmB;IACpE,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IAEtC,qBAAqB;IACrB,IAAI,CAAC;QACJ,MAAM,gBAAgB,GAAG,IAAI,CAAC,aAAa,EAAE,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC;QAC5E,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,cAAc,CAAC,gBAAgB,CAAC,CAGrE,CAAC;QACF,MAAM,aAAa,GAAG,CAAC,YAAY,CAAC,MAAM,IAAI,EAAE,CAAa,CAAC;QAC9D,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YAClC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;IACF,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;IAC7D,CAAC;IAED,yCAAyC;IACzC,IAAI,UAAU,EAAE,CAAC;QAChB,IAAI,CAAC;YACJ,MAAM,iBAAiB,GAAG,IAAI,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAC;YAChE,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,cAAc,CAAC,iBAAiB,CAAC,CAGvE,CAAC;YACF,MAAM,cAAc,GAAG,CAAC,aAAa,CAAC,MAAM,IAAI,EAAE,CAAa,CAAC;YAChE,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;gBACnC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACvB,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,MAAM,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAC9D,CAAC;IACF,CAAC;IAED,gDAAgD;IAChD,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,EAAE,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;IACrE,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACtC,IAAI,CAAC;YACJ,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;YAClD,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;YACtD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,cAAc,CAAC,WAAW,CAAC,CAA4B,CAAC;YAEzF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,CAAC;YACtC,MAAM,OAAO,GAAI,OAAO,CAAC,OAAkB,IAAI,SAAS,CAAC;YACzD,MAAM,GAAG,GAAG,GAAG,UAAU,IAAI,OAAO,EAAE,CAAC;YAEvC,OAAO,CAAC,IAAI,CAAC;gBACZ,GAAG;gBACH,WAAW;gBACX,OAAO;gBACP,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE;aACtC,CAAC,CAAC;YAEH,MAAM,CAAC,KAAK,CAAC,0BAA0B,GAAG,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;QAChE,CAAC;QAAC,MAAM,CAAC;YACR,MAAM,CAAC,IAAI,CAAC,8CAA8C,UAAU,EAAE,CAAC,CAAC;QACzE,CAAC;IACF,CAAC;IAED,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,oBAAoB,CAClC,UAAkB,EAClB,OAAe,EACf,MAAc;IAEd,MAAM,OAAO,GAAiB,EAAE,CAAC;IAEjC,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACJ,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,iCAAiC,UAAU,EAAE,CAAC,CAAC;QACtE,OAAO,EAAE,CAAC;IACX,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAEzC,8BAA8B;QAC9B,IAAI,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACvD,SAAS;QACV,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC;YACnC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;gBAAE,SAAS;YAE9B,8BAA8B;YAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YACpE,MAAM,OAAO,GAAG,SAAS,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,+BAA+B;YACjF,MAAM,GAAG,GAAG,GAAG,OAAO,IAAI,IAAI,IAAI,OAAO,EAAE,CAAC;YAE5C,OAAO,CAAC,IAAI,CAAC;gBACZ,GAAG;gBACH,WAAW,EAAE,QAAQ;gBACrB,OAAO;gBACP,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE;aACtC,CAAC,CAAC;YAEH,MAAM,CAAC,KAAK,CAAC,4BAA4B,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACR,MAAM,CAAC,IAAI,CAAC,gCAAgC,KAAK,EAAE,CAAC,CAAC;QACtD,CAAC;IACF,CAAC;IAED,OAAO,OAAO,CAAC;AAChB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Startup and session scan handlers for OpenCode.
3
+ * Thin wrapper over core's createScanHandler.
4
+ */
5
+ import { type Branding, type Logger } from "@gendigital/sage-core";
6
+ export declare function createSessionScanHandler(logger: Logger, projectDir?: string, onResult?: (msg: string) => void, branding?: Branding): () => Promise<void>;
7
+ //# sourceMappingURL=startup-scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup-scan.d.ts","sourceRoot":"","sources":["../src/startup-scan.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACN,KAAK,QAAQ,EAGb,KAAK,MAAM,EACX,MAAM,uBAAuB,CAAC;AAmB/B,wBAAgB,wBAAwB,CACvC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,EAChC,QAAQ,GAAE,QAA0B,GAClC,MAAM,OAAO,CAAC,IAAI,CAAC,CAiBrB"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Startup and session scan handlers for OpenCode.
3
+ * Thin wrapper over core's createScanHandler.
4
+ */
5
+ import { createRequire } from "node:module";
6
+ import { createScanHandler as coreScanHandler, defaultBranding, } from "@gendigital/sage-core";
7
+ import { getBundledDataDirs, getSageVersion } from "./bundled-dirs.js";
8
+ import { discoverOpenCodePlugins } from "./plugin-discovery.js";
9
+ /**
10
+ * OpenCode is published as an unbundled ESM plugin (tsc only). The model
11
+ * download worker therefore lives in the `@gendigital/sage-core` package
12
+ * as `dist/model-download-worker.js`. Resolve via `require.resolve` so we
13
+ * pick it up wherever pnpm/node placed it.
14
+ */
15
+ function resolveModelDownloadWorkerPath() {
16
+ try {
17
+ const req = createRequire(import.meta.url);
18
+ return req.resolve("@gendigital/sage-core/dist/model-download-worker.js");
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ export function createSessionScanHandler(logger, projectDir, onResult, branding = defaultBranding) {
25
+ const { threatsDir, allowlistsDir } = getBundledDataDirs();
26
+ const version = getSageVersion();
27
+ return coreScanHandler({
28
+ logger,
29
+ context: "session",
30
+ discoverPlugins: () => discoverOpenCodePlugins(logger, projectDir, branding),
31
+ selfPrefix: "@gendigital/sage-opencode@",
32
+ threatsDir,
33
+ allowlistsDir,
34
+ version,
35
+ agentRuntime: "opencode",
36
+ branding,
37
+ onResult,
38
+ modelDownloadWorkerPath: resolveModelDownloadWorkerPath(),
39
+ });
40
+ }
41
+ //# sourceMappingURL=startup-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup-scan.js","sourceRoot":"","sources":["../src/startup-scan.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAEN,iBAAiB,IAAI,eAAe,EACpC,eAAe,GAEf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE;;;;;GAKG;AACH,SAAS,8BAA8B;IACtC,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3C,OAAO,GAAG,CAAC,OAAO,CAAC,qDAAqD,CAAC,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,wBAAwB,CACvC,MAAc,EACd,UAAmB,EACnB,QAAgC,EAChC,WAAqB,eAAe;IAEpC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,kBAAkB,EAAE,CAAC;IAC3D,MAAM,OAAO,GAAG,cAAc,EAAE,CAAC;IAEjC,OAAO,eAAe,CAAC;QACtB,MAAM;QACN,OAAO,EAAE,SAAS;QAClB,eAAe,EAAE,GAAG,EAAE,CAAC,uBAAuB,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC;QAC5E,UAAU,EAAE,4BAA4B;QACxC,UAAU;QACV,aAAa;QACb,OAAO;QACP,YAAY,EAAE,UAAU;QACxB,QAAQ;QACR,QAAQ;QACR,uBAAuB,EAAE,8BAA8B,EAAE;KACzD,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { type ApprovalStore, type Branding, type Logger } from "@gendigital/sage-core";
2
+ export interface ToolHandlerOptions {
3
+ showToast?: (msg: string, variant: "info" | "success" | "warning" | "error") => void;
4
+ }
5
+ export declare function createToolHandlers(logger: Logger, approvalStore: ApprovalStore, threatsDir: string, allowlistsDir: string, options?: ToolHandlerOptions, branding?: Branding): {
6
+ "tool.execute.before": (input: {
7
+ tool: string;
8
+ sessionID: string;
9
+ callID: string;
10
+ }, output: {
11
+ args: Record<string, unknown>;
12
+ }) => Promise<void>;
13
+ };
14
+ //# sourceMappingURL=tool-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-handler.d.ts","sourceRoot":"","sources":["../src/tool-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,QAAQ,EAOb,KAAK,MAAM,EACX,MAAM,uBAAuB,CAAC;AA+C/B,MAAM,WAAW,kBAAkB;IAClC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,KAAK,IAAI,CAAC;CACrF;AAED,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,kBAAkB,EAC5B,QAAQ,GAAE,QAA0B;mCAG5B;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,UAClD;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,KACvC,OAAO,CAAC,IAAI,CAAC;EA+DhB"}
@@ -0,0 +1,93 @@
1
+ import { canonicalizeToolName, defaultBranding, formatAskMessage, formatDenyMessage, guardToolCall, } from "@gendigital/sage-core";
2
+ const OPENCODE_TOOL_MAP = {
3
+ bash: "Bash",
4
+ webfetch: "WebFetch",
5
+ write: "Write",
6
+ edit: "Edit",
7
+ read: "Read",
8
+ glob: "Glob",
9
+ grep: "Grep",
10
+ ls: "List",
11
+ codesearch: "CodeSearch",
12
+ websearch: "WebSearch",
13
+ question: "Question",
14
+ task: "Task",
15
+ read_lines: "ReadLines",
16
+ };
17
+ import { SageVerdictBlockError, SageVerdictError } from "./error.js";
18
+ import { extractFromOpenCodeTool } from "./extractors.js";
19
+ function asString(value) {
20
+ return typeof value === "string" ? value : undefined;
21
+ }
22
+ function normalizeToolInput(toolName, args) {
23
+ switch (toolName) {
24
+ case "write":
25
+ return {
26
+ ...args,
27
+ file_path: asString(args.filePath) ?? asString(args.file_path) ?? "",
28
+ content: asString(args.content) ?? "",
29
+ };
30
+ case "edit":
31
+ return {
32
+ ...args,
33
+ file_path: asString(args.filePath) ?? asString(args.file_path) ?? "",
34
+ new_string: asString(args.newString) ?? asString(args.new_string) ?? "",
35
+ };
36
+ default:
37
+ return args;
38
+ }
39
+ }
40
+ export function createToolHandlers(logger, approvalStore, threatsDir, allowlistsDir, options, branding = defaultBranding) {
41
+ const beforeToolUse = async (input, output) => {
42
+ logger.debug(`tool.execute.before hook invoked (tool=${input.tool})`);
43
+ try {
44
+ const args = output.args ?? {};
45
+ const artifacts = extractFromOpenCodeTool(input.tool, args);
46
+ if (!artifacts || artifacts.length === 0) {
47
+ return;
48
+ }
49
+ const { verdict, actionId } = await guardToolCall({
50
+ sessionId: input.sessionID,
51
+ conversationId: input.sessionID,
52
+ agentRuntime: "opencode",
53
+ toolName: canonicalizeToolName(OPENCODE_TOOL_MAP, input.tool),
54
+ toolInput: normalizeToolInput(input.tool, args),
55
+ artifacts,
56
+ }, {
57
+ threatsDir,
58
+ allowlistsDir,
59
+ logger,
60
+ }, approvalStore);
61
+ if (verdict.decision === "allow") {
62
+ return;
63
+ }
64
+ try {
65
+ const toastMsg = verdict.decision === "deny"
66
+ ? `${branding.name} blocked: ${verdict.reasons[0] ?? "Threat detected"} (${verdict.category})`
67
+ : `${branding.name} flagged: ${verdict.reasons[0] ?? "Action flagged"} (${verdict.category})`;
68
+ options?.showToast?.(toastMsg, verdict.severity === "critical" ? "error" : "warning");
69
+ }
70
+ catch {
71
+ // Toast failure is non-critical — never prevent enforcement
72
+ }
73
+ if (verdict.decision === "deny") {
74
+ throw new SageVerdictBlockError(formatDenyMessage(verdict, branding));
75
+ }
76
+ // ask — actionId is always set for ask verdicts
77
+ throw new SageVerdictBlockError(formatAskMessage(actionId, verdict, artifacts, branding));
78
+ }
79
+ catch (error) {
80
+ // Verdict errors are expected (blocks and asks); others are logged as fail-open
81
+ if (error instanceof SageVerdictError)
82
+ throw error;
83
+ logger.error(`${branding.name} opencode hook failed open`, {
84
+ error: String(error),
85
+ tool: input.tool,
86
+ });
87
+ }
88
+ };
89
+ return {
90
+ "tool.execute.before": beforeToolUse,
91
+ };
92
+ }
93
+ //# sourceMappingURL=tool-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-handler.js","sourceRoot":"","sources":["../src/tool-handler.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@gendigital/sage-opencode",
3
+ "license": "Apache-2.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "version": "0.9.0",
8
+ "type": "module",
9
+ "description": "Safety for Agents - ADR layer for OpenCode",
10
+ "homepage": "https://github.com/gendigitalinc/sage",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/gendigitalinc/sage.git",
14
+ "directory": "packages/opencode"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/gendigitalinc/sage/issues"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist/**",
28
+ "resources/**",
29
+ "package.json",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "dependencies": {
34
+ "@gendigital/sage-core": "0.9.0"
35
+ },
36
+ "devDependencies": {
37
+ "@opencode-ai/plugin": "^1.2.10",
38
+ "@types/node": "^22.0.0",
39
+ "typescript": "^5.9.0",
40
+ "vitest": "^4.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "keywords": [
46
+ "opencode",
47
+ "opencode-plugin",
48
+ "security",
49
+ "adr",
50
+ "agent-security",
51
+ "safety",
52
+ "malware-detection"
53
+ ],
54
+ "scripts": {
55
+ "build": "pnpm -C ../core build && pnpm run clean && pnpm run sync:assets && tsc",
56
+ "clean": "node ../../scripts/clean.mjs dist resources tsconfig.tsbuildinfo",
57
+ "sync:assets": "node scripts/sync-assets.mjs",
58
+ "test": "vitest run"
59
+ }
60
+ }
@@ -0,0 +1,55 @@
1
+ # Trusted installer domains for Sage
2
+ #
3
+ # Pipe-to-shell commands (curl | bash) targeting these domains are suppressed
4
+ # from CLT-CMD-001, CLT-CMD-002, CLT-SUPPLY-001, and CLT-SUPPLY-004 heuristic matches.
5
+ #
6
+ # Only dedicated installer endpoints are listed here — generic hosting
7
+ # platforms (e.g. raw.githubusercontent.com) are deliberately excluded.
8
+ #
9
+ # Format: list of {domain, reason} entries. Domains are matched by suffix
10
+ # with dot boundary (e.g. "bun.sh" matches "cdn.bun.sh" but not "notbun.sh").
11
+
12
+ - domain: bun.sh
13
+ reason: Bun JavaScript runtime installer
14
+
15
+ - domain: astral.sh
16
+ reason: Astral tools (uv, ruff) installer
17
+
18
+ - domain: brew.sh
19
+ reason: Homebrew package manager installer
20
+
21
+ - domain: sh.rustup.rs
22
+ reason: Rust toolchain installer (rustup)
23
+
24
+ - domain: rustup.rs
25
+ reason: Rust toolchain installer (alternate domain)
26
+
27
+ - domain: get.docker.com
28
+ reason: Docker installer
29
+
30
+ - domain: install.python-poetry.org
31
+ reason: Python Poetry installer
32
+
33
+ - domain: get.volta.sh
34
+ reason: Volta Node.js version manager installer
35
+
36
+ - domain: volta.sh
37
+ reason: Volta Node.js version manager (alternate domain)
38
+
39
+ - domain: get.sdkman.io
40
+ reason: SDKMAN JVM toolchain manager installer
41
+
42
+ - domain: pyenv.run
43
+ reason: pyenv Python version manager installer
44
+
45
+ - domain: deno.land
46
+ reason: Deno runtime installer
47
+
48
+ - domain: get.pnpm.io
49
+ reason: pnpm package manager installer
50
+
51
+ - domain: nodesource.com
52
+ reason: Node.js binary distribution installer
53
+
54
+ - domain: ohmyz.sh
55
+ reason: Oh My Zsh shell framework installer
@@ -0,0 +1,45 @@
1
+ Detection Rule License (DRL) 1.1
2
+
3
+ Copyright 2026 Gen Digital Inc.
4
+
5
+ Default author for all rules in this directory: Gen Digital Inc.
6
+ Individual rules may specify a different author in their "author" field.
7
+
8
+ ---
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this rule set and associated documentation files (the "Rules"), to deal in
12
+ the Rules without restriction, including without limitation the rights to use,
13
+ copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
+ the Rules, and to permit persons to whom the Rules are furnished to do so,
15
+ subject to the following conditions:
16
+
17
+ If you share the Rules (including in modified form), you must retain the
18
+ following if it is supplied within the Rules:
19
+
20
+ 1. identification of the authors(s) ("author" field) of the Rule and any
21
+ others designated to receive attribution, in any reasonable manner
22
+ requested by the Rule author (including by pseudonym if designated).
23
+
24
+ 2. a URI or hyperlink to the Rule set or explicit Rule to the extent
25
+ reasonably practicable
26
+
27
+ 3. indicate the Rules are licensed under this Detection Rule License, and
28
+ include the text of, or the URI or hyperlink to, this Detection Rule
29
+ License to the extent reasonably practicable
30
+
31
+ If you use the Rules (including in modified form) on data, messages based on
32
+ matches with the Rules must retain the following if it is supplied within the
33
+ Rules:
34
+
35
+ 1. identification of the authors(s) ("author" field) of the Rule and any
36
+ others designated to receive attribution, in any reasonable manner
37
+ requested by the Rule author (including by pseudonym if designated).
38
+
39
+ THE RULES ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
41
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
42
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
43
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
44
+ OUT OF OR IN CONNECTION WITH THE RULES OR THE USE OR OTHER DEALINGS IN THE
45
+ RULES.