@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.
- package/.opencode/plugins/bdb-aos.js +171 -0
- package/installer.js +104 -5
- package/package.json +1 -1
|
@@ -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
|
|
4027
|
-
const
|
|
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:
|
|
4031
|
-
enabled:
|
|
4032
|
-
...(
|
|
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.
|
|
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": {
|