thinkpool-pair 0.7.234 → 0.7.235

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/bridge.mjs CHANGED
@@ -264,6 +264,39 @@ if (argv[0] === 'set-default-dir') {
264
264
  process.exit(0)
265
265
  }
266
266
  if (argv[0] === 'provider') { const { runProvider } = await import('./provider.mjs'); await runProvider(argv.slice(1)); process.exit(0) }
267
+ if (argv[0] === 'setup-hermes') {
268
+ const clone = argv.includes('--clone')
269
+ const clean = argv.includes('--clean')
270
+ if (clone && clean) { console.error(' choose one Hermes setup mode: --clone or --clean'); process.exit(1) }
271
+ let mode = clone ? 'clone' : clean ? 'clean' : undefined
272
+ if (!mode && process.stdin.isTTY) {
273
+ const answer = await new Promise((resolve) => {
274
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
275
+ rl.question('\n Clone your active Hermes provider/config into an isolated thinkpool profile? [Y/n] ', (value) => { rl.close(); resolve(String(value).trim()) })
276
+ })
277
+ mode = /^n/i.test(answer) ? 'clean' : 'clone'
278
+ }
279
+ try {
280
+ const { setupHermesRuntime } = await import('./hermes-setup.mjs')
281
+ const result = setupHermesRuntime({ mode })
282
+ process.stderr.write(`\n ◆ Hermes ${result.version || ''} is ready for ThinkPool ordinary lanes.\n profile: ${result.profile} (${result.created ? `${result.mode} created` : 'existing'})\n hidden delegation: blocked\n${result.created && result.mode === 'clean' ? ' provider: run `thinkpool setup` before the first model turn\n' : ''}\n`)
283
+ process.exit(0)
284
+ } catch (error) {
285
+ console.error(`\n ◇ Hermes setup failed: ${error?.message || error}\n New profile: npx thinkpool-pair@latest setup-hermes --clone\n Clean profile: npx thinkpool-pair@latest setup-hermes --clean\n`)
286
+ process.exit(1)
287
+ }
288
+ }
289
+ if (argv[0] === 'verify-hermes-isolation') {
290
+ try {
291
+ const { verifyHermesMcpIsolation } = await import('./hermes-isolation.mjs')
292
+ const evidence = await verifyHermesMcpIsolation({ rounds: 2 })
293
+ process.stderr.write(`\n ◆ Hermes MCP isolation HELD for ${evidence.length} consecutive two-lane rounds.\n Each ACP session advertised only its lane-bound marker endpoint.\n No model inference was used.\n\n`)
294
+ process.exit(0)
295
+ } catch (error) {
296
+ console.error(`\n ◇ Hermes MCP isolation VIOLATED or unproven: ${error?.message || error}\n`)
297
+ process.exit(1)
298
+ }
299
+ }
267
300
 
268
301
  // One-command launcher: a bare invocation on a TTY (or the `setup` alias) opens the
269
302
  // interactive menu instead of going straight to account mode. The menu only ASSEMBLES
@@ -310,6 +343,12 @@ if ((!argv[0] || argv[0] === 'setup') && process.stdin.isTTY) {
310
343
  : ['anthropic']
311
344
  await runProvider(args)
312
345
  },
346
+ setupHermes: async ({ mode }) => {
347
+ const { setupHermesRuntime } = await import('./hermes-setup.mjs')
348
+ const result = setupHermesRuntime({ mode })
349
+ io.print(`\n ✓ Hermes ${result.version || ''} ready — isolated profile, ACP healthy, hidden delegation blocked.`)
350
+ if (result.created && result.mode === 'clean') io.print(' Run `thinkpool setup` once to choose a provider before the first model turn.')
351
+ },
313
352
  }
314
353
  await runLauncher({ actions, io, refresh: detectState })
