@ravefox/space 0.0.0-stage → 0.2.1
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/LICENSE +205 -0
- package/NOTICE +55 -0
- package/README.md +63 -2
- package/assets/webhook-sdk/python/bot.py +63 -0
- package/assets/webhook-sdk/python/commonly.py +232 -0
- package/node_modules/@commonlyai/seats/README.md +25 -0
- package/node_modules/@commonlyai/seats/package.json +22 -0
- package/node_modules/@commonlyai/seats/src/adapters/hermes.js +264 -0
- package/node_modules/@commonlyai/seats/src/adapters/stub.js +24 -0
- package/node_modules/@commonlyai/seats/src/contract.js +48 -0
- package/node_modules/@commonlyai/seats/src/failures.js +44 -0
- package/node_modules/@commonlyai/seats/src/harness-plan.js +329 -0
- package/node_modules/@commonlyai/seats/src/index.js +22 -0
- package/node_modules/@commonlyai/seats/src/process-group.js +269 -0
- package/node_modules/@commonlyai/seats/src/record/format.js +737 -0
- package/node_modules/@commonlyai/seats/src/record/otel.js +238 -0
- package/node_modules/@commonlyai/seats/src/runtime-observation.js +29 -0
- package/node_modules/@commonlyai/seats/src/seat-model.js +17 -0
- package/node_modules/@commonlyai/seats/src/supervisor.js +308 -0
- package/node_modules/@commonlyai/seats/src/trace/events.js +46 -0
- package/node_modules/@commonlyai/seats/src/trace/store.js +276 -0
- package/node_modules/@commonlyai/seats/src/turn-processes.js +226 -0
- package/node_modules/@commonlyai/seats/src/upstream-refusal.js +418 -0
- package/node_modules/@commonlyai/seats/src/valet/env.js +201 -0
- package/node_modules/@commonlyai/seats/src/valet/grant.js +123 -0
- package/node_modules/@commonlyai/seats/src/valet/permissions.js +213 -0
- package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +529 -0
- package/package.json +63 -4
- package/skills/commonly/SKILL.md +233 -0
- package/src/commands/agent.js +6334 -0
- package/src/commands/daemon.js +933 -0
- package/src/commands/dev.js +153 -0
- package/src/commands/doctor.js +141 -0
- package/src/commands/inbox.js +330 -0
- package/src/commands/login.js +194 -0
- package/src/commands/mcp.js +56 -0
- package/src/commands/pod.js +120 -0
- package/src/commands/task.js +279 -0
- package/src/commands/verify.js +48 -0
- package/src/index.js +99 -0
- package/src/lib/adapter-login.js +76 -0
- package/src/lib/adapters/broker-bridge-entry.js +76 -0
- package/src/lib/adapters/claude.js +2207 -0
- package/src/lib/adapters/codex-broker-bridge.mjs +298 -0
- package/src/lib/adapters/codex.js +1898 -0
- package/src/lib/adapters/dsh.js +513 -0
- package/src/lib/adapters/goose.js +510 -0
- package/src/lib/adapters/hermes.js +18 -0
- package/src/lib/adapters/index.js +68 -0
- package/src/lib/adapters/opencode.js +1681 -0
- package/src/lib/adapters/pi-commonly-mcp.mjs +71 -0
- package/src/lib/adapters/pi-mcp-client.mjs +617 -0
- package/src/lib/adapters/pi-shell-env.mjs +41 -0
- package/src/lib/adapters/pi.js +1025 -0
- package/src/lib/adapters/qwen.js +523 -0
- package/src/lib/adapters/seat-model.js +8 -0
- package/src/lib/adapters/stub.js +1 -0
- package/src/lib/adapters/tested-version.js +106 -0
- package/src/lib/api.js +205 -0
- package/src/lib/claim-outcome.js +128 -0
- package/src/lib/claimed-work-gate.js +73 -0
- package/src/lib/cli-hint.js +33 -0
- package/src/lib/commit-attribution.js +391 -0
- package/src/lib/config.js +155 -0
- package/src/lib/content-refusal.js +94 -0
- package/src/lib/credential-file.js +208 -0
- package/src/lib/daemon-agent-states.js +95 -0
- package/src/lib/daemon-hint.js +18 -0
- package/src/lib/daemon-logs.js +43 -0
- package/src/lib/daemon-service-windows.js +263 -0
- package/src/lib/daemon-service.js +486 -0
- package/src/lib/daemon-state.js +84 -0
- package/src/lib/daemon-store.js +119 -0
- package/src/lib/daemon-supervisor.js +629 -0
- package/src/lib/daemon-watch.js +115 -0
- package/src/lib/declared-mcp-guard.js +284 -0
- package/src/lib/default-environment.js +207 -0
- package/src/lib/device-login.js +126 -0
- package/src/lib/digest-frame.js +151 -0
- package/src/lib/doctor.js +291 -0
- package/src/lib/egress-proxy.js +160 -0
- package/src/lib/enforcement.js +946 -0
- package/src/lib/environment.js +681 -0
- package/src/lib/first-output-watchdog.js +191 -0
- package/src/lib/git-readonly-server.js +138 -0
- package/src/lib/git-readonly.js +463 -0
- package/src/lib/git-remote-commonly.mjs +100 -0
- package/src/lib/grant-broker-guard.js +345 -0
- package/src/lib/hooks-config.js +238 -0
- package/src/lib/host-gh-broker.js +152 -0
- package/src/lib/host-gh-shim.mjs +98 -0
- package/src/lib/inbox.js +483 -0
- package/src/lib/instance-proxy.js +76 -0
- package/src/lib/machine-host.js +29 -0
- package/src/lib/mcp-credential-delivery.js +121 -0
- package/src/lib/mcp-first-run.js +50 -0
- package/src/lib/mcp-home.js +644 -0
- package/src/lib/mcp-ready.js +68 -0
- package/src/lib/mcp-server-version.js +123 -0
- package/src/lib/mcp-token.js +36 -0
- package/src/lib/mcp-warm-child.mjs +412 -0
- package/src/lib/memory-bridge.js +191 -0
- package/src/lib/memory-import.js +156 -0
- package/src/lib/missing-harness.js +31 -0
- package/src/lib/model-unavailable.js +154 -0
- package/src/lib/native-runtime-inventory.js +40 -0
- package/src/lib/outsider-grant-mcp.js +82 -0
- package/src/lib/own-key-launch.js +42 -0
- package/src/lib/pod-catch-up.js +55 -0
- package/src/lib/pod-focus.js +224 -0
- package/src/lib/pod-tail.js +68 -0
- package/src/lib/poll-liveness.js +103 -0
- package/src/lib/poll-retry.js +81 -0
- package/src/lib/poller.js +200 -0
- package/src/lib/provider-proxy.js +42 -0
- package/src/lib/public-seat-mcp-flag.js +36 -0
- package/src/lib/public-tool-error.js +60 -0
- package/src/lib/public-withheld-tools.js +48 -0
- package/src/lib/reply-receipt.js +55 -0
- package/src/lib/resolve-on-path.js +22 -0
- package/src/lib/runtime-inventory.js +56 -0
- package/src/lib/runtime-observation.js +37 -0
- package/src/lib/sandbox/bwrap.js +275 -0
- package/src/lib/sandbox/codex-perimeter.js +16 -0
- package/src/lib/sandbox/host-check.js +268 -0
- package/src/lib/sandbox/mode.js +49 -0
- package/src/lib/sandbox/opencode-loopback.mjs +68 -0
- package/src/lib/sandbox/seatbelt.js +684 -0
- package/src/lib/sandbox/turn-dir.js +56 -0
- package/src/lib/sandbox/windows-unsandboxed.js +183 -0
- package/src/lib/sandbox/workspace-git-config.js +243 -0
- package/src/lib/sandbox/workspace-pin.js +158 -0
- package/src/lib/seat-runtime-controls.js +61 -0
- package/src/lib/seat-stop.js +97 -0
- package/src/lib/server-skills.js +32 -0
- package/src/lib/session-recycle.js +219 -0
- package/src/lib/session-store.js +215 -0
- package/src/lib/skill-prompt.js +74 -0
- package/src/lib/skills-import.js +113 -0
- package/src/lib/spawn-credential.js +402 -0
- package/src/lib/spawn-retry.js +311 -0
- package/src/lib/stable-install.js +176 -0
- package/src/lib/supervised-turn.js +352 -0
- package/src/lib/task.js +235 -0
- package/src/lib/turn-failure-notice.js +177 -0
- package/src/lib/unconfined-seat-guard.js +94 -0
- package/src/lib/update-check.js +192 -0
- package/src/lib/upstream-refusal.js +1 -0
- package/src/lib/webhook-server.js +109 -0
- package/src/lib/windows-command.js +393 -0
- package/src/lib/workspace-git.js +87 -0
- package/src/lib/workspace-trust.js +162 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/** Hermes 0.22 local CLI wrapper. Hermes owns provider I/O; this file only spawns it. */
|
|
2
|
+
import { spawn as childSpawn, spawnSync } from 'child_process';
|
|
3
|
+
import { createHash, randomBytes } from 'crypto';
|
|
4
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'fs/promises';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { StringDecoder } from 'string_decoder';
|
|
8
|
+
import { observeSpawn, controlsFromArguments } from '../runtime-observation.js';
|
|
9
|
+
import { requireSeatModel } from '../seat-model.js';
|
|
10
|
+
import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
|
|
11
|
+
|
|
12
|
+
export const SUPPORTED_HERMES_VERSION_RANGE = '>=0.22.5 <0.23.0';
|
|
13
|
+
const supportsHermesVersion = (version) => {
|
|
14
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version || '');
|
|
15
|
+
if (!match) return false;
|
|
16
|
+
return Number(match[1]) === 0 && Number(match[2]) === 22 && Number(match[3]) >= 5;
|
|
17
|
+
};
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
19
|
+
const VERSION_TIMEOUT_MS = 15000;
|
|
20
|
+
const MAX_STDERR_CHARS = 4096;
|
|
21
|
+
const MAX_LINE_CHARS = 1024 * 1024;
|
|
22
|
+
// Keys the wrapper owns in the profile's .env. Every other line is the user's.
|
|
23
|
+
const PROFILE_ENV_KEYS = ['OPENROUTER_API_KEY', 'OPENROUTER_BASE_URL'];
|
|
24
|
+
const PLAIN_ENV_VALUE = /^[A-Za-z0-9_\-.:/+=@%~]+$/;
|
|
25
|
+
|
|
26
|
+
const definedKey = (line) => {
|
|
27
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
28
|
+
return match ? match[1] : null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// From 0.22.5 the detached gateway serves a profiles/<hash> home with a child
|
|
32
|
+
// env scrubbed of credentials and overlaid with that profile's own .env
|
|
33
|
+
// (tools/environments/local.py, served_profile_child_env), so a key in the
|
|
34
|
+
// spawn env never reaches the model. Hermes documents <profile>/.env as where
|
|
35
|
+
// a profile's API keys live (website/docs/user-guide/profiles.md). The file
|
|
36
|
+
// stays after the turn: the gateway outlives the spawn and rereads it.
|
|
37
|
+
export const syncProfileEnv = async (home, env) => {
|
|
38
|
+
const owned = PROFILE_ENV_KEYS.filter((key) => env[key]);
|
|
39
|
+
for (const key of owned) {
|
|
40
|
+
if (!PLAIN_ENV_VALUE.test(env[key])) {
|
|
41
|
+
throw new Error(`${key} contains characters a Hermes .env line cannot carry unquoted; refusing to write it`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const path = join(home, '.env');
|
|
45
|
+
let current = '';
|
|
46
|
+
try {
|
|
47
|
+
current = await readFile(path, 'utf8');
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error?.code !== 'ENOENT') throw new Error(`Cannot read the Hermes profile .env (${error?.code || 'read failed'})`);
|
|
50
|
+
}
|
|
51
|
+
const kept = current.split(/\r?\n/).filter((line, index, lines) => (
|
|
52
|
+
!owned.includes(definedKey(line)) && !(index === lines.length - 1 && line === '')
|
|
53
|
+
));
|
|
54
|
+
const next = `${[...kept, ...owned.map((key) => `${key}=${env[key]}`)].join('\n')}\n`;
|
|
55
|
+
await chmod(home, 0o700);
|
|
56
|
+
if (next === current) {
|
|
57
|
+
await chmod(path, 0o600);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const temp = join(home, `.env.${process.pid}.${randomBytes(6).toString('hex')}.tmp`);
|
|
61
|
+
try {
|
|
62
|
+
await writeFile(temp, next, { mode: 0o600, flag: 'wx' });
|
|
63
|
+
await chmod(temp, 0o600);
|
|
64
|
+
await rename(temp, path);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
await rm(temp, { force: true });
|
|
67
|
+
throw new Error(`Cannot write the Hermes profile .env (${error?.code || 'write failed'})`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const readVersion = (env, spawnSyncImpl = spawnSync) => {
|
|
72
|
+
let probe;
|
|
73
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
74
|
+
probe = spawnSyncImpl('hermes', ['--version'], {
|
|
75
|
+
encoding: 'utf8', env, timeout: VERSION_TIMEOUT_MS,
|
|
76
|
+
});
|
|
77
|
+
if (probe.error?.code === 'ENOENT') return null;
|
|
78
|
+
if (probe.error || probe.status !== 0) continue;
|
|
79
|
+
const version = String(probe.stdout || '').match(/Hermes Agent v([^\s]+)/i)?.[1];
|
|
80
|
+
if (!supportsHermesVersion(version)) {
|
|
81
|
+
throw new Error(`Hermes ${version || 'unknown'} is unsupported; this adapter supports ${SUPPORTED_HERMES_VERSION_RANGE}. Run hermes update if yours is older than 0.22.5; releases 0.23 and later are not supported yet`);
|
|
82
|
+
}
|
|
83
|
+
return version;
|
|
84
|
+
}
|
|
85
|
+
const status = probe.status ?? 'none';
|
|
86
|
+
const signal = probe.signal ?? 'none';
|
|
87
|
+
const code = probe.error?.code ?? 'none';
|
|
88
|
+
throw new Error(`Hermes version probe failed after 2 attempts (status=${status}, signal=${signal}, code=${code}, timeout=${VERSION_TIMEOUT_MS}ms); check the Hermes installation or startup log`);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const readHermesJsonl = (line, state) => {
|
|
92
|
+
let event;
|
|
93
|
+
try { event = JSON.parse(line); } catch { throw new Error('Hermes emitted invalid JSONL'); }
|
|
94
|
+
if (state.result) throw new Error('Hermes emitted data after its terminal result');
|
|
95
|
+
if (event?.type === 'system' && event.subtype === 'init') {
|
|
96
|
+
state.started = true;
|
|
97
|
+
} else if (event?.type === 'result') {
|
|
98
|
+
state.result = event;
|
|
99
|
+
} else if (!state.started) {
|
|
100
|
+
throw new Error('Hermes emitted an event before init');
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const runHermes = ({ args, prompt, cwd, env, timeoutMs, spawnImpl, credentials, onSpawn, controls = {} }) => new Promise((resolve, reject) => {
|
|
105
|
+
const child = (spawnImpl || childSpawn)('hermes', args, {
|
|
106
|
+
cwd, env, stdio: ['pipe', 'pipe', 'pipe'],
|
|
107
|
+
});
|
|
108
|
+
observeSpawn(child, onSpawn, controls);
|
|
109
|
+
const state = { started: false, result: null };
|
|
110
|
+
const decoder = new StringDecoder('utf8');
|
|
111
|
+
let buffer = '';
|
|
112
|
+
let stderr = '';
|
|
113
|
+
let timedOut = false;
|
|
114
|
+
let settled = false;
|
|
115
|
+
let closed = false;
|
|
116
|
+
let stopping = false;
|
|
117
|
+
let killTimer;
|
|
118
|
+
const finish = (error, result) => {
|
|
119
|
+
if (settled) return;
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
if (error) reject(error); else resolve(result);
|
|
123
|
+
};
|
|
124
|
+
const stop = () => {
|
|
125
|
+
if (stopping || closed) return;
|
|
126
|
+
stopping = true;
|
|
127
|
+
child.kill('SIGTERM');
|
|
128
|
+
killTimer = setTimeout(() => {
|
|
129
|
+
if (!closed) child.kill('SIGKILL');
|
|
130
|
+
}, 5000);
|
|
131
|
+
};
|
|
132
|
+
const failAndStop = (error) => {
|
|
133
|
+
stop();
|
|
134
|
+
finish(error);
|
|
135
|
+
};
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
timedOut = true;
|
|
138
|
+
stop();
|
|
139
|
+
}, timeoutMs);
|
|
140
|
+
child.on('error', (error) => failAndStop(adapterFailure('Hermes', error?.message || 'spawn failed', { credentials })));
|
|
141
|
+
child.stdout.on('data', (chunk) => {
|
|
142
|
+
if (settled) return;
|
|
143
|
+
buffer += decoder.write(chunk);
|
|
144
|
+
let newline = buffer.indexOf('\n');
|
|
145
|
+
while (newline !== -1) {
|
|
146
|
+
if (newline > MAX_LINE_CHARS) {
|
|
147
|
+
failAndStop(new Error('Hermes JSONL line exceeded 1 MiB'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const line = buffer.slice(0, newline).trim();
|
|
151
|
+
buffer = buffer.slice(newline + 1);
|
|
152
|
+
if (line) {
|
|
153
|
+
try { readHermesJsonl(line, state); } catch (error) {
|
|
154
|
+
failAndStop(error);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
newline = buffer.indexOf('\n');
|
|
159
|
+
}
|
|
160
|
+
if (buffer.length > MAX_LINE_CHARS) {
|
|
161
|
+
failAndStop(new Error('Hermes JSONL line exceeded 1 MiB'));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
child.stdout.on('error', (error) => failAndStop(adapterFailure('Hermes stdout', error?.message || 'read failed', { credentials })));
|
|
165
|
+
child.stderr.on('data', (chunk) => {
|
|
166
|
+
stderr = `${stderr}${chunk.toString()}`.slice(-MAX_STDERR_CHARS);
|
|
167
|
+
});
|
|
168
|
+
child.on('close', (code) => {
|
|
169
|
+
closed = true;
|
|
170
|
+
clearTimeout(killTimer);
|
|
171
|
+
if (settled) return;
|
|
172
|
+
if (timedOut) return finish(new Error(`Hermes timed out after ${timeoutMs}ms`));
|
|
173
|
+
buffer += decoder.end();
|
|
174
|
+
if (buffer.trim()) {
|
|
175
|
+
try { readHermesJsonl(buffer.trim(), state); } catch (error) { return finish(error); }
|
|
176
|
+
}
|
|
177
|
+
const result = state.result;
|
|
178
|
+
if (!result || !state.started) return finish(new Error('Hermes exited without a complete JSONL result'));
|
|
179
|
+
if (code !== 0 || result.exit_code !== 0) {
|
|
180
|
+
return finish(adapterFailure('Hermes', result.error || stderr || `exit ${code}`, {
|
|
181
|
+
credentials, exitCode: code,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
if (typeof result.text !== 'string' || !result.session_id) {
|
|
185
|
+
return finish(new Error('Hermes result omitted text or session_id'));
|
|
186
|
+
}
|
|
187
|
+
return finish(null, { text: result.text, newSessionId: result.session_id });
|
|
188
|
+
});
|
|
189
|
+
child.stdin.on('error', (error) => failAndStop(adapterFailure('Hermes stdin', error?.message || 'write failed', { credentials })));
|
|
190
|
+
child.stdin.end(prompt);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {import('../contract.js').SeatHost & { profileRoot?: string }} [host]
|
|
195
|
+
* profileRoot is where each seat's isolated HERMES_HOME is created.
|
|
196
|
+
*/
|
|
197
|
+
export const createHermesAdapter = ({
|
|
198
|
+
memoryPreamble = (prompt) => prompt,
|
|
199
|
+
effectiveTrust = (trust) => trust,
|
|
200
|
+
seatModelRequired = () => false,
|
|
201
|
+
profileRoot = join(homedir(), '.seats', 'hermes', 'profiles'),
|
|
202
|
+
} = {}) => ({
|
|
203
|
+
get requiresModel() { return seatModelRequired(); },
|
|
204
|
+
name: 'hermes',
|
|
205
|
+
runtimeType: 'hermes',
|
|
206
|
+
supportsProvider: false,
|
|
207
|
+
providerKeyEnv: 'OPENROUTER_API_KEY',
|
|
208
|
+
|
|
209
|
+
async validateEnvironment(environment) {
|
|
210
|
+
if (environment?.provider !== undefined) {
|
|
211
|
+
throw new Error('Hermes adapter does not consume environment.provider; supply OPENROUTER_API_KEY in the host environment');
|
|
212
|
+
}
|
|
213
|
+
if (environment?.mcp?.length) {
|
|
214
|
+
throw new Error('Hermes adapter does not yet consume environment.mcp declarations');
|
|
215
|
+
}
|
|
216
|
+
if (effectiveTrust(environment?.sandbox?.trust) === 'public'
|
|
217
|
+
|| (environment?.sandbox?.mode && environment.sandbox.mode !== 'none')) {
|
|
218
|
+
throw new Error('Hermes public or sandbox-declared seats are unsupported: no enforced confinement adapter exists');
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async detect() {
|
|
223
|
+
const version = readVersion(process.env);
|
|
224
|
+
return version ? { path: 'hermes', version } : null;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async spawn(prompt, ctx = {}) {
|
|
228
|
+
const seatModel = requireSeatModel(ctx.environment, 'hermes', seatModelRequired());
|
|
229
|
+
await this.validateEnvironment(ctx.environment);
|
|
230
|
+
// Keep Hermes away from the operator's default profile, including any
|
|
231
|
+
// Nous Portal/Cloud credentials there. The wrapper provides the sole key.
|
|
232
|
+
const seat = `${ctx.agentName || ''}\0${ctx.cwd || ''}`;
|
|
233
|
+
const home = ctx._hermesHome || join(profileRoot,
|
|
234
|
+
createHash('sha256').update(seat).digest('hex').slice(0, 24));
|
|
235
|
+
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
236
|
+
const env = { ...(ctx.env || process.env), HERMES_HOME: home };
|
|
237
|
+
if (!readVersion(env, ctx._spawnSyncImpl)) {
|
|
238
|
+
throw new Error(`Hermes Agent ${SUPPORTED_HERMES_VERSION_RANGE} is not on PATH; install for your OS: https://hermes-agent.nousresearch.com/docs/getting-started/installation`);
|
|
239
|
+
}
|
|
240
|
+
if (!env.OPENROUTER_API_KEY) {
|
|
241
|
+
throw new Error('Supply the operator OPENROUTER_API_KEY through Secret Manager before running Hermes');
|
|
242
|
+
}
|
|
243
|
+
await syncProfileEnv(home, env);
|
|
244
|
+
const cwd = ctx.cwd || process.cwd();
|
|
245
|
+
// 0.22.5's gateway rejects --no-restore-cwd; --in pins the workspace on
|
|
246
|
+
// both fresh and resumed turns. 0.22.2-0.22.4 are refused above, so no
|
|
247
|
+
// version-keyed flag is needed.
|
|
248
|
+
const args = ['chat', '--oneshot', '--query-file', '-', '--format', 'stream-json',
|
|
249
|
+
'--provider', 'openrouter', '--source', 'tool', '--in', cwd];
|
|
250
|
+
if (seatModel) args.push('--model', seatModel);
|
|
251
|
+
if (ctx.sessionId) args.push('--resume', ctx.sessionId);
|
|
252
|
+
const fullPrompt = memoryPreamble(prompt, ctx.memoryLongTerm, {
|
|
253
|
+
freshSession: !ctx.sessionId,
|
|
254
|
+
});
|
|
255
|
+
const requestedTimeout = Number(ctx.timeoutMs);
|
|
256
|
+
const timeoutMs = Number.isFinite(requestedTimeout) && requestedTimeout > 0
|
|
257
|
+
? Math.min(requestedTimeout, DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
258
|
+
return runHermes({
|
|
259
|
+
args, prompt: fullPrompt, cwd, env, timeoutMs,
|
|
260
|
+
spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, controls: controlsFromArguments(args, { modelFlag: '--model', valueFlags: ['--query-file', '--format', '--provider', '--source', '--in', '--resume'] }),
|
|
261
|
+
credentials: spawnCredentials(ctx, ['OPENROUTER_API_KEY']),
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stub adapter — the no-op reference implementation of the ADR-005 adapter
|
|
3
|
+
* contract. Used by `commonly agent attach stub` and the Phase-1a test
|
|
4
|
+
* harness so the run loop can be reviewed end-to-end without a real CLI
|
|
5
|
+
* on PATH.
|
|
6
|
+
*
|
|
7
|
+
* Contract (ADR-005 §Adapter pattern):
|
|
8
|
+
* detect(): Promise<{ path, version } | null>
|
|
9
|
+
* spawn(prompt, ctx): Promise<{ text, newSessionId?, memorySummary? }>
|
|
10
|
+
*
|
|
11
|
+
* The adapter MUST be pure: input (argv + env + prompt) → output (text + optional
|
|
12
|
+
* next session-id + optional memory summary). No direct network, no direct API
|
|
13
|
+
* calls, no hidden state. The run loop handles all Commonly-facing I/O.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
name: 'stub',
|
|
18
|
+
async detect() {
|
|
19
|
+
return { path: '(builtin)', version: '0.0.0' };
|
|
20
|
+
},
|
|
21
|
+
async spawn(prompt, _ctx) {
|
|
22
|
+
return { text: `(stub) received: ${String(prompt ?? '').slice(0, 200)}` };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter contract (ADR-005 §Adapter pattern). An adapter turns one prompt
|
|
3
|
+
* into one CLI run and one reply. It owns argv, env and output parsing for its
|
|
4
|
+
* CLI; the caller owns everything that talks to a Commonly instance.
|
|
5
|
+
*
|
|
6
|
+
* @typedef {object} Adapter
|
|
7
|
+
* @property {string} name
|
|
8
|
+
* @property {string} [runtimeType]
|
|
9
|
+
* @property {string} [providerKeyEnv] host env var the adapter's provider key arrives in
|
|
10
|
+
* @property {() => Promise<{ path: string, version: string } | null>} detect
|
|
11
|
+
* @property {(environment: object) => Promise<void>} [validateEnvironment] refuse before spawn
|
|
12
|
+
* @property {(prompt: string, ctx: object) => Promise<SpawnResult>} spawn
|
|
13
|
+
*
|
|
14
|
+
* @typedef {object} SpawnResult
|
|
15
|
+
* @property {string} text
|
|
16
|
+
* @property {string} [newSessionId]
|
|
17
|
+
* @property {number} [contextTokens]
|
|
18
|
+
* @property {string} [memorySummary]
|
|
19
|
+
*
|
|
20
|
+
* Adapters that need host policy are built by a factory that takes a SeatHost.
|
|
21
|
+
* Every field is optional; an absent field takes the neutral default named.
|
|
22
|
+
*
|
|
23
|
+
* @typedef {object} SeatHost
|
|
24
|
+
* @property {(prompt: string, longTerm: string | null | undefined, opts: { freshSession: boolean }) => string} [memoryPreamble] default: the prompt unchanged
|
|
25
|
+
* @property {(trust: string | undefined) => string | undefined} [effectiveTrust] default: the declared trust unchanged
|
|
26
|
+
* @property {() => boolean} [seatModelRequired] default: false
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {Adapter[]} adapters
|
|
31
|
+
*/
|
|
32
|
+
export const createRegistry = (adapters) => {
|
|
33
|
+
const byName = {};
|
|
34
|
+
for (const adapter of adapters) {
|
|
35
|
+
if (!adapter?.name) throw new Error('every adapter needs a name');
|
|
36
|
+
if (byName[adapter.name]) throw new Error(`duplicate adapter name: ${adapter.name}`);
|
|
37
|
+
byName[adapter.name] = adapter;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
adapters: byName,
|
|
41
|
+
names: () => Object.keys(byName),
|
|
42
|
+
get: (name) => byName[name] || null,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const providerKeyEnvNames = (adapters) => [...new Set(
|
|
47
|
+
Object.values(adapters).map((adapter) => adapter.providerKeyEnv).filter(Boolean),
|
|
48
|
+
)];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named run failures. Every way a run can end badly has one kind, so a trace,
|
|
3
|
+
* a retry policy and a dashboard all read the same word. A kind is a contract:
|
|
4
|
+
* add one here, never mint one at a call site.
|
|
5
|
+
*/
|
|
6
|
+
import { scrubAdapterFailure } from './upstream-refusal.js';
|
|
7
|
+
|
|
8
|
+
export const FAILURE_KINDS = Object.freeze({
|
|
9
|
+
stuck: { retryable: true, meaning: 'no progress signal within the liveness window' },
|
|
10
|
+
timeout: { retryable: true, meaning: 'the run exceeded its wall-clock budget' },
|
|
11
|
+
exited: { retryable: true, meaning: 'the CLI process ended before the turn settled' },
|
|
12
|
+
protocol: { retryable: false, meaning: 'the CLI broke its wire protocol' },
|
|
13
|
+
refusal: { retryable: false, meaning: 'the model or provider refused; never retried' },
|
|
14
|
+
max_tokens: { retryable: false, meaning: 'the turn hit its token limit' },
|
|
15
|
+
max_turn_requests: { retryable: false, meaning: 'the turn hit its request limit' },
|
|
16
|
+
cancelled: { retryable: false, meaning: 'the run was cancelled' },
|
|
17
|
+
credential_denied: { retryable: false, meaning: 'the run asked for a credential its grant does not allow' },
|
|
18
|
+
unsupported: { retryable: false, meaning: 'the CLI or its version is outside what this adapter supports' },
|
|
19
|
+
// The harness cannot serve anyone right now. Retrying on the same harness is
|
|
20
|
+
// pointless; a run config's fallback moves to its next entry (harness-plan.js).
|
|
21
|
+
not_authenticated: { retryable: false, meaning: 'the CLI is not signed in, or its sign-in was rejected' },
|
|
22
|
+
quota: { retryable: false, meaning: 'the account behind the CLI is out of quota or credits' },
|
|
23
|
+
provider_unsupported: { retryable: false, meaning: 'the provider does not serve this model to this account' },
|
|
24
|
+
capacity: { retryable: true, meaning: 'every entry the run config allows is at a host cap or held; it may run once one frees' },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export class RunFailure extends Error {
|
|
28
|
+
constructor(kind, detail, { cause } = {}) {
|
|
29
|
+
if (!FAILURE_KINDS[kind]) throw new Error(`unknown failure kind: ${kind}`);
|
|
30
|
+
super(detail ? `${kind}: ${detail}` : kind, cause ? { cause } : undefined);
|
|
31
|
+
this.name = 'RunFailure';
|
|
32
|
+
this.kind = kind;
|
|
33
|
+
this.retryable = FAILURE_KINDS[kind].retryable;
|
|
34
|
+
this.detail = detail || '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Build a failure whose detail has known credentials and secret shapes scrubbed. */
|
|
39
|
+
export const runFailure = (kind, text, { credentials = [], limit } = {}) => {
|
|
40
|
+
const { detail } = scrubAdapterFailure(String(text ?? ''), { credentials, limit });
|
|
41
|
+
return new RunFailure(kind, detail);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const failureKindOf = (error) => (error instanceof RunFailure ? error.kind : null);
|