@hasna/machines 0.0.32 → 0.0.34
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 +48 -47
- package/dist/cli/index.js +154 -1323
- package/dist/commands/heal-daemon.d.ts.map +1 -1
- package/dist/commands/screen.d.ts +2 -1
- package/dist/commands/screen.d.ts.map +1 -1
- package/dist/consumer.js +18 -3
- package/dist/index.js +50 -547
- package/dist/mcp/index.js +103 -604
- package/dist/topology.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11287,6 +11287,19 @@ function manifestHostReachable(target) {
|
|
|
11287
11287
|
return null;
|
|
11288
11288
|
return overrides.has(target);
|
|
11289
11289
|
}
|
|
11290
|
+
function userFromSshAddress(address) {
|
|
11291
|
+
if (!address)
|
|
11292
|
+
return null;
|
|
11293
|
+
const at = address.indexOf("@");
|
|
11294
|
+
if (at <= 0)
|
|
11295
|
+
return null;
|
|
11296
|
+
return address.slice(0, at);
|
|
11297
|
+
}
|
|
11298
|
+
function commandTargetForRoute(target, user) {
|
|
11299
|
+
if (target.kind === "local" || target.target.includes("@") || !user)
|
|
11300
|
+
return target.target;
|
|
11301
|
+
return `${user}@${target.target}`;
|
|
11302
|
+
}
|
|
11290
11303
|
function routeHints(input) {
|
|
11291
11304
|
const hints = [];
|
|
11292
11305
|
if (input.machineId === input.localMachineId) {
|
|
@@ -11337,6 +11350,7 @@ function buildEntry(input) {
|
|
|
11337
11350
|
});
|
|
11338
11351
|
const selectedRoute = selectRouteHint(hints);
|
|
11339
11352
|
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
11353
|
+
const routeUser = userFromSshAddress(manifest?.sshAddress) ?? (typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null);
|
|
11340
11354
|
return {
|
|
11341
11355
|
machine_id: input.machineId,
|
|
11342
11356
|
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
@@ -11357,7 +11371,7 @@ function buildEntry(input) {
|
|
|
11357
11371
|
ssh: {
|
|
11358
11372
|
address: manifest?.sshAddress ?? null,
|
|
11359
11373
|
route,
|
|
11360
|
-
command_target: selectedRoute
|
|
11374
|
+
command_target: selectedRoute ? commandTargetForRoute(selectedRoute, routeUser) : null
|
|
11361
11375
|
},
|
|
11362
11376
|
route_hints: hints,
|
|
11363
11377
|
tags: manifest?.tags ?? [],
|
|
@@ -11596,6 +11610,7 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
11596
11610
|
const local = route === "local" || machine.machine_id === topology.local_machine_id;
|
|
11597
11611
|
const confidence = routeConfidence({ machine, hint: selectedHint, matchedBy });
|
|
11598
11612
|
const ok = Boolean(selectedHint?.target);
|
|
11613
|
+
const commandTarget = selectedHint ? commandTargetForRoute(selectedHint, userFromSshAddress(machine.ssh.address) ?? machine.user) : null;
|
|
11599
11614
|
return {
|
|
11600
11615
|
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
11601
11616
|
package: topology.package,
|
|
@@ -11606,7 +11621,7 @@ function resolveMachineRoute(machineId, options = {}) {
|
|
|
11606
11621
|
route,
|
|
11607
11622
|
source: route,
|
|
11608
11623
|
target: selectedHint?.target ?? null,
|
|
11609
|
-
command_target:
|
|
11624
|
+
command_target: commandTarget,
|
|
11610
11625
|
confidence,
|
|
11611
11626
|
local,
|
|
11612
11627
|
evidence: {
|
|
@@ -12153,7 +12168,7 @@ function resolveSshTarget(machineId, options = {}) {
|
|
|
12153
12168
|
}
|
|
12154
12169
|
return {
|
|
12155
12170
|
machineId: resolved.machine_id ?? machineId,
|
|
12156
|
-
target: resolved.target,
|
|
12171
|
+
target: resolved.command_target ?? resolved.target,
|
|
12157
12172
|
route: resolved.route,
|
|
12158
12173
|
confidence: resolved.confidence,
|
|
12159
12174
|
warnings: resolved.warnings
|
|
@@ -13440,528 +13455,7 @@ function listPorts(machineId) {
|
|
|
13440
13455
|
// src/commands/runtime.ts
|
|
13441
13456
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
13442
13457
|
import { setTimeout as sleep } from "timers/promises";
|
|
13443
|
-
|
|
13444
|
-
// node_modules/@hasna/events/dist/index.js
|
|
13445
|
-
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
13446
|
-
import { existsSync as existsSync7 } from "fs";
|
|
13447
|
-
import { homedir as homedir4 } from "os";
|
|
13448
|
-
import { join as join6 } from "path";
|
|
13449
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
13450
|
-
import { randomUUID } from "crypto";
|
|
13451
|
-
import { spawn } from "child_process";
|
|
13452
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
13453
|
-
function getPathValue(input, path) {
|
|
13454
|
-
return path.split(".").reduce((value, part) => {
|
|
13455
|
-
if (value && typeof value === "object" && part in value) {
|
|
13456
|
-
return value[part];
|
|
13457
|
-
}
|
|
13458
|
-
return;
|
|
13459
|
-
}, input);
|
|
13460
|
-
}
|
|
13461
|
-
function wildcardToRegExp(pattern) {
|
|
13462
|
-
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
13463
|
-
return new RegExp(`^${escaped}$`);
|
|
13464
|
-
}
|
|
13465
|
-
function matchString(value, matcher) {
|
|
13466
|
-
if (matcher === undefined)
|
|
13467
|
-
return true;
|
|
13468
|
-
if (value === undefined)
|
|
13469
|
-
return false;
|
|
13470
|
-
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
13471
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
13472
|
-
}
|
|
13473
|
-
function matchRecord(input, matcher) {
|
|
13474
|
-
if (!matcher)
|
|
13475
|
-
return true;
|
|
13476
|
-
return Object.entries(matcher).every(([path, expected]) => {
|
|
13477
|
-
const actual = getPathValue(input, path);
|
|
13478
|
-
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
13479
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
13480
|
-
}
|
|
13481
|
-
return actual === expected;
|
|
13482
|
-
});
|
|
13483
|
-
}
|
|
13484
|
-
function eventMatchesFilter(event, filter) {
|
|
13485
|
-
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
13486
|
-
}
|
|
13487
|
-
function channelMatchesEvent(channel, event) {
|
|
13488
|
-
if (!channel.enabled)
|
|
13489
|
-
return false;
|
|
13490
|
-
if (!channel.filters || channel.filters.length === 0)
|
|
13491
|
-
return true;
|
|
13492
|
-
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
13493
|
-
}
|
|
13494
|
-
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
13495
|
-
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
13496
|
-
function getEventsDataDir(override) {
|
|
13497
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join6(homedir4(), ".hasna", "events");
|
|
13498
|
-
}
|
|
13499
|
-
|
|
13500
|
-
class JsonEventsStore {
|
|
13501
|
-
dataDir;
|
|
13502
|
-
channelsPath;
|
|
13503
|
-
eventsPath;
|
|
13504
|
-
deliveriesPath;
|
|
13505
|
-
constructor(dataDir = getEventsDataDir()) {
|
|
13506
|
-
this.dataDir = dataDir;
|
|
13507
|
-
this.channelsPath = join6(dataDir, "channels.json");
|
|
13508
|
-
this.eventsPath = join6(dataDir, "events.json");
|
|
13509
|
-
this.deliveriesPath = join6(dataDir, "deliveries.json");
|
|
13510
|
-
}
|
|
13511
|
-
async init() {
|
|
13512
|
-
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
13513
|
-
await chmod(this.dataDir, 448).catch(() => {
|
|
13514
|
-
return;
|
|
13515
|
-
});
|
|
13516
|
-
await this.ensureArrayFile(this.channelsPath);
|
|
13517
|
-
await this.ensureArrayFile(this.eventsPath);
|
|
13518
|
-
await this.ensureArrayFile(this.deliveriesPath);
|
|
13519
|
-
}
|
|
13520
|
-
async addChannel(channel) {
|
|
13521
|
-
await this.init();
|
|
13522
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
13523
|
-
const index = channels.findIndex((item) => item.id === channel.id);
|
|
13524
|
-
if (index >= 0) {
|
|
13525
|
-
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
13526
|
-
} else {
|
|
13527
|
-
channels.push(channel);
|
|
13528
|
-
}
|
|
13529
|
-
await this.writeJson(this.channelsPath, channels);
|
|
13530
|
-
return index >= 0 ? channels[index] : channel;
|
|
13531
|
-
}
|
|
13532
|
-
async listChannels() {
|
|
13533
|
-
await this.init();
|
|
13534
|
-
return this.readJson(this.channelsPath, []);
|
|
13535
|
-
}
|
|
13536
|
-
async getChannel(id) {
|
|
13537
|
-
const channels = await this.listChannels();
|
|
13538
|
-
return channels.find((channel) => channel.id === id);
|
|
13539
|
-
}
|
|
13540
|
-
async removeChannel(id) {
|
|
13541
|
-
await this.init();
|
|
13542
|
-
const channels = await this.readJson(this.channelsPath, []);
|
|
13543
|
-
const next = channels.filter((channel) => channel.id !== id);
|
|
13544
|
-
await this.writeJson(this.channelsPath, next);
|
|
13545
|
-
return next.length !== channels.length;
|
|
13546
|
-
}
|
|
13547
|
-
async appendEvent(event) {
|
|
13548
|
-
await this.init();
|
|
13549
|
-
const events = await this.readJson(this.eventsPath, []);
|
|
13550
|
-
events.push(event);
|
|
13551
|
-
await this.writeJson(this.eventsPath, events);
|
|
13552
|
-
return event;
|
|
13553
|
-
}
|
|
13554
|
-
async listEvents() {
|
|
13555
|
-
await this.init();
|
|
13556
|
-
return this.readJson(this.eventsPath, []);
|
|
13557
|
-
}
|
|
13558
|
-
async findEventByIdentity(identity) {
|
|
13559
|
-
const events = await this.listEvents();
|
|
13560
|
-
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
13561
|
-
}
|
|
13562
|
-
async appendDelivery(result) {
|
|
13563
|
-
await this.init();
|
|
13564
|
-
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
13565
|
-
deliveries.push(result);
|
|
13566
|
-
await this.writeJson(this.deliveriesPath, deliveries);
|
|
13567
|
-
return result;
|
|
13568
|
-
}
|
|
13569
|
-
async listDeliveries() {
|
|
13570
|
-
await this.init();
|
|
13571
|
-
return this.readJson(this.deliveriesPath, []);
|
|
13572
|
-
}
|
|
13573
|
-
async exportData() {
|
|
13574
|
-
return {
|
|
13575
|
-
channels: await this.listChannels(),
|
|
13576
|
-
events: await this.listEvents(),
|
|
13577
|
-
deliveries: await this.listDeliveries()
|
|
13578
|
-
};
|
|
13579
|
-
}
|
|
13580
|
-
async ensureArrayFile(path) {
|
|
13581
|
-
if (!existsSync7(path)) {
|
|
13582
|
-
await writeFile(path, `[]
|
|
13583
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
13584
|
-
}
|
|
13585
|
-
await chmod(path, 384).catch(() => {
|
|
13586
|
-
return;
|
|
13587
|
-
});
|
|
13588
|
-
}
|
|
13589
|
-
async readJson(path, fallback) {
|
|
13590
|
-
try {
|
|
13591
|
-
const raw = await readFile(path, "utf-8");
|
|
13592
|
-
if (!raw.trim())
|
|
13593
|
-
return fallback;
|
|
13594
|
-
return JSON.parse(raw);
|
|
13595
|
-
} catch (error) {
|
|
13596
|
-
if (error.code === "ENOENT")
|
|
13597
|
-
return fallback;
|
|
13598
|
-
throw error;
|
|
13599
|
-
}
|
|
13600
|
-
}
|
|
13601
|
-
async writeJson(path, value) {
|
|
13602
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
13603
|
-
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
13604
|
-
`, { encoding: "utf-8", mode: 384 });
|
|
13605
|
-
await rename(tempPath, path);
|
|
13606
|
-
await chmod(path, 384).catch(() => {
|
|
13607
|
-
return;
|
|
13608
|
-
});
|
|
13609
|
-
}
|
|
13610
|
-
}
|
|
13611
|
-
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
13612
|
-
function buildSignatureBase(timestamp, body) {
|
|
13613
|
-
return `${timestamp}.${body}`;
|
|
13614
|
-
}
|
|
13615
|
-
function signPayload(secret, timestamp, body) {
|
|
13616
|
-
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
13617
|
-
return `sha256=${digest}`;
|
|
13618
|
-
}
|
|
13619
|
-
function now() {
|
|
13620
|
-
return new Date().toISOString();
|
|
13621
|
-
}
|
|
13622
|
-
function truncate(value, max = 4096) {
|
|
13623
|
-
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
13624
|
-
}
|
|
13625
|
-
function buildWebhookRequest(event, channel) {
|
|
13626
|
-
if (!channel.webhook)
|
|
13627
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13628
|
-
const body = JSON.stringify(event);
|
|
13629
|
-
const timestamp = event.time;
|
|
13630
|
-
const headers = {
|
|
13631
|
-
"Content-Type": "application/json",
|
|
13632
|
-
"User-Agent": "@hasna/events",
|
|
13633
|
-
"X-Hasna-Event-Id": event.id,
|
|
13634
|
-
"X-Hasna-Event-Type": event.type,
|
|
13635
|
-
"X-Hasna-Timestamp": timestamp,
|
|
13636
|
-
...channel.webhook.headers
|
|
13637
|
-
};
|
|
13638
|
-
if (channel.webhook.secret) {
|
|
13639
|
-
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
13640
|
-
}
|
|
13641
|
-
return { body, headers };
|
|
13642
|
-
}
|
|
13643
|
-
async function dispatchWebhook2(event, channel, options = {}) {
|
|
13644
|
-
if (!channel.webhook)
|
|
13645
|
-
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
13646
|
-
const startedAt = now();
|
|
13647
|
-
const { body, headers } = buildWebhookRequest(event, channel);
|
|
13648
|
-
const controller = new AbortController;
|
|
13649
|
-
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
13650
|
-
try {
|
|
13651
|
-
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
13652
|
-
method: "POST",
|
|
13653
|
-
headers,
|
|
13654
|
-
body,
|
|
13655
|
-
signal: controller.signal
|
|
13656
|
-
});
|
|
13657
|
-
const responseBody = truncate(await response.text());
|
|
13658
|
-
return {
|
|
13659
|
-
attempt: 1,
|
|
13660
|
-
status: response.ok ? "success" : "failed",
|
|
13661
|
-
startedAt,
|
|
13662
|
-
completedAt: now(),
|
|
13663
|
-
responseStatus: response.status,
|
|
13664
|
-
responseBody,
|
|
13665
|
-
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
13666
|
-
};
|
|
13667
|
-
} catch (error) {
|
|
13668
|
-
return {
|
|
13669
|
-
attempt: 1,
|
|
13670
|
-
status: "failed",
|
|
13671
|
-
startedAt,
|
|
13672
|
-
completedAt: now(),
|
|
13673
|
-
error: error instanceof Error ? error.message : String(error)
|
|
13674
|
-
};
|
|
13675
|
-
} finally {
|
|
13676
|
-
clearTimeout(timeout);
|
|
13677
|
-
}
|
|
13678
|
-
}
|
|
13679
|
-
async function dispatchCommand2(event, channel) {
|
|
13680
|
-
if (!channel.command)
|
|
13681
|
-
throw new Error(`Channel ${channel.id} has no command config`);
|
|
13682
|
-
const startedAt = now();
|
|
13683
|
-
const eventJson = JSON.stringify(event);
|
|
13684
|
-
const env = {
|
|
13685
|
-
...process.env,
|
|
13686
|
-
...channel.command.env,
|
|
13687
|
-
HASNA_CHANNEL_ID: channel.id,
|
|
13688
|
-
HASNA_EVENT_ID: event.id,
|
|
13689
|
-
HASNA_EVENT_TYPE: event.type,
|
|
13690
|
-
HASNA_EVENT_SOURCE: event.source,
|
|
13691
|
-
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
13692
|
-
HASNA_EVENT_SEVERITY: event.severity,
|
|
13693
|
-
HASNA_EVENT_TIME: event.time,
|
|
13694
|
-
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
13695
|
-
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
13696
|
-
HASNA_EVENT_JSON: eventJson
|
|
13697
|
-
};
|
|
13698
|
-
return new Promise((resolve2) => {
|
|
13699
|
-
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
13700
|
-
cwd: channel.command.cwd,
|
|
13701
|
-
env,
|
|
13702
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
13703
|
-
});
|
|
13704
|
-
let stdout = "";
|
|
13705
|
-
let stderr = "";
|
|
13706
|
-
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
13707
|
-
child.stdin.end(eventJson);
|
|
13708
|
-
child.stdout.on("data", (chunk) => {
|
|
13709
|
-
stdout += chunk.toString();
|
|
13710
|
-
});
|
|
13711
|
-
child.stderr.on("data", (chunk) => {
|
|
13712
|
-
stderr += chunk.toString();
|
|
13713
|
-
});
|
|
13714
|
-
child.on("error", (error) => {
|
|
13715
|
-
clearTimeout(timeout);
|
|
13716
|
-
resolve2({
|
|
13717
|
-
attempt: 1,
|
|
13718
|
-
status: "failed",
|
|
13719
|
-
startedAt,
|
|
13720
|
-
completedAt: now(),
|
|
13721
|
-
stdout: truncate(stdout),
|
|
13722
|
-
stderr: truncate(stderr),
|
|
13723
|
-
error: error.message
|
|
13724
|
-
});
|
|
13725
|
-
});
|
|
13726
|
-
child.on("close", (code, signal) => {
|
|
13727
|
-
clearTimeout(timeout);
|
|
13728
|
-
const success = code === 0;
|
|
13729
|
-
resolve2({
|
|
13730
|
-
attempt: 1,
|
|
13731
|
-
status: success ? "success" : "failed",
|
|
13732
|
-
startedAt,
|
|
13733
|
-
completedAt: now(),
|
|
13734
|
-
stdout: truncate(stdout),
|
|
13735
|
-
stderr: truncate(stderr),
|
|
13736
|
-
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
13737
|
-
});
|
|
13738
|
-
});
|
|
13739
|
-
});
|
|
13740
|
-
}
|
|
13741
|
-
async function dispatchChannel2(event, channel, options = {}) {
|
|
13742
|
-
if (channel.transport === "webhook")
|
|
13743
|
-
return dispatchWebhook2(event, channel, options);
|
|
13744
|
-
if (channel.transport === "command")
|
|
13745
|
-
return dispatchCommand2(event, channel);
|
|
13746
|
-
return {
|
|
13747
|
-
attempt: 1,
|
|
13748
|
-
status: "skipped",
|
|
13749
|
-
startedAt: now(),
|
|
13750
|
-
completedAt: now(),
|
|
13751
|
-
error: `Unsupported transport: ${channel.transport}`
|
|
13752
|
-
};
|
|
13753
|
-
}
|
|
13754
|
-
function createDeliveryResult(event, channel, attempts) {
|
|
13755
|
-
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
13756
|
-
return {
|
|
13757
|
-
id: randomUUID(),
|
|
13758
|
-
eventId: event.id,
|
|
13759
|
-
channelId: channel.id,
|
|
13760
|
-
transport: channel.transport,
|
|
13761
|
-
status,
|
|
13762
|
-
attempts,
|
|
13763
|
-
createdAt: attempts[0]?.startedAt ?? now(),
|
|
13764
|
-
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
13765
|
-
};
|
|
13766
|
-
}
|
|
13767
|
-
function createEvent(input) {
|
|
13768
|
-
return {
|
|
13769
|
-
id: input.id ?? randomUUID2(),
|
|
13770
|
-
source: input.source,
|
|
13771
|
-
type: input.type,
|
|
13772
|
-
time: normalizeTime(input.time),
|
|
13773
|
-
subject: input.subject,
|
|
13774
|
-
severity: input.severity ?? "info",
|
|
13775
|
-
data: input.data ?? {},
|
|
13776
|
-
message: input.message,
|
|
13777
|
-
dedupeKey: input.dedupeKey,
|
|
13778
|
-
schemaVersion: input.schemaVersion ?? "1.0",
|
|
13779
|
-
metadata: input.metadata ?? {}
|
|
13780
|
-
};
|
|
13781
|
-
}
|
|
13782
|
-
|
|
13783
|
-
class EventsClient {
|
|
13784
|
-
store;
|
|
13785
|
-
redactors;
|
|
13786
|
-
transportOptions;
|
|
13787
|
-
constructor(options = {}) {
|
|
13788
|
-
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
13789
|
-
this.redactors = options.redactors ?? [];
|
|
13790
|
-
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
13791
|
-
}
|
|
13792
|
-
async addChannel(input) {
|
|
13793
|
-
const timestamp = new Date().toISOString();
|
|
13794
|
-
return this.store.addChannel({
|
|
13795
|
-
...input,
|
|
13796
|
-
createdAt: input.createdAt ?? timestamp,
|
|
13797
|
-
updatedAt: input.updatedAt ?? timestamp
|
|
13798
|
-
});
|
|
13799
|
-
}
|
|
13800
|
-
async listChannels() {
|
|
13801
|
-
return this.store.listChannels();
|
|
13802
|
-
}
|
|
13803
|
-
async removeChannel(id) {
|
|
13804
|
-
return this.store.removeChannel(id);
|
|
13805
|
-
}
|
|
13806
|
-
async emit(input, options = {}) {
|
|
13807
|
-
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
13808
|
-
if (options.dedupe !== false) {
|
|
13809
|
-
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
13810
|
-
if (existing) {
|
|
13811
|
-
return { event: existing, deliveries: [], deduped: true };
|
|
13812
|
-
}
|
|
13813
|
-
}
|
|
13814
|
-
await this.store.appendEvent(event);
|
|
13815
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
13816
|
-
return { event, deliveries, deduped: false };
|
|
13817
|
-
}
|
|
13818
|
-
async listEvents() {
|
|
13819
|
-
return this.store.listEvents();
|
|
13820
|
-
}
|
|
13821
|
-
async listDeliveries() {
|
|
13822
|
-
return this.store.listDeliveries();
|
|
13823
|
-
}
|
|
13824
|
-
async deliver(event) {
|
|
13825
|
-
const channels = await this.store.listChannels();
|
|
13826
|
-
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
13827
|
-
const deliveries = [];
|
|
13828
|
-
for (const channel of selected) {
|
|
13829
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13830
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13831
|
-
await this.store.appendDelivery(result);
|
|
13832
|
-
deliveries.push(result);
|
|
13833
|
-
}
|
|
13834
|
-
return deliveries;
|
|
13835
|
-
}
|
|
13836
|
-
async testChannel(id, input = {}) {
|
|
13837
|
-
const channel = await this.store.getChannel(id);
|
|
13838
|
-
if (!channel)
|
|
13839
|
-
throw new Error(`Channel not found: ${id}`);
|
|
13840
|
-
const event = createEvent({
|
|
13841
|
-
source: input.source ?? "hasna.events",
|
|
13842
|
-
type: input.type ?? "events.test",
|
|
13843
|
-
subject: input.subject ?? id,
|
|
13844
|
-
severity: input.severity ?? "info",
|
|
13845
|
-
data: input.data ?? { test: true },
|
|
13846
|
-
message: input.message ?? "Hasna events test delivery",
|
|
13847
|
-
dedupeKey: input.dedupeKey,
|
|
13848
|
-
schemaVersion: input.schemaVersion,
|
|
13849
|
-
metadata: input.metadata,
|
|
13850
|
-
time: input.time,
|
|
13851
|
-
id: input.id
|
|
13852
|
-
});
|
|
13853
|
-
const eventForChannel = await this.applyRedaction(event, channel);
|
|
13854
|
-
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
13855
|
-
await this.store.appendDelivery(result);
|
|
13856
|
-
return result;
|
|
13857
|
-
}
|
|
13858
|
-
async replay(options = {}) {
|
|
13859
|
-
const events = (await this.store.listEvents()).filter((event) => {
|
|
13860
|
-
if (options.eventId && event.id !== options.eventId)
|
|
13861
|
-
return false;
|
|
13862
|
-
if (options.source && event.source !== options.source)
|
|
13863
|
-
return false;
|
|
13864
|
-
if (options.type && event.type !== options.type)
|
|
13865
|
-
return false;
|
|
13866
|
-
return true;
|
|
13867
|
-
});
|
|
13868
|
-
if (options.dryRun)
|
|
13869
|
-
return { events, deliveries: [] };
|
|
13870
|
-
const deliveries = [];
|
|
13871
|
-
for (const event of events) {
|
|
13872
|
-
deliveries.push(...await this.deliver(event));
|
|
13873
|
-
}
|
|
13874
|
-
return { events, deliveries };
|
|
13875
|
-
}
|
|
13876
|
-
async applyRedaction(event, channel) {
|
|
13877
|
-
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
13878
|
-
for (const redactor of this.redactors) {
|
|
13879
|
-
next = await redactor(next, channel);
|
|
13880
|
-
}
|
|
13881
|
-
return next;
|
|
13882
|
-
}
|
|
13883
|
-
async deliverWithRetry(event, channel) {
|
|
13884
|
-
const policy = normalizeRetryPolicy(channel.retry);
|
|
13885
|
-
const attempts = [];
|
|
13886
|
-
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
13887
|
-
const attempt = await dispatchChannel2(event, channel, this.transportOptions);
|
|
13888
|
-
attempt.attempt = index + 1;
|
|
13889
|
-
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
13890
|
-
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
13891
|
-
}
|
|
13892
|
-
attempts.push(attempt);
|
|
13893
|
-
if (attempt.status !== "failed")
|
|
13894
|
-
break;
|
|
13895
|
-
if (attempt.nextBackoffMs)
|
|
13896
|
-
await Bun.sleep(attempt.nextBackoffMs);
|
|
13897
|
-
}
|
|
13898
|
-
return createDeliveryResult(event, channel, attempts);
|
|
13899
|
-
}
|
|
13900
|
-
}
|
|
13901
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
13902
|
-
if (paths.length === 0)
|
|
13903
|
-
return event;
|
|
13904
|
-
const copy = structuredClone(event);
|
|
13905
|
-
for (const path of paths) {
|
|
13906
|
-
setPath(copy, path, replacement);
|
|
13907
|
-
}
|
|
13908
|
-
return copy;
|
|
13909
|
-
}
|
|
13910
|
-
function sanitizeChannelForOutput(channel) {
|
|
13911
|
-
const copy = structuredClone(channel);
|
|
13912
|
-
if (copy.webhook?.secret)
|
|
13913
|
-
copy.webhook.secret = "[REDACTED]";
|
|
13914
|
-
if (copy.command?.env) {
|
|
13915
|
-
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
13916
|
-
}
|
|
13917
|
-
return copy;
|
|
13918
|
-
}
|
|
13919
|
-
function sanitizeChannelsForOutput(channels) {
|
|
13920
|
-
return channels.map(sanitizeChannelForOutput);
|
|
13921
|
-
}
|
|
13922
|
-
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
13923
|
-
return redactValue(event, replacement);
|
|
13924
|
-
}
|
|
13925
|
-
function shouldRedactKey(key) {
|
|
13926
|
-
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
13927
|
-
}
|
|
13928
|
-
function redactValue(value, replacement) {
|
|
13929
|
-
if (Array.isArray(value))
|
|
13930
|
-
return value.map((item) => redactValue(item, replacement));
|
|
13931
|
-
if (!value || typeof value !== "object")
|
|
13932
|
-
return value;
|
|
13933
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
13934
|
-
key,
|
|
13935
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
13936
|
-
]));
|
|
13937
|
-
}
|
|
13938
|
-
function setPath(input, path, replacement) {
|
|
13939
|
-
const parts = path.split(".");
|
|
13940
|
-
let cursor = input;
|
|
13941
|
-
for (const part of parts.slice(0, -1)) {
|
|
13942
|
-
const next = cursor[part];
|
|
13943
|
-
if (!next || typeof next !== "object")
|
|
13944
|
-
return;
|
|
13945
|
-
cursor = next;
|
|
13946
|
-
}
|
|
13947
|
-
const last = parts.at(-1);
|
|
13948
|
-
if (last && last in cursor)
|
|
13949
|
-
cursor[last] = replacement;
|
|
13950
|
-
}
|
|
13951
|
-
function normalizeTime(value) {
|
|
13952
|
-
if (!value)
|
|
13953
|
-
return new Date().toISOString();
|
|
13954
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
13955
|
-
}
|
|
13956
|
-
function normalizeRetryPolicy(policy) {
|
|
13957
|
-
return {
|
|
13958
|
-
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
13959
|
-
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
13960
|
-
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
13961
|
-
};
|
|
13962
|
-
}
|
|
13963
|
-
|
|
13964
|
-
// src/commands/runtime.ts
|
|
13458
|
+
import { EventsClient } from "@hasna/events";
|
|
13965
13459
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
13966
13460
|
const checkedAt = new Date().toISOString();
|
|
13967
13461
|
const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
@@ -14046,6 +13540,9 @@ async function emitTmuxEvent(client, type, probe, lastPresent, deliver) {
|
|
|
14046
13540
|
};
|
|
14047
13541
|
return client.emit(input, { deliver });
|
|
14048
13542
|
}
|
|
13543
|
+
// src/commands/serve.ts
|
|
13544
|
+
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
13545
|
+
|
|
14049
13546
|
// src/commands/status.ts
|
|
14050
13547
|
function getStatus() {
|
|
14051
13548
|
const manifest = readManifest();
|
|
@@ -14271,7 +13768,7 @@ function jsonError(message, status = 400) {
|
|
|
14271
13768
|
}
|
|
14272
13769
|
function startDashboardServer(options = {}) {
|
|
14273
13770
|
const info = getServeInfo(options);
|
|
14274
|
-
const events = new
|
|
13771
|
+
const events = new EventsClient2;
|
|
14275
13772
|
return Bun.serve({
|
|
14276
13773
|
hostname: info.host,
|
|
14277
13774
|
port: info.port,
|
|
@@ -14425,7 +13922,7 @@ function runSelfTest() {
|
|
|
14425
13922
|
};
|
|
14426
13923
|
}
|
|
14427
13924
|
// src/commands/setup.ts
|
|
14428
|
-
import { homedir as
|
|
13925
|
+
import { homedir as homedir4 } from "os";
|
|
14429
13926
|
function quote3(value) {
|
|
14430
13927
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14431
13928
|
}
|
|
@@ -14498,7 +13995,7 @@ function buildSetupPlan(machineId) {
|
|
|
14498
13995
|
const target = selected || {
|
|
14499
13996
|
id: currentMachineId,
|
|
14500
13997
|
platform: "linux",
|
|
14501
|
-
workspacePath: `${
|
|
13998
|
+
workspacePath: `${homedir4()}/workspace`
|
|
14502
13999
|
};
|
|
14503
14000
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
14504
14001
|
return {
|
|
@@ -14542,7 +14039,8 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
14542
14039
|
return summary;
|
|
14543
14040
|
}
|
|
14544
14041
|
// src/commands/screen.ts
|
|
14545
|
-
var
|
|
14042
|
+
var SCREEN_SECRET_NAMESPACE_ENV = "HASNA_MACHINES_SCREEN_SECRET_NAMESPACE";
|
|
14043
|
+
var DEFAULT_SCREEN_SECRET_NAMESPACE = "machines/screen-sharing";
|
|
14546
14044
|
function shellQuote7(value) {
|
|
14547
14045
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
14548
14046
|
}
|
|
@@ -14566,7 +14064,8 @@ function splitTarget(target) {
|
|
|
14566
14064
|
return [target.slice(0, at), target.slice(at + 1)];
|
|
14567
14065
|
}
|
|
14568
14066
|
function defaultScreenPasswordSecretKey(machineId) {
|
|
14569
|
-
|
|
14067
|
+
const namespace = process.env[SCREEN_SECRET_NAMESPACE_ENV]?.trim() || DEFAULT_SCREEN_SECRET_NAMESPACE;
|
|
14068
|
+
return `${namespace}/screen-${machineId}-vnc-password`;
|
|
14570
14069
|
}
|
|
14571
14070
|
function resolveScreenTarget(machineId, options = {}) {
|
|
14572
14071
|
const resolved = resolveMachineRoute(machineId, options);
|
|
@@ -14663,8 +14162,8 @@ function buildScreenEnableCommand(machineId, options = {}) {
|
|
|
14663
14162
|
};
|
|
14664
14163
|
}
|
|
14665
14164
|
// src/commands/sync.ts
|
|
14666
|
-
import { existsSync as
|
|
14667
|
-
import { homedir as
|
|
14165
|
+
import { existsSync as existsSync7, lstatSync, readFileSync as readFileSync5, symlinkSync, copyFileSync } from "fs";
|
|
14166
|
+
import { homedir as homedir5 } from "os";
|
|
14668
14167
|
function quote4(value) {
|
|
14669
14168
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
14670
14169
|
}
|
|
@@ -14716,8 +14215,8 @@ function detectFileActions(machine) {
|
|
|
14716
14215
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
14717
14216
|
}
|
|
14718
14217
|
return (machine.files || []).map((file, index) => {
|
|
14719
|
-
const sourceExists =
|
|
14720
|
-
const targetExists =
|
|
14218
|
+
const sourceExists = existsSync7(file.source);
|
|
14219
|
+
const targetExists = existsSync7(file.target);
|
|
14721
14220
|
let status = "missing";
|
|
14722
14221
|
if (sourceExists && targetExists) {
|
|
14723
14222
|
if (file.mode === "symlink") {
|
|
@@ -14748,7 +14247,7 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
14748
14247
|
const target = selected || {
|
|
14749
14248
|
id: currentMachineId,
|
|
14750
14249
|
platform: "linux",
|
|
14751
|
-
workspacePath: `${
|
|
14250
|
+
workspacePath: `${homedir5()}/workspace`
|
|
14752
14251
|
};
|
|
14753
14252
|
const actions = [
|
|
14754
14253
|
...detectPackageActions(target, runner),
|
|
@@ -14968,6 +14467,9 @@ function repairWorkspaceManifestMappings(options) {
|
|
|
14968
14467
|
warnings
|
|
14969
14468
|
};
|
|
14970
14469
|
}
|
|
14470
|
+
// src/mcp/server.ts
|
|
14471
|
+
import { EventsClient as EventsClient3, sanitizeChannelForOutput, sanitizeChannelsForOutput as sanitizeChannelsForOutput2 } from "@hasna/events";
|
|
14472
|
+
|
|
14971
14473
|
// node_modules/zod/v4/core/core.js
|
|
14972
14474
|
var NEVER2 = Object.freeze({
|
|
14973
14475
|
status: "aborted"
|
|
@@ -23940,7 +23442,7 @@ function buildServer(version2 = getPackageVersion()) {
|
|
|
23940
23442
|
}
|
|
23941
23443
|
function createMcpServer(version2) {
|
|
23942
23444
|
const server = new McpServer({ name: "machines", version: version2 });
|
|
23943
|
-
const events = new
|
|
23445
|
+
const events = new EventsClient3;
|
|
23944
23446
|
server.tool("machines_status", "Return local machine fleet status paths and machine identity.", {}, async () => ({
|
|
23945
23447
|
content: [{ type: "text", text: JSON.stringify(getStatus(), null, 2) }]
|
|
23946
23448
|
}));
|
|
@@ -24094,7 +23596,7 @@ function createMcpServer(version2) {
|
|
|
24094
23596
|
}));
|
|
24095
23597
|
server.tool("machines_notifications_dispatch", "Dispatch an event to matching notification channels.", { event: exports_external.string().describe("Event name"), message: exports_external.string().describe("Message body"), channel_id: exports_external.string().optional().describe("Limit delivery to one channel") }, async ({ event, message, channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(await dispatchNotificationEvent(event, message, { channelId: channel_id }), null, 2) }] }));
|
|
24096
23598
|
server.tool("machines_notifications_remove", "Remove a notification channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify(removeNotificationChannel(channel_id), null, 2) }] }));
|
|
24097
|
-
server.tool("machines_webhooks_add", "Add or replace a shared
|
|
23599
|
+
server.tool("machines_webhooks_add", "Add or replace a shared event webhook channel.", {
|
|
24098
23600
|
channel_id: exports_external.string().describe("Channel identifier"),
|
|
24099
23601
|
url: exports_external.string().url().describe("Webhook URL"),
|
|
24100
23602
|
event_type: exports_external.string().optional().describe("Optional event type filter, e.g. machines.*"),
|
|
@@ -24102,26 +23604,26 @@ function createMcpServer(version2) {
|
|
|
24102
23604
|
secret: exports_external.string().optional().describe("Optional HMAC secret"),
|
|
24103
23605
|
enabled: exports_external.boolean().optional().describe("Whether the channel is enabled")
|
|
24104
23606
|
}, async ({ channel_id, url, event_type, source, secret, enabled }) => {
|
|
24105
|
-
const
|
|
23607
|
+
const now = new Date().toISOString();
|
|
24106
23608
|
const channel = await events.addChannel({
|
|
24107
23609
|
id: channel_id,
|
|
24108
23610
|
enabled: enabled ?? true,
|
|
24109
23611
|
transport: "webhook",
|
|
24110
23612
|
filters: event_type || source ? [{ type: event_type, source }] : undefined,
|
|
24111
23613
|
webhook: { url, secret },
|
|
24112
|
-
createdAt:
|
|
24113
|
-
updatedAt:
|
|
23614
|
+
createdAt: now,
|
|
23615
|
+
updatedAt: now
|
|
24114
23616
|
});
|
|
24115
23617
|
return { content: [{ type: "text", text: JSON.stringify(sanitizeChannelForOutput(channel), null, 2) }] };
|
|
24116
23618
|
});
|
|
24117
|
-
server.tool("machines_webhooks_list", "List shared
|
|
24118
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
23619
|
+
server.tool("machines_webhooks_list", "List shared event webhook channels.", {}, async () => ({
|
|
23620
|
+
content: [{ type: "text", text: JSON.stringify(sanitizeChannelsForOutput2(await events.listChannels()), null, 2) }]
|
|
24119
23621
|
}));
|
|
24120
|
-
server.tool("machines_webhooks_test", "Send a test event to one shared
|
|
23622
|
+
server.tool("machines_webhooks_test", "Send a test event to one shared event channel.", { channel_id: exports_external.string().describe("Channel identifier"), event_type: exports_external.string().optional().describe("Event type"), message: exports_external.string().optional().describe("Message body") }, async ({ channel_id, event_type, message }) => ({
|
|
24121
23623
|
content: [{ type: "text", text: JSON.stringify(await events.testChannel(channel_id, { source: "machines", type: event_type ?? "events.test", message }), null, 2) }]
|
|
24122
23624
|
}));
|
|
24123
|
-
server.tool("machines_webhooks_remove", "Remove a shared
|
|
24124
|
-
server.tool("machines_events_emit", "Emit a shared
|
|
23625
|
+
server.tool("machines_webhooks_remove", "Remove a shared event channel.", { channel_id: exports_external.string().describe("Channel identifier") }, async ({ channel_id }) => ({ content: [{ type: "text", text: JSON.stringify({ removed: await events.removeChannel(channel_id) }, null, 2) }] }));
|
|
23626
|
+
server.tool("machines_events_emit", "Emit a shared event from machines.", {
|
|
24125
23627
|
event_type: exports_external.string().describe("Event type"),
|
|
24126
23628
|
subject: exports_external.string().optional().describe("Event subject"),
|
|
24127
23629
|
severity: exports_external.enum(["debug", "info", "notice", "warning", "error", "critical"]).optional().describe("Event severity"),
|
|
@@ -24142,10 +23644,10 @@ function createMcpServer(version2) {
|
|
|
24142
23644
|
dedupeKey: dedupe_key
|
|
24143
23645
|
}, { deliver: deliver !== false }), null, 2) }]
|
|
24144
23646
|
}));
|
|
24145
|
-
server.tool("machines_events_list", "List shared
|
|
23647
|
+
server.tool("machines_events_list", "List shared events.", {}, async () => ({
|
|
24146
23648
|
content: [{ type: "text", text: JSON.stringify(await events.listEvents(), null, 2) }]
|
|
24147
23649
|
}));
|
|
24148
|
-
server.tool("machines_events_replay", "Replay shared
|
|
23650
|
+
server.tool("machines_events_replay", "Replay shared events.", { event_id: exports_external.string().optional().describe("Event id"), source: exports_external.string().optional().describe("Source filter"), event_type: exports_external.string().optional().describe("Event type filter"), dry_run: exports_external.boolean().optional().describe("Preview without delivery") }, async ({ event_id, source, event_type, dry_run }) => ({
|
|
24149
23651
|
content: [{ type: "text", text: JSON.stringify(await events.replay({ eventId: event_id, source, type: event_type, dryRun: dry_run }), null, 2) }]
|
|
24150
23652
|
}));
|
|
24151
23653
|
server.tool("machines_serve_info", "Preview the dashboard server bind address and routes.", { host: exports_external.string().optional().describe("Host interface"), port: exports_external.number().optional().describe("Port number") }, async ({ host, port }) => ({ content: [{ type: "text", text: JSON.stringify(getServeInfo({ host, port }), null, 2) }] }));
|
|
@@ -24276,6 +23778,7 @@ export {
|
|
|
24276
23778
|
STORAGE_TABLES,
|
|
24277
23779
|
STORAGE_MODE_ENV,
|
|
24278
23780
|
STORAGE_DATABASE_ENV,
|
|
23781
|
+
SCREEN_SECRET_NAMESPACE_ENV,
|
|
24279
23782
|
MACHINE_MCP_TOOL_NAMES,
|
|
24280
23783
|
MACHINES_STORAGE_TABLES,
|
|
24281
23784
|
MACHINES_STORAGE_MODE_FALLBACK_ENV,
|