nexo-brain 7.12.0 → 7.12.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/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/bin/nexo-brain.js +15 -2
- package/bin/nexo.js +10 -0
- package/bin/windows-wsl-bridge.js +162 -0
- package/package.json +2 -1
- package/src/agent_runner.py +0 -9
- package/src/auto_update.py +49 -0
- package/src/client_preferences.py +17 -24
- package/src/email_config.py +0 -2
- package/src/health_check.py +14 -1
- package/src/hook_guardrails.py +177 -4
- package/src/local_models.py +5 -0
- package/src/scripts/nexo-agent-run.py +17 -1
- package/src/scripts/nexo-email-migrate-config.py +0 -1
- package/src/scripts/nexo-email-monitor.py +16 -27
- package/src/scripts/nexo_personal_automation.py +145 -20
- package/src/support_snapshot.py +7 -2
- package/src/windows_runtime.py +70 -0
- package/templates/nexo_helper.py +10 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.12.
|
|
3
|
+
"version": "7.12.2",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.12.
|
|
21
|
+
Version `7.12.2` is the current packaged-runtime line. Patch release — legacy headless automation paths now stay on the resonance engine: `task_profile` no longer pre-fills model/effort, `email-monitor` stops carrying a private routing override, personal automation helpers stop injecting a default model, and runtime updates scrub the last stale email-profile field automatically. Result: email daemon, personal scripts, and updated installs all converge on the same `caller`/`tier` → backend → `(model, effort)` resolution path already used by Deep Sleep and morning-agent.
|
|
22
22
|
|
|
23
|
-
Previously in `7.
|
|
23
|
+
Previously in `7.12.0`: minor release — adds `nexo support-snapshot` for generic local runtime diagnostics and completes the silent-reminder hardening on the live Protocol Enforcer path. The support collector emits one JSON bundle with version/platform metadata, runtime path presence, health-check output, and recent event/operation tails, while map-driven reminders (`nexo_startup`, `nexo_smart_startup`, `nexo_heartbeat`, `nexo_reminders`, `nexo_session_diary_*`, `nexo_stop`, `nexo_task_close`, compaction checkpoint prompts) now say explicitly that silence owns the entire reminder turn.
|
|
24
24
|
|
|
25
25
|
Previously in `7.11.5`: patch release — Desktop-managed installs now block the standalone dashboard at the same product-mode layer as evolution, so `installation_live`, cron sync, and watchdog no longer disagree about whether `com.nexo.dashboard` should exist. Validation: `125` targeted tests across product-mode, cron sync, and doctor, plus a full pre-release wrapper (`2321 passed, 2 skipped, 1 xfailed, 4 xpassed`).
|
|
26
26
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -17,8 +17,21 @@
|
|
|
17
17
|
const { execSync, spawnSync } = require("child_process");
|
|
18
18
|
const crypto = require("crypto");
|
|
19
19
|
const fs = require("fs");
|
|
20
|
+
const { createRequire } = require("module");
|
|
20
21
|
const path = require("path");
|
|
21
22
|
const readline = require("readline");
|
|
23
|
+
// Force relative launcher helpers to resolve from bin/ even under test harnesses.
|
|
24
|
+
require = createRequire(path.join(__dirname, "nexo-brain.js"));
|
|
25
|
+
const { runViaWsl } = require("./windows-wsl-bridge");
|
|
26
|
+
|
|
27
|
+
if (process.platform === "win32") {
|
|
28
|
+
const bridged = runViaWsl({
|
|
29
|
+
scriptPath: __filename,
|
|
30
|
+
args: process.argv.slice(2),
|
|
31
|
+
label: "NEXO Brain",
|
|
32
|
+
});
|
|
33
|
+
process.exit(bridged?.status ?? 1);
|
|
34
|
+
}
|
|
22
35
|
|
|
23
36
|
let NEXO_HOME = process.env.NEXO_HOME || path.join(require("os").homedir(), ".nexo");
|
|
24
37
|
const DEFAULT_ASSISTANT_NAME = "Nova";
|
|
@@ -2425,9 +2438,9 @@ async function runSetup() {
|
|
|
2425
2438
|
// Check prerequisites
|
|
2426
2439
|
const platform = process.platform;
|
|
2427
2440
|
if (platform === "win32") {
|
|
2428
|
-
log("Windows detected
|
|
2441
|
+
log("Windows detected, but the automatic WSL bridge was not available.");
|
|
2429
2442
|
log("Install WSL: https://learn.microsoft.com/en-us/windows/wsl/install");
|
|
2430
|
-
log("Then run this command inside WSL (Ubuntu terminal)
|
|
2443
|
+
log("Then run this command again, or launch it directly inside WSL (Ubuntu terminal).");
|
|
2431
2444
|
process.exit(1);
|
|
2432
2445
|
}
|
|
2433
2446
|
if (platform !== "darwin" && platform !== "linux") {
|
package/bin/nexo.js
CHANGED
|
@@ -11,6 +11,16 @@ const { spawnSync } = require("child_process");
|
|
|
11
11
|
const fs = require("fs");
|
|
12
12
|
const os = require("os");
|
|
13
13
|
const path = require("path");
|
|
14
|
+
const { runViaWsl } = require("./windows-wsl-bridge");
|
|
15
|
+
|
|
16
|
+
if (process.platform === "win32") {
|
|
17
|
+
const bridged = runViaWsl({
|
|
18
|
+
scriptPath: __filename,
|
|
19
|
+
args: process.argv.slice(2),
|
|
20
|
+
label: "NEXO CLI",
|
|
21
|
+
});
|
|
22
|
+
process.exit(bridged?.status ?? 1);
|
|
23
|
+
}
|
|
14
24
|
|
|
15
25
|
function resolveNexoHome(rawValue) {
|
|
16
26
|
const homeDir = os.homedir();
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const { spawnSync } = require("child_process");
|
|
2
|
+
|
|
3
|
+
function isWindowsHost(platform = process.platform) {
|
|
4
|
+
return platform === "win32";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isWindowsStylePath(value) {
|
|
8
|
+
const text = String(value || "").trim();
|
|
9
|
+
if (!text) return false;
|
|
10
|
+
return /^[A-Za-z]:[\\/]/.test(text) || text.startsWith("\\\\");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseWslUncPath(rawValue) {
|
|
14
|
+
const value = String(rawValue || "").trim();
|
|
15
|
+
const lowered = value.toLowerCase();
|
|
16
|
+
if (!lowered.startsWith("\\\\wsl$\\") && !lowered.startsWith("\\\\wsl.localhost\\")) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const parts = value.split("\\").filter(Boolean);
|
|
20
|
+
if (parts.length < 3) return null;
|
|
21
|
+
return {
|
|
22
|
+
distro: parts[1],
|
|
23
|
+
linuxPath: `/${parts.slice(2).join("/")}`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toWslPath(rawValue) {
|
|
28
|
+
const value = String(rawValue || "").trim();
|
|
29
|
+
if (!value) return "";
|
|
30
|
+
if (value.startsWith("/")) return value;
|
|
31
|
+
const unc = parseWslUncPath(value);
|
|
32
|
+
if (unc) return unc.linuxPath;
|
|
33
|
+
|
|
34
|
+
const normalized = value.replace(/\\/g, "/");
|
|
35
|
+
const driveMatch = normalized.match(/^([A-Za-z]):(\/.*)?$/);
|
|
36
|
+
if (driveMatch) {
|
|
37
|
+
return `/mnt/${driveMatch[1].toLowerCase()}${driveMatch[2] || ""}`;
|
|
38
|
+
}
|
|
39
|
+
if (normalized.startsWith("//")) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveExplicitLinuxPath(rawValue) {
|
|
46
|
+
const value = String(rawValue || "").trim();
|
|
47
|
+
if (!value) return "";
|
|
48
|
+
if (value.startsWith("/")) return value;
|
|
49
|
+
if (isWindowsStylePath(value)) return toWslPath(value);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveLinuxEnv(env = process.env) {
|
|
54
|
+
const linuxEnv = {
|
|
55
|
+
NEXO_WINDOWS_BRIDGE: "1",
|
|
56
|
+
NEXO_WINDOWS_HOST: "1",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const explicitHome = resolveExplicitLinuxPath(env.NEXO_WSL_HOME);
|
|
60
|
+
if (explicitHome) {
|
|
61
|
+
linuxEnv.NEXO_HOME = explicitHome;
|
|
62
|
+
} else if (env.NEXO_HOME && !isWindowsStylePath(env.NEXO_HOME)) {
|
|
63
|
+
linuxEnv.NEXO_HOME = String(env.NEXO_HOME).trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const explicitCode = resolveExplicitLinuxPath(env.NEXO_WSL_CODE);
|
|
67
|
+
if (explicitCode) {
|
|
68
|
+
linuxEnv.NEXO_CODE = explicitCode;
|
|
69
|
+
} else if (env.NEXO_CODE && !isWindowsStylePath(env.NEXO_CODE)) {
|
|
70
|
+
linuxEnv.NEXO_CODE = String(env.NEXO_CODE).trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return linuxEnv;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveWslNodeBinary(env = process.env) {
|
|
77
|
+
const explicitNode = resolveExplicitLinuxPath(env.NEXO_WSL_NODE);
|
|
78
|
+
return explicitNode || "node";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveWslDistro(scriptPath, env = process.env) {
|
|
82
|
+
const explicitDistro = String(env.NEXO_WSL_DISTRO || "").trim();
|
|
83
|
+
if (explicitDistro) return explicitDistro;
|
|
84
|
+
const unc = parseWslUncPath(scriptPath);
|
|
85
|
+
return unc ? unc.distro : "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildWslExecSpec({ scriptPath, args = [], env = process.env, platform = process.platform }) {
|
|
89
|
+
if (!isWindowsHost(platform)) return null;
|
|
90
|
+
const translatedScriptPath = toWslPath(scriptPath);
|
|
91
|
+
if (!translatedScriptPath) {
|
|
92
|
+
return {
|
|
93
|
+
error: `Unable to translate Windows path to WSL path: ${scriptPath}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const linuxEnv = resolveLinuxEnv(env);
|
|
98
|
+
const wslArgs = [];
|
|
99
|
+
const distro = resolveWslDistro(scriptPath, env);
|
|
100
|
+
if (distro) {
|
|
101
|
+
wslArgs.push("-d", distro);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
wslArgs.push(
|
|
105
|
+
"--exec",
|
|
106
|
+
"env",
|
|
107
|
+
"-u",
|
|
108
|
+
"NEXO_HOME",
|
|
109
|
+
"-u",
|
|
110
|
+
"NEXO_CODE",
|
|
111
|
+
"-u",
|
|
112
|
+
"NEXO_WSL_HOME",
|
|
113
|
+
"-u",
|
|
114
|
+
"NEXO_WSL_CODE"
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
for (const [key, value] of Object.entries(linuxEnv)) {
|
|
118
|
+
wslArgs.push(`${key}=${value}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
wslArgs.push(resolveWslNodeBinary(env), translatedScriptPath, ...args);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
command: "wsl.exe",
|
|
125
|
+
args: wslArgs,
|
|
126
|
+
linuxEnv,
|
|
127
|
+
translatedScriptPath,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function logWslBridgeFailure(label, message) {
|
|
132
|
+
console.error(`${label} could not start through WSL.`);
|
|
133
|
+
console.error(message);
|
|
134
|
+
console.error("Install WSL: https://learn.microsoft.com/en-us/windows/wsl/install");
|
|
135
|
+
console.error("Then run the same command again from PowerShell/CMD or inside your Ubuntu WSL shell.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function runViaWsl({ scriptPath, args = [], env = process.env, platform = process.platform, stdio = "inherit", label = "NEXO" }) {
|
|
139
|
+
const spec = buildWslExecSpec({ scriptPath, args, env, platform });
|
|
140
|
+
if (!spec) return null;
|
|
141
|
+
if (spec.error) {
|
|
142
|
+
logWslBridgeFailure(label, spec.error);
|
|
143
|
+
return { status: 1 };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result = spawnSync(spec.command, spec.args, { stdio, env });
|
|
147
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
148
|
+
logWslBridgeFailure(label, "The Windows host does not have `wsl.exe` available.");
|
|
149
|
+
return { status: 1 };
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
buildWslExecSpec,
|
|
156
|
+
isWindowsHost,
|
|
157
|
+
isWindowsStylePath,
|
|
158
|
+
parseWslUncPath,
|
|
159
|
+
resolveLinuxEnv,
|
|
160
|
+
runViaWsl,
|
|
161
|
+
toWslPath,
|
|
162
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.12.
|
|
3
|
+
"version": "7.12.2",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"files": [
|
|
74
74
|
"bin/nexo-brain.js",
|
|
75
75
|
"bin/nexo.js",
|
|
76
|
+
"bin/windows-wsl-bridge.js",
|
|
76
77
|
"bin/postinstall.js",
|
|
77
78
|
"scripts/sync_release_artifacts.py",
|
|
78
79
|
"src/",
|
package/src/agent_runner.py
CHANGED
|
@@ -27,7 +27,6 @@ from client_preferences import (
|
|
|
27
27
|
load_client_preferences,
|
|
28
28
|
normalize_client_key,
|
|
29
29
|
resolve_automation_backend,
|
|
30
|
-
resolve_automation_task_profile,
|
|
31
30
|
resolve_client_runtime_profile,
|
|
32
31
|
resolve_terminal_client,
|
|
33
32
|
)
|
|
@@ -936,14 +935,6 @@ def run_automation_prompt(
|
|
|
936
935
|
selected_backend = backend or resolve_automation_backend(preferences=prefs)
|
|
937
936
|
if selected_backend == BACKEND_NONE:
|
|
938
937
|
raise AutomationBackendUnavailableError("Automation backend is disabled in config.")
|
|
939
|
-
|
|
940
|
-
if task_profile:
|
|
941
|
-
profile = resolve_automation_task_profile(task_profile, preferences=prefs)
|
|
942
|
-
selected_backend = profile["backend"] or selected_backend
|
|
943
|
-
if not model:
|
|
944
|
-
model = profile["model"]
|
|
945
|
-
if not reasoning_effort:
|
|
946
|
-
reasoning_effort = profile["reasoning_effort"]
|
|
947
938
|
selected_backend = _resolve_available_backend(selected_backend, preferences=prefs)
|
|
948
939
|
|
|
949
940
|
# Resonance map decides (model, effort) for every call. ``caller`` is
|
package/src/auto_update.py
CHANGED
|
@@ -1630,6 +1630,47 @@ def _migrate_effort_to_resonance(dest: Path = NEXO_HOME) -> list[str]:
|
|
|
1630
1630
|
return actions
|
|
1631
1631
|
|
|
1632
1632
|
|
|
1633
|
+
def _cleanup_legacy_email_routing_config(dest: Path = NEXO_HOME) -> list[str]:
|
|
1634
|
+
"""Remove retired email routing overrides from persisted runtime config.
|
|
1635
|
+
|
|
1636
|
+
Older runtimes stored ``automation_task_profile`` inside
|
|
1637
|
+
``runtime/nexo-email/config.json`` (and earlier under ``nexo-email/``).
|
|
1638
|
+
The email monitor now resolves model/effort exclusively from its caller in
|
|
1639
|
+
``resonance_map.py``, so keeping that key around only preserves a stale,
|
|
1640
|
+
misleading second source of truth.
|
|
1641
|
+
"""
|
|
1642
|
+
import json as _json
|
|
1643
|
+
|
|
1644
|
+
actions: list[str] = []
|
|
1645
|
+
candidates = [
|
|
1646
|
+
dest / "runtime" / "nexo-email" / "config.json",
|
|
1647
|
+
dest / "nexo-email" / "config.json",
|
|
1648
|
+
]
|
|
1649
|
+
for path in candidates:
|
|
1650
|
+
if not path.is_file():
|
|
1651
|
+
continue
|
|
1652
|
+
try:
|
|
1653
|
+
payload = _json.loads(path.read_text())
|
|
1654
|
+
except Exception as exc:
|
|
1655
|
+
actions.append(
|
|
1656
|
+
f"email-routing-cleanup-warning:{path.name}:{exc.__class__.__name__}"
|
|
1657
|
+
)
|
|
1658
|
+
continue
|
|
1659
|
+
if not isinstance(payload, dict):
|
|
1660
|
+
continue
|
|
1661
|
+
if "automation_task_profile" not in payload:
|
|
1662
|
+
continue
|
|
1663
|
+
payload.pop("automation_task_profile", None)
|
|
1664
|
+
try:
|
|
1665
|
+
path.write_text(_json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
1666
|
+
actions.append(f"email-routing-cleanup:{path}")
|
|
1667
|
+
except Exception as exc:
|
|
1668
|
+
actions.append(
|
|
1669
|
+
f"email-routing-cleanup-warning:{path.name}:{exc.__class__.__name__}"
|
|
1670
|
+
)
|
|
1671
|
+
return actions
|
|
1672
|
+
|
|
1673
|
+
|
|
1633
1674
|
def _relocate_resonance_tiers_contract(dest: Path = NEXO_HOME) -> list[str]:
|
|
1634
1675
|
"""Ensure ``resonance_tiers.json`` lives at the public contract path
|
|
1635
1676
|
``NEXO_HOME/personal/brain/resonance_tiers.json`` and purge the legacy
|
|
@@ -4688,6 +4729,14 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4688
4729
|
except Exception as exc:
|
|
4689
4730
|
actions.append(f"v6-purge-warning:{exc.__class__.__name__}")
|
|
4690
4731
|
|
|
4732
|
+
try:
|
|
4733
|
+
_emit_progress(progress_fn, "Cleaning legacy email routing overrides...")
|
|
4734
|
+
email_actions = _cleanup_legacy_email_routing_config(dest)
|
|
4735
|
+
for action in email_actions:
|
|
4736
|
+
actions.append(action)
|
|
4737
|
+
except Exception as exc:
|
|
4738
|
+
actions.append(f"email-routing-cleanup-warning:{exc.__class__.__name__}")
|
|
4739
|
+
|
|
4691
4740
|
try:
|
|
4692
4741
|
_emit_progress(progress_fn, "Ensuring local classifier baseline...")
|
|
4693
4742
|
if _is_ephemeral_runtime_install(dest):
|
|
@@ -59,8 +59,12 @@ DEFAULT_CLAUDE_CODE_MODEL = _CLAUDE_DEFAULTS["model"]
|
|
|
59
59
|
DEFAULT_CLAUDE_CODE_REASONING_EFFORT = _CLAUDE_DEFAULTS["reasoning_effort"]
|
|
60
60
|
DEFAULT_CODEX_MODEL = _CODEX_DEFAULTS["model"]
|
|
61
61
|
DEFAULT_CODEX_REASONING_EFFORT = _CODEX_DEFAULTS["reasoning_effort"]
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
AUTOMATION_TASK_PROFILE_TIERS = {
|
|
63
|
+
"default": "",
|
|
64
|
+
"fast": "bajo",
|
|
65
|
+
"balanced": "medio",
|
|
66
|
+
"deep": "maximo",
|
|
67
|
+
}
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
def _user_home() -> Path:
|
|
@@ -385,9 +389,9 @@ def default_automation_task_profiles() -> dict[str, dict[str, str]]:
|
|
|
385
389
|
"reasoning_effort": "",
|
|
386
390
|
},
|
|
387
391
|
"deep": {
|
|
388
|
-
"backend":
|
|
389
|
-
"model":
|
|
390
|
-
"reasoning_effort":
|
|
392
|
+
"backend": "",
|
|
393
|
+
"model": "",
|
|
394
|
+
"reasoning_effort": "",
|
|
391
395
|
},
|
|
392
396
|
}
|
|
393
397
|
|
|
@@ -443,16 +447,10 @@ def normalize_automation_task_profiles(value) -> dict[str, dict[str, str]]:
|
|
|
443
447
|
continue
|
|
444
448
|
if not isinstance(raw_value, dict):
|
|
445
449
|
continue
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
normalized[profile_key] =
|
|
450
|
-
"backend": backend or defaults[profile_key]["backend"],
|
|
451
|
-
"model": str(raw_value.get("model") or defaults[profile_key]["model"]).strip(),
|
|
452
|
-
"reasoning_effort": str(
|
|
453
|
-
raw_value.get("reasoning_effort") or defaults[profile_key]["reasoning_effort"]
|
|
454
|
-
).strip().lower(),
|
|
455
|
-
}
|
|
450
|
+
# These profiles are no longer allowed to route backend/model/effort.
|
|
451
|
+
# Older installs may still persist those keys; normalize drops them
|
|
452
|
+
# so schedule.json cannot bypass resonance resolution silently.
|
|
453
|
+
normalized[profile_key] = dict(defaults[profile_key])
|
|
456
454
|
return normalized
|
|
457
455
|
|
|
458
456
|
|
|
@@ -769,18 +767,13 @@ def resolve_automation_task_profile(
|
|
|
769
767
|
*,
|
|
770
768
|
preferences: dict | None = None,
|
|
771
769
|
) -> dict[str, str]:
|
|
772
|
-
normalized = preferences or load_client_preferences()
|
|
773
|
-
defaults = default_automation_task_profiles()
|
|
774
770
|
profile_key = str(profile or "").strip().lower() or "default"
|
|
775
771
|
if profile_key not in AUTOMATION_TASK_PROFILE_KEYS:
|
|
776
772
|
profile_key = "default"
|
|
777
|
-
configured = normalize_automation_task_profiles(normalized.get("automation_task_profiles"))
|
|
778
|
-
selected = dict(configured.get(profile_key) or defaults[profile_key])
|
|
779
|
-
backend = selected.get("backend") or resolve_automation_backend(normalized)
|
|
780
|
-
runtime_profile = resolve_client_runtime_profile(backend, preferences=normalized)
|
|
781
773
|
return {
|
|
782
774
|
"name": profile_key,
|
|
783
|
-
"backend":
|
|
784
|
-
"model":
|
|
785
|
-
"reasoning_effort":
|
|
775
|
+
"backend": "",
|
|
776
|
+
"model": "",
|
|
777
|
+
"reasoning_effort": "",
|
|
778
|
+
"tier": AUTOMATION_TASK_PROFILE_TIERS.get(profile_key, ""),
|
|
786
779
|
}
|
package/src/email_config.py
CHANGED
|
@@ -95,7 +95,6 @@ def _account_to_runtime_account(account: dict) -> dict:
|
|
|
95
95
|
"retry_backoff_seconds": metadata.get("retry_backoff_seconds", 60),
|
|
96
96
|
"claude_binary": metadata.get("claude_binary", ""),
|
|
97
97
|
"working_dir": metadata.get("working_dir", str(Path.home())),
|
|
98
|
-
"automation_task_profile": metadata.get("automation_task_profile", "deep"),
|
|
99
98
|
"max_process_time": metadata.get("max_process_time"),
|
|
100
99
|
"metadata": metadata,
|
|
101
100
|
}
|
|
@@ -139,7 +138,6 @@ def _account_to_legacy_shape(
|
|
|
139
138
|
"retry_backoff_seconds": runtime_account["retry_backoff_seconds"],
|
|
140
139
|
"claude_binary": runtime_account["claude_binary"],
|
|
141
140
|
"working_dir": runtime_account["working_dir"],
|
|
142
|
-
"automation_task_profile": runtime_account["automation_task_profile"],
|
|
143
141
|
"max_process_time": runtime_account["max_process_time"],
|
|
144
142
|
"label": runtime_account["label"],
|
|
145
143
|
"role": runtime_account["role"],
|
package/src/health_check.py
CHANGED
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
import json
|
|
19
19
|
import os
|
|
20
|
+
import platform
|
|
20
21
|
import re
|
|
21
22
|
import sqlite3
|
|
22
23
|
import subprocess
|
|
@@ -24,6 +25,8 @@ import time
|
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
from typing import Any
|
|
26
27
|
|
|
28
|
+
from windows_runtime import running_inside_wsl, windows_runtime_status
|
|
29
|
+
|
|
27
30
|
|
|
28
31
|
def _nexo_home() -> Path:
|
|
29
32
|
return Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
@@ -31,8 +34,16 @@ def _nexo_home() -> Path:
|
|
|
31
34
|
|
|
32
35
|
def _check_runtime() -> dict:
|
|
33
36
|
home = _nexo_home()
|
|
37
|
+
system = platform.system()
|
|
38
|
+
release = platform.release()
|
|
39
|
+
windows_status = windows_runtime_status(home, system=system, release=release)
|
|
34
40
|
ver_file = home / "version.json"
|
|
35
|
-
out: dict[str, Any] = {
|
|
41
|
+
out: dict[str, Any] = {
|
|
42
|
+
"nexo_home": str(home),
|
|
43
|
+
"exists": home.is_dir(),
|
|
44
|
+
"is_wsl": running_inside_wsl(system=system, release=release),
|
|
45
|
+
"windows_runtime": windows_status,
|
|
46
|
+
}
|
|
36
47
|
if ver_file.is_file():
|
|
37
48
|
try:
|
|
38
49
|
payload = json.loads(ver_file.read_text())
|
|
@@ -43,6 +54,8 @@ def _check_runtime() -> dict:
|
|
|
43
54
|
else:
|
|
44
55
|
out["version"] = "missing"
|
|
45
56
|
out["status"] = "ok" if out["exists"] and out.get("version") not in ("missing", "unreadable") else "degraded"
|
|
57
|
+
if windows_status["warnings"]:
|
|
58
|
+
out["status"] = "degraded"
|
|
46
59
|
return out
|
|
47
60
|
|
|
48
61
|
|
package/src/hook_guardrails.py
CHANGED
|
@@ -191,6 +191,14 @@ _SFTP_BATCH_RE = re.compile(
|
|
|
191
191
|
r"\bsftp\b(?:[^|&;]*\s)?-b\s+\S+",
|
|
192
192
|
re.IGNORECASE,
|
|
193
193
|
)
|
|
194
|
+
_SSH_REMOTE_PIPE_RE = re.compile(
|
|
195
|
+
r"\|\s*ssh\b",
|
|
196
|
+
re.IGNORECASE,
|
|
197
|
+
)
|
|
198
|
+
_SSH_REMOTE_STDIN_RE = re.compile(
|
|
199
|
+
r"\bssh\b[^\n|&;]*(?:<\s*\S+|<<-?\s*(?:['\"]?[A-Za-z0-9_]+['\"]?))",
|
|
200
|
+
re.IGNORECASE,
|
|
201
|
+
)
|
|
194
202
|
|
|
195
203
|
|
|
196
204
|
def _classify_ssh_remote_write(command: str) -> str | None:
|
|
@@ -231,6 +239,10 @@ def _classify_ssh_remote_write(command: str) -> str | None:
|
|
|
231
239
|
for pattern in _SSH_REMOTE_WRITE_VERBS:
|
|
232
240
|
if pattern.search(trimmed):
|
|
233
241
|
return "ssh_remote_shell_write"
|
|
242
|
+
if _SSH_REMOTE_PIPE_RE.search(cmd):
|
|
243
|
+
return "ssh_remote_shell_write"
|
|
244
|
+
if _SSH_REMOTE_STDIN_RE.search(cmd):
|
|
245
|
+
return "ssh_remote_shell_write"
|
|
234
246
|
return None
|
|
235
247
|
|
|
236
248
|
|
|
@@ -270,6 +282,64 @@ _PATH_ARTIFACT_RE = re.compile(
|
|
|
270
282
|
)
|
|
271
283
|
_DATE_LIKE_PATH_RE = re.compile(r"^/\d{1,4}/\d{1,4}(?:/\d{1,4})?$")
|
|
272
284
|
_STRICT_WRITE_HEARTBEAT_WINDOW_SECONDS = 300
|
|
285
|
+
_G3_CORTEX_AUTH_WINDOW_SECONDS = max(
|
|
286
|
+
60,
|
|
287
|
+
int(os.environ.get("NEXO_G3_CORTEX_AUTH_WINDOW_SECONDS", "900")),
|
|
288
|
+
)
|
|
289
|
+
_CORTEX_NEGATIVE_TOKENS = (
|
|
290
|
+
"abort",
|
|
291
|
+
"avoid",
|
|
292
|
+
"block",
|
|
293
|
+
"cancel",
|
|
294
|
+
"decline",
|
|
295
|
+
"defer",
|
|
296
|
+
"deny",
|
|
297
|
+
"do_not",
|
|
298
|
+
"dont",
|
|
299
|
+
"no_",
|
|
300
|
+
"not_now",
|
|
301
|
+
"reject",
|
|
302
|
+
"skip",
|
|
303
|
+
"wait",
|
|
304
|
+
)
|
|
305
|
+
_G3_CORTEX_GENERIC_APPROVAL_TOKENS = (
|
|
306
|
+
"allow",
|
|
307
|
+
"apply",
|
|
308
|
+
"approve",
|
|
309
|
+
"continue",
|
|
310
|
+
"deploy",
|
|
311
|
+
"execute",
|
|
312
|
+
"go_ahead",
|
|
313
|
+
"proceed",
|
|
314
|
+
"publish",
|
|
315
|
+
"retry",
|
|
316
|
+
"run",
|
|
317
|
+
)
|
|
318
|
+
_G3_CORTEX_FAMILY_TOKENS = {
|
|
319
|
+
"destructive": (
|
|
320
|
+
"chmod",
|
|
321
|
+
"cleanup",
|
|
322
|
+
"delete",
|
|
323
|
+
"drop",
|
|
324
|
+
"force",
|
|
325
|
+
"git_push",
|
|
326
|
+
"purge",
|
|
327
|
+
"remove",
|
|
328
|
+
"rm",
|
|
329
|
+
"truncate",
|
|
330
|
+
"wipe",
|
|
331
|
+
),
|
|
332
|
+
"ssh": (
|
|
333
|
+
"deploy",
|
|
334
|
+
"remote",
|
|
335
|
+
"rsync",
|
|
336
|
+
"scp",
|
|
337
|
+
"sftp",
|
|
338
|
+
"ssh",
|
|
339
|
+
"sync",
|
|
340
|
+
"upload",
|
|
341
|
+
),
|
|
342
|
+
}
|
|
273
343
|
|
|
274
344
|
# Single-segment ``/word`` candidates that match a small dictionary block-list
|
|
275
345
|
# of confirmed false positives observed in the live debt log.
|
|
@@ -655,6 +725,7 @@ def _find_open_task_for_file(conn, sid: str, filepath: str) -> dict | None:
|
|
|
655
725
|
target = _normalize_file_path(filepath)
|
|
656
726
|
rows = conn.execute(
|
|
657
727
|
"""SELECT task_id, files, guard_has_blocking, guard_acknowledged, task_type, plan, unknowns,
|
|
728
|
+
opened_at,
|
|
658
729
|
verification_step, opened_with_guard, must_change_log, must_verify
|
|
659
730
|
FROM protocol_tasks
|
|
660
731
|
WHERE session_id = ? AND status = 'open'
|
|
@@ -675,6 +746,7 @@ def _find_open_task_for_file(conn, sid: str, filepath: str) -> dict | None:
|
|
|
675
746
|
def _find_any_open_task(conn, sid: str) -> dict | None:
|
|
676
747
|
row = conn.execute(
|
|
677
748
|
"""SELECT task_id, files, guard_has_blocking, guard_acknowledged, task_type, plan, unknowns,
|
|
749
|
+
opened_at,
|
|
678
750
|
verification_step, opened_with_guard, must_change_log, must_verify
|
|
679
751
|
FROM protocol_tasks
|
|
680
752
|
WHERE session_id = ? AND status = 'open'
|
|
@@ -685,6 +757,86 @@ def _find_any_open_task(conn, sid: str) -> dict | None:
|
|
|
685
757
|
return dict(row) if row else None
|
|
686
758
|
|
|
687
759
|
|
|
760
|
+
def _normalize_cortex_tokens(value: str) -> str:
|
|
761
|
+
return re.sub(r"[^a-z0-9]+", "_", str(value or "").lower()).strip("_")
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _text_has_any_token(value: str, tokens: tuple[str, ...]) -> bool:
|
|
765
|
+
normalized = _normalize_cortex_tokens(value)
|
|
766
|
+
if not normalized:
|
|
767
|
+
return False
|
|
768
|
+
return any(token in normalized for token in tokens)
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _cortex_choice_is_negative(value: str) -> bool:
|
|
772
|
+
return _text_has_any_token(value, _CORTEX_NEGATIVE_TOKENS)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _find_recent_cortex_authorization(
|
|
776
|
+
conn,
|
|
777
|
+
*,
|
|
778
|
+
sid: str,
|
|
779
|
+
task: dict | None,
|
|
780
|
+
gate_family: str,
|
|
781
|
+
pattern_name: str = "",
|
|
782
|
+
) -> dict | None:
|
|
783
|
+
if not sid or not task:
|
|
784
|
+
return None
|
|
785
|
+
task_id = str(task.get("task_id") or "").strip()
|
|
786
|
+
if not task_id:
|
|
787
|
+
return None
|
|
788
|
+
params: list[object] = [
|
|
789
|
+
sid,
|
|
790
|
+
task_id,
|
|
791
|
+
f"-{_G3_CORTEX_AUTH_WINDOW_SECONDS} seconds",
|
|
792
|
+
]
|
|
793
|
+
sql = (
|
|
794
|
+
"""SELECT id, task_id, recommended_choice, selected_choice,
|
|
795
|
+
recommended_reasoning, selection_reason, context_hint, created_at
|
|
796
|
+
FROM cortex_evaluations
|
|
797
|
+
WHERE session_id = ?
|
|
798
|
+
AND task_id = ?
|
|
799
|
+
AND created_at >= datetime('now', ?)"""
|
|
800
|
+
)
|
|
801
|
+
opened_at = str(task.get("opened_at") or "").strip()
|
|
802
|
+
if opened_at:
|
|
803
|
+
sql += " AND created_at >= ?"
|
|
804
|
+
params.append(opened_at)
|
|
805
|
+
sql += " ORDER BY created_at DESC, id DESC LIMIT 5"
|
|
806
|
+
try:
|
|
807
|
+
rows = conn.execute(sql, params).fetchall()
|
|
808
|
+
except sqlite3.OperationalError:
|
|
809
|
+
return None
|
|
810
|
+
family_tokens = _G3_CORTEX_FAMILY_TOKENS.get(gate_family, ())
|
|
811
|
+
pattern_tokens = tuple(
|
|
812
|
+
token for token in _normalize_cortex_tokens(pattern_name).split("_") if token
|
|
813
|
+
)
|
|
814
|
+
fallback_candidates: list[dict] = []
|
|
815
|
+
for row in rows:
|
|
816
|
+
item = dict(row)
|
|
817
|
+
choice = str(item.get("selected_choice") or item.get("recommended_choice") or "").strip()
|
|
818
|
+
if not choice or _cortex_choice_is_negative(choice):
|
|
819
|
+
continue
|
|
820
|
+
combined = " ".join(
|
|
821
|
+
[
|
|
822
|
+
choice,
|
|
823
|
+
str(item.get("selection_reason") or ""),
|
|
824
|
+
str(item.get("recommended_reasoning") or ""),
|
|
825
|
+
str(item.get("context_hint") or ""),
|
|
826
|
+
]
|
|
827
|
+
)
|
|
828
|
+
if (
|
|
829
|
+
_text_has_any_token(choice, _G3_CORTEX_GENERIC_APPROVAL_TOKENS)
|
|
830
|
+
or _text_has_any_token(combined, family_tokens)
|
|
831
|
+
or _text_has_any_token(combined, pattern_tokens)
|
|
832
|
+
):
|
|
833
|
+
return item
|
|
834
|
+
fallback_candidates.append(item)
|
|
835
|
+
if len(fallback_candidates) == 1:
|
|
836
|
+
return fallback_candidates[0]
|
|
837
|
+
return None
|
|
838
|
+
|
|
839
|
+
|
|
688
840
|
def _find_any_open_workflow(conn, sid: str) -> dict | None:
|
|
689
841
|
row = conn.execute(
|
|
690
842
|
"""SELECT run_id, protocol_task_id, current_step_key
|
|
@@ -1164,6 +1316,7 @@ def process_pre_tool_event(payload: dict) -> dict:
|
|
|
1164
1316
|
if not claude_sid:
|
|
1165
1317
|
claude_sid = _read_claude_session_id_from_coordination()
|
|
1166
1318
|
sid = _resolve_nexo_sid(conn, claude_sid)
|
|
1319
|
+
open_task = _find_any_open_task(conn, sid) if sid else None
|
|
1167
1320
|
automation_blocks = _collect_automation_live_repo_blocks(
|
|
1168
1321
|
conn,
|
|
1169
1322
|
sid=sid,
|
|
@@ -1228,12 +1381,22 @@ def process_pre_tool_event(payload: dict) -> dict:
|
|
|
1228
1381
|
if g3_mode in {"shadow", "hard"} and tool_name == "Bash":
|
|
1229
1382
|
shell_command = _extract_bash_command(tool_input)
|
|
1230
1383
|
destructive_pattern = _classify_destructive_intent(shell_command)
|
|
1384
|
+
if destructive_pattern:
|
|
1385
|
+
if _find_recent_cortex_authorization(
|
|
1386
|
+
conn,
|
|
1387
|
+
sid=sid,
|
|
1388
|
+
task=open_task,
|
|
1389
|
+
gate_family="destructive",
|
|
1390
|
+
pattern_name=destructive_pattern,
|
|
1391
|
+
):
|
|
1392
|
+
destructive_pattern = None
|
|
1231
1393
|
if destructive_pattern:
|
|
1232
1394
|
severity = "error" if g3_mode == "hard" else "warn"
|
|
1395
|
+
task_id = str((open_task or {}).get("task_id") or "").strip()
|
|
1233
1396
|
debt = _ensure_protocol_debt(
|
|
1234
1397
|
conn,
|
|
1235
1398
|
session_id=sid,
|
|
1236
|
-
task_id=
|
|
1399
|
+
task_id=task_id,
|
|
1237
1400
|
debt_type="g3_destructive_command_requires_cortex",
|
|
1238
1401
|
severity=severity,
|
|
1239
1402
|
evidence=(
|
|
@@ -1253,7 +1416,7 @@ def process_pre_tool_event(payload: dict) -> dict:
|
|
|
1253
1416
|
"blocks": [
|
|
1254
1417
|
{
|
|
1255
1418
|
"file": "",
|
|
1256
|
-
"task_id":
|
|
1419
|
+
"task_id": task_id,
|
|
1257
1420
|
"debt_id": debt.get("id"),
|
|
1258
1421
|
"debt_type": "g3_destructive_command_requires_cortex",
|
|
1259
1422
|
"reason_code": "g3_destructive_blocked",
|
|
@@ -1283,12 +1446,22 @@ def process_pre_tool_event(payload: dict) -> dict:
|
|
|
1283
1446
|
if g3_ssh_mode in {"shadow", "hard"} and tool_name == "Bash":
|
|
1284
1447
|
shell_command = _extract_bash_command(tool_input)
|
|
1285
1448
|
ssh_pattern = _classify_ssh_remote_write(shell_command)
|
|
1449
|
+
if ssh_pattern:
|
|
1450
|
+
if _find_recent_cortex_authorization(
|
|
1451
|
+
conn,
|
|
1452
|
+
sid=sid,
|
|
1453
|
+
task=open_task,
|
|
1454
|
+
gate_family="ssh",
|
|
1455
|
+
pattern_name=ssh_pattern,
|
|
1456
|
+
):
|
|
1457
|
+
ssh_pattern = None
|
|
1286
1458
|
if ssh_pattern:
|
|
1287
1459
|
severity = "error" if g3_ssh_mode == "hard" else "warn"
|
|
1460
|
+
task_id = str((open_task or {}).get("task_id") or "").strip()
|
|
1288
1461
|
debt = _ensure_protocol_debt(
|
|
1289
1462
|
conn,
|
|
1290
1463
|
session_id=sid,
|
|
1291
|
-
task_id=
|
|
1464
|
+
task_id=task_id,
|
|
1292
1465
|
debt_type="g3_ssh_remote_write_requires_cortex",
|
|
1293
1466
|
severity=severity,
|
|
1294
1467
|
evidence=(
|
|
@@ -1309,7 +1482,7 @@ def process_pre_tool_event(payload: dict) -> dict:
|
|
|
1309
1482
|
"blocks": [
|
|
1310
1483
|
{
|
|
1311
1484
|
"file": "",
|
|
1312
|
-
"task_id":
|
|
1485
|
+
"task_id": task_id,
|
|
1313
1486
|
"debt_id": debt.get("id"),
|
|
1314
1487
|
"debt_type": "g3_ssh_remote_write_requires_cortex",
|
|
1315
1488
|
"reason_code": "g3_ssh_remote_write_blocked",
|
package/src/local_models.py
CHANGED
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import hashlib
|
|
17
17
|
import json
|
|
18
|
+
import logging
|
|
18
19
|
import os
|
|
19
20
|
import re
|
|
20
21
|
import shutil
|
|
@@ -29,6 +30,7 @@ import paths
|
|
|
29
30
|
|
|
30
31
|
MANIFEST_PATH = Path(__file__).resolve().with_name("local_model_manifest.json")
|
|
31
32
|
MODEL_LOCK_FILENAME = ".nexo-model-lock.json"
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
@dataclass(frozen=True)
|
|
@@ -79,6 +81,9 @@ def _lock_payload(spec: LocalModelSpec) -> dict[str, Any]:
|
|
|
79
81
|
|
|
80
82
|
@lru_cache(maxsize=1)
|
|
81
83
|
def _load_manifest() -> dict[str, LocalModelSpec]:
|
|
84
|
+
if not MANIFEST_PATH.exists():
|
|
85
|
+
logger.warning("local_model_manifest.json missing — running with empty manifest")
|
|
86
|
+
return {}
|
|
82
87
|
payload = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
83
88
|
specs: dict[str, LocalModelSpec] = {}
|
|
84
89
|
for raw in payload.get("models", []) or []:
|
|
@@ -30,7 +30,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
30
30
|
parser.add_argument("--prompt", default="", help="Prompt text")
|
|
31
31
|
parser.add_argument("--prompt-file", default="", help="Read prompt text from a file")
|
|
32
32
|
parser.add_argument("--cwd", default="", help="Working directory for the backend")
|
|
33
|
-
parser.add_argument(
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--task-profile",
|
|
35
|
+
default="",
|
|
36
|
+
help="Legacy semantic label kept for compatibility. Routing now comes from caller/tier.",
|
|
37
|
+
)
|
|
34
38
|
parser.add_argument("--model", default="", help="Backend model hint")
|
|
35
39
|
parser.add_argument("--reasoning-effort", default="", help="Backend reasoning effort/profile")
|
|
36
40
|
parser.add_argument(
|
|
@@ -50,6 +54,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
50
54
|
parser.add_argument("--timeout", type=int, default=AUTOMATION_SUBPROCESS_TIMEOUT, help="Timeout in seconds")
|
|
51
55
|
parser.add_argument("--output-format", default="text", help="Requested output format")
|
|
52
56
|
parser.add_argument("--allowed-tools", default="", help="Claude-style allowed tools contract")
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--bare-mode",
|
|
59
|
+
choices=("auto", "on", "off"),
|
|
60
|
+
default="auto",
|
|
61
|
+
help="Bare mode for one-shot runs: auto|on|off.",
|
|
62
|
+
)
|
|
53
63
|
parser.add_argument("--append-system-prompt", default="", help="Extra system prompt text")
|
|
54
64
|
parser.add_argument("--append-system-prompt-file", default="", help="Read extra system prompt from a file")
|
|
55
65
|
args = parser.parse_args(argv)
|
|
@@ -62,6 +72,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
62
72
|
return 1
|
|
63
73
|
|
|
64
74
|
append_system_prompt = args.append_system_prompt or _read_text(args.append_system_prompt_file)
|
|
75
|
+
bare_mode = None
|
|
76
|
+
if args.bare_mode == "on":
|
|
77
|
+
bare_mode = True
|
|
78
|
+
elif args.bare_mode == "off":
|
|
79
|
+
bare_mode = False
|
|
65
80
|
|
|
66
81
|
try:
|
|
67
82
|
result = run_automation_prompt(
|
|
@@ -76,6 +91,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
76
91
|
output_format=args.output_format,
|
|
77
92
|
append_system_prompt=append_system_prompt,
|
|
78
93
|
allowed_tools=args.allowed_tools,
|
|
94
|
+
bare_mode=bare_mode,
|
|
79
95
|
)
|
|
80
96
|
except AutomationBackendUnavailableError as exc:
|
|
81
97
|
print(str(exc), file=sys.stderr)
|
|
@@ -106,7 +106,6 @@ def main(argv: list[str]) -> int:
|
|
|
106
106
|
"retry_backoff_seconds": legacy.get("retry_backoff_seconds", 60),
|
|
107
107
|
"claude_binary": legacy.get("claude_binary", ""),
|
|
108
108
|
"working_dir": legacy.get("working_dir", str(Path.home())),
|
|
109
|
-
"automation_task_profile": legacy.get("automation_task_profile", "deep"),
|
|
110
109
|
"max_process_time": legacy.get("max_process_time"),
|
|
111
110
|
"sent_folder": legacy.get("sent_folder", "INBOX.Sent"),
|
|
112
111
|
"operator_aliases": legacy_operator_aliases,
|
|
@@ -57,8 +57,6 @@ if str(NEXO_CODE) not in sys.path:
|
|
|
57
57
|
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
58
58
|
from client_preferences import (
|
|
59
59
|
resolve_automation_backend,
|
|
60
|
-
resolve_automation_task_profile,
|
|
61
|
-
resolve_client_runtime_profile,
|
|
62
60
|
)
|
|
63
61
|
from core_prompts import render_core_prompt
|
|
64
62
|
|
|
@@ -85,7 +83,6 @@ ZOMBIE_TIMEOUT_HOURS = 2
|
|
|
85
83
|
MAX_AUTOMATION_TIMEOUT_SECONDS = 1800
|
|
86
84
|
STALE_EMAIL_SESSION_MINUTES = 45
|
|
87
85
|
WORKER_STALE_MAX_MINUTES = 120
|
|
88
|
-
DEFAULT_AUTOMATION_TASK_PROFILE = "fast"
|
|
89
86
|
CONCURRENT_THRESHOLD_MINUTES = 15
|
|
90
87
|
MAX_CONCURRENT_SESSIONS = 2
|
|
91
88
|
MAX_EMAIL_ATTEMPTS = 3
|
|
@@ -386,7 +383,10 @@ def load_config():
|
|
|
386
383
|
except Exception:
|
|
387
384
|
pass
|
|
388
385
|
with open(CONFIG_PATH) as f:
|
|
389
|
-
|
|
386
|
+
payload = json.load(f)
|
|
387
|
+
if isinstance(payload, dict):
|
|
388
|
+
payload.pop("automation_task_profile", None)
|
|
389
|
+
return payload
|
|
390
390
|
|
|
391
391
|
|
|
392
392
|
def _safe_int(value, default=0):
|
|
@@ -1636,7 +1636,7 @@ def _reset_batch_to_pending(batch, reason):
|
|
|
1636
1636
|
log.warning(f"Failed to reset claimed batch to pending: {exc}")
|
|
1637
1637
|
|
|
1638
1638
|
|
|
1639
|
-
def _write_worker_job(*, batch, debt_block, max_retries, retry_backoff
|
|
1639
|
+
def _write_worker_job(*, batch, debt_block, max_retries, retry_backoff):
|
|
1640
1640
|
job_id = f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-{os.getpid()}-{uuid.uuid4().hex[:8]}"
|
|
1641
1641
|
job_path = WORKER_JOBS_DIR / f"job-{job_id}.json"
|
|
1642
1642
|
payload = {
|
|
@@ -1645,7 +1645,6 @@ def _write_worker_job(*, batch, debt_block, max_retries, retry_backoff, task_pro
|
|
|
1645
1645
|
"debt_block": debt_block or "",
|
|
1646
1646
|
"max_retries": int(max_retries),
|
|
1647
1647
|
"retry_backoff_seconds": int(retry_backoff),
|
|
1648
|
-
"task_profile": str(task_profile or DEFAULT_AUTOMATION_TASK_PROFILE),
|
|
1649
1648
|
"created_at": datetime.now().isoformat(timespec="seconds"),
|
|
1650
1649
|
}
|
|
1651
1650
|
job_path.write_text(json.dumps(payload, indent=2, ensure_ascii=True))
|
|
@@ -1689,16 +1688,12 @@ def _run_worker_job(job_path):
|
|
|
1689
1688
|
debt_block = payload.get("debt_block") or ""
|
|
1690
1689
|
max_retries = max(1, int(payload.get("max_retries") or 1))
|
|
1691
1690
|
retry_backoff = max(1, int(payload.get("retry_backoff_seconds") or 60))
|
|
1692
|
-
task_profile = str(payload.get("task_profile") or DEFAULT_AUTOMATION_TASK_PROFILE).strip().lower()
|
|
1693
1691
|
except Exception as exc:
|
|
1694
1692
|
log.error(f"Failed to load worker job {job_file}: {exc}")
|
|
1695
1693
|
job_file.unlink(missing_ok=True)
|
|
1696
1694
|
return 1
|
|
1697
1695
|
|
|
1698
1696
|
config = load_config()
|
|
1699
|
-
if task_profile:
|
|
1700
|
-
config = dict(config)
|
|
1701
|
-
config["automation_task_profile"] = task_profile
|
|
1702
1697
|
|
|
1703
1698
|
log.info(
|
|
1704
1699
|
f"Worker job started: {job_file.name} "
|
|
@@ -1998,20 +1993,17 @@ def launch_nexo(config, debt_block="", target_emails=None):
|
|
|
1998
1993
|
env["HOME"] = str(Path.home())
|
|
1999
1994
|
|
|
2000
1995
|
backend = resolve_automation_backend()
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
log.info(
|
|
2012
|
-
f"Launching NEXO via {launch_backend}"
|
|
2013
|
-
f"{f' [{task_profile}]' if task_profile else ''} ({profile_label})..."
|
|
2014
|
-
)
|
|
1996
|
+
profile_label = "default"
|
|
1997
|
+
try:
|
|
1998
|
+
from resonance_map import resolve_model_and_effort
|
|
1999
|
+
|
|
2000
|
+
mapped_model, mapped_effort = resolve_model_and_effort("email_monitor", backend)
|
|
2001
|
+
profile_label = mapped_model or "default"
|
|
2002
|
+
if mapped_effort:
|
|
2003
|
+
profile_label = f"{profile_label}/{mapped_effort}"
|
|
2004
|
+
except Exception:
|
|
2005
|
+
pass
|
|
2006
|
+
log.info(f"Launching NEXO via {backend} ({profile_label})...")
|
|
2015
2007
|
requested_timeout = int(config.get("max_process_time", MAX_AUTOMATION_TIMEOUT_SECONDS) or MAX_AUTOMATION_TIMEOUT_SECONDS)
|
|
2016
2008
|
effective_timeout = max(60, min(requested_timeout, MAX_AUTOMATION_TIMEOUT_SECONDS))
|
|
2017
2009
|
|
|
@@ -2042,7 +2034,6 @@ def launch_nexo(config, debt_block="", target_emails=None):
|
|
|
2042
2034
|
result = run_automation_prompt(
|
|
2043
2035
|
prompt,
|
|
2044
2036
|
caller="email_monitor",
|
|
2045
|
-
task_profile=task_profile,
|
|
2046
2037
|
cwd=config.get("working_dir", str(Path.home())),
|
|
2047
2038
|
env=env,
|
|
2048
2039
|
timeout=effective_timeout,
|
|
@@ -2343,13 +2334,11 @@ def main():
|
|
|
2343
2334
|
|
|
2344
2335
|
max_retries = config.get("max_retries", 3)
|
|
2345
2336
|
retry_backoff = config.get("retry_backoff_seconds", 60)
|
|
2346
|
-
task_profile = str(config.get("automation_task_profile") or DEFAULT_AUTOMATION_TASK_PROFILE).strip().lower()
|
|
2347
2337
|
job_path = _write_worker_job(
|
|
2348
2338
|
batch=batch,
|
|
2349
2339
|
debt_block=debt_block,
|
|
2350
2340
|
max_retries=max_retries,
|
|
2351
2341
|
retry_backoff=retry_backoff,
|
|
2352
|
-
task_profile=task_profile,
|
|
2353
2342
|
)
|
|
2354
2343
|
try:
|
|
2355
2344
|
worker_pid = _spawn_worker(job_path, config)
|
|
@@ -17,8 +17,11 @@ Both paths are probed so dev and live operators get identical behaviour.
|
|
|
17
17
|
"""
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import inspect
|
|
20
21
|
import os
|
|
22
|
+
import re
|
|
21
23
|
import sys
|
|
24
|
+
import time
|
|
22
25
|
from pathlib import Path
|
|
23
26
|
|
|
24
27
|
|
|
@@ -31,6 +34,12 @@ if str(_repo_src) not in sys.path:
|
|
|
31
34
|
|
|
32
35
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
33
36
|
DEFAULT_ALLOWED_TOOLS = "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"
|
|
37
|
+
DEFAULT_SHORT_TEXT_ALLOWED_TOOLS = ""
|
|
38
|
+
DEFAULT_SHORT_TEXT_TIMEOUT = max(
|
|
39
|
+
30,
|
|
40
|
+
int(os.environ.get("NEXO_PERSONAL_AUTOMATION_TIMEOUT", "180")),
|
|
41
|
+
)
|
|
42
|
+
_PROCESS_LOCK_COUNTS: dict[str, int] = {}
|
|
34
43
|
|
|
35
44
|
# Templates live next to the code at repo time and at ``~/.nexo/templates``
|
|
36
45
|
# once installed. Probe both and surface whichever exists first so the
|
|
@@ -41,45 +50,161 @@ for _candidate in (_repo_root / "templates", NEXO_HOME / "templates"):
|
|
|
41
50
|
if _candidate.exists() and _cand not in sys.path:
|
|
42
51
|
sys.path.insert(0, _cand)
|
|
43
52
|
|
|
44
|
-
try:
|
|
45
|
-
from client_preferences import resolve_user_model
|
|
46
|
-
_USER_MODEL = resolve_user_model()
|
|
47
|
-
except Exception:
|
|
48
|
-
_USER_MODEL = ""
|
|
49
|
-
|
|
50
53
|
from nexo_helper import run_automation_text as _run_automation_text
|
|
51
54
|
|
|
52
55
|
|
|
56
|
+
def _infer_personal_caller() -> str:
|
|
57
|
+
env_caller = str(os.environ.get("NEXO_AUTOMATION_CALLER") or "").strip()
|
|
58
|
+
if env_caller:
|
|
59
|
+
return env_caller
|
|
60
|
+
candidates: list[Path] = []
|
|
61
|
+
argv0 = str(sys.argv[0] or "").strip()
|
|
62
|
+
if argv0:
|
|
63
|
+
candidates.append(Path(argv0).expanduser())
|
|
64
|
+
current = Path(__file__).resolve()
|
|
65
|
+
for frame in inspect.stack()[1:]:
|
|
66
|
+
try:
|
|
67
|
+
path = Path(frame.filename).resolve()
|
|
68
|
+
except Exception:
|
|
69
|
+
continue
|
|
70
|
+
if path != current:
|
|
71
|
+
candidates.append(path)
|
|
72
|
+
for candidate in candidates:
|
|
73
|
+
parts = candidate.parts
|
|
74
|
+
if "personal" in parts and "scripts" in parts:
|
|
75
|
+
stem = candidate.stem.strip()
|
|
76
|
+
if stem:
|
|
77
|
+
return f"personal/{stem}"
|
|
78
|
+
if argv0:
|
|
79
|
+
stem = Path(argv0).stem.strip()
|
|
80
|
+
if stem and stem not in {"python", "python3", "-m"}:
|
|
81
|
+
return f"personal/{stem}"
|
|
82
|
+
return "agent_run/generic"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _caller_lock_path(caller: str) -> Path:
|
|
86
|
+
slug = re.sub(r"[^A-Za-z0-9_.-]+", "-", caller).strip("-") or "generic"
|
|
87
|
+
return NEXO_HOME / "runtime" / "locks" / "personal-automation" / f"{slug}.lock"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _read_lock_pid(path: Path) -> int:
|
|
91
|
+
try:
|
|
92
|
+
raw = path.read_text().splitlines()
|
|
93
|
+
except Exception:
|
|
94
|
+
return 0
|
|
95
|
+
if not raw:
|
|
96
|
+
return 0
|
|
97
|
+
try:
|
|
98
|
+
return int(raw[0].strip())
|
|
99
|
+
except Exception:
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _pid_is_alive(pid: int) -> bool:
|
|
104
|
+
if pid <= 0:
|
|
105
|
+
return False
|
|
106
|
+
try:
|
|
107
|
+
os.kill(pid, 0)
|
|
108
|
+
except ProcessLookupError:
|
|
109
|
+
return False
|
|
110
|
+
except PermissionError:
|
|
111
|
+
return True
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _acquire_personal_caller_lock(caller: str) -> str:
|
|
116
|
+
clean = str(caller or "").strip()
|
|
117
|
+
if not clean.startswith("personal/"):
|
|
118
|
+
return ""
|
|
119
|
+
if _PROCESS_LOCK_COUNTS.get(clean, 0) > 0:
|
|
120
|
+
_PROCESS_LOCK_COUNTS[clean] += 1
|
|
121
|
+
return clean
|
|
122
|
+
pid = os.getpid()
|
|
123
|
+
lock_path = _caller_lock_path(clean)
|
|
124
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
while True:
|
|
126
|
+
try:
|
|
127
|
+
fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
128
|
+
except FileExistsError:
|
|
129
|
+
existing_pid = _read_lock_pid(lock_path)
|
|
130
|
+
if existing_pid == pid:
|
|
131
|
+
_PROCESS_LOCK_COUNTS[clean] = 1
|
|
132
|
+
return clean
|
|
133
|
+
if existing_pid and _pid_is_alive(existing_pid):
|
|
134
|
+
raise RuntimeError(
|
|
135
|
+
f"Automation caller {clean} already has a live run (pid {existing_pid})."
|
|
136
|
+
)
|
|
137
|
+
try:
|
|
138
|
+
lock_path.unlink()
|
|
139
|
+
except FileNotFoundError:
|
|
140
|
+
pass
|
|
141
|
+
continue
|
|
142
|
+
with os.fdopen(fd, "w", encoding="ascii") as handle:
|
|
143
|
+
handle.write(f"{pid}\n{int(time.time())}\n{clean}\n")
|
|
144
|
+
_PROCESS_LOCK_COUNTS[clean] = 1
|
|
145
|
+
return clean
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _release_personal_caller_lock(caller: str) -> None:
|
|
149
|
+
clean = str(caller or "").strip()
|
|
150
|
+
if not clean.startswith("personal/"):
|
|
151
|
+
return
|
|
152
|
+
count = _PROCESS_LOCK_COUNTS.get(clean, 0)
|
|
153
|
+
if count > 1:
|
|
154
|
+
_PROCESS_LOCK_COUNTS[clean] = count - 1
|
|
155
|
+
return
|
|
156
|
+
_PROCESS_LOCK_COUNTS.pop(clean, None)
|
|
157
|
+
lock_path = _caller_lock_path(clean)
|
|
158
|
+
if _read_lock_pid(lock_path) == os.getpid():
|
|
159
|
+
try:
|
|
160
|
+
lock_path.unlink()
|
|
161
|
+
except FileNotFoundError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
|
|
53
165
|
def run_personal_automation_text(
|
|
54
166
|
prompt: str,
|
|
55
167
|
*,
|
|
56
168
|
model: str = "",
|
|
57
169
|
cwd: str = "",
|
|
58
|
-
timeout: int =
|
|
59
|
-
allowed_tools: str =
|
|
170
|
+
timeout: int = DEFAULT_SHORT_TEXT_TIMEOUT,
|
|
171
|
+
allowed_tools: str = DEFAULT_SHORT_TEXT_ALLOWED_TOOLS,
|
|
60
172
|
append_system_prompt: str = "",
|
|
173
|
+
caller: str = "",
|
|
174
|
+
tier: str = "",
|
|
175
|
+
bare_mode: bool | None = True,
|
|
61
176
|
) -> str:
|
|
62
177
|
"""Run ``prompt`` through the configured NEXO automation backend.
|
|
63
178
|
|
|
64
|
-
``model`` empty
|
|
65
|
-
|
|
66
|
-
|
|
179
|
+
``model`` stays empty unless the caller provides an explicit override.
|
|
180
|
+
Backend/model/effort resolution belongs to the resonance engine via
|
|
181
|
+
``caller`` and ``tier``.
|
|
67
182
|
``cwd`` empty → inherit the current working directory.
|
|
68
183
|
Every other kwarg passes through verbatim.
|
|
69
184
|
"""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
185
|
+
effective_caller = caller or _infer_personal_caller()
|
|
186
|
+
lock_token = _acquire_personal_caller_lock(effective_caller)
|
|
187
|
+
try:
|
|
188
|
+
return _run_automation_text(
|
|
189
|
+
prompt,
|
|
190
|
+
model=model,
|
|
191
|
+
cwd=cwd or "",
|
|
192
|
+
timeout=timeout,
|
|
193
|
+
allowed_tools=allowed_tools,
|
|
194
|
+
append_system_prompt=append_system_prompt,
|
|
195
|
+
include_bootstrap=False,
|
|
196
|
+
caller=effective_caller,
|
|
197
|
+
tier=tier,
|
|
198
|
+
bare_mode=bare_mode,
|
|
199
|
+
)
|
|
200
|
+
finally:
|
|
201
|
+
_release_personal_caller_lock(lock_token)
|
|
79
202
|
|
|
80
203
|
|
|
81
204
|
__all__ = [
|
|
82
205
|
"DEFAULT_ALLOWED_TOOLS",
|
|
206
|
+
"DEFAULT_SHORT_TEXT_ALLOWED_TOOLS",
|
|
207
|
+
"DEFAULT_SHORT_TEXT_TIMEOUT",
|
|
83
208
|
"NEXO_HOME",
|
|
84
209
|
"run_personal_automation_text",
|
|
85
210
|
]
|
package/src/support_snapshot.py
CHANGED
|
@@ -20,6 +20,7 @@ import paths
|
|
|
20
20
|
from doctor.formatters import format_report
|
|
21
21
|
from doctor.orchestrator import run_doctor
|
|
22
22
|
from health_check import collect as collect_health
|
|
23
|
+
from windows_runtime import running_inside_wsl, windows_runtime_status
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _nexo_home() -> Path:
|
|
@@ -111,15 +112,19 @@ def _recent_logs(lines: int = 80) -> dict[str, Any]:
|
|
|
111
112
|
|
|
112
113
|
|
|
113
114
|
def collect_snapshot(*, log_lines: int = 80, include_doctor: bool = False) -> dict[str, Any]:
|
|
115
|
+
system = platform.system()
|
|
116
|
+
release = platform.release()
|
|
114
117
|
payload: dict[str, Any] = {
|
|
115
118
|
"generated_at": time.time(),
|
|
116
119
|
"version": _read_version(),
|
|
117
120
|
"platform": {
|
|
118
|
-
"system":
|
|
119
|
-
"release":
|
|
121
|
+
"system": system,
|
|
122
|
+
"release": release,
|
|
120
123
|
"machine": platform.machine(),
|
|
121
124
|
"python": platform.python_version(),
|
|
125
|
+
"is_wsl": running_inside_wsl(system=system, release=release),
|
|
122
126
|
},
|
|
127
|
+
"windows_runtime": windows_runtime_status(_nexo_home(), system=system, release=release),
|
|
123
128
|
"paths": _path_status(),
|
|
124
129
|
"health": collect_health(),
|
|
125
130
|
"logs": _recent_logs(log_lines),
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Helpers for Windows/WSL runtime diagnostics."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def running_inside_wsl(*, system: str | None = None, release: str | None = None) -> bool:
|
|
11
|
+
resolved_system = str(system or platform.system() or "").strip()
|
|
12
|
+
resolved_release = str(release or platform.release() or "").strip().lower()
|
|
13
|
+
if resolved_system != "Linux":
|
|
14
|
+
return False
|
|
15
|
+
if "microsoft" in resolved_release:
|
|
16
|
+
return True
|
|
17
|
+
if str(os.environ.get("WSL_DISTRO_NAME", "")).strip():
|
|
18
|
+
return True
|
|
19
|
+
if str(os.environ.get("WSL_INTEROP", "")).strip():
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def running_from_windows_host() -> bool:
|
|
25
|
+
value = str(os.environ.get("NEXO_WINDOWS_HOST", "")).strip().lower()
|
|
26
|
+
return value not in ("", "0", "false", "no", "off")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def bridge_mode() -> str:
|
|
30
|
+
value = str(os.environ.get("NEXO_WINDOWS_BRIDGE", "")).strip()
|
|
31
|
+
return "wsl-exec" if value else ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_windows_mount_path(candidate: Path) -> bool:
|
|
35
|
+
normalized = str(candidate or "").replace("\\", "/").lower()
|
|
36
|
+
return normalized.startswith("/mnt/")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def windows_runtime_status(nexo_home: Path, *, system: str | None = None, release: str | None = None) -> dict[str, Any]:
|
|
40
|
+
resolved_system = str(system or platform.system() or "").strip()
|
|
41
|
+
resolved_release = str(release or platform.release() or "").strip()
|
|
42
|
+
inside_wsl = running_inside_wsl(system=resolved_system, release=resolved_release)
|
|
43
|
+
on_windows_mount = is_windows_mount_path(nexo_home)
|
|
44
|
+
warnings: list[dict[str, str]] = []
|
|
45
|
+
|
|
46
|
+
if resolved_system == "Windows":
|
|
47
|
+
warnings.append(
|
|
48
|
+
{
|
|
49
|
+
"code": "brain_requires_wsl",
|
|
50
|
+
"message": "NEXO Brain on Windows is supported via WSL, not native win32 mode.",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
if on_windows_mount:
|
|
54
|
+
warnings.append(
|
|
55
|
+
{
|
|
56
|
+
"code": "nexo_home_on_windows_mount",
|
|
57
|
+
"message": "NEXO_HOME is inside /mnt/*; keep the canonical Brain runtime inside the WSL filesystem.",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
"supported_brain_mode": "wsl",
|
|
63
|
+
"inside_wsl": inside_wsl,
|
|
64
|
+
"windows_host_bridge": running_from_windows_host(),
|
|
65
|
+
"bridge_mode": bridge_mode(),
|
|
66
|
+
"wsl_distro": str(os.environ.get("WSL_DISTRO_NAME", "")).strip(),
|
|
67
|
+
"wsl_interop": bool(str(os.environ.get("WSL_INTEROP", "")).strip()),
|
|
68
|
+
"nexo_home_on_windows_mount": on_windows_mount,
|
|
69
|
+
"warnings": warnings,
|
|
70
|
+
}
|
package/templates/nexo_helper.py
CHANGED
|
@@ -226,6 +226,7 @@ def run_automation_text(
|
|
|
226
226
|
include_bootstrap: bool = True,
|
|
227
227
|
caller: str = "",
|
|
228
228
|
tier: str = "",
|
|
229
|
+
bare_mode: bool | None = None,
|
|
229
230
|
) -> str:
|
|
230
231
|
"""Run the configured NEXO automation backend and return text output.
|
|
231
232
|
|
|
@@ -264,6 +265,10 @@ def run_automation_text(
|
|
|
264
265
|
cmd.extend(["--append-system-prompt", "\n\n".join(merged_system_prompt)])
|
|
265
266
|
if allowed_tools:
|
|
266
267
|
cmd.extend(["--allowed-tools", allowed_tools])
|
|
268
|
+
if bare_mode is True:
|
|
269
|
+
cmd.extend(["--bare-mode", "on"])
|
|
270
|
+
elif bare_mode is False:
|
|
271
|
+
cmd.extend(["--bare-mode", "off"])
|
|
267
272
|
|
|
268
273
|
env = os.environ.copy()
|
|
269
274
|
env.setdefault("NEXO_HOME", str(NEXO_HOME))
|
|
@@ -292,6 +297,7 @@ def run_automation_json(
|
|
|
292
297
|
include_bootstrap: bool = True,
|
|
293
298
|
caller: str = "",
|
|
294
299
|
tier: str = "",
|
|
300
|
+
bare_mode: bool | None = None,
|
|
295
301
|
) -> dict:
|
|
296
302
|
"""Run the configured backend and return a parsed JSON object.
|
|
297
303
|
|
|
@@ -326,6 +332,10 @@ def run_automation_json(
|
|
|
326
332
|
cmd.extend(["--append-system-prompt", "\n\n".join(merged_system_prompt)])
|
|
327
333
|
if allowed_tools:
|
|
328
334
|
cmd.extend(["--allowed-tools", allowed_tools])
|
|
335
|
+
if bare_mode is True:
|
|
336
|
+
cmd.extend(["--bare-mode", "on"])
|
|
337
|
+
elif bare_mode is False:
|
|
338
|
+
cmd.extend(["--bare-mode", "off"])
|
|
329
339
|
|
|
330
340
|
env = os.environ.copy()
|
|
331
341
|
env.setdefault("NEXO_HOME", str(NEXO_HOME))
|