@hasna/machines 0.0.45 → 0.0.47
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/README.md +53 -4
- package/dist/agent/index.d.ts +0 -1
- package/dist/agent/index.js +250 -15
- package/dist/agent/runtime.d.ts +0 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +1659 -233
- package/dist/cli-utils.d.ts +0 -1
- package/dist/commands/apps.d.ts +7 -5
- package/dist/commands/backup.d.ts +0 -1
- package/dist/commands/cert.d.ts +0 -1
- package/dist/commands/clipboard-daemon.d.ts +0 -1
- package/dist/commands/clipboard-server.d.ts +0 -1
- package/dist/commands/clipboard.d.ts +0 -1
- package/dist/commands/daemon.d.ts +0 -1
- package/dist/commands/diff.d.ts +0 -1
- package/dist/commands/dns.d.ts +0 -1
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/heal-daemon.d.ts +0 -1
- package/dist/commands/heal.d.ts +0 -1
- package/dist/commands/hosts.d.ts +81 -0
- package/dist/commands/install-claude.d.ts +5 -3
- package/dist/commands/install-tailscale.d.ts +5 -3
- package/dist/commands/manifest.d.ts +0 -1
- package/dist/commands/mutation-approval.d.ts +54 -0
- package/dist/commands/notifications.d.ts +14 -2
- package/dist/commands/ports.d.ts +0 -1
- package/dist/commands/runtime.d.ts +15 -1
- package/dist/commands/screen.d.ts +4 -1
- package/dist/commands/self-test.d.ts +0 -1
- package/dist/commands/serve.d.ts +0 -1
- package/dist/commands/setup.d.ts +5 -3
- package/dist/commands/ssh.d.ts +8 -1
- package/dist/commands/status.d.ts +0 -1
- package/dist/commands/sync.d.ts +5 -3
- package/dist/commands/workspace.d.ts +0 -1
- package/dist/compatibility.d.ts +0 -1
- package/dist/consumer-schema.d.ts +0 -1
- package/dist/consumer.d.ts +0 -1
- package/dist/consumer.js +253 -12
- package/dist/cross-project-types.d.ts +0 -1
- package/dist/db.d.ts +0 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1092 -185
- package/dist/manifests.d.ts +0 -1
- package/dist/mcp/http.d.ts +26 -2
- package/dist/mcp/index.d.ts +0 -1
- package/dist/mcp/index.js +1004 -162
- package/dist/mcp/server.d.ts +5 -3
- package/dist/paths.d.ts +0 -1
- package/dist/pg-migrations.d.ts +0 -1
- package/dist/redaction.d.ts +0 -1
- package/dist/remote-storage.d.ts +0 -1
- package/dist/remote.d.ts +14 -5
- package/dist/storage-sync.d.ts +0 -1
- package/dist/storage.d.ts +0 -1
- package/dist/storage.js +18 -0
- package/dist/topology.d.ts +0 -1
- package/dist/types.d.ts +3 -1
- package/dist/version.d.ts +0 -1
- package/package.json +5 -3
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/runtime.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli-utils.d.ts.map +0 -1
- package/dist/commands/apps.d.ts.map +0 -1
- package/dist/commands/backup.d.ts.map +0 -1
- package/dist/commands/cert.d.ts.map +0 -1
- package/dist/commands/clipboard-daemon.d.ts.map +0 -1
- package/dist/commands/clipboard-server.d.ts.map +0 -1
- package/dist/commands/clipboard.d.ts.map +0 -1
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/diff.d.ts.map +0 -1
- package/dist/commands/dns.d.ts.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/heal-daemon.d.ts.map +0 -1
- package/dist/commands/heal.d.ts.map +0 -1
- package/dist/commands/install-claude.d.ts.map +0 -1
- package/dist/commands/install-tailscale.d.ts.map +0 -1
- package/dist/commands/manifest.d.ts.map +0 -1
- package/dist/commands/notifications.d.ts.map +0 -1
- package/dist/commands/ports.d.ts.map +0 -1
- package/dist/commands/runtime.d.ts.map +0 -1
- package/dist/commands/screen.d.ts.map +0 -1
- package/dist/commands/self-test.d.ts.map +0 -1
- package/dist/commands/serve.d.ts.map +0 -1
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/ssh.d.ts.map +0 -1
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/workspace.d.ts.map +0 -1
- package/dist/compatibility.d.ts.map +0 -1
- package/dist/consumer-schema.d.ts.map +0 -1
- package/dist/consumer.d.ts.map +0 -1
- package/dist/cross-project-types.d.ts.map +0 -1
- package/dist/db.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/manifests.d.ts.map +0 -1
- package/dist/mcp/http.d.ts.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/paths.d.ts.map +0 -1
- package/dist/pg-migrations.d.ts.map +0 -1
- package/dist/redaction.d.ts.map +0 -1
- package/dist/remote-storage.d.ts.map +0 -1
- package/dist/remote.d.ts.map +0 -1
- package/dist/storage-sync.d.ts.map +0 -1
- package/dist/storage.d.ts.map +0 -1
- package/dist/topology.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/version.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -23,6 +23,12 @@ Endpoints on `127.0.0.1` only:
|
|
|
23
23
|
- `GET /health` → `{"status":"ok","name":"machines"}`
|
|
24
24
|
- `POST /mcp` → MCP Streamable HTTP
|
|
25
25
|
|
|
26
|
+
HTTP mode rejects browser requests with untrusted `Origin` headers, caps JSON
|
|
27
|
+
bodies at `MACHINES_HTTP_MAX_BODY_BYTES` (default 1 MiB), and requires either
|
|
28
|
+
`MACHINES_API_KEY` or loopback-only `MACHINES_ALLOW_UNAUTHENTICATED=1`. Use
|
|
29
|
+
`MACHINES_HTTP_ALLOWED_ORIGINS=https://ops.example` for an explicit browser
|
|
30
|
+
origin allowlist.
|
|
31
|
+
|
|
26
32
|
## Manifest
|
|
27
33
|
|
|
28
34
|
`machines.json` is the desired fleet declaration.
|
|
@@ -373,6 +379,7 @@ machines install-tailscale --machine mac-lab-01 --json
|
|
|
373
379
|
|
|
374
380
|
```bash
|
|
375
381
|
machines notifications add --id ops --type webhook --target https://example.com/hook --event sync_failed
|
|
382
|
+
machines notifications add --id cmd --type command --target /bin/sh --arg -c --arg 'printf "%s\n" "$HASNA_MACHINES_NOTIFICATION_EVENT"'
|
|
376
383
|
machines notifications list
|
|
377
384
|
machines notifications test --channel ops
|
|
378
385
|
machines notifications test --channel ops --apply --yes
|
|
@@ -381,7 +388,9 @@ machines notifications dispatch --event manual.test --message "hello fleet"
|
|
|
381
388
|
|
|
382
389
|
- `email` channels deliver through local `sendmail` or `mail` when available
|
|
383
390
|
- `webhook` channels deliver JSON via HTTP POST
|
|
384
|
-
- `command` channels execute
|
|
391
|
+
- `command` channels execute an explicit command executable plus optional `--arg`
|
|
392
|
+
values with `HASNA_MACHINES_NOTIFICATION_*` env vars; use `/bin/sh -c ...`
|
|
393
|
+
explicitly if a shell is required
|
|
385
394
|
|
|
386
395
|
## Runtime Events
|
|
387
396
|
|
|
@@ -390,19 +399,57 @@ events without sending keys, killing panes, or changing tmux state.
|
|
|
390
399
|
|
|
391
400
|
```bash
|
|
392
401
|
machines runtime tmux-watch %11 --once --json
|
|
393
|
-
machines runtime tmux-watch session:0.1 --interval-ms 5000
|
|
402
|
+
machines runtime tmux-watch session:0.1 --interval-ms 5000 --approval-token "$TOKEN"
|
|
403
|
+
machines runtime tmux-hook-plan --trusted-local-mutation --json
|
|
404
|
+
machines runtime tmux-hook-plan --approval-token "$TOKEN"
|
|
394
405
|
machines webhooks add https://example.com/hook --id tmux-alerts --type machines.tmux.pane_died
|
|
395
406
|
```
|
|
396
407
|
|
|
397
408
|
When a pane was present and later disappears, the command records
|
|
398
409
|
`machines.tmux.pane_died`. With `--once`, a missing pane records
|
|
399
410
|
`machines.tmux.pane_missing`; add `--no-deliver` to record without webhook
|
|
400
|
-
delivery.
|
|
411
|
+
delivery. Runtime event delivery requires a scoped mutation approval token; local
|
|
412
|
+
no-deliver recording remains available for diagnostics.
|
|
413
|
+
|
|
414
|
+
`machines runtime tmux-hook-plan` prints a native tmux `pane-died` hook command
|
|
415
|
+
for operators that prefer tmux hooks over polling. It is read-only and does not
|
|
416
|
+
install hooks. Pass `--approval-token` when you want the generated hook command
|
|
417
|
+
to be scoped to a short-lived approval token, or pass
|
|
418
|
+
`--trusted-local-mutation` to generate a process-local
|
|
419
|
+
`HASNA_MACHINES_ALLOW_MUTATIONS=1` prefix for local event recording.
|
|
420
|
+
|
|
421
|
+
## Fleet hostnames (`machines hosts`)
|
|
422
|
+
|
|
423
|
+
Make every fleet machine reachable by its bare name from any other machine —
|
|
424
|
+
`curl http://machine001:3000` works the same on every box — without depending on
|
|
425
|
+
Tailscale MagicDNS being configured. `machines hosts` writes a managed block into
|
|
426
|
+
`/etc/hosts` for each machine in the manifest, choosing the best address:
|
|
427
|
+
|
|
428
|
+
1. `metadata.lanAddress` from the manifest, when it is on the local machine's `/24`
|
|
429
|
+
2. the peer's live direct Tailscale LAN endpoint (`CurAddr`) on the local `/24`
|
|
430
|
+
3. the peer's tailnet IP (`100.64.0.0/10`) — always routable, auto-routed over the
|
|
431
|
+
LAN when co-located
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
machines hosts # dry-run plan (default)
|
|
435
|
+
machines hosts plan -j # JSON plan
|
|
436
|
+
machines hosts apply # write /etc/hosts (uses sudo when the file is root-owned)
|
|
437
|
+
machines hosts plan --no-warm # skip discovering LAN endpoints (faster, tailnet IPs)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
By default the command first runs `tailscale ping` against online peers so their
|
|
441
|
+
LAN endpoints become visible and same-LAN machines resolve to their `192.168.x.x`
|
|
442
|
+
address (true LAN-direct) instead of the tailnet IP. Off-LAN or offline peers fall
|
|
443
|
+
back to the tailnet IP. The local machine is skipped. The managed block is delimited
|
|
444
|
+
by markers, so re-running `apply` only rewrites that block and leaves the rest of
|
|
445
|
+
`/etc/hosts` untouched.
|
|
401
446
|
|
|
402
447
|
## Dashboard
|
|
403
448
|
|
|
404
449
|
```bash
|
|
405
450
|
machines serve --json
|
|
451
|
+
machines serve --port 7676
|
|
452
|
+
# Explicitly expose beyond loopback only on a trusted network:
|
|
406
453
|
machines serve --host 0.0.0.0 --port 7676
|
|
407
454
|
```
|
|
408
455
|
|
|
@@ -416,13 +463,15 @@ The dashboard exposes:
|
|
|
416
463
|
- `/api/daemon/status` daemon heartbeat rows
|
|
417
464
|
- `/api/manifest` current manifest JSON
|
|
418
465
|
- `/api/notifications` notification channel JSON
|
|
466
|
+
- `/api/webhooks` shared event webhook channel JSON
|
|
467
|
+
- `/api/events` shared event JSON
|
|
419
468
|
- `/api/doctor` doctor report JSON
|
|
420
469
|
- `/api/self-test` smoke-check JSON
|
|
421
470
|
- `/api/apps/status` app inventory JSON
|
|
422
471
|
- `/api/apps/diff` app drift JSON
|
|
423
472
|
- `/api/install-claude/status` CLI inventory JSON
|
|
424
473
|
- `/api/install-claude/diff` CLI drift JSON
|
|
425
|
-
- `/api/notifications/test` POST
|
|
474
|
+
- `/api/events`, `/api/notifications/test`, `/api/webhooks/test` POST mutation routes require scoped dashboard mutation approval tokens
|
|
426
475
|
|
|
427
476
|
## Local development
|
|
428
477
|
|
package/dist/agent/index.d.ts
CHANGED
package/dist/agent/index.js
CHANGED
|
@@ -992,7 +992,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
992
992
|
this._exitCallback = (err) => {
|
|
993
993
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
994
994
|
throw err;
|
|
995
|
-
}
|
|
995
|
+
} else {}
|
|
996
996
|
};
|
|
997
997
|
}
|
|
998
998
|
return this;
|
|
@@ -6999,6 +6999,7 @@ class SqliteAdapter {
|
|
|
6999
6999
|
raw;
|
|
7000
7000
|
constructor(path) {
|
|
7001
7001
|
this.raw = new Database(path);
|
|
7002
|
+
this.raw.exec("PRAGMA busy_timeout = 5000");
|
|
7002
7003
|
}
|
|
7003
7004
|
close() {
|
|
7004
7005
|
this.raw.close();
|
|
@@ -7064,6 +7065,23 @@ function createTables(db) {
|
|
|
7064
7065
|
updated_at TEXT NOT NULL
|
|
7065
7066
|
)
|
|
7066
7067
|
`);
|
|
7068
|
+
db.exec(`
|
|
7069
|
+
CREATE TABLE IF NOT EXISTS mutation_approval_nonces (
|
|
7070
|
+
nonce_sha256 TEXT PRIMARY KEY,
|
|
7071
|
+
token_sha256 TEXT NOT NULL,
|
|
7072
|
+
surface TEXT NOT NULL,
|
|
7073
|
+
operation TEXT NOT NULL,
|
|
7074
|
+
caller_id TEXT NOT NULL,
|
|
7075
|
+
run_id TEXT NOT NULL,
|
|
7076
|
+
transport TEXT NOT NULL,
|
|
7077
|
+
expires_at INTEGER NOT NULL,
|
|
7078
|
+
used_at INTEGER NOT NULL
|
|
7079
|
+
)
|
|
7080
|
+
`);
|
|
7081
|
+
db.exec(`
|
|
7082
|
+
CREATE INDEX IF NOT EXISTS mutation_approval_nonces_expires_at_idx
|
|
7083
|
+
ON mutation_approval_nonces (expires_at)
|
|
7084
|
+
`);
|
|
7067
7085
|
}
|
|
7068
7086
|
function migrateAgentHeartbeats(db) {
|
|
7069
7087
|
const columns = db.query("PRAGMA table_info(agent_heartbeats)").all();
|
|
@@ -11387,7 +11405,9 @@ function readManifestWithSource(options = {}) {
|
|
|
11387
11405
|
|
|
11388
11406
|
// src/remote.ts
|
|
11389
11407
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
11390
|
-
import {
|
|
11408
|
+
import { existsSync as existsSync5, mkdtempSync, readFileSync as readFileSync3, rmSync } from "fs";
|
|
11409
|
+
import { hostname as hostname3, tmpdir } from "os";
|
|
11410
|
+
import { join as join3 } from "path";
|
|
11391
11411
|
|
|
11392
11412
|
// src/topology.ts
|
|
11393
11413
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -11845,6 +11865,16 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
11845
11865
|
function shellQuote(value) {
|
|
11846
11866
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
11847
11867
|
}
|
|
11868
|
+
function validateSshTarget(target) {
|
|
11869
|
+
const trimmed = target.trim();
|
|
11870
|
+
if (!trimmed || trimmed.startsWith("-") || /[\s"'`$\\;&|<>()[\]{}]/.test(trimmed)) {
|
|
11871
|
+
throw new Error(`Unsafe SSH target: ${target}`);
|
|
11872
|
+
}
|
|
11873
|
+
if (!/^(?:[A-Za-z0-9._%+-]+@)?[A-Za-z0-9._:-]+$/.test(trimmed)) {
|
|
11874
|
+
throw new Error(`Unsafe SSH target: ${target}`);
|
|
11875
|
+
}
|
|
11876
|
+
return trimmed;
|
|
11877
|
+
}
|
|
11848
11878
|
function resolveSshTarget(machineId, options = {}) {
|
|
11849
11879
|
const resolved = resolveMachineRoute(machineId, options);
|
|
11850
11880
|
if (!resolved.ok || !resolved.target) {
|
|
@@ -11855,15 +11885,22 @@ function resolveSshTarget(machineId, options = {}) {
|
|
|
11855
11885
|
}
|
|
11856
11886
|
return {
|
|
11857
11887
|
machineId: resolved.machine_id ?? machineId,
|
|
11858
|
-
target: resolved.command_target ?? resolved.target,
|
|
11888
|
+
target: validateSshTarget(resolved.command_target ?? resolved.target),
|
|
11859
11889
|
route: resolved.route,
|
|
11860
11890
|
confidence: resolved.confidence,
|
|
11861
11891
|
warnings: resolved.warnings
|
|
11862
11892
|
};
|
|
11863
11893
|
}
|
|
11864
|
-
function
|
|
11894
|
+
function buildSshCommandPlan(machineId, remoteCommand, options = {}) {
|
|
11865
11895
|
const resolved = resolveSshTarget(machineId, options);
|
|
11866
|
-
|
|
11896
|
+
const args = remoteCommand ? [resolved.target, remoteCommand] : [resolved.target];
|
|
11897
|
+
const shellCommand = `ssh ${args.map(shellQuote).join(" ")}`;
|
|
11898
|
+
return {
|
|
11899
|
+
...resolved,
|
|
11900
|
+
command: "ssh",
|
|
11901
|
+
args,
|
|
11902
|
+
shellCommand
|
|
11903
|
+
};
|
|
11867
11904
|
}
|
|
11868
11905
|
|
|
11869
11906
|
// src/remote.ts
|
|
@@ -11875,35 +11912,233 @@ function machineIsLocal(machineId, localMachineId) {
|
|
|
11875
11912
|
}
|
|
11876
11913
|
function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
|
|
11877
11914
|
if (machineIsLocal(machineId, localMachineId)) {
|
|
11878
|
-
return { source: "local", shellCommand: command };
|
|
11915
|
+
return { source: "local", command: "bash", args: ["-c", command], shellCommand: command, usesShell: true };
|
|
11879
11916
|
}
|
|
11880
11917
|
try {
|
|
11918
|
+
const plan = buildSshCommandPlan(machineId, command);
|
|
11881
11919
|
return {
|
|
11882
|
-
source:
|
|
11883
|
-
|
|
11920
|
+
source: plan.route,
|
|
11921
|
+
command: plan.command,
|
|
11922
|
+
args: plan.args,
|
|
11923
|
+
shellCommand: plan.shellCommand,
|
|
11924
|
+
usesShell: false
|
|
11884
11925
|
};
|
|
11885
11926
|
} catch (error) {
|
|
11886
11927
|
const message = String(error.message ?? error);
|
|
11887
11928
|
if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
|
|
11888
|
-
|
|
11929
|
+
const target = validateSshTarget(machineId);
|
|
11930
|
+
return {
|
|
11931
|
+
source: "ssh",
|
|
11932
|
+
command: "ssh",
|
|
11933
|
+
args: [target, command],
|
|
11934
|
+
shellCommand: `ssh ${shellQuote2(target)} ${shellQuote2(command)}`,
|
|
11935
|
+
usesShell: false
|
|
11936
|
+
};
|
|
11889
11937
|
}
|
|
11890
11938
|
throw error;
|
|
11891
11939
|
}
|
|
11892
11940
|
}
|
|
11893
|
-
function runMachineCommand(machineId, command) {
|
|
11941
|
+
function runMachineCommand(machineId, command, options = {}) {
|
|
11894
11942
|
const resolved = resolveMachineCommand(machineId, command);
|
|
11895
|
-
|
|
11943
|
+
if (options.timeoutMs && options.timeoutMs > 0 && process.platform !== "win32") {
|
|
11944
|
+
return runMachineCommandWithProcessGroupTimeout(machineId, resolved, options);
|
|
11945
|
+
}
|
|
11946
|
+
const result = spawnSync2(resolved.command, resolved.args, {
|
|
11896
11947
|
encoding: "utf8",
|
|
11897
|
-
env: process.env
|
|
11948
|
+
env: process.env,
|
|
11949
|
+
timeout: options.timeoutMs,
|
|
11950
|
+
killSignal: "SIGTERM"
|
|
11898
11951
|
});
|
|
11952
|
+
const timedOut = Boolean(result.error && "code" in result.error && result.error.code === "ETIMEDOUT");
|
|
11953
|
+
const timeoutMessage = timedOut ? `Command timed out after ${options.timeoutMs}ms.` : "";
|
|
11954
|
+
const stderr = [result.stderr || "", timeoutMessage].filter(Boolean).join(result.stderr ? `
|
|
11955
|
+
` : "");
|
|
11899
11956
|
return {
|
|
11900
11957
|
machineId,
|
|
11901
11958
|
source: resolved.source,
|
|
11902
11959
|
stdout: result.stdout || "",
|
|
11903
|
-
stderr
|
|
11904
|
-
exitCode: result.status ?? 1
|
|
11905
|
-
|
|
11960
|
+
stderr,
|
|
11961
|
+
exitCode: timedOut ? 124 : result.status ?? 1,
|
|
11962
|
+
timedOut,
|
|
11963
|
+
signal: result.signal
|
|
11964
|
+
};
|
|
11965
|
+
}
|
|
11966
|
+
function runMachineCommandWithProcessGroupTimeout(machineId, resolved, options) {
|
|
11967
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? 1);
|
|
11968
|
+
const killGraceMs = Math.max(1, options.killGraceMs ?? 1000);
|
|
11969
|
+
const helperDir = mkdtempSync(join3(tmpdir(), "machines-timeout-helper-"));
|
|
11970
|
+
const pgidFile = join3(helperDir, "pgid");
|
|
11971
|
+
const helper = spawnSync2(process.execPath, ["--eval", PROCESS_GROUP_TIMEOUT_HELPER], {
|
|
11972
|
+
input: JSON.stringify({ command: resolved.command, args: resolved.args }),
|
|
11973
|
+
encoding: "utf8",
|
|
11974
|
+
env: {
|
|
11975
|
+
...process.env,
|
|
11976
|
+
HASNA_MACHINES_COMMAND_TIMEOUT_MS: String(timeoutMs),
|
|
11977
|
+
HASNA_MACHINES_COMMAND_KILL_GRACE_MS: String(killGraceMs),
|
|
11978
|
+
HASNA_MACHINES_COMMAND_PGID_FILE: pgidFile
|
|
11979
|
+
},
|
|
11980
|
+
timeout: timeoutMs + killGraceMs + 2000,
|
|
11981
|
+
killSignal: "SIGKILL",
|
|
11982
|
+
maxBuffer: 64 * 1024 * 1024
|
|
11983
|
+
});
|
|
11984
|
+
try {
|
|
11985
|
+
const parsed = parseHelperResult(helper.stdout);
|
|
11986
|
+
if (parsed) {
|
|
11987
|
+
return {
|
|
11988
|
+
machineId,
|
|
11989
|
+
source: resolved.source,
|
|
11990
|
+
stdout: parsed.stdout,
|
|
11991
|
+
stderr: parsed.stderr,
|
|
11992
|
+
exitCode: parsed.exitCode,
|
|
11993
|
+
timedOut: parsed.timedOut,
|
|
11994
|
+
signal: parsed.signal
|
|
11995
|
+
};
|
|
11996
|
+
}
|
|
11997
|
+
const helperTimedOut = Boolean(helper.error && "code" in helper.error && helper.error.code === "ETIMEDOUT");
|
|
11998
|
+
if (helperTimedOut)
|
|
11999
|
+
killPublishedProcessGroup(pgidFile);
|
|
12000
|
+
const timeoutMessage = helperTimedOut ? `Command timed out after ${timeoutMs}ms; timeout helper exceeded cleanup grace ${killGraceMs}ms.` : "";
|
|
12001
|
+
const stderr = [helper.stderr || "", timeoutMessage].filter(Boolean).join(helper.stderr ? `
|
|
12002
|
+
` : "");
|
|
12003
|
+
return {
|
|
12004
|
+
machineId,
|
|
12005
|
+
source: resolved.source,
|
|
12006
|
+
stdout: "",
|
|
12007
|
+
stderr,
|
|
12008
|
+
exitCode: helperTimedOut ? 124 : helper.status ?? 1,
|
|
12009
|
+
timedOut: helperTimedOut,
|
|
12010
|
+
signal: helper.signal
|
|
12011
|
+
};
|
|
12012
|
+
} finally {
|
|
12013
|
+
rmSync(helperDir, { recursive: true, force: true });
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
function killPublishedProcessGroup(pgidFile) {
|
|
12017
|
+
if (!existsSync5(pgidFile))
|
|
12018
|
+
return;
|
|
12019
|
+
try {
|
|
12020
|
+
const pid = Number.parseInt(readFileSync3(pgidFile, "utf8").trim(), 10);
|
|
12021
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
12022
|
+
return;
|
|
12023
|
+
process.kill(-pid, "SIGKILL");
|
|
12024
|
+
} catch {}
|
|
11906
12025
|
}
|
|
12026
|
+
function parseHelperResult(stdout) {
|
|
12027
|
+
if (!stdout)
|
|
12028
|
+
return null;
|
|
12029
|
+
try {
|
|
12030
|
+
const parsed = JSON.parse(stdout);
|
|
12031
|
+
if (typeof parsed.stdout !== "string" || typeof parsed.stderr !== "string" || typeof parsed.exitCode !== "number")
|
|
12032
|
+
return null;
|
|
12033
|
+
return {
|
|
12034
|
+
machineId: "",
|
|
12035
|
+
source: "local",
|
|
12036
|
+
stdout: parsed.stdout,
|
|
12037
|
+
stderr: parsed.stderr,
|
|
12038
|
+
exitCode: parsed.exitCode,
|
|
12039
|
+
timedOut: parsed.timedOut === true,
|
|
12040
|
+
signal: typeof parsed.signal === "string" ? parsed.signal : null
|
|
12041
|
+
};
|
|
12042
|
+
} catch {
|
|
12043
|
+
return null;
|
|
12044
|
+
}
|
|
12045
|
+
}
|
|
12046
|
+
var PROCESS_GROUP_TIMEOUT_HELPER = `
|
|
12047
|
+
const { spawn } = require("node:child_process");
|
|
12048
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
12049
|
+
|
|
12050
|
+
const plan = JSON.parse(readFileSync(0, "utf8"));
|
|
12051
|
+
const command = String(plan.command || "");
|
|
12052
|
+
const args = Array.isArray(plan.args) ? plan.args.map(String) : [];
|
|
12053
|
+
const timeoutMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_TIMEOUT_MS || "1", 10));
|
|
12054
|
+
const killGraceMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_KILL_GRACE_MS || "1000", 10));
|
|
12055
|
+
const pgidFile = process.env.HASNA_MACHINES_COMMAND_PGID_FILE || "";
|
|
12056
|
+
let stdout = "";
|
|
12057
|
+
let stderr = "";
|
|
12058
|
+
let timedOut = false;
|
|
12059
|
+
let finished = false;
|
|
12060
|
+
let timeoutTimer;
|
|
12061
|
+
let killTimer;
|
|
12062
|
+
let sigkillSent = false;
|
|
12063
|
+
let pendingExit = null;
|
|
12064
|
+
|
|
12065
|
+
const child = spawn(command, args, {
|
|
12066
|
+
detached: true,
|
|
12067
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
12068
|
+
env: process.env,
|
|
12069
|
+
});
|
|
12070
|
+
|
|
12071
|
+
if (pgidFile && child.pid) {
|
|
12072
|
+
try {
|
|
12073
|
+
writeFileSync(pgidFile, String(child.pid), { mode: 0o600 });
|
|
12074
|
+
} catch {}
|
|
12075
|
+
}
|
|
12076
|
+
|
|
12077
|
+
function appendText(target, chunk) {
|
|
12078
|
+
return target + String(chunk);
|
|
12079
|
+
}
|
|
12080
|
+
|
|
12081
|
+
function killTarget(signal) {
|
|
12082
|
+
if (!child.pid) return;
|
|
12083
|
+
if (process.platform === "win32") {
|
|
12084
|
+
try {
|
|
12085
|
+
process.kill(child.pid, signal);
|
|
12086
|
+
} catch {}
|
|
12087
|
+
return;
|
|
12088
|
+
}
|
|
12089
|
+
try {
|
|
12090
|
+
process.kill(-child.pid, signal);
|
|
12091
|
+
} catch {}
|
|
12092
|
+
}
|
|
12093
|
+
|
|
12094
|
+
function finish(code, signal) {
|
|
12095
|
+
if (finished) return;
|
|
12096
|
+
if (timedOut && !sigkillSent) {
|
|
12097
|
+
pendingExit = { code, signal };
|
|
12098
|
+
return;
|
|
12099
|
+
}
|
|
12100
|
+
finished = true;
|
|
12101
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
12102
|
+
if (killTimer) clearTimeout(killTimer);
|
|
12103
|
+
if (timedOut) {
|
|
12104
|
+
stderr = [stderr, "Command timed out after " + timeoutMs + "ms."].filter(Boolean).join(stderr ? "\\n" : "");
|
|
12105
|
+
}
|
|
12106
|
+
const exitCode = timedOut ? 124 : code ?? 1;
|
|
12107
|
+
process.stdout.write(JSON.stringify({
|
|
12108
|
+
stdout,
|
|
12109
|
+
stderr,
|
|
12110
|
+
exitCode,
|
|
12111
|
+
timedOut,
|
|
12112
|
+
signal: signal ?? null,
|
|
12113
|
+
}), () => process.exit(exitCode));
|
|
12114
|
+
}
|
|
12115
|
+
|
|
12116
|
+
child.stdout.setEncoding("utf8");
|
|
12117
|
+
child.stderr.setEncoding("utf8");
|
|
12118
|
+
child.stdout.on("data", (chunk) => { stdout = appendText(stdout, chunk); });
|
|
12119
|
+
child.stderr.on("data", (chunk) => { stderr = appendText(stderr, chunk); });
|
|
12120
|
+
let childExit = { code: null, signal: null };
|
|
12121
|
+
child.on("error", (error) => {
|
|
12122
|
+
stderr = [stderr, error instanceof Error ? error.message : String(error)].filter(Boolean).join(stderr ? "\\n" : "");
|
|
12123
|
+
finish(1, null);
|
|
12124
|
+
});
|
|
12125
|
+
child.on("exit", (code, signal) => {
|
|
12126
|
+
childExit = { code, signal };
|
|
12127
|
+
});
|
|
12128
|
+
child.on("close", (code, signal) => {
|
|
12129
|
+
finish(code ?? childExit.code, signal ?? childExit.signal);
|
|
12130
|
+
});
|
|
12131
|
+
|
|
12132
|
+
timeoutTimer = setTimeout(() => {
|
|
12133
|
+
timedOut = true;
|
|
12134
|
+
killTarget("SIGTERM");
|
|
12135
|
+
killTimer = setTimeout(() => {
|
|
12136
|
+
sigkillSent = true;
|
|
12137
|
+
killTarget("SIGKILL");
|
|
12138
|
+
if (pendingExit) finish(pendingExit.code, pendingExit.signal);
|
|
12139
|
+
}, killGraceMs);
|
|
12140
|
+
}, timeoutMs);
|
|
12141
|
+
`;
|
|
11907
12142
|
|
|
11908
12143
|
// src/commands/doctor.ts
|
|
11909
12144
|
var DOCTOR_OPTIONAL_ADAPTER_DOMAINS = ["secrets", "configs", "monitor", "repos", "mcps", "shield"];
|
package/dist/agent/runtime.d.ts
CHANGED
|
@@ -37,4 +37,3 @@ export declare function writeHeartbeat(status?: "online" | "offline", options?:
|
|
|
37
37
|
export declare function writeHeartbeatTick(status?: "online" | "offline", options?: AgentTickOptions): Promise<AgentRuntimeStatus>;
|
|
38
38
|
export declare function markOffline(options?: AgentRuntimeOptions): AgentRuntimeStatus;
|
|
39
39
|
export declare function getAgentStatus(machineId?: string, options?: AgentStatusOptions): AgentRuntimeStatus[];
|
|
40
|
-
//# sourceMappingURL=runtime.d.ts.map
|
package/dist/cli/index.d.ts
CHANGED