@hybridlabor-api/aos 4.5.1 → 4.5.2

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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * BDB Agent OS (AOS) plugin for OpenCode.
3
+ *
4
+ * Implements:
5
+ * 1. tool.execute.before: Releases gatekeeper (go-gate). Guards outward-facing
6
+ * or hard-to-reverse actions (git push, npm publish, npm version, rm -rf)
7
+ * unless the user explicitly authorized it with the literal word "GO".
8
+ * 2. chat.message: Ambient memory injection (memB). Injects relevant project & user
9
+ * memories as ephemeral context from ~/.MemBDB/memb.db via node:sqlite.
10
+ *
11
+ * Zero external runtime dependencies. Fails open on memory lookup, fails closed
12
+ * on unguarded destructive actions.
13
+ */
14
+
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+
19
+ const GUARDED_PATTERNS = [
20
+ /^\s*git\s+push\b/i,
21
+ /^\s*npm\s+publish\b/i,
22
+ /^\s*npm\s+version\b/i,
23
+ /^\s*rm\s+(-\w*[rR]\w*|--recursive)\b/i,
24
+ ];
25
+
26
+ let lastHumanPrompt = '';
27
+
28
+ function isGuardedCommand(cmd) {
29
+ if (typeof cmd !== 'string') return false;
30
+ return GUARDED_PATTERNS.some((re) => re.test(cmd));
31
+ }
32
+
33
+ function verifyGoAuthorized(lastPrompt) {
34
+ const trimmed = (lastPrompt || '').trim();
35
+ return trimmed.toUpperCase() === 'GO';
36
+ }
37
+
38
+ // Extract ambient memory from local memB database
39
+ async function getMembContext(prompt, currentDir) {
40
+ const home = os.homedir();
41
+ const dbPath = path.join(home, '.MemBDB', 'memb.db');
42
+ if (!existsSync(dbPath)) return null;
43
+
44
+ try {
45
+ const { DatabaseSync } = await import('node:sqlite');
46
+ const db = new DatabaseSync(dbPath, { readOnly: true });
47
+ db.exec('PRAGMA busy_timeout = 3000;');
48
+
49
+ const contextItems = [];
50
+
51
+ // Standing persona facts
52
+ const personaFile = path.join(home, '.MemBDB', 'ambient-persona.txt');
53
+ if (existsSync(personaFile)) {
54
+ try {
55
+ for (const line of readFileSync(personaFile, 'utf8').split('\n')) {
56
+ const t = line.trim();
57
+ if (t && !t.startsWith('#')) contextItems.push(`- ${t}`);
58
+ }
59
+ } catch {}
60
+ }
61
+
62
+ // Resolve project ID candidate
63
+ let projectName = path.basename(currentDir);
64
+ const pkgPath = path.join(currentDir, 'package.json');
65
+ if (existsSync(pkgPath)) {
66
+ try {
67
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
68
+ if (pkg?.name) projectName = pkg.name;
69
+ } catch {}
70
+ }
71
+
72
+ // Query project memories
73
+ try {
74
+ const sql = `
75
+ SELECT payload FROM memb_vectors
76
+ WHERE collection = 'bdb_agent_memory'
77
+ AND (
78
+ json_extract(payload, '$.project_id') = ?
79
+ OR json_extract(payload, '$.metadata.project_id') = ?
80
+ OR json_extract(payload, '$.project') = ?
81
+ OR json_extract(payload, '$.metadata.project') = ?
82
+ )
83
+ ORDER BY rowid DESC
84
+ LIMIT 3
85
+ `;
86
+ const rows = db.prepare(sql).all(projectName, projectName, projectName, projectName);
87
+ for (const r of rows) {
88
+ try {
89
+ const p = JSON.parse(r.payload);
90
+ const text = (p.memory || p.data || '').trim().replace(/\s+/g, ' ');
91
+ if (text.length > 10) {
92
+ contextItems.push(`- Project [${projectName}]: ${text.slice(0, 180)}`);
93
+ }
94
+ } catch {}
95
+ }
96
+ } catch {}
97
+
98
+ db.close();
99
+
100
+ if (contextItems.length === 0) return null;
101
+ return `[AOS Ambient Memory - memB]\n${contextItems.join('\n')}`;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ export default async function bdbAosPlugin(input) {
108
+ const directory = input.directory || process.cwd();
109
+
110
+ return {
111
+ 'chat.message': async (msgInput, msgOutput) => {
112
+ // Capture the user prompt text for GO-gate verification
113
+ const textParts = (msgOutput.parts || []).filter((p) => p && p.type === 'text' && typeof p.text === 'string');
114
+ const fullText = textParts.map((p) => p.text).join('\n').trim();
115
+ if (fullText) {
116
+ lastHumanPrompt = fullText;
117
+ }
118
+
119
+ // Inject memB ambient memory
120
+ try {
121
+ const membContext = await getMembContext(fullText, directory);
122
+ if (membContext) {
123
+ msgOutput.parts.unshift({
124
+ id: `memb-${Date.now()}`,
125
+ sessionID: msgInput.sessionID,
126
+ messageID: msgInput.messageID || '',
127
+ type: 'text',
128
+ text: membContext,
129
+ synthetic: true,
130
+ });
131
+ }
132
+ } catch {}
133
+ },
134
+
135
+ 'tool.execute.before': async (toolInput, toolOutput) => {
136
+ const toolName = (toolInput.tool || '').toLowerCase();
137
+ // Intercept bash, terminal, or command execution tools
138
+ if (toolName === 'bash' || toolName === 'terminal' || toolName === 'shell' || toolName === 'exec' || toolName === 'run_command') {
139
+ const cmd = toolOutput?.args?.command || toolOutput?.args?.cmd || toolOutput?.args?.script || '';
140
+ if (isGuardedCommand(cmd)) {
141
+ let authorized = verifyGoAuthorized(lastHumanPrompt);
142
+
143
+ // If last cached prompt wasn't GO, query recent session messages via OpenCode client as fallback
144
+ if (!authorized && input.client && toolInput.sessionID) {
145
+ try {
146
+ const res = await input.client.session.messages({ path: { id: toolInput.sessionID } });
147
+ const msgs = res?.data || [];
148
+ for (let i = msgs.length - 1; i >= 0; i--) {
149
+ const m = msgs[i];
150
+ if (m.role === 'user') {
151
+ const parts = m.parts || [];
152
+ const text = parts.filter((p) => p.type === 'text').map((p) => p.text).join('\n').trim();
153
+ if (text) {
154
+ authorized = (text.toUpperCase() === 'GO');
155
+ break;
156
+ }
157
+ }
158
+ }
159
+ } catch {}
160
+ }
161
+
162
+ if (!authorized) {
163
+ throw new Error(
164
+ `Blocked by BDB go-gate: Command "${cmd.slice(0, 80)}" is guarded and requires explicit authorization with the literal word "GO" before proceeding.`
165
+ );
166
+ }
167
+ }
168
+ }
169
+ },
170
+ };
171
+ }
package/installer.js CHANGED
@@ -3213,6 +3213,24 @@ function installGlobalHooks() {
3213
3213
  copyDirRecursiveSync(workflowsSrc, codexWorkflowsDir);
3214
3214
  }
3215
3215
  mergeCodexTomlHooks(path.join(homeDir, '.codex', 'config.toml'));
3216
+
3217
+ // 4. OpenCode CLI
3218
+ const opencodeDir = process.platform === 'win32'
3219
+ ? path.join(process.env.APPDATA || homeDir, 'opencode')
3220
+ : path.join(homeDir, '.config', 'opencode');
3221
+ const opencodePluginSrc = path.join(srcDir, '.opencode', 'plugins', 'bdb-aos.js');
3222
+ if (fs.existsSync(opencodePluginSrc)) {
3223
+ const opencodePluginsDir = path.join(opencodeDir, 'plugins');
3224
+ const opencodePluginDest = path.join(opencodePluginsDir, 'bdb-aos.js');
3225
+ try {
3226
+ fs.mkdirSync(opencodePluginsDir, { recursive: true });
3227
+ fs.copyFileSync(opencodePluginSrc, opencodePluginDest);
3228
+ try { fs.chmodSync(opencodePluginDest, 0o644); } catch (e) { logDebug(e, 'chmod opencode plugin'); }
3229
+ log.step(`Installed OpenCode plugin to ${opencodePluginDest}`);
3230
+ } catch (e) {
3231
+ log.warn(`Could not install OpenCode plugin: ${e.message}`);
3232
+ }
3233
+ }
3216
3234
  }
3217
3235
 
3218
3236
  // Merge the BDB hooks -- the two gates plus the memB ambient-memory hook --
@@ -4022,17 +4040,98 @@ async function universalHarnessSync(primaryMcpConfigPath) {
4022
4040
  const existing = readJsoncFile(targetPath);
4023
4041
  const data = existing && existing.mcp ? existing : Object.assign({}, existing || {}, { mcp: {} });
4024
4042
  if (masterMcpData.mcpServers) {
4043
+ // OpenCode provider APIs (e.g. OpenAI-compatible, Console) enforce tool name length limits
4044
+ // (e.g. max 64 chars) and context limits. Loading 20+ MCPs causes provider errors (e.g. 73-char tool names).
4045
+ // OpenCode uses a slim profile: only core servers (memb_mcp, zavora_computer_use) are enabled by default.
4046
+ const OPENCODE_DEFAULT_SLIM = new Set(['memb_mcp', 'zavora_computer_use']);
4047
+ const existingMcp = existing && existing.mcp ? existing.mcp : {};
4048
+ const hasExistingKeys = Object.keys(existingMcp).length > 0;
4049
+
4025
4050
  for (const [key, val] of Object.entries(masterMcpData.mcpServers)) {
4026
- const cmd = Array.isArray(val.command) ? val.command : [val.command];
4027
- const args = Array.isArray(val.args) ? val.args : [];
4051
+ const rawCmd = Array.isArray(val.command) ? val.command : [val.command];
4052
+ const rawArgs = Array.isArray(val.args) ? val.args : [];
4053
+ const fullCmd = [...rawCmd, ...rawArgs].map(c => {
4054
+ if (c === '__PYTHON_BIN__') {
4055
+ return process.platform === 'win32'
4056
+ ? path.join(homeDir, '.gemini', 'config', 'mcps', 'memb-mcp', '.venv', 'Scripts', 'python.exe')
4057
+ : path.join(homeDir, '.gemini', 'config', 'mcps', 'memb-mcp', '.venv', 'bin', 'python');
4058
+ }
4059
+ return c;
4060
+ });
4061
+
4062
+ const existingEntry = existingMcp[key];
4063
+
4064
+ // If existing config is intentionally slimmed (has keys, but omitted this one), don't resurrect unless in slim set
4065
+ if (hasExistingKeys && !existingEntry && !OPENCODE_DEFAULT_SLIM.has(key)) {
4066
+ continue;
4067
+ }
4068
+
4069
+ // Preserve user's explicit enabled/disabled setting; otherwise default to true only for slim set
4070
+ const isEnabled = existingEntry && typeof existingEntry.enabled === 'boolean'
4071
+ ? existingEntry.enabled
4072
+ : OPENCODE_DEFAULT_SLIM.has(key);
4073
+
4074
+ let envObj = val.environment || val.env;
4075
+ if (envObj) {
4076
+ envObj = Object.assign({}, envObj);
4077
+ for (const [eKey, eVal] of Object.entries(envObj)) {
4078
+ if (eVal === '__GEMINI_API_KEY__') {
4079
+ envObj[eKey] = '${GEMINI_API_KEY}';
4080
+ }
4081
+ }
4082
+ }
4083
+
4028
4084
  data.mcp[key] = {
4029
4085
  type: "local",
4030
- command: [...cmd, ...args],
4031
- enabled: true,
4032
- ...(val.environment || val.env ? { environment: val.environment || val.env } : {})
4086
+ command: fullCmd,
4087
+ enabled: isEnabled,
4088
+ ...(envObj ? { environment: envObj } : {})
4033
4089
  };
4034
4090
  }
4035
4091
  }
4092
+
4093
+ // Wire BDB AOS Plugin for OpenCode
4094
+ const opencodeDir = path.dirname(targetPath);
4095
+ const pluginsDir = path.join(opencodeDir, 'plugins');
4096
+ const pluginFile = path.join(pluginsDir, 'bdb-aos.js');
4097
+ const pluginSrc = path.join(srcDir, '.opencode', 'plugins', 'bdb-aos.js');
4098
+
4099
+ try {
4100
+ if (fs.existsSync(pluginSrc)) {
4101
+ fs.mkdirSync(pluginsDir, { recursive: true });
4102
+ fs.copyFileSync(pluginSrc, pluginFile);
4103
+ try { fs.chmodSync(pluginFile, 0o644); } catch (e) { logDebug(e, 'chmod pluginFile'); }
4104
+ }
4105
+ } catch (pluginErr) {
4106
+ log.warn(`Could not install OpenCode plugin: ${pluginErr.message}`);
4107
+ }
4108
+
4109
+ // Register plugin in opencode.jsonc if plugin file exists
4110
+ if (fs.existsSync(pluginFile)) {
4111
+ if (!Array.isArray(data.plugin)) {
4112
+ data.plugin = [];
4113
+ }
4114
+ const pluginPathNormalized = pluginFile.replace(/\\/g, '/');
4115
+ const alreadyRegistered = data.plugin.some(p => {
4116
+ const str = typeof p === 'string' ? p : (Array.isArray(p) ? p[0] : '');
4117
+ return str.includes('bdb-aos');
4118
+ });
4119
+ if (!alreadyRegistered) {
4120
+ data.plugin.push(pluginPathNormalized);
4121
+ }
4122
+ }
4123
+
4124
+ // Register skills paths for OpenCode
4125
+ if (!data.skills || typeof data.skills !== 'object') {
4126
+ data.skills = { paths: [".agents/skills"] };
4127
+ } else if (Array.isArray(data.skills.paths)) {
4128
+ if (!data.skills.paths.includes(".agents/skills")) {
4129
+ data.skills.paths.push(".agents/skills");
4130
+ }
4131
+ } else {
4132
+ data.skills.paths = [".agents/skills"];
4133
+ }
4134
+
4036
4135
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
4037
4136
  fs.writeFileSync(targetPath, JSON.stringify(data, null, 2), { mode: 0o600 });
4038
4137
  try { fs.chmodSync(targetPath, 0o600); } catch (e) { logDebug(e, 'chmod targetPath'); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridlabor-api/aos",
3
- "version": "4.5.1",
3
+ "version": "4.5.2",
4
4
  "description": "AOS — A Curated AI AGENT OS. Optimized agent skills and add-ons like memB, OpenWiki, Heimdall Token Saver, and Godmode architectures.",
5
5
  "main": "installer.js",
6
6
  "engines": {