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 +9 -3
- package/hermes-acp-bootstrap.py +18 -0
- package/hermes-probe.mjs +4 -0
- package/hermes-setup.mjs +10 -0
- package/package.json +1 -1
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 (!
|
|
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
|
|
package/hermes-acp-bootstrap.py
CHANGED
|
@@ -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 || []
|