@staix/agent-hub 0.7.9 → 0.7.11
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/CHANGELOG.md +8 -0
- package/README.md +2 -2
- package/docs/operations.md +6 -6
- package/docs/quickstart.md +1 -1
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1 -1
- package/src/adapters/pi.ts +6 -5
- package/src/cli/setup.ts +3 -2
- package/src/hub/project.ts +20 -0
- package/src/local/deny.ts +9 -2
- package/src/local/sandbox.ts +6 -5
- package/src/local/tools.ts +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.7.11
|
|
8
|
+
|
|
9
|
+
- A project whose path contains a backslash works with the local worker and Pi again: Bun 1.3.14's `realpathSync` throws for such paths, so the sandbox profile, the file tools' path guard and the Pi resume check now resolve paths through `realPath`, which then asks the system `realpath`; it returns each name as stored on disk, so another spelling of `.git/config` or `.env` is still refused (#26).
|
|
10
|
+
|
|
11
|
+
## 0.7.10
|
|
12
|
+
|
|
13
|
+
- A `local.deny` entry, or a project path, containing `"` no longer breaks the local worker's sandbox profile (its bash and git tools stopped with a parse error); the deny rules are written as plain SBPL strings, which can hold a quote (#23).
|
|
14
|
+
|
|
7
15
|
## 0.7.9
|
|
8
16
|
|
|
9
17
|
- A committed `.agenthub/config.json` can no longer choose what the hub runs, which files it sends as credentials, where task text goes, or how far the local worker's sandbox reaches: those machine-local fields (launch commands, checks, the legacy MLX binary and model directory, gateway URLs and key files, the memory endpoint, sandbox widening) apply only from a file git confirms nobody committed, such as the new `.agenthub/config.local.json`, and are otherwise ignored with a log line and a doctor row. A tracked file is recognised by identity, which also closes a way past the 0.7.8 completion-check gate: a committed file under a spelling macOS opens as `config.json` but git does not match (such as `ſ` for `s`) counted as uncommitted (#17).
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Cod
|
|
|
4
4
|
hub-owned local-LLM worker collaborate as peers in independent project directories, with
|
|
5
5
|
task-aware model routing (Switchyard) in front of a self-hosted gateway (OmniRoute).
|
|
6
6
|
|
|
7
|
-
Status: 0.7.
|
|
7
|
+
Status: 0.7.11, control protocol 10. Durable delivery records distinguish queued
|
|
8
8
|
work from uncertain execution. The [smoke checklist](docs/smoke.md) records
|
|
9
9
|
verified paths and remaining prerequisites.
|
|
10
10
|
|
|
@@ -28,7 +28,7 @@ cd <your project> && ahub init && ahub up && ahub tail
|
|
|
28
28
|
Or install the same version from GitHub:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
bun add -g github:STAIxBWLB/agent-hub#v0.7.
|
|
31
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.7.11 && ahub setup
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
The installed commands remain `ahub` and `agent-hub`.
|
package/docs/operations.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Operations guide
|
|
2
2
|
|
|
3
|
-
This guide describes ahub 0.7.
|
|
3
|
+
This guide describes ahub 0.7.11 and control protocol 10. Live verification
|
|
4
4
|
results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
|
|
5
5
|
|
|
6
6
|
## Install and start
|
|
@@ -226,8 +226,8 @@ source and carries every recovery fix released up to it. Protocol 8 and older
|
|
|
226
226
|
project directory, without replacing the global CLI first:
|
|
227
227
|
|
|
228
228
|
```bash
|
|
229
|
-
bunx --package @staix/agent-hub@0.7.
|
|
230
|
-
bunx --package @staix/agent-hub@0.7.
|
|
229
|
+
bunx --package @staix/agent-hub@0.7.11 ahub upgrade --to 0.7.11 --dry-run
|
|
230
|
+
bunx --package @staix/agent-hub@0.7.11 ahub upgrade --to 0.7.11 --yes
|
|
231
231
|
```
|
|
232
232
|
|
|
233
233
|
| Running now | Coordinator to use |
|
|
@@ -249,19 +249,19 @@ The coordinator verifies and retains the exact target package, preserves its
|
|
|
249
249
|
own source, and promotes the global CLI only after restored projects pass
|
|
250
250
|
readback.
|
|
251
251
|
|
|
252
|
-
Once the installed CLI is 0.7.
|
|
252
|
+
Once the installed CLI is 0.7.11, review the current project or all registered
|
|
253
253
|
projects first:
|
|
254
254
|
|
|
255
255
|
```bash
|
|
256
256
|
ahub restart --dry-run
|
|
257
|
-
ahub upgrade --to 0.7.
|
|
257
|
+
ahub upgrade --to 0.7.11 --dry-run
|
|
258
258
|
```
|
|
259
259
|
|
|
260
260
|
Apply only after reviewing the plan:
|
|
261
261
|
|
|
262
262
|
```bash
|
|
263
263
|
ahub restart --yes
|
|
264
|
-
ahub upgrade --to 0.7.
|
|
264
|
+
ahub upgrade --to 0.7.11 --yes
|
|
265
265
|
ahub recovery status <operation-id>
|
|
266
266
|
ahub recovery resume <operation-id>
|
|
267
267
|
ahub recovery abort <operation-id>
|
package/docs/quickstart.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.11",
|
|
4
4
|
"description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Young Joon Lee",
|
|
@@ -15727,7 +15727,7 @@ class ControlClient {
|
|
|
15727
15727
|
// package.json
|
|
15728
15728
|
var package_default = {
|
|
15729
15729
|
name: "@staix/agent-hub",
|
|
15730
|
-
version: "0.7.
|
|
15730
|
+
version: "0.7.11",
|
|
15731
15731
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
15732
15732
|
license: "MIT",
|
|
15733
15733
|
type: "module",
|
package/src/adapters/pi.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve, relative } from "node:path";
|
|
4
4
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
5
5
|
import { renderDigest, replyAudience, replyParent, type Envelope, type PeerId } from "../hub/envelope.ts";
|
|
6
6
|
import { BasePeer } from "../hub/peers.ts";
|
|
7
7
|
import { stopOwnedProcess } from "../hub/child-process.ts";
|
|
8
|
+
import { realPath } from "../hub/project.ts";
|
|
8
9
|
|
|
9
10
|
export interface PiModelDescriptor { id: string; name?: string; contextWindow?: number; maxTokens?: number; reasoning?: boolean; }
|
|
10
11
|
export interface PiToolSchema { name: string; description?: string; parameters: Record<string, unknown>; }
|
|
@@ -139,11 +140,11 @@ export class PiPeer extends BasePeer {
|
|
|
139
140
|
const sessions = join(this.opts.stateDir, "pi-sessions");
|
|
140
141
|
mkdirSync(sessions, { recursive: true, mode: 0o700 });
|
|
141
142
|
if (this.opts.sessionFile) {
|
|
142
|
-
const file =
|
|
143
|
-
const rel = relative(
|
|
144
|
-
if (!rel || rel.startsWith("..") || resolve(
|
|
143
|
+
const file = realPath(this.opts.sessionFile);
|
|
144
|
+
const rel = relative(realPath(sessions), file);
|
|
145
|
+
if (!rel || rel.startsWith("..") || resolve(realPath(sessions), rel) !== file) throw new Error("Pi session file is outside the managed project session directory");
|
|
145
146
|
const header = JSON.parse(readFileSync(file, "utf8").split("\n", 1)[0]!);
|
|
146
|
-
if (header.type !== "session" || typeof header.cwd !== "string" ||
|
|
147
|
+
if (header.type !== "session" || typeof header.cwd !== "string" || realPath(header.cwd) !== realPath(this.opts.cwd) || (this.opts.sessionId && header.id !== this.opts.sessionId)) throw new Error("Pi session header does not match the requested project/session");
|
|
147
148
|
}
|
|
148
149
|
const token = randomUUID();
|
|
149
150
|
this.tuiExit = new Promise((resolvePromise) => { this.resolveTuiExit = resolvePromise; });
|
package/src/cli/setup.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFileSync
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { realPath } from "../hub/project.ts";
|
|
3
4
|
import { VERSION } from "../version.ts";
|
|
4
5
|
|
|
5
6
|
export const PLUGIN = "agent-hub@agent-hub";
|
|
@@ -27,7 +28,7 @@ export function parseList<T>(json: string): T[] {
|
|
|
27
28
|
|
|
28
29
|
const real = (p: string) => {
|
|
29
30
|
try {
|
|
30
|
-
return
|
|
31
|
+
return realPath(p);
|
|
31
32
|
} catch {
|
|
32
33
|
return p;
|
|
33
34
|
}
|
package/src/hub/project.ts
CHANGED
|
@@ -17,6 +17,26 @@ export function canonicalPath(path: string): string {
|
|
|
17
17
|
return canonical(path);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* realpathSync, except that Bun 1.3.14 throws ENOENT for an existing path containing a backslash (issue #26). Such a
|
|
22
|
+
* path goes to the system realpath, which handles it and returns every component as stored on disk: guardPath checks
|
|
23
|
+
* names, and on a case-insensitive disk `.GIT/config` or `id_rſa` would otherwise pass for another file. A missing
|
|
24
|
+
* path or a dangling symlink still throws, which guardPath relies on. Other paths take realpathSync's answer unchanged.
|
|
25
|
+
*/
|
|
26
|
+
export function realPath(path: string): string {
|
|
27
|
+
try {
|
|
28
|
+
return realpathSync(path);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!resolve(path).includes("\\")) throw error; // a relative path can sit under a backslash directory too
|
|
31
|
+
// The path as given: resolve() would fold a `..` after a symlink by text. GNU realpath accepts a dangling last
|
|
32
|
+
// component unless told -e; the BSD one on macOS refuses it by default.
|
|
33
|
+
const argv = process.platform === "darwin" ? ["/bin/realpath", "--", path] : ["realpath", "-e", "--", path];
|
|
34
|
+
const r = spawnSync(argv[0]!, argv.slice(1), { encoding: "utf8" });
|
|
35
|
+
if (r.status !== 0 || !r.stdout.endsWith("\n")) throw error;
|
|
36
|
+
return r.stdout.slice(0, -1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
function gitRoot(dir: string): string | undefined {
|
|
21
41
|
const result = spawnSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
22
42
|
if (result.status !== 0) return undefined;
|
package/src/local/deny.ts
CHANGED
|
@@ -17,7 +17,14 @@ export function isDenied(path: string, extra: string[] = []): boolean {
|
|
|
17
17
|
return DENY_SEGMENTS.some((s) => hasSegment(norm, s)) || DENY_NAMES.some((re) => new RegExp(re).test(basename(norm))) || extra.some((e) => e && norm.includes(e));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/** Regex metacharacters only; `sbplString` quotes the finished regex. */
|
|
21
|
+
const sbplEscape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* An SBPL string literal. Only the plain `"..."` form can hold a quote: in the raw `#"..."` form a backslash escapes
|
|
25
|
+
* nothing, so a `"` in a path ended the literal and broke the whole profile (issue #23).
|
|
26
|
+
*/
|
|
27
|
+
export const sbplString = (s: string) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* Seatbelt regex filters equivalent to isDenied. Seatbelt sees absolute paths, so the `local.deny` substrings are
|
|
@@ -32,5 +39,5 @@ export function denyRegexes(root: string, extra: string[] = []): string[] {
|
|
|
32
39
|
...DENY_SEGMENTS.map((s) => `/${sbplEscape(s)}(/|$)`),
|
|
33
40
|
...names,
|
|
34
41
|
...extra.filter(Boolean).map((e) => `^${sbplEscape(root)}/.*${sbplEscape(e)}`),
|
|
35
|
-
].map((re) => `(regex
|
|
42
|
+
].map((re) => `(regex ${sbplString(re)})`);
|
|
36
43
|
}
|
package/src/local/sandbox.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { realPath } from "../hub/project.ts";
|
|
6
|
+
import { denyRegexes, sbplString } from "./deny.ts";
|
|
6
7
|
|
|
7
8
|
const SANDBOX_EXEC = "/usr/bin/sandbox-exec";
|
|
8
9
|
export const OUTPUT_CAP = 20_000;
|
|
9
10
|
|
|
10
11
|
export const sandboxAvailable = () => process.platform === "darwin" && existsSync(SANDBOX_EXEC);
|
|
11
12
|
|
|
12
|
-
const q =
|
|
13
|
+
const q = sbplString;
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Seatbelt profile for everything the local worker executes (bash, git). Path checks cannot scope a shell,
|
|
@@ -30,8 +31,8 @@ function externalGitDirs(root: string): string[] {
|
|
|
30
31
|
export function profile(cwd: string, network: boolean, readAllow: string[] = [], deny: string[] = []): string {
|
|
31
32
|
const home = homedir();
|
|
32
33
|
const inHome = (p: string) => (p.startsWith("~/") ? join(home, p.slice(2)) : p);
|
|
33
|
-
const root =
|
|
34
|
-
const tmp =
|
|
34
|
+
const root = realPath(cwd);
|
|
35
|
+
const tmp = realPath(tmpdir());
|
|
35
36
|
const gitDirs = externalGitDirs(root);
|
|
36
37
|
const creds = [".ssh", ".aws", ".gnupg", ".config/gh", ".config/gcloud", ".kube", ".docker", ".netrc", ".npmrc", ".omniroute", ".claude", ".codex", ".kimi-code", "Library/Keychains"];
|
|
37
38
|
return [
|
package/src/local/tools.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { lstatSync, mkdirSync, readFileSync,
|
|
1
|
+
import { lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { realPath } from "../hub/project.ts";
|
|
3
4
|
import { hasSegment, isDenied } from "./deny.ts";
|
|
4
5
|
import { OUTPUT_CAP, sandboxedExec } from "./sandbox.ts";
|
|
5
6
|
|
|
@@ -31,7 +32,7 @@ const lexists = (p: string) => {
|
|
|
31
32
|
|
|
32
33
|
/** Absolute path inside cwd after resolving symlinks, or an Error. `write` also refuses .git and .agenthub. */
|
|
33
34
|
export function guardPath(ctx: Pick<ToolContext, "cwd" | "deny">, path: string, mode: "read" | "write"): string {
|
|
34
|
-
const root =
|
|
35
|
+
const root = realPath(ctx.cwd);
|
|
35
36
|
const abs = resolve(root, path);
|
|
36
37
|
// Resolve symlinks on the deepest part that exists: a link pointing outside must not pass as an inside path.
|
|
37
38
|
// lstat, not exists: a dangling symlink "does not exist" to existsSync, yet a write through it lands at its target.
|
|
@@ -39,7 +40,7 @@ export function guardPath(ctx: Pick<ToolContext, "cwd" | "deny">, path: string,
|
|
|
39
40
|
while (!lexists(existing)) existing = dirname(existing);
|
|
40
41
|
let realExisting: string;
|
|
41
42
|
try {
|
|
42
|
-
realExisting =
|
|
43
|
+
realExisting = realPath(existing);
|
|
43
44
|
} catch {
|
|
44
45
|
throw new Error(`${path} goes through a dangling symlink`);
|
|
45
46
|
}
|