@hasna/machines 0.0.47 → 0.0.49

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/dist/cli/index.js CHANGED
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
993
993
  this._exitCallback = (err) => {
994
994
  if (err.code !== "commander.executeSubCommandAsync") {
995
995
  throw err;
996
- } else {}
996
+ }
997
997
  };
998
998
  }
999
999
  return this;
@@ -2283,6 +2283,289 @@ var init_db = __esm(() => {
2283
2283
  ];
2284
2284
  });
2285
2285
 
2286
+ // src/commands/mutation-approval.ts
2287
+ import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
2288
+ import { resolve as resolve2 } from "path";
2289
+ function createTrustedSdkMutationApproval() {
2290
+ const approval = {};
2291
+ trustedSdkMutationApprovals.add(approval);
2292
+ return approval;
2293
+ }
2294
+ function isTrustedSdkMutationApproval(approval) {
2295
+ return typeof approval === "object" && approval !== null && trustedSdkMutationApprovals.has(approval);
2296
+ }
2297
+ function isTruthy(value) {
2298
+ return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
2299
+ }
2300
+ function nowMs(now) {
2301
+ if (typeof now === "number")
2302
+ return now;
2303
+ if (now instanceof Date)
2304
+ return now.getTime();
2305
+ return Date.now();
2306
+ }
2307
+ function signingSecret(env2, explicitSecret) {
2308
+ return explicitSecret?.trim() || env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim();
2309
+ }
2310
+ function hmac(payload, secret) {
2311
+ return createHmac("sha256", secret).update(payload).digest("base64url");
2312
+ }
2313
+ function sha256Hex(payload) {
2314
+ return createHash("sha256").update(payload).digest("hex");
2315
+ }
2316
+ function replayDbPath(env2) {
2317
+ const configured = env2[MUTATION_APPROVAL_REPLAY_PATH_ENV]?.trim();
2318
+ return configured ? resolve2(configured) : undefined;
2319
+ }
2320
+ function replayNonceKey(claims) {
2321
+ return sha256Hex(JSON.stringify({ nonce: claims.nonce }));
2322
+ }
2323
+ function recordReplayNonce(env2, claims, tokenPayload, now) {
2324
+ const dbPath = replayDbPath(env2);
2325
+ if (!dbPath)
2326
+ return;
2327
+ if (!claims.nonce) {
2328
+ return { approved: false, reason: "approval_token nonce claim is required for replay protection." };
2329
+ }
2330
+ try {
2331
+ const db = getDb(dbPath);
2332
+ db.query("DELETE FROM mutation_approval_nonces WHERE expires_at <= ?").run(now);
2333
+ const result = db.query(`
2334
+ INSERT OR IGNORE INTO mutation_approval_nonces (
2335
+ nonce_sha256,
2336
+ token_sha256,
2337
+ surface,
2338
+ operation,
2339
+ caller_id,
2340
+ run_id,
2341
+ transport,
2342
+ expires_at,
2343
+ used_at
2344
+ )
2345
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
2346
+ `).run(replayNonceKey(claims), sha256Hex(tokenPayload), claims.surface, claims.operation, claims.callerId ?? "", claims.runId ?? "", claims.transport ?? "", claims.expiresAt, now);
2347
+ if (result.changes !== 1) {
2348
+ return { approved: false, reason: "approval_token nonce has already been used." };
2349
+ }
2350
+ return;
2351
+ } catch (error) {
2352
+ const message = error instanceof Error ? error.message : String(error);
2353
+ return { approved: false, reason: `approval_token replay store is unavailable: ${message}` };
2354
+ }
2355
+ }
2356
+ function safeEqual(left, right) {
2357
+ const leftBuffer = Buffer.from(left);
2358
+ const rightBuffer = Buffer.from(right);
2359
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
2360
+ }
2361
+ function canonicalizeMutationArg(value, inArray = false) {
2362
+ if (value === undefined)
2363
+ return inArray ? null : undefined;
2364
+ if (value === null || typeof value === "boolean" || typeof value === "string")
2365
+ return value;
2366
+ if (typeof value === "number")
2367
+ return Number.isFinite(value) ? value : null;
2368
+ if (Array.isArray(value)) {
2369
+ return value.map((entry) => canonicalizeMutationArg(entry, true) ?? null);
2370
+ }
2371
+ if (value instanceof Date)
2372
+ return value.toISOString();
2373
+ if (typeof value === "object") {
2374
+ const result = {};
2375
+ for (const key of Object.keys(value).sort()) {
2376
+ if (key === "approval_token" || key === "approvalToken")
2377
+ continue;
2378
+ const canonicalValue = canonicalizeMutationArg(value[key]);
2379
+ if (canonicalValue !== undefined)
2380
+ result[key] = canonicalValue;
2381
+ }
2382
+ return result;
2383
+ }
2384
+ return inArray ? null : undefined;
2385
+ }
2386
+ function canonicalMutationArgs(value) {
2387
+ return JSON.stringify(canonicalizeMutationArg(value) ?? {});
2388
+ }
2389
+ function mutationArgsSha256(value) {
2390
+ return sha256Hex(canonicalMutationArgs(value));
2391
+ }
2392
+ function stripPlanRuntimeFields(value) {
2393
+ if (Array.isArray(value))
2394
+ return value.map(stripPlanRuntimeFields);
2395
+ if (value instanceof Date)
2396
+ return value;
2397
+ if (value && typeof value === "object") {
2398
+ const result = {};
2399
+ for (const [key, entry] of Object.entries(value)) {
2400
+ if (key === "planDigest" || key === "plan_digest" || key === "mode" || key === "executed")
2401
+ continue;
2402
+ result[key] = stripPlanRuntimeFields(entry);
2403
+ }
2404
+ return result;
2405
+ }
2406
+ return value;
2407
+ }
2408
+ function mutationPlanDigest(plan) {
2409
+ return mutationArgsSha256(stripPlanRuntimeFields(plan));
2410
+ }
2411
+ function attachMutationPlanDigest(plan) {
2412
+ return {
2413
+ ...plan,
2414
+ planDigest: mutationPlanDigest(plan)
2415
+ };
2416
+ }
2417
+ function assertMutationPlanDigest(plan, expectedPlanDigest) {
2418
+ if (expectedPlanDigest && mutationPlanDigest(plan) !== expectedPlanDigest) {
2419
+ throw new Error("Approved plan digest does not match the current execution plan.");
2420
+ }
2421
+ }
2422
+ function parseToken(token) {
2423
+ if (!token)
2424
+ return null;
2425
+ const parts = token.split(".");
2426
+ if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX)
2427
+ return null;
2428
+ try {
2429
+ const claims = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
2430
+ return { payload: parts[1] ?? "", signature: parts[2] ?? "", claims };
2431
+ } catch {
2432
+ return null;
2433
+ }
2434
+ }
2435
+ function claimMatches(expected, actual) {
2436
+ if (expected === undefined)
2437
+ return actual === undefined;
2438
+ return actual === expected;
2439
+ }
2440
+ function verifyMutationApprovalToken(options) {
2441
+ const env2 = options.env ?? process.env;
2442
+ const secret = signingSecret(env2);
2443
+ if (!secret)
2444
+ return { approved: false, reason: `${MUTATION_APPROVAL_TOKEN_ENV} is not configured.` };
2445
+ const parsed = parseToken(options.approvalToken);
2446
+ if (!parsed)
2447
+ return { approved: false, reason: "approval_token is not a scoped mutation token." };
2448
+ if (!safeEqual(hmac(parsed.payload, secret), parsed.signature)) {
2449
+ return { approved: false, reason: "approval_token signature is invalid." };
2450
+ }
2451
+ const claims = parsed.claims;
2452
+ if (claims.version !== 1)
2453
+ return { approved: false, reason: "approval_token version is unsupported." };
2454
+ if (!claims.callerId || !claims.runId) {
2455
+ return { approved: false, reason: "approval_token must include caller and run claims." };
2456
+ }
2457
+ if (!claims.transport) {
2458
+ return { approved: false, reason: "approval_token must include a transport claim." };
2459
+ }
2460
+ if (!Number.isFinite(claims.expiresAt) || claims.expiresAt <= nowMs(options.now)) {
2461
+ return { approved: false, reason: "approval_token is expired." };
2462
+ }
2463
+ const now = nowMs(options.now);
2464
+ if (!Number.isFinite(claims.issuedAt) || claims.issuedAt > now + MAX_CLOCK_SKEW_MS) {
2465
+ return { approved: false, reason: "approval_token issue time is invalid." };
2466
+ }
2467
+ if (claims.expiresAt - claims.issuedAt > MAX_TOKEN_TTL_MS) {
2468
+ return { approved: false, reason: "approval_token TTL is too long." };
2469
+ }
2470
+ for (const key of ["surface", "operation", "machineId", "resourceId", "transport"]) {
2471
+ if (!claimMatches(options[key], claims[key])) {
2472
+ return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
2473
+ }
2474
+ }
2475
+ for (const key of ["callerId", "runId"]) {
2476
+ if (options[key] !== undefined && options[key] !== claims[key]) {
2477
+ return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
2478
+ }
2479
+ }
2480
+ const expectedArgsSha256 = options.argsSha256 || (options.args === undefined ? undefined : mutationArgsSha256(options.args));
2481
+ if (expectedArgsSha256 !== undefined && claims.args_sha256 !== expectedArgsSha256) {
2482
+ return { approved: false, reason: "approval_token args_sha256 claim does not match this mutation." };
2483
+ }
2484
+ const replayDecision = recordReplayNonce(env2, claims, parsed.payload, now);
2485
+ if (replayDecision)
2486
+ return replayDecision;
2487
+ return { approved: true, claims };
2488
+ }
2489
+ function isMutationApproved(options = {}) {
2490
+ const env2 = options.env ?? process.env;
2491
+ const surface = options.surface ?? "cli";
2492
+ if (surface === "mcp") {
2493
+ if (!options.operation)
2494
+ return false;
2495
+ return verifyMutationApprovalToken({
2496
+ surface,
2497
+ operation: options.operation,
2498
+ machineId: options.machineId,
2499
+ resourceId: options.resourceId,
2500
+ callerId: options.callerId,
2501
+ runId: options.runId,
2502
+ transport: options.transport ?? "mcp",
2503
+ args: options.args,
2504
+ argsSha256: options.argsSha256,
2505
+ approvalToken: options.approvalToken,
2506
+ env: env2,
2507
+ now: options.now
2508
+ }).approved;
2509
+ }
2510
+ if (options.approvalToken) {
2511
+ const decision = options.operation ? verifyMutationApprovalToken({
2512
+ surface,
2513
+ operation: options.operation,
2514
+ machineId: options.machineId,
2515
+ resourceId: options.resourceId,
2516
+ callerId: options.callerId,
2517
+ runId: options.runId,
2518
+ transport: options.transport ?? surface,
2519
+ args: options.args,
2520
+ argsSha256: options.argsSha256,
2521
+ approvalToken: options.approvalToken,
2522
+ env: env2,
2523
+ now: options.now
2524
+ }) : { approved: false };
2525
+ if (decision.approved)
2526
+ return true;
2527
+ if (env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim())
2528
+ return false;
2529
+ }
2530
+ return isTruthy(env2[MUTATION_APPROVAL_FLAG_ENV]) || isTruthy(env2[LEGACY_MUTATION_APPROVAL_FLAG_ENV]);
2531
+ }
2532
+ function assertMutationApproved(options) {
2533
+ if (isMutationApproved(options)) {
2534
+ return;
2535
+ }
2536
+ const env2 = options.env ?? process.env;
2537
+ const tokenConfigured = Boolean(env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim());
2538
+ const approvalHint = options.surface === "mcp" ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV}` : tokenConfigured ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV} or set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session` : `set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session or configure ${MUTATION_APPROVAL_TOKEN_ENV}`;
2539
+ throw new Error(`Fleet mutation blocked: ${options.surface}.${options.operation} requires operator approval; ${approvalHint}.`);
2540
+ }
2541
+ function assertSdkMutationApproved(scope, options = {}) {
2542
+ if (isTrustedSdkMutationApproval(options.trustedLocalMutation))
2543
+ return;
2544
+ const decision = verifyMutationApprovalToken({
2545
+ surface: "sdk",
2546
+ operation: scope.operation,
2547
+ machineId: scope.machineId,
2548
+ resourceId: scope.resourceId,
2549
+ callerId: options.callerId,
2550
+ runId: options.runId,
2551
+ transport: "sdk",
2552
+ args: scope.args,
2553
+ argsSha256: scope.argsSha256,
2554
+ approvalToken: options.approvalToken,
2555
+ env: process.env
2556
+ });
2557
+ if (decision.approved)
2558
+ return;
2559
+ throw new Error(`Fleet mutation blocked: sdk.${scope.operation} requires a scoped SDK approval token.`);
2560
+ }
2561
+ var MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_ALLOW_MUTATIONS", LEGACY_MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_MUTATION_APPROVAL", MUTATION_APPROVAL_TOKEN_ENV = "HASNA_MACHINES_MUTATION_TOKEN", MUTATION_APPROVAL_CALLER_ENV = "HASNA_MACHINES_MUTATION_CALLER_ID", MUTATION_APPROVAL_RUN_ENV = "HASNA_MACHINES_MUTATION_RUN_ID", MUTATION_APPROVAL_REPLAY_PATH_ENV = "HASNA_MACHINES_MUTATION_REPLAY_PATH", TOKEN_PREFIX = "machines-mut-v1", DEFAULT_TOKEN_TTL_MS, MAX_TOKEN_TTL_MS, MAX_CLOCK_SKEW_MS = 30000, trustedSdkMutationApprovals;
2562
+ var init_mutation_approval = __esm(() => {
2563
+ init_db();
2564
+ DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
2565
+ MAX_TOKEN_TTL_MS = 5 * 60 * 1000;
2566
+ trustedSdkMutationApprovals = new WeakSet;
2567
+ });
2568
+
2286
2569
  // src/pg-migrations.ts
