@turmind/halo 1.3.4-c3090bf → 1.4.0
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/admin-out/404.html +1 -1
- package/admin-out/_next/static/chunks/989-6a0ec35b8ae2cb39.js +1 -0
- package/admin-out/_next/static/chunks/app/page-bf9c0b21ae431c92.js +1 -0
- package/admin-out/_next/static/css/c0ea31cd787f7aa7.css +3 -0
- package/admin-out/index.html +1 -1
- package/admin-out/index.txt +5 -5
- package/bundled-docs/design/agent.md +12 -6
- package/bundled-docs/design/cron.md +19 -11
- package/bundled-docs/design/express-self.md +1 -1
- package/bundled-docs/design/storage.md +7 -3
- package/bundled-docs/design/wechat.md +1 -1
- package/bundled-docs/design/ws.md +2 -1
- package/bundled-docs/dev/add-model-provider.md +1 -1
- package/bundled-docs/dev/api.md +6 -6
- package/bundled-docs/dev/deploy.md +1 -1
- package/bundled-docs/dev/desktop-packaging.md +1 -1
- package/bundled-docs/dev/env.md +4 -4
- package/bundled-docs/dev/tools.md +39 -17
- package/bundled-docs/guide/chat.md +10 -0
- package/bundled-docs/guide/cli.md +8 -3
- package/bundled-docs/requirements/chat.md +8 -0
- package/dist/index.js +498 -68
- package/package.json +1 -1
- package/templates/agents/__apply_agent__/agent.yaml +1 -1
- package/templates/agents/__evo_agent__/agent.yaml +1 -1
- package/templates/agents/__score__/agent.yaml +1 -1
- package/templates/agents/deep-executor/agent.yaml +1 -1
- package/templates/agents/default/agent.yaml +1 -1
- package/templates/agents/goal/agent.yaml +1 -1
- package/templates/models/mimo-token-plan-china.yaml +13 -6
- package/templates/skills/agent/SKILL.md +2 -2
- package/templates/skills/cron/SKILL.md +15 -0
- package/templates/skills/cron/manage-cron.py +28 -3
- package/templates/skills/send-file/SKILL.md +1 -0
- package/admin-out/_next/static/chunks/989-e660d0dd799950f2.js +0 -1
- package/admin-out/_next/static/chunks/app/page-18ab83dbdcc3897c.js +0 -1
- package/admin-out/_next/static/css/59402ac06b2b1a10.css +0 -3
- /package/admin-out/_next/static/{BVMDWdlRNum62_KgIRZVc → 9_eFiojb_-MS8CJ2oJg8_}/_buildManifest.js +0 -0
- /package/admin-out/_next/static/{BVMDWdlRNum62_KgIRZVc → 9_eFiojb_-MS8CJ2oJg8_}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -513,12 +513,12 @@ function generalSection() {
|
|
|
513
513
|
{ key: "compact.max_summary_input", type: "int", globalOnly: true, description: "Max chars fed into local truncation fallback", description_zh: "\u672C\u5730\u622A\u65AD\u515C\u5E95\u65F6\u7684\u603B\u8F93\u5165\u5B57\u7B26\u4E0A\u9650", default: "15000" },
|
|
514
514
|
{ key: "compact.max_message_slice", type: "int", globalOnly: true, description: "Max chars kept per old message during local truncation", description_zh: "\u672C\u5730\u622A\u65AD\u515C\u5E95\u65F6\u6BCF\u6761\u65E7\u6D88\u606F\u4FDD\u7559\u7684\u6700\u5927\u5B57\u7B26\u6570", default: "800" },
|
|
515
515
|
{ key: "compact.summarize_timeout_sec", type: "int", globalOnly: true, description: "LLM summarization timeout (seconds)", description_zh: "LLM \u6458\u8981\u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09", default: "300" },
|
|
516
|
-
// sandbox (Linux bwrap
|
|
516
|
+
// sandbox (Linux bwrap / macOS sandbox-exec) — global-only: these define the security
|
|
517
517
|
// boundary agents run inside; a workspace overriding them could lift
|
|
518
518
|
// its own sandbox constraints.
|
|
519
|
-
{ key: "sandbox.hidden_dirs", globalOnly: true, description: "Comma-separated dirs hidden
|
|
520
|
-
{ key: "sandbox.writable_dirs", globalOnly: true, description: "Comma-separated dirs
|
|
521
|
-
{ key: "sandbox.hidden_files", globalOnly: true, description: "Comma-separated files hidden
|
|
519
|
+
{ key: "sandbox.hidden_dirs", globalOnly: true, description: "Comma-separated dirs hidden from workspace/readonly sessions (Linux bwrap / macOS sandbox-exec)", description_zh: "\u5BF9 workspace/readonly \u4F1A\u8BDD\u9690\u85CF\u7684\u76EE\u5F55\uFF08\u9017\u53F7\u5206\u9694\uFF0CLinux bwrap / macOS sandbox-exec\uFF09", default: "~/.halo/secrets,~/.aws,~/.ssh,~/.gnupg,~/.docker,~/.config/gh,~/.halo/global/internal-sessions,~/.halo/global/logs" },
|
|
520
|
+
{ key: "sandbox.writable_dirs", globalOnly: true, description: "Comma-separated dirs writable inside the sandbox besides the workspace (Linux / macOS) \u2014 for external CLIs that keep local state, e.g. ~/.kiro,~/.local/share/kiro-cli. Not applied to readonly sessions.", description_zh: "\u9664 workspace \u5916\u5728\u6C99\u7BB1\u5185\u53EF\u5199\u7684\u76EE\u5F55\uFF08\u9017\u53F7\u5206\u9694\uFF0CLinux / macOS\uFF09\u2014\u2014\u7ED9\u9700\u8981\u672C\u5730\u72B6\u6001\u7684\u5916\u90E8 CLI \u7528\uFF0C\u5982 ~/.kiro,~/.local/share/kiro-cli\u3002readonly \u4F1A\u8BDD\u4E0D\u751F\u6548\u3002", default: "" },
|
|
521
|
+
{ key: "sandbox.hidden_files", globalOnly: true, description: "Comma-separated files hidden from workspace/readonly sessions \u2014 read as empty (Linux / macOS)", description_zh: "\u5BF9 workspace/readonly \u4F1A\u8BDD\u9690\u85CF\u7684\u6587\u4EF6\uFF0C\u8BFB\u5230\u7684\u662F\u7A7A\u6587\u4EF6\uFF08\u9017\u53F7\u5206\u9694\uFF0CLinux / macOS\uFF09", default: "~/.npmrc,~/.bash_history,~/.gitconfig,~/.git-credentials,~/.netrc,~/.halo/global/evo.db,~/.halo/global/evo.db-wal,~/.halo/global/evo.db-shm,~/.halo/global/cron.db,~/.halo/global/cron.db-wal,~/.halo/global/cron.db-shm,~/.halo/global/runs.db,~/.halo/global/runs.db-wal,~/.halo/global/runs.db-shm" },
|
|
522
522
|
// logging
|
|
523
523
|
{ key: "logging.level", type: "enum", options: ["debug", "info", "warn", "error"], globalOnly: true, description: "Log level", description_zh: "\u65E5\u5FD7\u7EA7\u522B", default: "warn" },
|
|
524
524
|
// observability (OpenTelemetry). Read once at boot by observability/otel.ts
|
|
@@ -2130,6 +2130,7 @@ __export(init_exports, {
|
|
|
2130
2130
|
describeOptionalSkill: () => describeOptionalSkill,
|
|
2131
2131
|
ensureHaloHome: () => ensureHaloHome,
|
|
2132
2132
|
ensureWorkspaceHalo: () => ensureWorkspaceHalo,
|
|
2133
|
+
hasWorkspaceHalo: () => hasWorkspaceHalo,
|
|
2133
2134
|
listOptionalSkillIds: () => listOptionalSkillIds,
|
|
2134
2135
|
mergeAgentYaml: () => mergeAgentYaml,
|
|
2135
2136
|
readInstalledOptionalSkills: () => readInstalledOptionalSkills,
|
|
@@ -2458,6 +2459,9 @@ function ensureHaloHome(haloHome2) {
|
|
|
2458
2459
|
} catch {
|
|
2459
2460
|
}
|
|
2460
2461
|
}
|
|
2462
|
+
function hasWorkspaceHalo(root) {
|
|
2463
|
+
return fs7.existsSync(path8.join(root, ".halo"));
|
|
2464
|
+
}
|
|
2461
2465
|
function ensureWorkspaceHalo(workspaceRoot) {
|
|
2462
2466
|
if (!fs7.existsSync(workspaceRoot)) {
|
|
2463
2467
|
throw new Error(`Workspace root does not exist: ${workspaceRoot}`);
|
|
@@ -2497,7 +2501,7 @@ var init_init = __esm({
|
|
|
2497
2501
|
path8.resolve(__dirname, "..", "bundled-docs"),
|
|
2498
2502
|
path8.resolve(__dirname, "..", "..", "..", ".halo", "docs")
|
|
2499
2503
|
];
|
|
2500
|
-
TEMPLATE_VERSION =
|
|
2504
|
+
TEMPLATE_VERSION = 65;
|
|
2501
2505
|
VERSION_FILE = ".template-version";
|
|
2502
2506
|
SKIP_NAMES = /* @__PURE__ */ new Set([".DS_Store", "schema.sql", "__pycache__", ".pytest_cache"]);
|
|
2503
2507
|
BUILTIN_AGENT_IDS = /* @__PURE__ */ new Set([
|
|
@@ -9491,7 +9495,7 @@ import { exec, execFile, spawn as spawn2 } from "node:child_process";
|
|
|
9491
9495
|
import { promisify } from "node:util";
|
|
9492
9496
|
import path25 from "node:path";
|
|
9493
9497
|
import { homedir as homedir10 } from "node:os";
|
|
9494
|
-
import { existsSync, realpathSync as realpathSync2 } from "node:fs";
|
|
9498
|
+
import { existsSync, lstatSync, mkdirSync, realpathSync as realpathSync2, rmSync, writeFileSync } from "node:fs";
|
|
9495
9499
|
function decodeWinOutput(buf) {
|
|
9496
9500
|
try {
|
|
9497
9501
|
return new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
@@ -9525,7 +9529,7 @@ async function bwrapExec(args, opts) {
|
|
|
9525
9529
|
}
|
|
9526
9530
|
function spawnGroupExec(command, opts) {
|
|
9527
9531
|
return new Promise((resolve2, reject) => {
|
|
9528
|
-
const child = spawn2(command, { shell: true, detached: true, cwd: opts.cwd, env: cleanChildEnv() });
|
|
9532
|
+
const child = opts.argv ? spawn2(opts.argv[0], opts.argv.slice(1), { detached: true, cwd: opts.cwd, env: opts.env ?? cleanChildEnv() }) : spawn2(command, { shell: true, detached: true, cwd: opts.cwd, env: cleanChildEnv() });
|
|
9529
9533
|
let stdout = "";
|
|
9530
9534
|
let stderr = "";
|
|
9531
9535
|
let killReason = null;
|
|
@@ -9637,12 +9641,68 @@ async function isBwrapAvailable() {
|
|
|
9637
9641
|
return false;
|
|
9638
9642
|
}
|
|
9639
9643
|
}
|
|
9640
|
-
function
|
|
9641
|
-
|
|
9644
|
+
async function isSeatbeltAvailable() {
|
|
9645
|
+
if (process.platform !== "darwin")
|
|
9646
|
+
return false;
|
|
9647
|
+
if (_seatbeltAvailable !== null)
|
|
9648
|
+
return _seatbeltAvailable;
|
|
9649
|
+
try {
|
|
9650
|
+
await execFileAsync(SANDBOX_EXEC, ["-p", "(version 1)(allow default)", "/usr/bin/true"], { timeout: 5e3 });
|
|
9651
|
+
_seatbeltAvailable = true;
|
|
9652
|
+
} catch (err) {
|
|
9653
|
+
console.warn(`[Sandbox] sandbox-exec probe failed (${String(err.message).split("\n")[0]}) \u2014 non-full shell_exec unavailable`);
|
|
9654
|
+
_seatbeltAvailable = false;
|
|
9655
|
+
}
|
|
9656
|
+
return _seatbeltAvailable;
|
|
9657
|
+
}
|
|
9658
|
+
function getSandboxBackend() {
|
|
9659
|
+
if (_bwrapAvailable === true)
|
|
9660
|
+
return "bwrap";
|
|
9661
|
+
if (_seatbeltAvailable === true)
|
|
9662
|
+
return "seatbelt";
|
|
9663
|
+
return null;
|
|
9664
|
+
}
|
|
9665
|
+
async function loadGitIdentity() {
|
|
9666
|
+
const read = async (key) => {
|
|
9667
|
+
try {
|
|
9668
|
+
return String((await execFileAsync("git", ["config", "--global", key], { timeout: 3e3 })).stdout).trim();
|
|
9669
|
+
} catch {
|
|
9670
|
+
return "";
|
|
9671
|
+
}
|
|
9672
|
+
};
|
|
9673
|
+
const [name, email] = await Promise.all([read("user.name"), read("user.email")]);
|
|
9674
|
+
_gitIdentity = { name, email };
|
|
9675
|
+
}
|
|
9676
|
+
function gitIdentityEnv() {
|
|
9677
|
+
const env = {};
|
|
9678
|
+
if (_gitIdentity.name) {
|
|
9679
|
+
env.GIT_AUTHOR_NAME = _gitIdentity.name;
|
|
9680
|
+
env.GIT_COMMITTER_NAME = _gitIdentity.name;
|
|
9681
|
+
}
|
|
9682
|
+
if (_gitIdentity.email) {
|
|
9683
|
+
env.GIT_AUTHOR_EMAIL = _gitIdentity.email;
|
|
9684
|
+
env.GIT_COMMITTER_EMAIL = _gitIdentity.email;
|
|
9685
|
+
}
|
|
9686
|
+
return env;
|
|
9642
9687
|
}
|
|
9643
9688
|
async function initBwrapCheck() {
|
|
9689
|
+
await loadGitIdentity();
|
|
9690
|
+
if (process.platform === "darwin")
|
|
9691
|
+
return isSeatbeltAvailable();
|
|
9644
9692
|
return isBwrapAvailable();
|
|
9645
9693
|
}
|
|
9694
|
+
function ensureEmptyMaskFile() {
|
|
9695
|
+
try {
|
|
9696
|
+
const st = lstatSync(EMPTY_MASK_FILE);
|
|
9697
|
+
if (st.isFile() && st.size === 0)
|
|
9698
|
+
return EMPTY_MASK_FILE;
|
|
9699
|
+
rmSync(EMPTY_MASK_FILE, { recursive: true, force: true });
|
|
9700
|
+
} catch {
|
|
9701
|
+
}
|
|
9702
|
+
mkdirSync(HALO_HOME3, { recursive: true });
|
|
9703
|
+
writeFileSync(EMPTY_MASK_FILE, "");
|
|
9704
|
+
return EMPTY_MASK_FILE;
|
|
9705
|
+
}
|
|
9646
9706
|
function expandTilde(p) {
|
|
9647
9707
|
return p.startsWith("~/") ? path25.join(HOME, p.slice(2)) : p;
|
|
9648
9708
|
}
|
|
@@ -9650,6 +9710,20 @@ function setSandboxHiddenPaths(dirs, files, writableDirs = []) {
|
|
|
9650
9710
|
_hiddenDirs = dirs;
|
|
9651
9711
|
_hiddenFiles = files;
|
|
9652
9712
|
_writableDirs = writableDirs;
|
|
9713
|
+
_resolvedLists = null;
|
|
9714
|
+
}
|
|
9715
|
+
function resolvedLists() {
|
|
9716
|
+
if (_resolvedLists)
|
|
9717
|
+
return _resolvedLists;
|
|
9718
|
+
const both = (raws) => [...new Set(raws.flatMap((raw) => {
|
|
9719
|
+
const p = path25.resolve(expandTilde(raw));
|
|
9720
|
+
return [p, realpathBounded(p)];
|
|
9721
|
+
}))];
|
|
9722
|
+
_resolvedLists = { hiddenDirs: both(_hiddenDirs), hiddenFiles: both(_hiddenFiles), writableDirs: both(_writableDirs) };
|
|
9723
|
+
return _resolvedLists;
|
|
9724
|
+
}
|
|
9725
|
+
function isUnder(p, dir) {
|
|
9726
|
+
return p === dir || p.startsWith(dir.endsWith("/") ? dir : dir + "/");
|
|
9653
9727
|
}
|
|
9654
9728
|
function buildBwrapArgs(opts) {
|
|
9655
9729
|
const args = [];
|
|
@@ -9660,10 +9734,11 @@ function buildBwrapArgs(opts) {
|
|
|
9660
9734
|
if (existsSync(dir))
|
|
9661
9735
|
args.push("--tmpfs", dir);
|
|
9662
9736
|
}
|
|
9737
|
+
const emptyFile = ensureEmptyMaskFile();
|
|
9663
9738
|
for (const raw of _hiddenFiles) {
|
|
9664
9739
|
const file = expandTilde(raw);
|
|
9665
9740
|
if (existsSync(file))
|
|
9666
|
-
args.push("--ro-bind",
|
|
9741
|
+
args.push("--ro-bind", emptyFile, file);
|
|
9667
9742
|
}
|
|
9668
9743
|
if (opts.accessLevel !== "readonly") {
|
|
9669
9744
|
args.push("--bind", opts.workspaceRoot, opts.workspaceRoot);
|
|
@@ -9683,7 +9758,7 @@ function buildBwrapArgs(opts) {
|
|
|
9683
9758
|
for (const rel of WORKSPACE_HIDDEN_FILES) {
|
|
9684
9759
|
const file = path25.join(opts.workspaceRoot, rel);
|
|
9685
9760
|
if (existsSync(file))
|
|
9686
|
-
args.push("--ro-bind",
|
|
9761
|
+
args.push("--ro-bind", emptyFile, file);
|
|
9687
9762
|
}
|
|
9688
9763
|
args.push("--proc", "/proc");
|
|
9689
9764
|
args.push("--dev", "/dev");
|
|
@@ -9691,6 +9766,8 @@ function buildBwrapArgs(opts) {
|
|
|
9691
9766
|
args.push("--setenv", "PATH", `${HOME}/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`);
|
|
9692
9767
|
args.push("--setenv", "HOME", HOME);
|
|
9693
9768
|
args.push("--setenv", "TERM", "xterm-256color");
|
|
9769
|
+
for (const [k, v] of Object.entries(gitIdentityEnv()))
|
|
9770
|
+
args.push("--setenv", k, v);
|
|
9694
9771
|
args.push("--die-with-parent");
|
|
9695
9772
|
return args;
|
|
9696
9773
|
}
|
|
@@ -9718,31 +9795,57 @@ function assertPathAllowed(filePath, opts, write = false) {
|
|
|
9718
9795
|
return path25.resolve(filePath);
|
|
9719
9796
|
const resolved = realpathBounded(filePath);
|
|
9720
9797
|
const wsRoot = realpathBounded(opts.workspaceRoot);
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
}
|
|
9725
|
-
if (write && opts.accessLevel === "readonly") {
|
|
9726
|
-
throw new Error(`Access denied: readonly session cannot write to "${filePath}"`);
|
|
9727
|
-
}
|
|
9728
|
-
return resolved;
|
|
9798
|
+
const inWorkspace = isUnder(resolved, wsRoot);
|
|
9799
|
+
if (inWorkspace && isHiddenWorkspacePath(resolved, wsRoot) || isHiddenHostPath(resolved)) {
|
|
9800
|
+
throw new Error(`Access denied: "${filePath}" is outside the allowed sandbox paths`);
|
|
9729
9801
|
}
|
|
9730
|
-
if (!write
|
|
9802
|
+
if (!write)
|
|
9731
9803
|
return resolved;
|
|
9804
|
+
if (opts.accessLevel === "readonly") {
|
|
9805
|
+
throw new Error(`Access denied: readonly session cannot write to "${filePath}"`);
|
|
9732
9806
|
}
|
|
9807
|
+
if (inWorkspace || resolvedLists().writableDirs.some((d) => isUnder(resolved, d)))
|
|
9808
|
+
return resolved;
|
|
9733
9809
|
throw new Error(`Access denied: "${filePath}" is outside the allowed sandbox paths`);
|
|
9734
9810
|
}
|
|
9735
|
-
function
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9811
|
+
function isHiddenHostPath(resolved) {
|
|
9812
|
+
const { hiddenDirs, hiddenFiles } = resolvedLists();
|
|
9813
|
+
return hiddenFiles.includes(resolved) || hiddenDirs.some((d) => isUnder(resolved, d));
|
|
9814
|
+
}
|
|
9815
|
+
function buildSeatbeltProfile(opts) {
|
|
9816
|
+
const q = (p) => `"${p.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
9817
|
+
const wsRoot = realpathBounded(opts.workspaceRoot);
|
|
9818
|
+
const lists = resolvedLists();
|
|
9819
|
+
const writable = ["/private/tmp", "/private/var/folders", "/dev"];
|
|
9820
|
+
if (opts.accessLevel !== "readonly")
|
|
9821
|
+
writable.unshift(wsRoot, ...lists.writableDirs);
|
|
9822
|
+
const hiddenDirs = [...lists.hiddenDirs, ...WORKSPACE_HIDDEN_DIRS.map((rel) => path25.join(wsRoot, rel))];
|
|
9823
|
+
const hiddenFiles = [...lists.hiddenFiles, ...WORKSPACE_HIDDEN_FILES.map((rel) => path25.join(wsRoot, rel))];
|
|
9824
|
+
return [
|
|
9825
|
+
"(version 1)",
|
|
9826
|
+
"(allow default)",
|
|
9827
|
+
"(deny file-write*)",
|
|
9828
|
+
`(allow file-write* ${writable.map((p) => `(subpath ${q(p)})`).join(" ")})`,
|
|
9829
|
+
`(deny file-read* file-write* ${[
|
|
9830
|
+
...hiddenDirs.map((p) => `(subpath ${q(p)})`),
|
|
9831
|
+
...hiddenFiles.map((p) => `(literal ${q(p)})`)
|
|
9832
|
+
].join(" ")})`
|
|
9833
|
+
].join("\n");
|
|
9834
|
+
}
|
|
9835
|
+
function seatbeltEnv() {
|
|
9836
|
+
const env = {
|
|
9837
|
+
PATH: `${HOME}/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin`,
|
|
9838
|
+
HOME,
|
|
9839
|
+
TERM: "xterm-256color",
|
|
9840
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
9841
|
+
NPM_CONFIG_USERCONFIG: "/dev/null",
|
|
9842
|
+
...gitIdentityEnv()
|
|
9843
|
+
};
|
|
9844
|
+
for (const k of ["TMPDIR", "USER", "LOGNAME", "LANG", "LC_ALL"]) {
|
|
9845
|
+
if (process.env[k])
|
|
9846
|
+
env[k] = process.env[k];
|
|
9744
9847
|
}
|
|
9745
|
-
return
|
|
9848
|
+
return env;
|
|
9746
9849
|
}
|
|
9747
9850
|
function isHiddenWorkspacePath(resolved, wsRoot) {
|
|
9748
9851
|
for (const rel of WORKSPACE_HIDDEN_FILES) {
|
|
@@ -9766,8 +9869,173 @@ function buildWriteScriptArgs(filePath, content) {
|
|
|
9766
9869
|
function buildReaddirScriptArgs(dirPath) {
|
|
9767
9870
|
return ["bash", "-c", 'ls -1ap "$1"', "bash", dirPath];
|
|
9768
9871
|
}
|
|
9872
|
+
function shellSegments(command) {
|
|
9873
|
+
const segments = [];
|
|
9874
|
+
let words = [];
|
|
9875
|
+
let cur = "";
|
|
9876
|
+
let inWord = false;
|
|
9877
|
+
let quote = null;
|
|
9878
|
+
const heredocs = [];
|
|
9879
|
+
const endWord = () => {
|
|
9880
|
+
if (inWord)
|
|
9881
|
+
words.push(cur);
|
|
9882
|
+
cur = "";
|
|
9883
|
+
inWord = false;
|
|
9884
|
+
};
|
|
9885
|
+
const endSegment = () => {
|
|
9886
|
+
endWord();
|
|
9887
|
+
if (words.length)
|
|
9888
|
+
segments.push(words);
|
|
9889
|
+
words = [];
|
|
9890
|
+
};
|
|
9891
|
+
for (let i = 0; i < command.length; i++) {
|
|
9892
|
+
const c = command[i];
|
|
9893
|
+
if (quote) {
|
|
9894
|
+
if (c === quote)
|
|
9895
|
+
quote = null;
|
|
9896
|
+
else if (c === "\\" && quote === '"' && i + 1 < command.length)
|
|
9897
|
+
cur += command[++i];
|
|
9898
|
+
else
|
|
9899
|
+
cur += c;
|
|
9900
|
+
continue;
|
|
9901
|
+
}
|
|
9902
|
+
if (c === "<" && command[i + 1] === "<" && command[i + 2] !== "<") {
|
|
9903
|
+
const m = /^<<(-?)\s*(['"]?)([A-Za-z0-9_.-]+)\2/.exec(command.slice(i));
|
|
9904
|
+
if (m) {
|
|
9905
|
+
endWord();
|
|
9906
|
+
heredocs.push({ delim: m[3], dash: m[1] === "-" });
|
|
9907
|
+
i += m[0].length - 1;
|
|
9908
|
+
continue;
|
|
9909
|
+
}
|
|
9910
|
+
}
|
|
9911
|
+
if (c === "\n" && heredocs.length) {
|
|
9912
|
+
endSegment();
|
|
9913
|
+
let pos = i + 1;
|
|
9914
|
+
for (const h of heredocs.splice(0)) {
|
|
9915
|
+
while (pos < command.length) {
|
|
9916
|
+
const eol = command.indexOf("\n", pos);
|
|
9917
|
+
const line = command.slice(pos, eol === -1 ? command.length : eol);
|
|
9918
|
+
pos = eol === -1 ? command.length : eol + 1;
|
|
9919
|
+
if ((h.dash ? line.replace(/^\t+/, "") : line) === h.delim)
|
|
9920
|
+
break;
|
|
9921
|
+
}
|
|
9922
|
+
}
|
|
9923
|
+
i = pos - 1;
|
|
9924
|
+
continue;
|
|
9925
|
+
}
|
|
9926
|
+
if (c === '"' || c === "'") {
|
|
9927
|
+
quote = c;
|
|
9928
|
+
inWord = true;
|
|
9929
|
+
} else if (c === "\\" && command[i + 1] === "\n")
|
|
9930
|
+
i++;
|
|
9931
|
+
else if (c === "\\" && i + 1 < command.length) {
|
|
9932
|
+
cur += command[++i];
|
|
9933
|
+
inWord = true;
|
|
9934
|
+
} else if (c === ";" || c === "&" || c === "|" || c === "\n")
|
|
9935
|
+
endSegment();
|
|
9936
|
+
else if (c === " " || c === " " || c === "\r")
|
|
9937
|
+
endWord();
|
|
9938
|
+
else {
|
|
9939
|
+
cur += c;
|
|
9940
|
+
inWord = true;
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
9943
|
+
endSegment();
|
|
9944
|
+
return segments;
|
|
9945
|
+
}
|
|
9946
|
+
function expandRmWord(word, cwd) {
|
|
9947
|
+
return word.replace(/^~(?=\/|$)/, HOME).replace(/\$\{HOME\}|\$HOME\b/g, HOME).replace(/\$\{PWD\}|\$PWD\b|\$\(pwd\)|`pwd`/g, cwd).replace(/\$\{[^}]*\}|\$\([^)]*\)|`[^`]*`|\$[A-Za-z_][A-Za-z0-9_]*|\$\d/g, "");
|
|
9948
|
+
}
|
|
9949
|
+
function stripSubshellClose(word) {
|
|
9950
|
+
let w = word;
|
|
9951
|
+
while (w.endsWith(")") && (w.match(/\(/g)?.length ?? 0) < (w.match(/\)/g)?.length ?? 0))
|
|
9952
|
+
w = w.slice(0, -1);
|
|
9953
|
+
return w;
|
|
9954
|
+
}
|
|
9955
|
+
function rmProtectedReason(target, wsRoot) {
|
|
9956
|
+
if (target === "/")
|
|
9957
|
+
return "the filesystem root";
|
|
9958
|
+
if (isUnder(HOME, target))
|
|
9959
|
+
return "the home directory or a parent of it";
|
|
9960
|
+
if (isUnder(HALO_HOME3, target))
|
|
9961
|
+
return "~/.halo or a parent of it";
|
|
9962
|
+
if (isUnder(wsRoot, target))
|
|
9963
|
+
return "the workspace root or a parent of it";
|
|
9964
|
+
if (RM_EXACT_DIRS.includes(target))
|
|
9965
|
+
return "a system directory";
|
|
9966
|
+
for (const d of RM_SYSTEM_DIRS) {
|
|
9967
|
+
if (target === d || path25.dirname(target) === d)
|
|
9968
|
+
return "a system directory";
|
|
9969
|
+
}
|
|
9970
|
+
return null;
|
|
9971
|
+
}
|
|
9972
|
+
function assertRmSafe(command, workspaceRoot) {
|
|
9973
|
+
const wsRoot = path25.resolve(workspaceRoot);
|
|
9974
|
+
let cwd = wsRoot;
|
|
9975
|
+
for (const words of shellSegments(command)) {
|
|
9976
|
+
let i = 0;
|
|
9977
|
+
let cmd = "";
|
|
9978
|
+
for (; i < words.length; i++) {
|
|
9979
|
+
cmd = words[i].replace(/^[({!]+/, "");
|
|
9980
|
+
if (!cmd || /^[A-Za-z_][A-Za-z0-9_]*=/.test(cmd))
|
|
9981
|
+
continue;
|
|
9982
|
+
if (RM_WRAPPERS.has(path25.basename(cmd))) {
|
|
9983
|
+
while (i + 1 < words.length && words[i + 1].startsWith("-"))
|
|
9984
|
+
i++;
|
|
9985
|
+
continue;
|
|
9986
|
+
}
|
|
9987
|
+
break;
|
|
9988
|
+
}
|
|
9989
|
+
if (i >= words.length)
|
|
9990
|
+
continue;
|
|
9991
|
+
if (cmd === "cd") {
|
|
9992
|
+
const dest = words[i + 1];
|
|
9993
|
+
if (dest === void 0)
|
|
9994
|
+
cwd = HOME;
|
|
9995
|
+
else if (dest !== "-")
|
|
9996
|
+
cwd = path25.resolve(cwd, expandRmWord(stripSubshellClose(dest), cwd) || ".");
|
|
9997
|
+
continue;
|
|
9998
|
+
}
|
|
9999
|
+
const base = path25.basename(cmd);
|
|
10000
|
+
if (base !== "rm" && base !== "rmdir")
|
|
10001
|
+
continue;
|
|
10002
|
+
let optionsDone = false;
|
|
10003
|
+
for (let j = i + 1; j < words.length; j++) {
|
|
10004
|
+
const raw = words[j];
|
|
10005
|
+
if (!optionsDone && raw === "--") {
|
|
10006
|
+
optionsDone = true;
|
|
10007
|
+
continue;
|
|
10008
|
+
}
|
|
10009
|
+
if (!optionsDone && raw.startsWith("-"))
|
|
10010
|
+
continue;
|
|
10011
|
+
if (/^(\d*|&)?[<>]/.test(raw)) {
|
|
10012
|
+
if (/^(\d*|&)?[<>]+&?$/.test(raw))
|
|
10013
|
+
j++;
|
|
10014
|
+
continue;
|
|
10015
|
+
}
|
|
10016
|
+
const word = expandRmWord(stripSubshellClose(raw), cwd);
|
|
10017
|
+
if (!word)
|
|
10018
|
+
continue;
|
|
10019
|
+
const abs = path25.resolve(cwd, word);
|
|
10020
|
+
const parts = abs.split("/");
|
|
10021
|
+
const globAt = parts.findIndex((p) => /[*?[]/.test(p));
|
|
10022
|
+
let target = abs;
|
|
10023
|
+
if (globAt !== -1) {
|
|
10024
|
+
if (!/^[.*?]+$/.test(parts[globAt]))
|
|
10025
|
+
continue;
|
|
10026
|
+
target = parts.slice(0, globAt).join("/") || "/";
|
|
10027
|
+
}
|
|
10028
|
+
const reason = rmProtectedReason(target, wsRoot);
|
|
10029
|
+
if (reason) {
|
|
10030
|
+
throw new Error(`[Sandbox] rm blocked: "${raw}" resolves to ${target}, which is ${reason}. Name the specific files or subdirectories to delete instead.`);
|
|
10031
|
+
}
|
|
10032
|
+
}
|
|
10033
|
+
}
|
|
10034
|
+
}
|
|
9769
10035
|
async function sandboxExec(command, opts) {
|
|
9770
10036
|
opts = normalizeOptsForPlatform(opts);
|
|
10037
|
+
if (process.platform !== "win32")
|
|
10038
|
+
assertRmSafe(command, opts.workspaceRoot);
|
|
9771
10039
|
if (opts.accessLevel === "full") {
|
|
9772
10040
|
if (process.platform === "win32") {
|
|
9773
10041
|
const result = await execAsync(`chcp 65001 >nul 2>&1 & ${command}`, {
|
|
@@ -9789,10 +10057,21 @@ async function sandboxExec(command, opts) {
|
|
|
9789
10057
|
signal: opts.signal
|
|
9790
10058
|
});
|
|
9791
10059
|
}
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
10060
|
+
if (process.platform === "darwin") {
|
|
10061
|
+
if (!await isSeatbeltAvailable())
|
|
10062
|
+
throw new Error(NO_SANDBOX_MSG);
|
|
10063
|
+
return spawnGroupExec(command, {
|
|
10064
|
+
cwd: opts.workspaceRoot,
|
|
10065
|
+
timeout: opts.timeout,
|
|
10066
|
+
maxBuffer: opts.maxBuffer,
|
|
10067
|
+
signal: opts.signal,
|
|
10068
|
+
argv: [SANDBOX_EXEC, "-p", buildSeatbeltProfile(opts), ...buildExecScriptArgs(opts.workspaceRoot, command)],
|
|
10069
|
+
env: seatbeltEnv()
|
|
10070
|
+
});
|
|
9795
10071
|
}
|
|
10072
|
+
const bwrapOk = await isBwrapAvailable();
|
|
10073
|
+
if (!bwrapOk)
|
|
10074
|
+
throw new Error(NO_SANDBOX_MSG);
|
|
9796
10075
|
const bwrapArgs = buildBwrapArgs(opts);
|
|
9797
10076
|
return bwrapExec([...bwrapArgs, "--", ...buildExecScriptArgs(opts.workspaceRoot, command)], {
|
|
9798
10077
|
timeout: opts.timeout,
|
|
@@ -9959,7 +10238,7 @@ async function sandboxReaddir(dirPath, opts) {
|
|
|
9959
10238
|
return { name: isDir ? name.slice(0, -1) : name, isDirectory: isDir };
|
|
9960
10239
|
});
|
|
9961
10240
|
}
|
|
9962
|
-
var execAsync, execFileAsync, HOME, HALO_HOME3,
|
|
10241
|
+
var execAsync, execFileAsync, HOME, HALO_HOME3, KILL_GRACE_MS, _bwrapAvailable, SANDBOX_EXEC, _seatbeltAvailable, _gitIdentity, EMPTY_MASK_FILE, DEFAULT_HIDDEN_DIRS, DEFAULT_HIDDEN_FILES, WORKSPACE_HIDDEN_DIRS, WORKSPACE_HIDDEN_FILES, _hiddenDirs, _hiddenFiles, _writableDirs, _resolvedLists, RM_SYSTEM_DIRS, RM_EXACT_DIRS, RM_WRAPPERS, NO_SANDBOX_MSG;
|
|
9963
10242
|
var init_sandbox = __esm({
|
|
9964
10243
|
"../server/dist/tools/sandbox.js"() {
|
|
9965
10244
|
"use strict";
|
|
@@ -9968,9 +10247,12 @@ var init_sandbox = __esm({
|
|
|
9968
10247
|
execFileAsync = promisify(execFile);
|
|
9969
10248
|
HOME = homedir10();
|
|
9970
10249
|
HALO_HOME3 = path25.join(HOME, ".halo");
|
|
9971
|
-
GLOBAL_DIR = path25.join(HALO_HOME3, "global");
|
|
9972
10250
|
KILL_GRACE_MS = 2e3;
|
|
9973
10251
|
_bwrapAvailable = null;
|
|
10252
|
+
SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
10253
|
+
_seatbeltAvailable = null;
|
|
10254
|
+
_gitIdentity = { name: "", email: "" };
|
|
10255
|
+
EMPTY_MASK_FILE = path25.join(HALO_HOME3, ".sandbox-empty");
|
|
9974
10256
|
DEFAULT_HIDDEN_DIRS = [
|
|
9975
10257
|
"~/.halo/secrets",
|
|
9976
10258
|
"~/.aws",
|
|
@@ -10011,6 +10293,38 @@ var init_sandbox = __esm({
|
|
|
10011
10293
|
_hiddenDirs = DEFAULT_HIDDEN_DIRS;
|
|
10012
10294
|
_hiddenFiles = DEFAULT_HIDDEN_FILES;
|
|
10013
10295
|
_writableDirs = [];
|
|
10296
|
+
_resolvedLists = null;
|
|
10297
|
+
RM_SYSTEM_DIRS = [
|
|
10298
|
+
"/bin",
|
|
10299
|
+
"/boot",
|
|
10300
|
+
"/dev",
|
|
10301
|
+
"/etc",
|
|
10302
|
+
"/home",
|
|
10303
|
+
"/lib",
|
|
10304
|
+
"/lib32",
|
|
10305
|
+
"/lib64",
|
|
10306
|
+
"/libx32",
|
|
10307
|
+
"/media",
|
|
10308
|
+
"/mnt",
|
|
10309
|
+
"/opt",
|
|
10310
|
+
"/proc",
|
|
10311
|
+
"/run",
|
|
10312
|
+
"/sbin",
|
|
10313
|
+
"/snap",
|
|
10314
|
+
"/srv",
|
|
10315
|
+
"/sys",
|
|
10316
|
+
"/usr",
|
|
10317
|
+
"/var",
|
|
10318
|
+
"/Applications",
|
|
10319
|
+
"/Library",
|
|
10320
|
+
"/System",
|
|
10321
|
+
"/Users",
|
|
10322
|
+
"/Volumes",
|
|
10323
|
+
"/private"
|
|
10324
|
+
];
|
|
10325
|
+
RM_EXACT_DIRS = ["/tmp", "/private/tmp", "/root"];
|
|
10326
|
+
RM_WRAPPERS = /* @__PURE__ */ new Set(["sudo", "command", "env", "xargs", "nohup", "time", "exec", "nice", "if", "then", "else", "elif", "do", "while", "until"]);
|
|
10327
|
+
NO_SANDBOX_MSG = "Access denied: shell_exec at workspace/readonly access needs an OS sandbox (bubblewrap on Linux, sandbox-exec on macOS) and none works on this host. Switch the session to Full, or on Linux install bubblewrap (apt install bubblewrap).";
|
|
10014
10328
|
}
|
|
10015
10329
|
});
|
|
10016
10330
|
|
|
@@ -10530,10 +10844,13 @@ Error: old_string appears more than once in ${input.path}. Either expand it with
|
|
|
10530
10844
|
STDERR: ${stderr}` : "")).trim() || "(no output)");
|
|
10531
10845
|
} catch (err) {
|
|
10532
10846
|
const error = err;
|
|
10533
|
-
|
|
10534
|
-
Command failed: ${error.message}
|
|
10847
|
+
const out = `${error.message}
|
|
10535
10848
|
${error.stdout ?? ""}
|
|
10536
|
-
${error.stderr ?? ""}
|
|
10849
|
+
${error.stderr ?? ""}`;
|
|
10850
|
+
const hint = accessLevel !== "full" && SANDBOX_DENIED_RE.test(out) ? `
|
|
10851
|
+
[Sandbox] This session runs at "${accessLevel}" access: writes are limited to the workspace${accessLevel === "readonly" ? " (readonly: nowhere)" : ""} and /tmp is discarded after each command. If the command needs more, ask the user to switch the access level to Full in the chat input box.` : "";
|
|
10852
|
+
return mask(`${TOOL_ERROR_MARKER}
|
|
10853
|
+
Command failed: ${out}${hint}`);
|
|
10537
10854
|
}
|
|
10538
10855
|
}
|
|
10539
10856
|
};
|
|
@@ -10698,7 +11015,7 @@ Error: ${error.message}`;
|
|
|
10698
11015
|
};
|
|
10699
11016
|
const visionTools = supportsVision ? [viewImage] : [];
|
|
10700
11017
|
const allTools = [fileRead, ...visionTools, fileWrite, fileEdit, fileList, shellExec, grepTool, globTool, webFetch];
|
|
10701
|
-
if (accessLevel === "readonly" &&
|
|
11018
|
+
if (accessLevel === "readonly" && getSandboxBackend() === null) {
|
|
10702
11019
|
return [fileRead, ...visionTools, fileList, grepTool, globTool];
|
|
10703
11020
|
}
|
|
10704
11021
|
return allTools;
|
|
@@ -10731,7 +11048,7 @@ function imageDimensions(buf, mediaType) {
|
|
|
10731
11048
|
}
|
|
10732
11049
|
return null;
|
|
10733
11050
|
}
|
|
10734
|
-
var SKIP_DIRS, SKIP_HALO_SUBDIRS, GLOB_MAX_RESULTS, HOME2, PARAMS_PATTERN, ENV_PATTERN3;
|
|
11051
|
+
var SKIP_DIRS, SKIP_HALO_SUBDIRS, GLOB_MAX_RESULTS, SANDBOX_DENIED_RE, HOME2, PARAMS_PATTERN, ENV_PATTERN3;
|
|
10735
11052
|
var init_workspace_tools = __esm({
|
|
10736
11053
|
"../server/dist/tools/workspace-tools.js"() {
|
|
10737
11054
|
"use strict";
|
|
@@ -10744,6 +11061,7 @@ var init_workspace_tools = __esm({
|
|
|
10744
11061
|
SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", "dist"]);
|
|
10745
11062
|
SKIP_HALO_SUBDIRS = /* @__PURE__ */ new Set(["sessions", "logs", "evo", "tmp", "assets", "canvas"]);
|
|
10746
11063
|
GLOB_MAX_RESULTS = 5e3;
|
|
11064
|
+
SANDBOX_DENIED_RE = /Read-only file system|Operation not permitted|Permission denied|EROFS|EPERM/;
|
|
10747
11065
|
HOME2 = homedir11();
|
|
10748
11066
|
PARAMS_PATTERN = /\{\{\s*([\w-]+)\.params\.([\w-][\w.-]*)\s*\}\}/g;
|
|
10749
11067
|
ENV_PATTERN3 = /<<([A-Z_][A-Z0-9_]*)>>/g;
|
|
@@ -11487,7 +11805,7 @@ async function initObservability() {
|
|
|
11487
11805
|
return true;
|
|
11488
11806
|
}
|
|
11489
11807
|
const { registerSdk: registerSdk2 } = await Promise.resolve().then(() => (init_otel_sdk(), otel_sdk_exports));
|
|
11490
|
-
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.
|
|
11808
|
+
providers = registerSdk2(process.env.OTEL_SERVICE_NAME, "1.4.0");
|
|
11491
11809
|
console.log(`[Observability] OTLP export \u2192 ${process.env.OTEL_EXPORTER_OTLP_ENDPOINT} (service.name=${process.env.OTEL_SERVICE_NAME}, capture_content=${config.observability.captureContent})`);
|
|
11492
11810
|
return true;
|
|
11493
11811
|
}
|
|
@@ -12483,6 +12801,24 @@ var init_session_manager = __esm({
|
|
|
12483
12801
|
dropUIState(sessionId) {
|
|
12484
12802
|
this.uiStore.dropUIState(sessionId);
|
|
12485
12803
|
}
|
|
12804
|
+
/** Another process (a cron `halo cli` child) just wrote this root session's
|
|
12805
|
+
* files. Drop the idle in-memory copies — the tree's AgentSessions (raw
|
|
12806
|
+
* history, loaded by any view via ensureSession and never evicted while
|
|
12807
|
+
* idle) and the root's UIState — WITHOUT saving, so the next view/message
|
|
12808
|
+
* re-reads disk instead of overwriting the other writer's turn with this
|
|
12809
|
+
* process's stale snapshot. Returns false (and drops nothing) while any
|
|
12810
|
+
* turn/compact in the tree is in flight: live state can't be discarded,
|
|
12811
|
+
* and the two writers race — the accepted cost of sharing a session. */
|
|
12812
|
+
forgetExternalWrite(rootId) {
|
|
12813
|
+
if (this.hasActiveWorkInTree(rootId))
|
|
12814
|
+
return false;
|
|
12815
|
+
for (const id of [...this.sessions.keys()]) {
|
|
12816
|
+
if (id === rootId || id.startsWith(`${rootId}>`))
|
|
12817
|
+
this.sessions.delete(id);
|
|
12818
|
+
}
|
|
12819
|
+
this.uiStore.dropUIState(rootId);
|
|
12820
|
+
return true;
|
|
12821
|
+
}
|
|
12486
12822
|
// ── Loop detection ──────────────────────────────────────────────────
|
|
12487
12823
|
static LOOP_EXEMPT_TOOLS = /* @__PURE__ */ new Set([
|
|
12488
12824
|
"session_list",
|
|
@@ -19077,7 +19413,7 @@ function createAgentConfigRoutes() {
|
|
|
19077
19413
|
if (wsIds.has(a.id))
|
|
19078
19414
|
a.overridden = true;
|
|
19079
19415
|
}
|
|
19080
|
-
const disabledSet = projectId ? getDisabledSet(getWorkspaceDb(projectId).db, "agent") : /* @__PURE__ */ new Set();
|
|
19416
|
+
const disabledSet = projectId && hasWorkspaceHalo(projectId) ? getDisabledSet(getWorkspaceDb(projectId).db, "agent") : /* @__PURE__ */ new Set();
|
|
19081
19417
|
const allAgents = [...globalAgents, ...workspaceAgents];
|
|
19082
19418
|
for (const a of allAgents) {
|
|
19083
19419
|
a.disabled = disabledSet.has(`${a.scope}:${a.id}`);
|
|
@@ -19427,6 +19763,7 @@ var init_agent_configs = __esm({
|
|
|
19427
19763
|
init_md_loader();
|
|
19428
19764
|
init_config();
|
|
19429
19765
|
init_db();
|
|
19766
|
+
init_init();
|
|
19430
19767
|
init_workspace_path();
|
|
19431
19768
|
init_mtime_cache();
|
|
19432
19769
|
_cachedTools = null;
|
|
@@ -19451,7 +19788,7 @@ function defaultSkillSettings() {
|
|
|
19451
19788
|
"",
|
|
19452
19789
|
"global.anthropic.claude-sonnet-4-6",
|
|
19453
19790
|
"global.anthropic.claude-haiku-4-5-20251001",
|
|
19454
|
-
"global.anthropic.claude-opus-
|
|
19791
|
+
"global.anthropic.claude-opus-5-5"
|
|
19455
19792
|
]
|
|
19456
19793
|
}
|
|
19457
19794
|
};
|
|
@@ -20498,6 +20835,8 @@ function createSessionRoutes(smRegistry) {
|
|
|
20498
20835
|
return c.json({ error: "session manager not initialized" }, 500);
|
|
20499
20836
|
if (!projectId)
|
|
20500
20837
|
return c.json({ error: "projectId required" }, 400);
|
|
20838
|
+
if (!hasWorkspaceHalo(projectId))
|
|
20839
|
+
return c.json({ sessions: [], nextCursor: null });
|
|
20501
20840
|
const sm = smRegistry.getOrCreate(projectId);
|
|
20502
20841
|
let parentId;
|
|
20503
20842
|
if (parentIdParam === void 0)
|
|
@@ -20647,6 +20986,7 @@ var init_sessions = __esm({
|
|
|
20647
20986
|
init_goal_mode();
|
|
20648
20987
|
init_broadcast();
|
|
20649
20988
|
init_workspace_path();
|
|
20989
|
+
init_init();
|
|
20650
20990
|
msgCounter2 = 0;
|
|
20651
20991
|
}
|
|
20652
20992
|
});
|
|
@@ -23618,7 +23958,8 @@ ${msg.message}` : markers.join("\n");
|
|
|
23618
23958
|
rememberChat(msg);
|
|
23619
23959
|
ackChat(msg);
|
|
23620
23960
|
console.debug(`[WS] Chat: session=${msg.sessionId}, project=${msg.projectId}, agent=${msg.agentId ?? client2.agentId}`);
|
|
23621
|
-
|
|
23961
|
+
const accessLevel = msg.accessLevel === void 0 ? void 0 : msg.accessLevel === "full" || getSandboxBackend() === null ? null : msg.accessLevel;
|
|
23962
|
+
sm.sendUserMessage(sid, msg.message, msg.images, accessLevel).catch((err) => {
|
|
23622
23963
|
console.debug(`[WS] Chat error: ${err instanceof Error ? err.message : String(err)}`);
|
|
23623
23964
|
sendJson(ws, { type: "error", error: err instanceof Error ? err.message : String(err) });
|
|
23624
23965
|
saveSession(client2);
|
|
@@ -23678,7 +24019,7 @@ ${msg.message}` : markers.join("\n");
|
|
|
23678
24019
|
const running = client2.sessionManager.isSessionRunning(client2.sessionId);
|
|
23679
24020
|
const messages3 = state2 ? running ? [...state2.messageLog] : [...createSaveSnapshot(state2)] : [];
|
|
23680
24021
|
const detachedSession = client2.sessionManager.getSessionById(client2.sessionId);
|
|
23681
|
-
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages3, sessionId: msg.sessionId, maxContextTokens: ctxConfig.maxTokens, agentId: detachedSession?.agentId, archiveCount: archiveCountFor(client2, client2.sessionId, detachedSession?.agentId) } });
|
|
24022
|
+
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages3, sessionId: msg.sessionId, maxContextTokens: ctxConfig.maxTokens, agentId: detachedSession?.agentId, archiveCount: archiveCountFor(client2, client2.sessionId, detachedSession?.agentId), accessLevel: detachedSession?.accessLevel ?? null } });
|
|
23682
24023
|
if (state2 && state2.contextTokens > 0) {
|
|
23683
24024
|
sendJson(ws, { type: "chat:usage", contextTokens: state2.contextTokens, outputTokens: state2.outputTokens });
|
|
23684
24025
|
}
|
|
@@ -23722,12 +24063,14 @@ ${msg.message}` : markers.join("\n");
|
|
|
23722
24063
|
}
|
|
23723
24064
|
}
|
|
23724
24065
|
let agentId;
|
|
24066
|
+
let accessLevel;
|
|
23725
24067
|
if (msg.sessionId && client2.sessionManager) {
|
|
23726
24068
|
const existingSession = client2.sessionManager.getSessionById(msg.sessionId);
|
|
23727
24069
|
if (existingSession) {
|
|
23728
24070
|
client2.sessionId = msg.sessionId;
|
|
23729
24071
|
client2.unsubscribeEvents = client2.sessionManager.registerEventListener(msg.sessionId, createEventListener(client2));
|
|
23730
24072
|
agentId = existingSession.agentId;
|
|
24073
|
+
accessLevel = existingSession.accessLevel ?? null;
|
|
23731
24074
|
}
|
|
23732
24075
|
}
|
|
23733
24076
|
let maxContextTokens = config.model.maxContextTokens;
|
|
@@ -23739,7 +24082,7 @@ ${msg.message}` : markers.join("\n");
|
|
|
23739
24082
|
}
|
|
23740
24083
|
const state = getState(client2);
|
|
23741
24084
|
const messages2 = state ? [...createSaveSnapshot(state)] : [];
|
|
23742
|
-
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages2, sessionId: msg.sessionId, maxContextTokens, agentId, archiveCount: msg.sessionId ? archiveCountFor(client2, msg.sessionId, agentId) : 0 } });
|
|
24085
|
+
sendJson(ws, { type: "state:snapshot", snapshot: { recentMessages: messages2, sessionId: msg.sessionId, maxContextTokens, agentId, archiveCount: msg.sessionId ? archiveCountFor(client2, msg.sessionId, agentId) : 0, accessLevel } });
|
|
23743
24086
|
if (state && state.contextTokens > 0) {
|
|
23744
24087
|
sendJson(ws, { type: "chat:usage", contextTokens: state.contextTokens, outputTokens: state.outputTokens });
|
|
23745
24088
|
}
|
|
@@ -23768,6 +24111,7 @@ var CLIENT_SILENCE_LIMIT_MS, detachedSessions;
|
|
|
23768
24111
|
var init_handler2 = __esm({
|
|
23769
24112
|
"../server/dist/ws/handler.js"() {
|
|
23770
24113
|
"use strict";
|
|
24114
|
+
init_sandbox();
|
|
23771
24115
|
init_ui_log_builder();
|
|
23772
24116
|
init_config();
|
|
23773
24117
|
init_watcher_pool();
|
|
@@ -23838,6 +24182,10 @@ var init_cron_db = __esm({
|
|
|
23838
24182
|
* it. NULL = default 3600 (runner.ts CLI_TIMEOUT_SEC). Range 60–21600,
|
|
23839
24183
|
* enforced at the write points (REST routes / skill helper). */
|
|
23840
24184
|
timeoutSec: integer5("timeout_sec"),
|
|
24185
|
+
/** Root session id the cli runs in. NULL = the job's own `cron-<jobId>`
|
|
24186
|
+
* session. When it names an existing session the cli resumes it with that
|
|
24187
|
+
* session's own agent (`agentId` only applies when the session is new). */
|
|
24188
|
+
sessionId: text5("session_id"),
|
|
23841
24189
|
/** JSON array of `{channelType, accountId}` records. Cron output (final
|
|
23842
24190
|
* assistant text) is dispatched to each one. Empty array = log only. */
|
|
23843
24191
|
targets: text5("targets").notNull().default("[]"),
|
|
@@ -23888,6 +24236,7 @@ CREATE TABLE IF NOT EXISTS cron_jobs (
|
|
|
23888
24236
|
run_at INTEGER,
|
|
23889
24237
|
timezone TEXT,
|
|
23890
24238
|
timeout_sec INTEGER,
|
|
24239
|
+
session_id TEXT,
|
|
23891
24240
|
targets TEXT NOT NULL DEFAULT '[]',
|
|
23892
24241
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
23893
24242
|
last_run_status TEXT,
|
|
@@ -23923,7 +24272,9 @@ CREATE INDEX IF NOT EXISTS idx_cron_runs_status ON cron_runs(status);
|
|
|
23923
24272
|
addColumnIfMissing(s, "cron_jobs", "run_at", "INTEGER");
|
|
23924
24273
|
addColumnIfMissing(s, "cron_jobs", "timeout_sec", "INTEGER");
|
|
23925
24274
|
addColumnIfMissing(s, "cron_runs", "pid", "INTEGER");
|
|
23926
|
-
}
|
|
24275
|
+
},
|
|
24276
|
+
// v2: `session_id` — run the job in a chosen root session instead of cron-<jobId>.
|
|
24277
|
+
(s) => addColumnIfMissing(s, "cron_jobs", "session_id", "TEXT")
|
|
23927
24278
|
];
|
|
23928
24279
|
_cronDb = null;
|
|
23929
24280
|
}
|
|
@@ -24048,14 +24399,30 @@ function killTreeHard(rootPid) {
|
|
|
24048
24399
|
}
|
|
24049
24400
|
}
|
|
24050
24401
|
}
|
|
24051
|
-
function isCronCliProcess(pid,
|
|
24402
|
+
function isCronCliProcess(pid, sessionId) {
|
|
24052
24403
|
try {
|
|
24053
24404
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf-8" });
|
|
24054
|
-
return args.includes(
|
|
24405
|
+
return args.trim().split(/\s+/).includes(sessionId);
|
|
24055
24406
|
} catch {
|
|
24056
24407
|
return false;
|
|
24057
24408
|
}
|
|
24058
24409
|
}
|
|
24410
|
+
function effectiveSessionId(job) {
|
|
24411
|
+
return job.sessionId ?? `cron-${job.id}`;
|
|
24412
|
+
}
|
|
24413
|
+
function sessionKeyFor(workspacePath, sessionId) {
|
|
24414
|
+
let ws;
|
|
24415
|
+
try {
|
|
24416
|
+
ws = fs51.realpathSync(workspacePath);
|
|
24417
|
+
} catch {
|
|
24418
|
+
ws = path60.resolve(workspacePath);
|
|
24419
|
+
}
|
|
24420
|
+
return `${ws}
|
|
24421
|
+
${sessionId}`;
|
|
24422
|
+
}
|
|
24423
|
+
function setCronSessionRegistry(r) {
|
|
24424
|
+
_sessionRegistry = r;
|
|
24425
|
+
}
|
|
24059
24426
|
function logsDir() {
|
|
24060
24427
|
return path60.join(homedir23(), ".halo", "global", "logs", "cron");
|
|
24061
24428
|
}
|
|
@@ -24072,14 +24439,24 @@ function sweepOrphanRuns(opts) {
|
|
|
24072
24439
|
const now = Date.now();
|
|
24073
24440
|
for (const row of rows) {
|
|
24074
24441
|
const { id: runId, jobId, pid } = row;
|
|
24442
|
+
const job = db.select().from(cronJobs).where(eq14(cronJobs.id, jobId)).get();
|
|
24443
|
+
const sessionId = effectiveSessionId(job ?? { id: jobId, sessionId: null });
|
|
24444
|
+
const sessionKey = job ? sessionKeyFor(job.workspacePath, sessionId) : null;
|
|
24075
24445
|
_inflight.add(jobId);
|
|
24446
|
+
if (sessionKey)
|
|
24447
|
+
_inflightSessions.add(sessionKey);
|
|
24448
|
+
const release = () => {
|
|
24449
|
+
_inflight.delete(jobId);
|
|
24450
|
+
if (sessionKey)
|
|
24451
|
+
_inflightSessions.delete(sessionKey);
|
|
24452
|
+
};
|
|
24076
24453
|
let needsKill = false;
|
|
24077
24454
|
let disposition;
|
|
24078
24455
|
if (process.platform === "win32") {
|
|
24079
24456
|
disposition = pid == null ? "no pid recorded" : `pid ${pid} not killed (Windows: cannot verify process identity)`;
|
|
24080
24457
|
} else if (pid == null) {
|
|
24081
24458
|
disposition = "no pid recorded";
|
|
24082
|
-
} else if (!isCronCliProcess(pid,
|
|
24459
|
+
} else if (!isCronCliProcess(pid, sessionId)) {
|
|
24083
24460
|
disposition = `pid ${pid} already dead (or pid reused by another process); not killed`;
|
|
24084
24461
|
} else {
|
|
24085
24462
|
needsKill = true;
|
|
@@ -24093,7 +24470,7 @@ function sweepOrphanRuns(opts) {
|
|
|
24093
24470
|
broadcast({ type: "cron:run_changed", jobId, runId, status: "failed" });
|
|
24094
24471
|
console.log(`[Cron] orphan sweep: run ${runId} (job ${jobId}) \u2014 ${disposition}`);
|
|
24095
24472
|
if (!needsKill || pid == null) {
|
|
24096
|
-
|
|
24473
|
+
release();
|
|
24097
24474
|
continue;
|
|
24098
24475
|
}
|
|
24099
24476
|
try {
|
|
@@ -24101,11 +24478,11 @@ function sweepOrphanRuns(opts) {
|
|
|
24101
24478
|
} catch {
|
|
24102
24479
|
}
|
|
24103
24480
|
const timer = setTimeout(() => {
|
|
24104
|
-
if (isCronCliProcess(pid,
|
|
24481
|
+
if (isCronCliProcess(pid, sessionId)) {
|
|
24105
24482
|
console.log(`[Cron] orphan sweep: pid ${pid} (job ${jobId}) survived SIGTERM \u2014 SIGKILL tree`);
|
|
24106
24483
|
killTreeHard(pid);
|
|
24107
24484
|
}
|
|
24108
|
-
|
|
24485
|
+
release();
|
|
24109
24486
|
}, graceMs);
|
|
24110
24487
|
timer.unref();
|
|
24111
24488
|
}
|
|
@@ -24269,7 +24646,11 @@ async function runJob(jobId, triggerKind) {
|
|
|
24269
24646
|
const startedAt = Date.now();
|
|
24270
24647
|
const logPath = logPathFor(runId);
|
|
24271
24648
|
fs51.mkdirSync(logsDir(), { recursive: true });
|
|
24272
|
-
|
|
24649
|
+
const sessionId = effectiveSessionId(job);
|
|
24650
|
+
const sessionKey = sessionKeyFor(job.workspacePath, sessionId);
|
|
24651
|
+
const host = _sessionRegistry?.peek(job.workspacePath);
|
|
24652
|
+
const skipReason = _inflight.has(jobId) ? "previous run still in progress" : _inflightSessions.has(sessionKey) ? `session ${sessionId} busy: another cron run is in progress` : host?.hasActiveWorkInTree(sessionId) ? `session ${sessionId} busy: a turn is running in it` : null;
|
|
24653
|
+
if (skipReason) {
|
|
24273
24654
|
db.insert(cronRuns).values({
|
|
24274
24655
|
id: runId,
|
|
24275
24656
|
jobId,
|
|
@@ -24279,7 +24660,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
24279
24660
|
completedAt: startedAt,
|
|
24280
24661
|
output: null,
|
|
24281
24662
|
exitCode: null,
|
|
24282
|
-
failureReason:
|
|
24663
|
+
failureReason: skipReason,
|
|
24283
24664
|
logPath: null,
|
|
24284
24665
|
dispatchResults: null
|
|
24285
24666
|
}).run();
|
|
@@ -24287,6 +24668,7 @@ async function runJob(jobId, triggerKind) {
|
|
|
24287
24668
|
return runId;
|
|
24288
24669
|
}
|
|
24289
24670
|
_inflight.add(jobId);
|
|
24671
|
+
_inflightSessions.add(sessionKey);
|
|
24290
24672
|
try {
|
|
24291
24673
|
db.insert(cronRuns).values({
|
|
24292
24674
|
id: runId,
|
|
@@ -24314,14 +24696,16 @@ async function runJob(jobId, triggerKind) {
|
|
|
24314
24696
|
return runId;
|
|
24315
24697
|
}
|
|
24316
24698
|
const cli = resolveHaloCli();
|
|
24317
|
-
const sessionId = `cron-${job.id}`;
|
|
24318
24699
|
const args = ["cli", "-a", job.agentId, "-s", sessionId, "-w", job.workspacePath];
|
|
24700
|
+
const accessLevel = readSessionAccessLevel(job.workspacePath, sessionId);
|
|
24701
|
+
if (accessLevel)
|
|
24702
|
+
args.push("--access", accessLevel);
|
|
24319
24703
|
const [spawnBin, spawnArgs] = process.platform === "win32" && cli.endsWith(".cmd") ? ["cmd.exe", ["/c", cli, ...args]] : [cli, args];
|
|
24320
24704
|
const timeoutSec = job.timeoutSec ?? CLI_TIMEOUT_SEC;
|
|
24321
24705
|
const teeStream = fs51.createWriteStream(logPath, { flags: "a" });
|
|
24322
24706
|
teeStream.write(`=== ${(/* @__PURE__ */ new Date()).toISOString()} cron run ${runId} job=${jobId} trigger=${triggerKind} ===
|
|
24323
24707
|
`);
|
|
24324
|
-
teeStream.write(`agent=${job.agentId} workspace=${job.workspacePath} schedule=${job.schedule}
|
|
24708
|
+
teeStream.write(`agent=${job.agentId} session=${sessionId}${accessLevel ? ` access=${accessLevel}` : ""} workspace=${job.workspacePath} schedule=${job.schedule}
|
|
24325
24709
|
`);
|
|
24326
24710
|
teeStream.write(`prompt: ${job.userPrompt}
|
|
24327
24711
|
|
|
@@ -24393,6 +24777,10 @@ ${job.userPrompt}`);
|
|
|
24393
24777
|
resolve2({ exitCode: 1 });
|
|
24394
24778
|
});
|
|
24395
24779
|
});
|
|
24780
|
+
const hostAfter = _sessionRegistry?.peek(job.workspacePath);
|
|
24781
|
+
if (hostAfter && !hostAfter.forgetExternalWrite(sessionId)) {
|
|
24782
|
+
console.warn(`[Cron] ${jobId}: session ${sessionId} had a server-side turn running when the cli exited; one of the two turns may be missing from its history`);
|
|
24783
|
+
}
|
|
24396
24784
|
const trimmedOut = stdout.trim();
|
|
24397
24785
|
let status;
|
|
24398
24786
|
let failureReason = null;
|
|
@@ -24427,6 +24815,17 @@ ${job.userPrompt}`);
|
|
|
24427
24815
|
return runId;
|
|
24428
24816
|
} finally {
|
|
24429
24817
|
_inflight.delete(jobId);
|
|
24818
|
+
_inflightSessions.delete(sessionKey);
|
|
24819
|
+
}
|
|
24820
|
+
}
|
|
24821
|
+
function readSessionAccessLevel(workspacePath, sessionId) {
|
|
24822
|
+
try {
|
|
24823
|
+
const row = getWorkspaceDb(workspacePath).db.select({ accessLevel: agentSessions.accessLevel }).from(agentSessions).where(eq14(agentSessions.id, sessionId)).get();
|
|
24824
|
+
if (!row)
|
|
24825
|
+
return null;
|
|
24826
|
+
return row.accessLevel === "workspace" || row.accessLevel === "readonly" ? row.accessLevel : "full";
|
|
24827
|
+
} catch {
|
|
24828
|
+
return null;
|
|
24430
24829
|
}
|
|
24431
24830
|
}
|
|
24432
24831
|
function finalize(runId, jobId, a) {
|
|
@@ -24508,12 +24907,14 @@ function pruneOldLogs() {
|
|
|
24508
24907
|
const dbCutoff = Date.now() - 2 * LOG_RETENTION_DAYS * DAY_MS2;
|
|
24509
24908
|
db.delete(cronRuns).where(lt5(cronRuns.startedAt, dbCutoff)).run();
|
|
24510
24909
|
}
|
|
24511
|
-
var DAY_MS2, CLI_TIMEOUT_SEC, KILL_GRACE_SEC, UNATTENDED_PREAMBLE, RUNS_PER_JOB_KEEP, LOG_RETENTION_DAYS, _active, _inflight, _fingerprint, _daemonTimers, _lastCronDataVersion;
|
|
24910
|
+
var DAY_MS2, CLI_TIMEOUT_SEC, KILL_GRACE_SEC, UNATTENDED_PREAMBLE, RUNS_PER_JOB_KEEP, LOG_RETENTION_DAYS, _sessionRegistry, _active, _inflight, _inflightSessions, _fingerprint, _daemonTimers, _lastCronDataVersion;
|
|
24512
24911
|
var init_runner = __esm({
|
|
24513
24912
|
"../server/dist/cron/runner.js"() {
|
|
24514
24913
|
"use strict";
|
|
24515
24914
|
init_cron_db();
|
|
24516
24915
|
init_raw_sqlite();
|
|
24916
|
+
init_db();
|
|
24917
|
+
init_schema();
|
|
24517
24918
|
init_dispatcher();
|
|
24518
24919
|
init_broadcast();
|
|
24519
24920
|
init_child_env();
|
|
@@ -24523,8 +24924,10 @@ var init_runner = __esm({
|
|
|
24523
24924
|
UNATTENDED_PREAMBLE = "[Unattended cron run \u2014 nobody will answer questions. Deliver the result directly; if genuinely blocked, state what's blocked in one line and stop.]";
|
|
24524
24925
|
RUNS_PER_JOB_KEEP = 100;
|
|
24525
24926
|
LOG_RETENTION_DAYS = 30;
|
|
24927
|
+
_sessionRegistry = null;
|
|
24526
24928
|
_active = /* @__PURE__ */ new Map();
|
|
24527
24929
|
_inflight = /* @__PURE__ */ new Set();
|
|
24930
|
+
_inflightSessions = /* @__PURE__ */ new Set();
|
|
24528
24931
|
_fingerprint = /* @__PURE__ */ new Map();
|
|
24529
24932
|
_daemonTimers = [];
|
|
24530
24933
|
_lastCronDataVersion = null;
|
|
@@ -24556,6 +24959,19 @@ function validateTimeoutSec(v) {
|
|
|
24556
24959
|
}
|
|
24557
24960
|
return null;
|
|
24558
24961
|
}
|
|
24962
|
+
function parseSessionId(v) {
|
|
24963
|
+
if (v === void 0 || v === null)
|
|
24964
|
+
return { sessionId: null };
|
|
24965
|
+
if (typeof v !== "string")
|
|
24966
|
+
return { error: "sessionId must be a string or null" };
|
|
24967
|
+
const s = v.trim();
|
|
24968
|
+
if (s.length === 0)
|
|
24969
|
+
return { sessionId: null };
|
|
24970
|
+
if (!SESSION_ID_RE.test(s)) {
|
|
24971
|
+
return { error: 'sessionId must be a root session id (letters, digits, _ : - only; no ">" sub-session path)' };
|
|
24972
|
+
}
|
|
24973
|
+
return { sessionId: s };
|
|
24974
|
+
}
|
|
24559
24975
|
function createCronRoutes() {
|
|
24560
24976
|
const router = new Hono14();
|
|
24561
24977
|
router.get("/cron/jobs", async (c) => {
|
|
@@ -24604,6 +25020,9 @@ function createCronRoutes() {
|
|
|
24604
25020
|
if (err)
|
|
24605
25021
|
return c.json({ error: err }, 400);
|
|
24606
25022
|
}
|
|
25023
|
+
const session = parseSessionId(body.sessionId);
|
|
25024
|
+
if ("error" in session)
|
|
25025
|
+
return c.json({ error: session.error }, 400);
|
|
24607
25026
|
const id = newJobId();
|
|
24608
25027
|
const now = Date.now();
|
|
24609
25028
|
getCronDb().insert(cronJobs).values({
|
|
@@ -24616,6 +25035,7 @@ function createCronRoutes() {
|
|
|
24616
25035
|
runAt: hasRunAt ? body.runAt : null,
|
|
24617
25036
|
timezone: body.timezone ?? null,
|
|
24618
25037
|
timeoutSec: body.timeoutSec ?? null,
|
|
25038
|
+
sessionId: session.sessionId,
|
|
24619
25039
|
targets: JSON.stringify(body.targets ?? []),
|
|
24620
25040
|
enabled: body.enabled === false ? 0 : 1,
|
|
24621
25041
|
lastRunStatus: null,
|
|
@@ -24657,6 +25077,9 @@ function createCronRoutes() {
|
|
|
24657
25077
|
if (err)
|
|
24658
25078
|
return c.json({ error: err }, 400);
|
|
24659
25079
|
}
|
|
25080
|
+
const session = parseSessionId(body.sessionId);
|
|
25081
|
+
if ("error" in session)
|
|
25082
|
+
return c.json({ error: session.error }, 400);
|
|
24660
25083
|
const hasSchedule = typeof body.schedule === "string" && body.schedule.trim().length > 0;
|
|
24661
25084
|
const hasRunAt = typeof body.runAt === "number";
|
|
24662
25085
|
if (hasSchedule && hasRunAt) {
|
|
@@ -24686,6 +25109,8 @@ function createCronRoutes() {
|
|
|
24686
25109
|
patch.timezone = body.timezone || null;
|
|
24687
25110
|
if (body.timeoutSec !== void 0)
|
|
24688
25111
|
patch.timeoutSec = body.timeoutSec;
|
|
25112
|
+
if (body.sessionId !== void 0)
|
|
25113
|
+
patch.sessionId = session.sessionId;
|
|
24689
25114
|
if (body.targets !== void 0)
|
|
24690
25115
|
patch.targets = JSON.stringify(body.targets);
|
|
24691
25116
|
if (body.enabled !== void 0)
|
|
@@ -24801,7 +25226,7 @@ function computeNextRun(j) {
|
|
|
24801
25226
|
return null;
|
|
24802
25227
|
}
|
|
24803
25228
|
}
|
|
24804
|
-
var TIMEOUT_SEC_MIN, TIMEOUT_SEC_MAX;
|
|
25229
|
+
var TIMEOUT_SEC_MIN, TIMEOUT_SEC_MAX, SESSION_ID_RE;
|
|
24805
25230
|
var init_cron = __esm({
|
|
24806
25231
|
"../server/dist/routes/cron.js"() {
|
|
24807
25232
|
"use strict";
|
|
@@ -24811,6 +25236,7 @@ var init_cron = __esm({
|
|
|
24811
25236
|
init_broadcast();
|
|
24812
25237
|
TIMEOUT_SEC_MIN = 60;
|
|
24813
25238
|
TIMEOUT_SEC_MAX = 21600;
|
|
25239
|
+
SESSION_ID_RE = /^[A-Za-z0-9_:-]{1,200}$/;
|
|
24814
25240
|
}
|
|
24815
25241
|
});
|
|
24816
25242
|
|
|
@@ -25544,7 +25970,7 @@ function startTelegramChannel(deps) {
|
|
|
25544
25970
|
if (!route)
|
|
25545
25971
|
return;
|
|
25546
25972
|
if (!isMediaPathAllowed(filePath, account.workspacePath)) {
|
|
25547
|
-
console.
|
|
25973
|
+
console.warn(`[Telegram] sendMedia blocked: ${filePath} not under workspace`);
|
|
25548
25974
|
return;
|
|
25549
25975
|
}
|
|
25550
25976
|
const kind = inferMediaKind(filePath);
|
|
@@ -26841,7 +27267,7 @@ function startWechatChannel(deps) {
|
|
|
26841
27267
|
if (!route)
|
|
26842
27268
|
return;
|
|
26843
27269
|
if (!isMediaPathAllowed(filePath, account.workspacePath)) {
|
|
26844
|
-
console.
|
|
27270
|
+
console.warn(`[WeChat] sendMedia blocked: ${filePath} not under workspace`);
|
|
26845
27271
|
return;
|
|
26846
27272
|
}
|
|
26847
27273
|
await sendMediaFile({
|
|
@@ -28347,7 +28773,7 @@ function startSlackChannel(deps) {
|
|
|
28347
28773
|
return;
|
|
28348
28774
|
const resolved = path65.resolve(filePath);
|
|
28349
28775
|
if (!isMediaPathAllowed(resolved, account.workspacePath)) {
|
|
28350
|
-
console.
|
|
28776
|
+
console.warn(`[Slack] sendMedia blocked: ${filePath} not under workspace`);
|
|
28351
28777
|
return;
|
|
28352
28778
|
}
|
|
28353
28779
|
try {
|
|
@@ -29493,7 +29919,7 @@ function startFeishuChannel(deps) {
|
|
|
29493
29919
|
return;
|
|
29494
29920
|
const resolved = path66.resolve(filePath);
|
|
29495
29921
|
if (!isMediaPathAllowed(resolved, account.workspacePath)) {
|
|
29496
|
-
console.
|
|
29922
|
+
console.warn(`[Feishu] sendMedia blocked: ${filePath} not under workspace`);
|
|
29497
29923
|
return;
|
|
29498
29924
|
}
|
|
29499
29925
|
try {
|
|
@@ -30356,7 +30782,7 @@ function startWecomChannel(deps) {
|
|
|
30356
30782
|
return;
|
|
30357
30783
|
const resolved = path67.resolve(filePath);
|
|
30358
30784
|
if (!isMediaPathAllowed(resolved, account.workspacePath)) {
|
|
30359
|
-
console.
|
|
30785
|
+
console.warn(`[WeCom] sendMedia blocked: ${filePath} not under workspace`);
|
|
30360
30786
|
return;
|
|
30361
30787
|
}
|
|
30362
30788
|
try {
|
|
@@ -31166,7 +31592,7 @@ var init_dist2 = __esm({
|
|
|
31166
31592
|
init_git_ssh();
|
|
31167
31593
|
__dirname3 = path68.dirname(fileURLToPath3(import.meta.url));
|
|
31168
31594
|
PORT = config.server.port;
|
|
31169
|
-
HALO_VERSION = "1.
|
|
31595
|
+
HALO_VERSION = "1.4.0";
|
|
31170
31596
|
PROJECT_ROOT = findProjectRoot();
|
|
31171
31597
|
HALO_HOME4 = path68.join(homedir24(), ".halo");
|
|
31172
31598
|
GIT_SHA = readGitSha();
|
|
@@ -31213,7 +31639,7 @@ var init_dist2 = __esm({
|
|
|
31213
31639
|
const { hiddenDirs, hiddenFiles, writableDirs } = reloadSandboxConfig();
|
|
31214
31640
|
setSandboxHiddenPaths(hiddenDirs, hiddenFiles, writableDirs);
|
|
31215
31641
|
});
|
|
31216
|
-
console.log(`[Server]
|
|
31642
|
+
console.log(`[Server] OS sandbox: ${getSandboxBackend() ?? "NOT available (non-full sessions: no shell_exec, file tools path-checked in-process)"}`);
|
|
31217
31643
|
ensureSshAgent();
|
|
31218
31644
|
console.log(`[Server] Halo home: ${HALO_HOME4}`);
|
|
31219
31645
|
console.log("[Server] Services initialized (ModelRuntime)");
|
|
@@ -31242,7 +31668,10 @@ var init_dist2 = __esm({
|
|
|
31242
31668
|
uptime: process.uptime(),
|
|
31243
31669
|
engine: "agent",
|
|
31244
31670
|
version: HALO_VERSION,
|
|
31245
|
-
gitSha: GIT_SHA
|
|
31671
|
+
gitSha: GIT_SHA,
|
|
31672
|
+
// OS sandbox backing non-full access levels; null → admin locks the
|
|
31673
|
+
// access-level selector to Full.
|
|
31674
|
+
sandbox: getSandboxBackend()
|
|
31246
31675
|
});
|
|
31247
31676
|
});
|
|
31248
31677
|
fileRoutes = createFileRoutes();
|
|
@@ -31273,6 +31702,7 @@ var init_dist2 = __esm({
|
|
|
31273
31702
|
}
|
|
31274
31703
|
registry = new SessionManagerRegistry({ reconcileOrphansOnBoot: true });
|
|
31275
31704
|
setRelayRegistry(registry);
|
|
31705
|
+
setCronSessionRegistry(registry);
|
|
31276
31706
|
for (const ws of listRunningWorkspaces()) {
|
|
31277
31707
|
if (!fs62.existsSync(path68.join(ws, ".halo")))
|
|
31278
31708
|
continue;
|
|
@@ -32101,7 +32531,7 @@ import path69 from "node:path";
|
|
|
32101
32531
|
import fs63 from "node:fs";
|
|
32102
32532
|
import { homedir as homedir25 } from "node:os";
|
|
32103
32533
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
32104
|
-
var VERSION = "1.
|
|
32534
|
+
var VERSION = "1.4.0";
|
|
32105
32535
|
var HELP_TOP = `Usage: halo <command> [options]
|
|
32106
32536
|
|
|
32107
32537
|
Commands:
|