thinkpool-pair 0.7.304 → 0.7.306

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/flow-models.mjs CHANGED
@@ -12,6 +12,9 @@ const CLAUDE_TIERS = {
12
12
  const CODEX_SCAFFOLD = ['gpt-5.6-luna', 'gpt-5.4-mini', 'gpt-5.3-codex-spark']
13
13
  const CODEX_BALANCED = ['gpt-5.6-terra', 'gpt-5.4']
14
14
  const HERMES_REVIEW = ['nous:anthropic/claude-sonnet-4.6', 'nous:anthropic/claude-sonnet-4.5', 'nous:anthropic/claude-sonnet-4']
15
+ const HERMES_MODEL_ALIASES = Object.freeze({
16
+ fable: 'nous:anthropic/claude-fable-5',
17
+ })
15
18
 
16
19
  export function normalizeFlowRuntime(runtime, fallback = null) {
17
20
  if (runtime === 'claude' || runtime === 'codex' || runtime === 'hermes') return runtime
@@ -25,8 +28,11 @@ export function modelCatalogValues(catalog = []) {
25
28
  }
26
29
 
27
30
  const exactHermesModelId = (value) => /^[A-Za-z0-9._-]+:[A-Za-z0-9._-]+\/[A-Za-z0-9._:/-]+$/.test(value)
31
+ const exactNousModelId = (value) => /^nous:/i.test(value) && exactHermesModelId(value)
28
32
 
29
33
  const expandHermesModelAlias = (value) => {
34
+ const named = HERMES_MODEL_ALIASES[value.toLowerCase()]
35
+ if (named) return named
30
36
  if (/^glm[-_.]/i.test(value)) return `nous:z-ai/${value.toLowerCase()}`
31
37
  const shorthand = value.match(/^([^:/]+):([^/]+)$/)
32
38
  if (!shorthand) return value
@@ -44,15 +50,15 @@ export function resolveHermesOpenModel(model, catalog = [], { strictCatalog = fa
44
50
  const requested = String(model || '').trim()
45
51
  if (!requested) return { ok: true, model: undefined, validatedBy: 'default' }
46
52
  const values = [...modelCatalogValues(catalog)]
47
- if (values.includes(requested)) return { ok: true, model: requested, validatedBy: 'catalog' }
53
+ if (values.includes(requested) && exactNousModelId(requested)) return { ok: true, model: requested, validatedBy: 'catalog' }
48
54
 
49
55
  const expanded = expandHermesModelAlias(requested)
50
56
  const normalized = (value) => String(value).toLowerCase().replace(/[^a-z0-9]+/g, '')
51
57
  const requestedKey = normalized(expanded).replace(/^nous/, '')
52
58
  const aliases = values.filter((value) => normalized(value).replace(/^nous/, '') === requestedKey)
53
- if (aliases.length === 1) return { ok: true, model: aliases[0], validatedBy: 'catalog' }
59
+ if (aliases.length === 1 && exactNousModelId(aliases[0])) return { ok: true, model: aliases[0], validatedBy: 'catalog' }
54
60
  if (strictCatalog && values.length) return { ok: false, error: `Could not open a Hermes terminal on ${JSON.stringify(requested)} — that model is not in this session's ACP catalog.` }
55
- if (!exactHermesModelId(expanded)) return { ok: false, error: `Could not resolve Hermes model ${JSON.stringify(requested)}. Use a full ACP model ID such as "nous:z-ai/glm-5.2".` }
61
+ if (!exactNousModelId(expanded)) return { ok: false, error: `Could not resolve Hermes model ${JSON.stringify(requested)}. ThinkPool Hermes only routes through Nous Portal; use a full ACP model ID such as "nous:z-ai/glm-5.2".` }
56
62
  return { ok: true, model: expanded, validatedBy: 'child-runtime' }
57
63
  }
58
64
 
@@ -148,6 +148,21 @@ acp_adapter.session._expand_acp_enabled_toolsets = constrained_expand
148
148
 
149
149
  import acp_adapter.server
150
150
 
151
+ # ThinkPool Hermes is a Nous Portal runtime, never a generic provider shell.
152
+ # The profile probe owns the first boundary; this process-local guard is the
153
+ # final defense against profile drift or a future ACP reconstruction bypass.
154
+ _build_model_state = acp_adapter.server.HermesACPAgent._build_model_state
155
+ def nous_only_model_state(self, state):
156
+ provider = str(getattr(getattr(state, "agent", None), "provider", "") or "").strip().lower()
157
+ if provider != "nous":
158
+ raise RuntimeError("ThinkPool Hermes requires the Nous Portal provider")
159
+ result = _build_model_state(self, state)
160
+ for item in list(getattr(result, "available_models", None) or []):
161
+ if not str(getattr(item, "model_id", "") or "").lower().startswith("nous:"):
162
+ raise RuntimeError("ThinkPool Hermes received a non-Nous ACP model")
163
+ return result
164
+ acp_adapter.server.HermesACPAgent._build_model_state = nous_only_model_state
165
+
151
166
  # ThinkPool-only commands live in this process patch rather than in Hermes'
152
167
  # profile. They are intentionally narrow: no identity, shared configuration,
153
168
  # account tokens, or lifecycle/admin controls enter the room surface.
@@ -413,6 +428,9 @@ acp_adapter.server.HermesACPAgent._register_session_mcp_servers = constrained_re
413
428
  # the replacement has re-registered and passed the same exact policy check.
414
429
  _set_model = acp_adapter.server.HermesACPAgent.set_session_model
415
430
  async def constrained_set_model(self, model_id, session_id, **kwargs):
431
+ rendered_model_id = str(model_id or "").strip().lower()
432
+ if ":" in rendered_model_id and not rendered_model_id.startswith("nous:"):
433
+ raise RuntimeError("ThinkPool Hermes only switches models through Nous Portal")
416
434
  state = self.session_manager.get_session(session_id)
417
435
  if state is None:
418
436
  return await _set_model(self, model_id, session_id, **kwargs)
package/hermes-probe.mjs CHANGED
@@ -6,6 +6,7 @@ const clean = (value) => String(value || '').replace(/[\r\n]+/g, ' ').trim()
6
6
 
7
7
  const INSTALL = /Install directory:\s*(.+?)(?:\r?\n|$)/i
8
8
  const PROFILE = /Config:\s*(.+?)(?:\r?\n|$)/i
9
+ const NOUS_PROVIDER = /Model:\s*\{[^\r\n]*['"]provider['"]\s*:\s*['"]nous['"][^\r\n]*\}/i
9
10
 
10
11
  // Resolve the installed venv and isolated profile once, then launch ACP through
11
12
  // bridge-owned code. `thinkpool` is only queried for inventory; it is never the
@@ -21,6 +22,7 @@ export function resolveHermesAcpRuntime({ command = 'thinkpool', execFile = exec
21
22
  // `thinkpool` is the dedicated profile wrapper on supported installs. Its
22
23
  // config output is evidence, not the ACP launch path.
23
24
  const configOutput = execFile(command, ['config', 'show'], { encoding: 'utf8', timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
25
+ if (!NOUS_PROVIDER.test(String(configOutput))) throw new Error('ThinkPool Hermes profile is not pinned to Nous Portal')
24
26
  const config = clean(String(configOutput).match(PROFILE)?.[1])
25
27
  const profile = config ? path.dirname(config) : ''
26
28
  if (!profile || !path.isAbsolute(profile) || path.basename(profile) !== 'thinkpool') throw new Error('Hermes did not report the isolated thinkpool profile')
@@ -34,6 +36,8 @@ export function probeHermesRuntime({ command = 'thinkpool', prefixArgs = [], exe
34
36
  try {
35
37
  const versionOutput = execFile(command, [...prefixArgs, '--version'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
36
38
  const version = clean(versionOutput).match(/Hermes Agent v([^\s]+)/i)?.[1] || null
39
+ const configOutput = execFile(command, [...prefixArgs, 'config', 'show'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
40
+ if (!NOUS_PROVIDER.test(String(configOutput))) return { available: false, version, reason: 'ThinkPool Hermes profile is not pinned to Nous Portal' }
37
41
  execFile(command, [...prefixArgs, 'acp', '--check'], { encoding: 'utf8', env, timeout: 15_000, stdio: ['ignore', 'pipe', 'pipe'] })
38
42
  const hooksOutput = execFile(command, [...prefixArgs, 'hooks', 'list'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
39
43
  const doctorOutput = execFile(command, [...prefixArgs, 'hooks', 'doctor'], { encoding: 'utf8', env, timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] })
package/hermes-setup.mjs CHANGED
@@ -6,6 +6,8 @@ import YAML from 'yaml'
6
6
  import { probeHermesRuntime } from './hermes-probe.mjs'
7
7
 
8
8
  export const HERMES_PROFILE = 'thinkpool'
9
+ export const HERMES_PROVIDER = 'nous'
10
+ export const HERMES_DEFAULT_MODEL = 'openai/gpt-5.6-luna'
9
11
  export const HERMES_GUARD_FILENAME = 'hermes-delegation-guard.mjs'
10
12
 
11
13
  const clean = (value) => String(value || '').replace(/[\r\n]+/g, ' ').trim()
@@ -112,6 +114,14 @@ export function setupHermesRuntime({
112
114
  config = parsed || {}
113
115
  }
114
116
  if (config.hooks != null && !isObject(config.hooks)) throw new Error('Hermes thinkpool hooks config is not a mapping; refusing to overwrite it')
117
+ if (config.model != null && !isObject(config.model)) throw new Error('Hermes thinkpool model config is not a mapping; refusing to overwrite it')
118
+ const model = config.model ||= {}
119
+ model.provider = HERMES_PROVIDER
120
+ model.default = HERMES_DEFAULT_MODEL
121
+ // Provider-specific transport overrides from a cloned profile must never
122
+ // survive into ThinkPool's Nous-only runtime.
123
+ delete model.base_url
124
+ delete model.api_mode
115
125
  const hooks = config.hooks ||= {}
116
126
  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')
117
127
  const prior = hooks.pre_tool_call || []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.304",
3
+ "version": "0.7.306",
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": {