2287
2570
  var PG_MIGRATIONS;
2288
2571
  var init_pg_migrations = __esm(() => {
@@ -2354,7 +2637,18 @@ function normalizeParams(params) {
2354
2637
  const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
2355
2638
  return flat.map((value) => value === undefined ? null : value);
2356
2639
  }
2357
- function sslConfigFor(connectionString) {
2640
+ function envFlag(env2, name) {
2641
+ const value = env2[name]?.trim().toLowerCase();
2642
+ return value === "1" || value === "true" || value === "yes" || value === "on";
2643
+ }
2644
+ function isLoopbackHost(hostname6) {
2645
+ const normalized = hostname6.replace(/^\[|\]$/g, "").toLowerCase();
2646
+ return normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1" || /^127(?:\.\d{1,3}){3}$/.test(normalized);
2647
+ }
2648
+ function allowsLocalInsecureTls(url, env2) {
2649
+ return isLoopbackHost(url.hostname) && envFlag(env2, MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV);
2650
+ }
2651
+ function sslConfigFor(connectionString, env2 = process.env) {
2358
2652
  let url;
2359
2653
  try {
2360
2654
  url = new URL(connectionString);
@@ -2363,12 +2657,21 @@ function sslConfigFor(connectionString) {
2363
2657
  }
2364
2658
  const sslMode = url.searchParams.get("sslmode")?.toLowerCase();
2365
2659
  const ssl = url.searchParams.get("ssl")?.toLowerCase();
2366
- if (sslMode === "disable" || ssl === "false")
2367
- return;
2368
- if (sslMode === "no-verify" || process.env["HASNA_MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED"] === "0") {
2660
+ const rejectUnauthorizedOverride = env2[MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED_ENV]?.trim() === "0";
2661
+ if (sslMode === "disable" || ssl === "false") {
2662
+ if (allowsLocalInsecureTls(url, env2))
2663
+ return;
2664
+ throw new Error(`Insecure PostgreSQL TLS mode is rejected for remote storage; use sslmode=require or set ${MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV}=1 only for loopback development databases.`);
2665
+ }
2666
+ if (sslMode === "no-verify" || rejectUnauthorizedOverride) {
2667
+ if (!allowsLocalInsecureTls(url, env2)) {
2668
+ throw new Error(`PostgreSQL TLS certificate verification cannot be disabled for remote storage; set ${MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV}=1 only for loopback development databases.`);
2669
+ }
2369
2670
  return { rejectUnauthorized: false };
2370
2671
  }
2371
- return sslMode || ssl === "true" ? { rejectUnauthorized: true } : undefined;
2672
+ if (sslMode || ssl === "true")
2673
+ return { rejectUnauthorized: true };
2674
+ return isLoopbackHost(url.hostname) ? undefined : { rejectUnauthorized: true };
2372
2675
  }
2373
2676
 
2374
2677
  class PgAdapterAsync {
@@ -2388,6 +2691,7 @@ class PgAdapterAsync {
2388
2691
  await this.pool.end();
2389
2692
  }
2390
2693
  }
2694
+ var MACHINES_DATABASE_ALLOW_INSECURE_TLS_ENV = "HASNA_MACHINES_ALLOW_INSECURE_DATABASE_TLS", MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED_ENV = "HASNA_MACHINES_DATABASE_SSL_REJECT_UNAUTHORIZED";
2391
2695
  var init_remote_storage = () => {};
2392
2696
 
2393
2697
  // src/storage-sync.ts
@@ -2660,15 +2964,14 @@ var init_storage_sync = __esm(() => {
2660
2964
  // src/storage.ts
2661
2965
  var exports_storage = {};
2662
2966
  __export(exports_storage, {
2663
- storageSync: () => storageSync,
2664
- storagePush: () => storagePush,
2665
- storagePull: () => storagePull,
2666
- runStorageMigrations: () => runStorageMigrations,
2967
+ storageSync: () => storageSync2,
2968
+ storagePush: () => storagePush2,
2969
+ storagePull: () => storagePull2,
2970
+ runStorageMigrations: () => runStorageMigrations2,
2667
2971
  resolveTables: () => resolveTables,
2668
2972
  parseStorageTables: () => parseStorageTables,
2669
2973
  getSyncMetaAll: () => getSyncMetaAll,
2670
2974
  getStorageStatus: () => getStorageStatus,
2671
- getStoragePg: () => getStoragePg,
2672
2975
  getStorageMode: () => getStorageMode,
2673
2976
  getStorageDatabaseUrl: () => getStorageDatabaseUrl,
2674
2977
  getStorageDatabaseEnvName: () => getStorageDatabaseEnvName,
@@ -2676,7 +2979,6 @@ __export(exports_storage, {
2676
2979
  STORAGE_TABLES: () => STORAGE_TABLES,
2677
2980
  STORAGE_MODE_ENV: () => STORAGE_MODE_ENV,
2678
2981
  STORAGE_DATABASE_ENV: () => STORAGE_DATABASE_ENV,
2679
- PgAdapterAsync: () => PgAdapterAsync,
2680
2982
  PG_MIGRATIONS: () => PG_MIGRATIONS,
2681
2983
  MACHINES_STORAGE_TABLES: () => MACHINES_STORAGE_TABLES,
2682
2984
  MACHINES_STORAGE_MODE_FALLBACK_ENV: () => MACHINES_STORAGE_MODE_FALLBACK_ENV,
@@ -2684,10 +2986,50 @@ __export(exports_storage, {
2684
2986
  MACHINES_STORAGE_FALLBACK_ENV: () => MACHINES_STORAGE_FALLBACK_ENV,
2685
2987
  MACHINES_STORAGE_ENV: () => MACHINES_STORAGE_ENV
2686
2988
  });
2989
+ function storageArgs(options = {}) {
2990
+ return {
2991
+ tables: options.tables?.length ? [...options.tables] : null
2992
+ };
2993
+ }
2994
+ function storageResourceId(operation, options = {}) {
2995
+ return `storage:${operation}:${mutationArgsSha256(storageArgs(options))}`;
2996
+ }
2997
+ async function runStorageMigrations2(remote, options = {}) {
2998
+ assertSdkMutationApproved({
2999
+ operation: "machines_storage_migrate",
3000
+ resourceId: "storage:migrations",
3001
+ args: {}
3002
+ }, options);
3003
+ return runStorageMigrations(remote);
3004
+ }
3005
+ async function storagePush2(options = {}) {
3006
+ assertSdkMutationApproved({
3007
+ operation: "machines_storage_push",
3008
+ resourceId: storageResourceId("push", options),
3009
+ args: storageArgs(options)
3010
+ }, options);
3011
+ return storagePush({ tables: options.tables });
3012
+ }
3013
+ async function storagePull2(options = {}) {
3014
+ assertSdkMutationApproved({
3015
+ operation: "machines_storage_pull",
3016
+ resourceId: storageResourceId("pull", options),
3017
+ args: storageArgs(options)
3018
+ }, options);
3019
+ return storagePull({ tables: options.tables });
3020
+ }
3021
+ async function storageSync2(options = {}) {
3022
+ assertSdkMutationApproved({
3023
+ operation: "machines_storage_sync",
3024
+ resourceId: storageResourceId("sync", options),
3025
+ args: storageArgs(options)
3026
+ }, options);
3027
+ return storageSync({ tables: options.tables });
3028
+ }
2687
3029
  var init_storage = __esm(() => {
2688
3030
  init_storage_sync();
3031
+ init_mutation_approval();
2689
3032
  init_pg_migrations();
2690
- init_remote_storage();
2691
3033
  });
2692
3034
 
2693
3035
  // node_modules/commander/esm.mjs
@@ -7439,372 +7781,114 @@ function readManifestWithSource(options = {}) {
7439
7781
  warnings
7440
7782
  }
7441
7783
  };
7442
- }
7443
- warnings.push(`private_manifest_adapter_empty:${redactIdentifier(options.adapter.id)}`);
7444
- } catch (error) {
7445
- warnings.push(`private_manifest_adapter_failed:${redactIdentifier(options.adapter.id)}`);
7446
- }
7447
- } else {
7448
- warnings.push("private_manifest_ref_without_adapter");
7449
- }
7450
- const fallbackSource = fileSourceRef(path);
7451
- const manifest = readManifest(path);
7452
- return {
7453
- manifest,
7454
- info: {
7455
- source,
7456
- loadedFrom: existsSync3(path) ? "fallback" : "default",
7457
- fallbackSource,
7458
- warnings
7459
- }
7460
- };
7461
- }
7462
- return {
7463
- manifest: readManifest(path),
7464
- info: {
7465
- source,
7466
- loadedFrom: existsSync3(path) ? "file" : "default",
7467
- warnings
7468
- }
7469
- };
7470
- }
7471
- function validateManifest(path = getManifestPath()) {
7472
- return readManifest(path);
7473
- }
7474
- function writeManifest(manifest, path = getManifestPath()) {
7475
- ensureParentDir(path);
7476
- const payload = {
7477
- ...manifest,
7478
- version: 1,
7479
- generatedAt: new Date().toISOString(),
7480
- machines: normalizeMachines(manifest.machines)
7481
- };
7482
- fleetSchema.parse(payload);
7483
- writeFileSync(path, `${JSON.stringify(payload, null, 2)}
7484
- `, "utf8");
7485
- return path;
7486
- }
7487
- function getManifestMachine(machineId, path = getManifestPath()) {
7488
- return readManifest(path).machines.find((machine) => machine.id === machineId) || null;
7489
- }
7490
- function detectCurrentMachineManifest() {
7491
- const machineId = process.env["HASNA_MACHINES_MACHINE_ID"] || hostname();
7492
- const user = userInfo().username;
7493
- const bunDir = dirname3(process.execPath);
7494
- return {
7495
- id: machineId,
7496
- hostname: hostname(),
7497
- sshAddress: `${user}@${machineId}`,
7498
- tailscaleName: machineId,
7499
- platform: normalizePlatform(),
7500
- connection: "local",
7501
- workspacePath: detectWorkspacePath(),
7502
- bunPath: bunDir,
7503
- tags: [`arch:${arch()}`],
7504
- packages: [],
7505
- files: []
7506
- };
7507
- }
7508
-
7509
- // src/commands/manifest.ts
7510
- init_paths();
7511
- function manifestInit() {
7512
- return writeManifest(getDefaultManifest(), getManifestPath());
7513
- }
7514
- function manifestList() {
7515
- return readManifest();
7516
- }
7517
- function manifestAdd(machine) {
7518
- const validatedMachine = machineSchema.parse(machine);
7519
- const manifest = readManifest();
7520
- const nextMachines = manifest.machines.filter((entry) => entry.id !== validatedMachine.id);
7521
- nextMachines.push(validatedMachine);
7522
- const nextManifest = { ...manifest, machines: nextMachines };
7523
- writeManifest(nextManifest);
7524
- return nextManifest;
7525
- }
7526
- function manifestBootstrapCurrentMachine() {
7527
- return manifestAdd(detectCurrentMachineManifest());
7528
- }
7529
- function manifestGet(machineId) {
7530
- return getManifestMachine(machineId);
7531
- }
7532
- function manifestRemove(machineId) {
7533
- const manifest = readManifest();
7534
- const nextManifest = {
7535
- ...manifest,
7536
- machines: manifest.machines.filter((machine) => machine.id !== machineId)
7537
- };
7538
- writeManifest(nextManifest);
7539
- return nextManifest;
7540
- }
7541
- function manifestValidate() {
7542
- return validateManifest(getManifestPath());
7543
- }
7544
-
7545
- // src/commands/setup.ts
7546
- import { homedir as homedir2 } from "os";
7547
- init_db();
7548
-
7549
- // src/commands/mutation-approval.ts
7550
- init_db();
7551
- import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
7552
- import { resolve as resolve2 } from "path";
7553
- var MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_ALLOW_MUTATIONS";
7554
- var LEGACY_MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_MUTATION_APPROVAL";
7555
- var MUTATION_APPROVAL_TOKEN_ENV = "HASNA_MACHINES_MUTATION_TOKEN";
7556
- var MUTATION_APPROVAL_CALLER_ENV = "HASNA_MACHINES_MUTATION_CALLER_ID";
7557
- var MUTATION_APPROVAL_RUN_ENV = "HASNA_MACHINES_MUTATION_RUN_ID";
7558
- var MUTATION_APPROVAL_REPLAY_PATH_ENV = "HASNA_MACHINES_MUTATION_REPLAY_PATH";
7559
- var TOKEN_PREFIX = "machines-mut-v1";
7560
- var DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
7561
- var MAX_TOKEN_TTL_MS = 5 * 60 * 1000;
7562
- var MAX_CLOCK_SKEW_MS = 30000;
7563
- function isTruthy(value) {
7564
- return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
7565
- }
7566
- function nowMs(now) {
7567
- if (typeof now === "number")
7568
- return now;
7569
- if (now instanceof Date)
7570
- return now.getTime();
7571
- return Date.now();
7572
- }
7573
- function signingSecret(env2, explicitSecret) {
7574
- return explicitSecret?.trim() || env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim();
7575
- }
7576
- function hmac(payload, secret) {
7577
- return createHmac("sha256", secret).update(payload).digest("base64url");
7578
- }
7579
- function sha256Hex(payload) {
7580
- return createHash("sha256").update(payload).digest("hex");
7581
- }
7582
- function replayDbPath(env2) {
7583
- const configured = env2[MUTATION_APPROVAL_REPLAY_PATH_ENV]?.trim();
7584
- return configured ? resolve2(configured) : undefined;
7585
- }
7586
- function replayNonceKey(claims) {
7587
- return sha256Hex(JSON.stringify({ nonce: claims.nonce }));
7588
- }
7589
- function recordReplayNonce(env2, claims, tokenPayload, now) {
7590
- const dbPath = replayDbPath(env2);
7591
- if (!dbPath)
7592
- return;
7593
- if (!claims.nonce) {
7594
- return { approved: false, reason: "approval_token nonce claim is required for replay protection." };
7595
- }
7596
- try {
7597
- const db = getDb(dbPath);
7598
- db.query("DELETE FROM mutation_approval_nonces WHERE expires_at <= ?").run(now);
7599
- const result = db.query(`
7600
- INSERT OR IGNORE INTO mutation_approval_nonces (
7601
- nonce_sha256,
7602
- token_sha256,
7603
- surface,
7604
- operation,
7605
- caller_id,
7606
- run_id,
7607
- transport,
7608
- expires_at,
7609
- used_at
7610
- )
7611
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
7612
- `).run(replayNonceKey(claims), sha256Hex(tokenPayload), claims.surface, claims.operation, claims.callerId ?? "", claims.runId ?? "", claims.transport ?? "", claims.expiresAt, now);
7613
- if (result.changes !== 1) {
7614
- return { approved: false, reason: "approval_token nonce has already been used." };
7615
- }
7616
- return;
7617
- } catch (error) {
7618
- const message = error instanceof Error ? error.message : String(error);
7619
- return { approved: false, reason: `approval_token replay store is unavailable: ${message}` };
7620
- }
7621
- }
7622
- function safeEqual(left, right) {
7623
- const leftBuffer = Buffer.from(left);
7624
- const rightBuffer = Buffer.from(right);
7625
- return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
7626
- }
7627
- function canonicalizeMutationArg(value, inArray = false) {
7628
- if (value === undefined)
7629
- return inArray ? null : undefined;
7630
- if (value === null || typeof value === "boolean" || typeof value === "string")
7631
- return value;
7632
- if (typeof value === "number")
7633
- return Number.isFinite(value) ? value : null;
7634
- if (Array.isArray(value)) {
7635
- return value.map((entry) => canonicalizeMutationArg(entry, true) ?? null);
7636
- }
7637
- if (value instanceof Date)
7638
- return value.toISOString();
7639
- if (typeof value === "object") {
7640
- const result = {};
7641
- for (const key of Object.keys(value).sort()) {
7642
- if (key === "approval_token" || key === "approvalToken")
7643
- continue;
7644
- const canonicalValue = canonicalizeMutationArg(value[key]);
7645
- if (canonicalValue !== undefined)
7646
- result[key] = canonicalValue;
7784
+ }
7785
+ warnings.push(`private_manifest_adapter_empty:${redactIdentifier(options.adapter.id)}`);
7786
+ } catch (error) {
7787
+ warnings.push(`private_manifest_adapter_failed:${redactIdentifier(options.adapter.id)}`);
7788
+ }
7789
+ } else {
7790
+ warnings.push("private_manifest_ref_without_adapter");
7647
7791
  }
7648
- return result;
7792
+ const fallbackSource = fileSourceRef(path);
7793
+ const manifest = readManifest(path);
7794
+ return {
7795
+ manifest,
7796
+ info: {
7797
+ source,
7798
+ loadedFrom: existsSync3(path) ? "fallback" : "default",
7799
+ fallbackSource,
7800
+ warnings
7801
+ }
7802
+ };
7649
7803
  }
7650
- return inArray ? null : undefined;
7651
- }
7652
- function canonicalMutationArgs(value) {
7653
- return JSON.stringify(canonicalizeMutationArg(value) ?? {});
7804
+ return {
7805
+ manifest: readManifest(path),
7806
+ info: {
7807
+ source,
7808
+ loadedFrom: existsSync3(path) ? "file" : "default",
7809
+ warnings
7810
+ }
7811
+ };
7654
7812
  }
7655
- function mutationArgsSha256(value) {
7656
- return sha256Hex(canonicalMutationArgs(value));
7813
+ function validateManifest(path = getManifestPath()) {
7814
+ return readManifest(path);
7657
7815
  }
7658
- function stripPlanRuntimeFields(value) {
7659
- if (Array.isArray(value))
7660
- return value.map(stripPlanRuntimeFields);
7661
- if (value instanceof Date)
7662
- return value;
7663
- if (value && typeof value === "object") {
7664
- const result = {};
7665
- for (const [key, entry] of Object.entries(value)) {
7666
- if (key === "planDigest" || key === "plan_digest" || key === "mode" || key === "executed")
7667
- continue;
7668
- result[key] = stripPlanRuntimeFields(entry);
7669
- }
7670
- return result;
7671
- }
7672
- return value;
7816
+ function writeManifest(manifest, path = getManifestPath()) {
7817
+ ensureParentDir(path);
7818
+ const payload = {
7819
+ ...manifest,
7820
+ version: 1,
7821
+ generatedAt: new Date().toISOString(),
7822
+ machines: normalizeMachines(manifest.machines)
7823
+ };
7824
+ fleetSchema.parse(payload);
7825
+ writeFileSync(path, `${JSON.stringify(payload, null, 2)}
7826
+ `, "utf8");
7827
+ return path;
7673
7828
  }
7674
- function mutationPlanDigest(plan) {
7675
- return mutationArgsSha256(stripPlanRuntimeFields(plan));
7829
+ function getManifestMachine(machineId, path = getManifestPath()) {
7830
+ return readManifest(path).machines.find((machine) => machine.id === machineId) || null;
7676
7831
  }
7677
- function attachMutationPlanDigest(plan) {
7832
+ function detectCurrentMachineManifest() {
7833
+ const machineId = process.env["HASNA_MACHINES_MACHINE_ID"] || hostname();
7834
+ const user = userInfo().username;
7835
+ const bunDir = dirname3(process.execPath);
7678
7836
  return {
7679
- ...plan,
7680
- planDigest: mutationPlanDigest(plan)
7837
+ id: machineId,
7838
+ hostname: hostname(),
7839
+ sshAddress: `${user}@${machineId}`,
7840
+ tailscaleName: machineId,
7841
+ platform: normalizePlatform(),
7842
+ connection: "local",
7843
+ workspacePath: detectWorkspacePath(),
7844
+ bunPath: bunDir,
7845
+ tags: [`arch:${arch()}`],
7846
+ packages: [],
7847
+ files: []
7681
7848
  };
7682
7849
  }
7683
- function assertMutationPlanDigest(plan, expectedPlanDigest) {
7684
- if (expectedPlanDigest && mutationPlanDigest(plan) !== expectedPlanDigest) {
7685
- throw new Error("Approved plan digest does not match the current execution plan.");
7686
- }
7850
+
7851
+ // src/commands/manifest.ts
7852
+ init_paths();
7853
+ function manifestInit() {
7854
+ return writeManifest(getDefaultManifest(), getManifestPath());
7687
7855
  }
7688
- function parseToken(token) {
7689
- if (!token)
7690
- return null;
7691
- const parts = token.split(".");
7692
- if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX)
7693
- return null;
7694
- try {
7695
- const claims = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
7696
- return { payload: parts[1] ?? "", signature: parts[2] ?? "", claims };
7697
- } catch {
7698
- return null;
7699
- }
7856
+ function manifestList() {
7857
+ return readManifest();
7700
7858
  }
7701
- function claimMatches(expected, actual) {
7702
- if (expected === undefined)
7703
- return actual === undefined;
7704
- return actual === expected;
7859
+ function manifestAdd(machine) {
7860
+ const validatedMachine = machineSchema.parse(machine);
7861
+ const manifest = readManifest();
7862
+ const nextMachines = manifest.machines.filter((entry) => entry.id !== validatedMachine.id);
7863
+ nextMachines.push(validatedMachine);
7864
+ const nextManifest = { ...manifest, machines: nextMachines };
7865
+ writeManifest(nextManifest);
7866
+ return nextManifest;
7705
7867
  }
7706
- function verifyMutationApprovalToken(options) {
7707
- const env2 = options.env ?? process.env;
7708
- const secret = signingSecret(env2);
7709
- if (!secret)
7710
- return { approved: false, reason: `${MUTATION_APPROVAL_TOKEN_ENV} is not configured.` };
7711
- const parsed = parseToken(options.approvalToken);
7712
- if (!parsed)
7713
- return { approved: false, reason: "approval_token is not a scoped mutation token." };
7714
- if (!safeEqual(hmac(parsed.payload, secret), parsed.signature)) {
7715
- return { approved: false, reason: "approval_token signature is invalid." };
7716
- }
7717
- const claims = parsed.claims;
7718
- if (claims.version !== 1)
7719
- return { approved: false, reason: "approval_token version is unsupported." };
7720
- if (!claims.callerId || !claims.runId) {
7721
- return { approved: false, reason: "approval_token must include caller and run claims." };
7722
- }
7723
- if (!claims.transport) {
7724
- return { approved: false, reason: "approval_token must include a transport claim." };
7725
- }
7726
- if (!Number.isFinite(claims.expiresAt) || claims.expiresAt <= nowMs(options.now)) {
7727
- return { approved: false, reason: "approval_token is expired." };
7728
- }
7729
- const now = nowMs(options.now);
7730
- if (!Number.isFinite(claims.issuedAt) || claims.issuedAt > now + MAX_CLOCK_SKEW_MS) {
7731
- return { approved: false, reason: "approval_token issue time is invalid." };
7732
- }
7733
- if (claims.expiresAt - claims.issuedAt > MAX_TOKEN_TTL_MS) {
7734
- return { approved: false, reason: "approval_token TTL is too long." };
7735
- }
7736
- for (const key of ["surface", "operation", "machineId", "resourceId", "transport"]) {
7737
- if (!claimMatches(options[key], claims[key])) {
7738
- return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
7739
- }
7740
- }
7741
- for (const key of ["callerId", "runId"]) {
7742
- if (options[key] !== undefined && options[key] !== claims[key]) {
7743
- return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
7744
- }
7745
- }
7746
- const expectedArgsSha256 = options.argsSha256 || (options.args === undefined ? undefined : mutationArgsSha256(options.args));
7747
- if (expectedArgsSha256 !== undefined && claims.args_sha256 !== expectedArgsSha256) {
7748
- return { approved: false, reason: "approval_token args_sha256 claim does not match this mutation." };
7749
- }
7750
- const replayDecision = recordReplayNonce(env2, claims, parsed.payload, now);
7751
- if (replayDecision)
7752
- return replayDecision;
7753
- return { approved: true, claims };
7868
+ function manifestBootstrapCurrentMachine() {
7869
+ return manifestAdd(detectCurrentMachineManifest());
7754
7870
  }
7755
- function isMutationApproved(options = {}) {
7756
- const env2 = options.env ?? process.env;
7757
- const surface = options.surface ?? "cli";
7758
- if (surface === "mcp") {
7759
- if (!options.operation)
7760
- return false;
7761
- return verifyMutationApprovalToken({
7762
- surface,
7763
- operation: options.operation,
7764
- machineId: options.machineId,
7765
- resourceId: options.resourceId,
7766
- callerId: options.callerId,
7767
- runId: options.runId,
7768
- transport: options.transport ?? "mcp",
7769
- args: options.args,
7770
- argsSha256: options.argsSha256,
7771
- approvalToken: options.approvalToken,
7772
- env: env2,
7773
- now: options.now
7774
- }).approved;
7775
- }
7776
- if (options.approvalToken) {
7777
- const decision = options.operation ? verifyMutationApprovalToken({
7778
- surface,
7779
- operation: options.operation,
7780
- machineId: options.machineId,
7781
- resourceId: options.resourceId,
7782
- callerId: options.callerId,
7783
- runId: options.runId,
7784
- transport: options.transport ?? surface,
7785
- args: options.args,
7786
- argsSha256: options.argsSha256,
7787
- approvalToken: options.approvalToken,
7788
- env: env2,
7789
- now: options.now
7790
- }) : { approved: false };
7791
- if (decision.approved)
7792
- return true;
7793
- if (env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim())
7794
- return false;
7795
- }
7796
- return isTruthy(env2[MUTATION_APPROVAL_FLAG_ENV]) || isTruthy(env2[LEGACY_MUTATION_APPROVAL_FLAG_ENV]);
7871
+ function manifestGet(machineId) {
7872
+ return getManifestMachine(machineId);
7797
7873
  }
7798
- function assertMutationApproved(options) {
7799
- if (isMutationApproved(options)) {
7800
- return;
7801
- }
7802
- const env2 = options.env ?? process.env;
7803
- const tokenConfigured = Boolean(env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim());
7804
- const approvalHint = options.surface === "mcp" ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV}` : tokenConfigured ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV} or set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session` : `set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session or configure ${MUTATION_APPROVAL_TOKEN_ENV}`;
7805
- throw new Error(`Fleet mutation blocked: ${options.surface}.${options.operation} requires operator approval; ${approvalHint}.`);
7874
+ function manifestRemove(machineId) {
7875
+ const manifest = readManifest();
7876
+ const nextManifest = {
7877
+ ...manifest,
7878
+ machines: manifest.machines.filter((machine) => machine.id !== machineId)
7879
+ };
7880
+ writeManifest(nextManifest);
7881
+ return nextManifest;
7882
+ }
7883
+ function manifestValidate() {
7884
+ return validateManifest(getManifestPath());
7806
7885
  }
7807
7886
 
7887
+ // src/commands/setup.ts
7888
+ import { homedir as homedir2 } from "os";
7889
+ init_db();
7890
+ init_mutation_approval();
7891
+
7808
7892
  // src/remote.ts
7809
7893
  init_db();
7810
7894
  import { spawnSync as spawnSync2 } from "child_process";
@@ -9660,12 +9744,13 @@ ${after}` : `
9660
9744
  return `${prefix}${block}
9661
9745
  `;
9662
9746
  }
9663
- function loadTailscale(runner, warnings) {
9664
- if (runner("command -v tailscale >/dev/null 2>&1").exitCode !== 0) {
9665
- warnings.push("tailscale_not_available");
9747
+ function loadTailscaleStatus(runner, binary, warnings) {
9748
+ if (!binary) {
9749
+ if (!warnings.includes("tailscale_not_available"))
9750
+ warnings.push("tailscale_not_available");
9666
9751
  return null;
9667
9752
  }
9668
- const result = runner("tailscale status --json");
9753
+ const result = runner(`"${binary}" status --json`);
9669
9754
  if (result.exitCode !== 0) {
9670
9755
  warnings.push("tailscale_status_failed");
9671
9756
  return null;
@@ -9694,9 +9779,23 @@ function collectPingTargets(tailscale, localSubnets) {
9694
9779
  }
9695
9780
  return targets;
9696
9781
  }
9697
- function warmDirectPaths(runner, targets, timeoutSeconds = 2) {
9782
+ var TAILSCALE_CANDIDATES = [
9783
+ "tailscale",
9784
+ "/usr/local/bin/tailscale",
9785
+ "/opt/homebrew/bin/tailscale",
9786
+ "/Applications/Tailscale.app/Contents/MacOS/Tailscale"
9787
+ ];
9788
+ function resolveTailscaleBinary(runner) {
9789
+ for (const candidate of TAILSCALE_CANDIDATES) {
9790
+ const check = candidate.includes("/") ? `test -x "${candidate}"` : `command -v ${candidate} >/dev/null 2>&1`;
9791
+ if (runner(check).exitCode === 0)
9792
+ return candidate;
9793
+ }
9794
+ return null;
9795
+ }
9796
+ function warmDirectPaths(runner, targets, binary, timeoutSeconds = 2) {
9698
9797
  for (const target of targets) {
9699
- runner(`tailscale ping --c 1 --timeout ${timeoutSeconds}s ${target} >/dev/null 2>&1 || true`);
9798
+ runner(`"${binary}" ping --c 1 --timeout ${timeoutSeconds}s ${target} >/dev/null 2>&1 || true`);
9700
9799
  }
9701
9800
  }
9702
9801
  function resolveLocalMachineId(tailscale, explicit) {
@@ -9707,14 +9806,15 @@ function resolveLocalMachineId(tailscale, explicit) {
9707
9806
  function planFleetHosts(options = {}) {
9708
9807
  const runner = options.runner ?? defaultRunner2;
9709
9808
  const warnings = [];
9710
- let tailscale = loadTailscale(runner, warnings);
9809
+ const binary = resolveTailscaleBinary(runner);
9810
+ let tailscale = loadTailscaleStatus(runner, binary, warnings);
9711
9811
  const manifest = readManifest();
9712
9812
  const localSubnets = options.localSubnets ?? localPrivateSubnets();
9713
- if (options.warm !== false && tailscale && localSubnets.length > 0) {
9813
+ if (options.warm !== false && tailscale && binary && localSubnets.length > 0) {
9714
9814
  const targets = collectPingTargets(tailscale, localSubnets);
9715
9815
  if (targets.length > 0) {
9716
- warmDirectPaths(runner, targets, options.warmTimeoutSeconds);
9717
- tailscale = loadTailscale(runner, warnings) ?? tailscale;
9816
+ warmDirectPaths(runner, targets, binary, options.warmTimeoutSeconds);
9817
+ tailscale = loadTailscaleStatus(runner, binary, warnings) ?? tailscale;
9718
9818
  }
9719
9819
  }
9720
9820
  const localMachineId = resolveLocalMachineId(tailscale, options.localMachineId);
@@ -9794,6 +9894,7 @@ function diffMachines(leftMachineId, rightMachineId) {
9794
9894
  }
9795
9895
 
9796
9896
  // src/commands/apps.ts
9897
+ init_mutation_approval();
9797
9898
  function getPackageName(app) {
9798
9899
  return app.packageName || app.name;
9799
9900
  }
@@ -9934,6 +10035,7 @@ function runAppsPlan(plan, options = {}, runner = runMachineCommand) {
9934
10035
  }
9935
10036
 
9936
10037
  // src/commands/install-claude.ts
10038
+ init_mutation_approval();
9937
10039
  var AI_CLI_PACKAGES = {
9938
10040
  claude: "@anthropic-ai/claude-code",
9939
10041
  codex: "@openai/codex",
@@ -10041,6 +10143,7 @@ function runClaudeInstallPlan(plan, options = {}, runner = runMachineCommand) {
10041
10143
  }
10042
10144
 
10043
10145
  // src/commands/install-tailscale.ts
10146
+ init_mutation_approval();
10044
10147
  function buildInstallSteps2(machine) {
10045
10148
  if (machine.platform === "macos") {
10046
10149
  return [
@@ -10108,6 +10211,7 @@ function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand)
10108
10211
  import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
10109
10212
  import { delimiter, isAbsolute, join as join7 } from "path";
10110
10213
  init_paths();
10214
+ init_mutation_approval();
10111
10215
  var notificationChannelSchema = exports_external.object({
10112
10216
  id: exports_external.string(),
10113
10217
  type: exports_external.enum(["email", "webhook", "command"]),
@@ -10451,9 +10555,20 @@ import { EventsClient } from "@hasna/events";
10451
10555
  function shellQuote5(value) {
10452
10556
  return `'${value.replace(/'/g, "'\\''")}'`;
10453
10557
  }
10558
+ function commandBasename(command) {
10559
+ const withoutArgs = command.trim().split(/\s+/, 1)[0] ?? "";
10560
+ return withoutArgs.split(/[\\/]/).filter(Boolean).at(-1) ?? withoutArgs;
10561
+ }
10562
+ function assertMachinesCommandSafe(command) {
10563
+ const basename = commandBasename(command);
10564
+ if (basename === "events" || basename === "hasna-events") {
10565
+ throw new Error("tmux-hook-plan machines command must invoke the machines CLI, not dependency-owned @hasna/events bins.");
10566
+ }
10567
+ }
10454
10568
  function buildTmuxPaneDiedHookPlan(options = {}) {
10455
10569
  const tmuxCommand = options.tmuxCommand ?? process.env["HASNA_MACHINES_TMUX_BIN"] ?? "tmux";
10456
10570
  const machinesCommand = options.machinesCommand ?? "machines";
10571
+ assertMachinesCommandSafe(machinesCommand);
10457
10572
  const deliver = options.deliver === true;
10458
10573
  const approvalToken = options.approvalToken?.trim();
10459
10574
  const trustedLocalMutation = approvalToken ? false : options.trustedLocalMutation === true;
@@ -10694,6 +10809,7 @@ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync7, sy
10694
10809
  import { homedir as homedir5 } from "os";
10695
10810
  init_paths();
10696
10811
  init_db();
10812
+ init_mutation_approval();
10697
10813
  function quote4(value) {
10698
10814
  return `'${value.replace(/'/g, `'\\''`)}'`;
10699
10815
  }
@@ -11478,6 +11594,9 @@ function runDoctor(machineId, options = {}) {
11478
11594
  };
11479
11595
  }
11480
11596
 
11597
+ // src/cli/index.ts
11598
+ init_mutation_approval();
11599
+
11481
11600
  // src/commands/daemon.ts
11482
11601
  import { execFileSync } from "child_process";
11483
11602
  import { chmodSync, existsSync as existsSync10, readFileSync as readFileSync8, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "fs";
@@ -12037,6 +12156,7 @@ function getAgentStatus(machineId, options = {}) {
12037
12156
  }
12038
12157
 
12039
12158
  // src/commands/serve.ts
12159
+ init_mutation_approval();
12040
12160
  function escapeHtml(value) {
12041
12161
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
12042
12162
  }
@@ -12496,6 +12616,16 @@ function startDashboardServer(options = {}) {
12496
12616
  function check(id, status, summary, detail) {
12497
12617
  return { id, status, summary, detail };
12498
12618
  }
12619
+ function summarize(checks) {
12620
+ const counts = checks.reduce((acc, entry) => {
12621
+ acc[entry.status] += 1;
12622
+ return acc;
12623
+ }, { ok: 0, warn: 0, fail: 0 });
12624
+ return {
12625
+ overall: counts.fail > 0 ? "fail" : counts.warn > 0 ? "warn" : "ok",
12626
+ counts
12627
+ };
12628
+ }
12499
12629
  function runSelfTest() {
12500
12630
  const version = getPackageVersion();
12501
12631
  const status = getStatus();
@@ -12507,19 +12637,21 @@ function runSelfTest() {
12507
12637
  const apps = listApps(machineId);
12508
12638
  const appsDiff = diffApps(machineId);
12509
12639
  const cliPlan = buildClaudeInstallPlan(machineId);
12640
+ const checks = [
12641
+ check("package-version", version === "0.0.0" ? "fail" : "ok", "Package version resolves", version),
12642
+ check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
12643
+ check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
12644
+ check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
12645
+ check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
12646
+ check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
12647
+ check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
12648
+ check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
12649
+ check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
12650
+ ];
12510
12651
  return {
12511
12652
  machineId,
12512
- checks: [
12513
- check("package-version", version === "0.0.0" ? "fail" : "ok", "Package version resolves", version),
12514
- check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
12515
- check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
12516
- check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
12517
- check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
12518
- check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
12519
- check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
12520
- check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
12521
- check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
12522
- ]
12653
+ ...summarize(checks),
12654
+ checks
12523
12655
  };
12524
12656
  }
12525
12657
 
@@ -13616,6 +13748,7 @@ function renderDoctorResult(report) {
13616
13748
  function renderSelfTestResult(result) {
13617
13749
  return [
13618
13750
  `machine: ${result.machineId}`,
13751
+ `overall: ${result.overall} ok=${result.counts.ok} warn=${result.counts.warn} fail=${result.counts.fail}`,
13619
13752
  ...result.checks.map((check2) => {
13620
13753
  const status = check2.status === "ok" ? source_default.green(check2.status) : check2.status === "warn" ? source_default.yellow(check2.status) : source_default.red(check2.status);
13621
13754
  return `${check2.id.padEnd(20)} ${status} ${check2.detail}`;
@@ -14749,10 +14882,10 @@ storageCommand.command("status").description("Show storage sync status").option(
14749
14882
  });
14750
14883
  storageCommand.command("push").description("Push local machine runtime data to storage PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
14751
14884
  try {
14752
- const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePush: storagePush2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14885
+ const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePush: storagePush3 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14753
14886
  const tables = resolveTables2(parseStorageTables2(options.tables));
14754
14887
  requireCliMutation("storage_push", options.approvalToken, { resourceId: cliResourceId("storage-push", tables.join(",")), args: { tables } });
14755
- const results = await storagePush2({ tables });
14888
+ const results = await storagePush3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
14756
14889
  printStorageResults(results, options.json);
14757
14890
  } catch (error) {
14758
14891
  printStorageError(error);
@@ -14760,10 +14893,10 @@ storageCommand.command("push").description("Push local machine runtime data to s
14760
14893
  });
14761
14894
  storageCommand.command("pull").description("Pull machine runtime data from storage PostgreSQL to local SQLite").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
14762
14895
  try {
14763
- const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePull: storagePull2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14896
+ const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePull: storagePull3 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14764
14897
  const tables = resolveTables2(parseStorageTables2(options.tables));
14765
14898
  requireCliMutation("storage_pull", options.approvalToken, { resourceId: cliResourceId("storage-pull", tables.join(",")), args: { tables } });
14766
- const results = await storagePull2({ tables });
14899
+ const results = await storagePull3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
14767
14900
  printStorageResults(results, options.json);
14768
14901
  } catch (error) {
14769
14902
  printStorageError(error);
@@ -14771,10 +14904,10 @@ storageCommand.command("pull").description("Pull machine runtime data from stora
14771
14904
  });
14772
14905
  storageCommand.command("sync").description("Bidirectional storage sync: pull then push").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
14773
14906
  try {
14774
- const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storageSync: storageSync2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14907
+ const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storageSync: storageSync3 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14775
14908
  const tables = resolveTables2(parseStorageTables2(options.tables));
14776
14909
  requireCliMutation("storage_sync", options.approvalToken, { resourceId: cliResourceId("storage-sync", tables.join(",")), args: { tables } });
14777
- const result = await storageSync2({ tables });
14910
+ const result = await storageSync3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
14778
14911
  if (options.json) {
14779
14912
  console.log(JSON.stringify(result, null, 2));
14780
14913
  return;