315
354
  process.exit(0)
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { randomUUID } from 'node:crypto'
5
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
6
+ import { startHermesSession } from './hermes-session.mjs'
7
+
8
+ const waitFor = (predicate, timeoutMs = 25_000) => new Promise((resolve, reject) => {
9
+ const started = Date.now()
10
+ const timer = setInterval(() => {
11
+ if (predicate()) { clearInterval(timer); resolve() }
12
+ else if (Date.now() - started > timeoutMs) { clearInterval(timer); reject(new Error('Hermes MCP isolation probe timed out')) }
13
+ }, 50)
14
+ })
15
+
16
+ export function assertHermesIsolation(outputs, markers) {
17
+ if (!Array.isArray(outputs) || outputs.length !== 2 || !Array.isArray(markers) || markers.length !== 2) {
18
+ throw new TypeError('Hermes MCP isolation requires exactly two session outputs and markers')
19
+ }
20
+ for (let i = 0; i < 2; i++) {
21
+ if (!outputs[i].includes(`mcp__thinkpool__${markers[i]}`)) throw new Error(`Hermes lane ${i + 1} did not advertise its own MCP marker`)
22
+ if (outputs[i].includes(`mcp__thinkpool__${markers[1 - i]}`)) throw new Error(`Hermes lane ${i + 1} advertised the other lane's MCP marker`)
23
+ }
24
+ return true
25
+ }
26
+
27
+ export async function verifyHermesMcpIsolation({ rounds = 2, sessionFactory = startHermesSession } = {}) {
28
+ const evidence = []
29
+ for (let round = 0; round < rounds; round++) {
30
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'tp-hermes-mcp-isolation-'))
31
+ const suffix = randomUUID().replaceAll('-', '').slice(0, 10)
32
+ const markers = [`lane_alpha_${suffix}`, `lane_beta_${suffix}`]
33
+ const events = [[], []]
34
+ const sessions = []
35
+ try {
36
+ for (let i = 0; i < 2; i++) {
37
+ const cwd = path.join(root, i === 0 ? 'alpha' : 'beta')
38
+ fs.mkdirSync(cwd)
39
+ const sdkServer = createSdkMcpServer({
40
+ name: 'thinkpool', version: '1.0.0',
41
+ tools: [tool(markers[i], 'Synthetic ThinkPool lane-isolation marker.', {}, async () => ({ content: [{ type: 'text', text: markers[i] }] }))],
42
+ })
43
+ sessions.push(sessionFactory({ cwd, lazy: true, mcpServers: { thinkpool: sdkServer }, onEvent: (event) => events[i].push(event) }))
44
+ }
45
+ sessions.forEach((session) => session.sendTurn('/tools'))
46
+ await waitFor(() => events.every((list) => list.some((event) => event.kind === 'result') || list.some((event) => event.kind === 'error')))
47
+ const errors = events.flatMap((list) => list.filter((event) => event.kind === 'error'))
48
+ if (errors.length) throw new Error(`Hermes MCP isolation probe failed: ${errors.map((event) => event.message).join('; ')}`)
49
+ const outputs = events.map((list) => list.filter((event) => event.kind === 'assistant_stream').map((event) => event.text).join(''))
50
+ assertHermesIsolation(outputs, markers)
51
+ evidence.push({ round: round + 1, markers, sessionIds: sessions.map((session) => session.sessionId) })
52
+ } finally {
53
+ sessions.forEach((session) => session.end())
54
+ await new Promise((resolve) => setTimeout(resolve, 250))
55
+ fs.rmSync(root, { recursive: true, force: true })
56
+ }
57
+ }
58
+ return evidence
59
+ }
package/hermes-probe.mjs CHANGED
@@ -2,13 +2,13 @@ import { execFileSync } from 'node:child_process'
2
2
 
3
3
  const clean = (value) => String(value || '').replace(/[\r\n]+/g, ' ').trim()
4
4
 
5
- export function probeHermesRuntime({ command = 'thinkpool', execFile = execFileSync, env = process.env } = {}) {
5
+ export function probeHermesRuntime({ command = 'thinkpool', prefixArgs = [], execFile = execFileSync, env = process.env } = {}) {
6
6
  try {
7
- const versionOutput = execFile(command, ['--version'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
7
+ const versionOutput = execFile(command, [...prefixArgs, '--version'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
8
8
  const version = clean(versionOutput).match(/Hermes Agent v([^\s]+)/i)?.[1] || null
9
- execFile(command, ['acp', '--check'], { encoding: 'utf8', env, timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] })
10
- const hooksOutput = execFile(command, ['hooks', 'list'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
11
- const guardOutput = execFile(command, ['hooks', 'test', 'pre_tool_call', '--for-tool', 'delegate_task'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
9
+ execFile(command, [...prefixArgs, 'acp', '--check'], { encoding: 'utf8', env, timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] })
10
+ const hooksOutput = execFile(command, [...prefixArgs, 'hooks', 'list'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
11
+ const guardOutput = execFile(command, [...prefixArgs, 'hooks', 'test', 'pre_tool_call', '--for-tool', 'delegate_task'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
12
12
  const delegationBlocked = /(?:action|decision)["']?\s*[:=]\s*["']block/i.test(guardOutput)
13
13
  && /ThinkPool blocks hidden Hermes delegation/i.test(guardOutput)
14
14
  && /pre_tool_call[\s\S]*delegate_task[\s\S]*(?:✓ allowed|allowed)/i.test(hooksOutput)
@@ -0,0 +1,145 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { execFileSync } from 'node:child_process'
5
+ import YAML from 'yaml'
6
+ import { probeHermesRuntime } from './hermes-probe.mjs'
7
+
8
+ export const HERMES_PROFILE = 'thinkpool'
9
+ export const HERMES_GUARD_FILENAME = 'hermes-delegation-guard.mjs'
10
+
11
+ const clean = (value) => String(value || '').replace(/[\r\n]+/g, ' ').trim()
12
+ const commandText = (guardPath) => `node ${JSON.stringify(guardPath)}`
13
+ const isObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value)
14
+ const isThinkPoolGuard = (entry) => isObject(entry)
15
+ && entry.matcher === '^delegate_task$'
16
+ && new RegExp(`${HERMES_GUARD_FILENAME.replaceAll('.', '\\.')}$`).test(String(entry.command || '').replace(/["']/g, ''))
17
+
18
+ export function hermesGuardPath({ homeDir = os.homedir(), env = process.env, platform = process.platform } = {}) {
19
+ const dataRoot = platform === 'win32'
20
+ ? (env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'))
21
+ : (env.XDG_DATA_HOME || path.join(homeDir, '.local', 'share'))
22
+ return path.join(dataRoot, 'thinkpool-pair', HERMES_GUARD_FILENAME)
23
+ }
24
+
25
+ function atomicWrite(filePath, content, { fsImpl = fs, mode = 0o600 } = {}) {
26
+ fsImpl.mkdirSync(path.dirname(filePath), { recursive: true })
27
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`
28
+ try {
29
+ fsImpl.writeFileSync(tempPath, content, { mode })
30
+ fsImpl.chmodSync?.(tempPath, mode)
31
+ fsImpl.renameSync(tempPath, filePath)
32
+ } finally {
33
+ try { fsImpl.rmSync(tempPath, { force: true }) } catch { /* already renamed */ }
34
+ }
35
+ }
36
+
37
+ function snapshotFile(filePath, fsImpl) {
38
+ if (!fsImpl.existsSync(filePath)) return null
39
+ return {
40
+ content: fsImpl.readFileSync(filePath),
41
+ mode: fsImpl.statSync(filePath).mode & 0o777,
42
+ }
43
+ }
44
+
45
+ function restoreFile(filePath, snapshot, fsImpl) {
46
+ if (snapshot) atomicWrite(filePath, snapshot.content, { fsImpl, mode: snapshot.mode })
47
+ else fsImpl.rmSync(filePath, { force: true })
48
+ }
49
+
50
+ function profileAliasReady(output, profile) {
51
+ const escaped = profile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
52
+ return new RegExp(`Alias:\\s+${escaped}\\s+.*hermes\\s+-p\\s+${escaped}`, 'i').test(String(output || ''))
53
+ }
54
+
55
+ export function setupHermesRuntime({
56
+ mode,
57
+ profile = HERMES_PROFILE,
58
+ command = 'hermes',
59
+ env = process.env,
60
+ homeDir = os.homedir(),
61
+ platform = process.platform,
62
+ execFile = execFileSync,
63
+ fsImpl = fs,
64
+ guardSource = fs.readFileSync(new URL('./hermes-delegation-guard.mjs', import.meta.url), 'utf8'),
65
+ } = {}) {
66
+ if (profile !== HERMES_PROFILE) throw new Error(`ThinkPool only manages the isolated Hermes profile ${HERMES_PROFILE}`)
67
+ if (mode != null && mode !== 'clone' && mode !== 'clean') throw new Error('Hermes setup mode must be clone or clean')
68
+
69
+ const run = (args, options = {}) => execFile(command, args, {
70
+ encoding: 'utf8', env: options.env || env, timeout: options.timeout || 20_000,
71
+ stdio: ['ignore', 'pipe', 'pipe'],
72
+ })
73
+ const profileArgs = ['-p', profile]
74
+ let show = ''
75
+ let created = false
76
+ try { show = run(['profile', 'show', profile]) } catch { /* absent */ }
77
+ if (!/Profile:\s*thinkpool/i.test(show)) {
78
+ if (!mode) throw new Error('No isolated Hermes thinkpool profile exists; choose --clone or --clean')
79
+ const createArgs = ['profile', 'create', profile, '--description', 'Isolated ThinkPool Code runtime.']
80
+ if (mode === 'clone') createArgs.push('--clone')
81
+ else createArgs.push('--no-skills')
82
+ run(createArgs, { timeout: 60_000 })
83
+ created = true
84
+ show = run(['profile', 'show', profile])
85
+ }
86
+
87
+ if (!profileAliasReady(show, profile)) {
88
+ run(['profile', 'alias', profile, '--name', profile])
89
+ show = run(['profile', 'show', profile])
90
+ if (!profileAliasReady(show, profile)) throw new Error('Hermes did not create the required thinkpool profile alias')
91
+ }
92
+
93
+ const rawConfigPath = clean(run([...profileArgs, 'config', 'path']))
94
+ if (!rawConfigPath) throw new Error('Hermes did not report the thinkpool profile config path')
95
+ const configPath = rawConfigPath.startsWith('~') ? path.join(homeDir, rawConfigPath.slice(1).replace(/^[/\\]/, '')) : path.resolve(rawConfigPath)
96
+ const profilePathMatch = String(show).match(/^Path:\s*(.+)$/mi)
97
+ if (!profilePathMatch) throw new Error('Hermes did not report the isolated thinkpool profile path')
98
+ const profilePath = profilePathMatch[1].trim().startsWith('~')
99
+ ? path.join(homeDir, profilePathMatch[1].trim().slice(1).replace(/^[/\\]/, ''))
100
+ : path.resolve(profilePathMatch[1].trim())
101
+ const relativeConfig = path.relative(profilePath, configPath)
102
+ if (relativeConfig.startsWith('..') || path.isAbsolute(relativeConfig)) throw new Error('Hermes reported a config path outside the isolated thinkpool profile')
103
+ let config = {}
104
+ if (fsImpl.existsSync(configPath)) {
105
+ const parsed = YAML.parse(fsImpl.readFileSync(configPath, 'utf8'))
106
+ if (parsed != null && !isObject(parsed)) throw new Error('Hermes thinkpool config is not a YAML mapping; refusing to overwrite it')
107
+ config = parsed || {}
108
+ }
109
+ if (config.hooks != null && !isObject(config.hooks)) throw new Error('Hermes thinkpool hooks config is not a mapping; refusing to overwrite it')
110
+ const hooks = config.hooks ||= {}
111
+ if (hooks.pre_tool_call != null && !Array.isArray(hooks.pre_tool_call)) throw new Error('Hermes pre_tool_call hooks are not a list; refusing to overwrite them')
112
+ const prior = hooks.pre_tool_call || []
113
+ hooks.pre_tool_call = [
114
+ ...prior.filter((entry) => !isThinkPoolGuard(entry)),
115
+ { matcher: '^delegate_task$', command: commandText(hermesGuardPath({ homeDir, env, platform })), timeout: 5 },
116
+ ]
117
+
118
+ const guardPath = hermesGuardPath({ homeDir, env, platform })
119
+ const guardSnapshot = snapshotFile(guardPath, fsImpl)
120
+ const configSnapshot = snapshotFile(configPath, fsImpl)
121
+ try {
122
+ atomicWrite(guardPath, guardSource, { fsImpl, mode: 0o700 })
123
+ atomicWrite(configPath, YAML.stringify(config, { lineWidth: 0 }), { fsImpl, mode: 0o600 })
124
+
125
+ const consentEnv = { ...env, HERMES_ACCEPT_HOOKS: '1' }
126
+ const testOutput = run([...profileArgs, 'hooks', 'test', 'pre_tool_call', '--for-tool', 'delegate_task'], { env: consentEnv })
127
+ if (!/(?:action|decision)["']?\s*[:=]\s*["']block/i.test(testOutput)
128
+ || !/ThinkPool blocks hidden Hermes delegation/i.test(testOutput)) {
129
+ throw new Error('Hermes ran the ThinkPool delegation guard but did not report an explicit block')
130
+ }
131
+ run([...profileArgs, 'hooks', 'doctor'], { env: consentEnv })
132
+ run([...profileArgs, 'acp', '--check'], { env: consentEnv })
133
+ const probe = probeHermesRuntime({ command, prefixArgs: profileArgs, execFile, env: consentEnv })
134
+ if (!probe.available) throw new Error(`Hermes setup did not pass the final runtime probe: ${probe.reason || 'unavailable'}`)
135
+ return { ...probe, profile, created, mode: created ? mode : 'existing', guardPath, configPath }
136
+ } catch (error) {
137
+ try {
138
+ restoreFile(configPath, configSnapshot, fsImpl)
139
+ restoreFile(guardPath, guardSnapshot, fsImpl)
140
+ } catch (rollbackError) {
141
+ error.message = `${error.message}; rollback also failed: ${clean(rollbackError.message)}`
142
+ }
143
+ throw error
144
+ }
145
+ }
package/launcher.mjs CHANGED
@@ -15,6 +15,7 @@ import fs from 'node:fs'
15
15
  import path from 'node:path'
16
16
  import { execSync } from 'node:child_process'
17
17
  import { detectProvider, fetchModels, ANTHROPIC_COMPATIBLE, gatewayHint } from './byok-detect.mjs'
18
+ import { probeHermesRuntime } from './hermes-probe.mjs'
18
19
 
19
20
  const HOME = os.homedir()
20
21
  const CFG_DIR = path.join(HOME, '.thinkpool-pair')
@@ -74,12 +75,16 @@ export function detectState() {
74
75
  // provider.json is written with { provider, baseUrl, model } (see provider.mjs).
75
76
  // Anything whose `provider` field is set and not "anthropic" is a custom endpoint.
76
77
  const isCustom = !!provider?.provider && provider.provider !== 'anthropic'
78
+ const hermesInstalled = onPath('hermes')
79
+ const hermesReady = onPath('thinkpool') && probeHermesRuntime().available
77
80
  return {
78
81
  loggedIn: !!auth?.refresh_token,
79
82
  email: auth?.email || auth?.user?.email || null,
80
83
  provider: isCustom ? `Custom (${provider.baseUrl || 'endpoint'})` : 'Anthropic (default)',
81
84
  providerModel: isCustom ? (provider.model || null) : null,
82
- agents: KNOWN_AGENTS.filter(a => onPath(a.cmd)),
85
+ agents: KNOWN_AGENTS.filter(a => onPath(a.cmd) && (a.cmd !== 'thinkpool' || hermesReady)),
86
+ hermesInstalled,
87
+ hermesReady,
83
88
  accountSvc: serviceLoaded(null),
84
89
  cwd: process.cwd(),
85
90
  version: VERSION,
@@ -206,24 +211,34 @@ export async function runLauncher({ actions, io, state = detectState(), refresh
206
211
 
207
212
  const settingsMenu = async () => {
208
213
  for (;;) {
209
- const pick = await askChoice('\n change', [
210
- { label: 'LLM provider', hint: `current: ${state.provider}` },
211
- { label: 'Account', hint: state.loggedIn ? `linked: ${state.email || 'yes'}` : 'not linked' },
212
- { label: 'Restart & update the bridge', hint: state.accountSvc ? 'pull the newest published version, then restart — sessions resume' : 'none installed' },
213
- { label: 'Remove the background service', hint: state.accountSvc ? '' : 'none installed' },
214
- { label: '← Back to main menu', hint: '' },
215
- ], 5) // Enter = Back
216
- if (pick === 4) return
217
- if (pick === 0) await providerMenu()
218
- else if (pick === 1) { await actions.login(); resync(); return }
219
- else if (pick === 2) {
214
+ const options = [
215
+ { key: 'provider', label: 'LLM provider', hint: `current: ${state.provider}` },
216
+ { key: 'account', label: 'Account', hint: state.loggedIn ? `linked: ${state.email || 'yes'}` : 'not linked' },
217
+ { key: 'restart', label: 'Restart & update the bridge', hint: state.accountSvc ? 'pull the newest published version, then restart — sessions resume' : 'none installed' },
218
+ { key: 'remove', label: 'Remove the background service', hint: state.accountSvc ? '' : 'none installed' },
219
+ ]
220
+ if (state.hermesInstalled) options.push({ key: 'hermes', label: 'Hermes Agent', hint: state.hermesReady ? 'isolated profile ready' : 'set up isolated profile + delegation guard' })
221
+ options.push({ key: 'back', label: '← Back to main menu', hint: '' })
222
+ const picked = options[await askChoice('\n change', options, options.length)]
223
+ if (picked.key === 'back') return
224
+ if (picked.key === 'provider') await providerMenu()
225
+ else if (picked.key === 'account') { await actions.login(); resync(); return }
226
+ else if (picked.key === 'restart') {
220
227
  if (state.accountSvc) { await actions.restartUpdateService({ room: null }); resync() }
221
228
  else io.print(' ' + C.yellow('no background service installed'))
222
229
  }
223
- else {
230
+ else if (picked.key === 'remove') {
224
231
  if (state.accountSvc) { await actions.uninstallService({ room: null }); resync() }
225
232
  else io.print(' ' + C.yellow('no background service installed'))
226
233
  }
234
+ else if (picked.key === 'hermes') {
235
+ const setup = await askChoice('\n Hermes profile', [
236
+ { label: 'Clone active profile', hint: 'copies provider/config explicitly; fresh ThinkPool session history' },
237
+ { label: 'Create clean profile', hint: 'no copied credentials or bundled skills; configure provider afterward' },
238
+ { label: '← Back', hint: '' },
239
+ ], 3)
240
+ if (setup !== 2) { await actions.setupHermes({ mode: setup === 0 ? 'clone' : 'clean' }); resync() }
241
+ }
227
242
  }
228
243
  }
229
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.234",
3
+ "version": "0.7.235",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,8 @@
24
24
  "hermes-session.mjs",
25
25
  "hermes-event-mapper.mjs",
26
26
  "hermes-probe.mjs",
27
+ "hermes-setup.mjs",
28
+ "hermes-isolation.mjs",
27
29
  "hermes-delegation-guard.mjs",
28
30
  "runtime-registry.mjs",
29
31
  "runtime-session.mjs",
@@ -81,6 +83,7 @@
81
83
  "@anthropic-ai/claude-agent-sdk": "^0.3.198",
82
84
  "@modelcontextprotocol/sdk": "^1.29.0",
83
85
  "@supabase/supabase-js": "^2.45.0",
86
+ "yaml": "2.9.0",
84
87
  "zod": "^4.4.3"
85
88
  },
86
89
  "optionalDependencies": {