@hasna/machines 0.0.48 → 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
@@ -7432,378 +7774,120 @@ function readManifestWithSource(options = {}) {
7432
7774
  const manifest2 = options.adapter.readManifest({ source, rawRef });
7433
7775
  if (manifest2) {
7434
7776
  return {
7435
- manifest: fleetSchema.parse(manifest2),
7436
- info: {
7437
- source,
7438
- loadedFrom: "private-ref",
7439
- warnings
7440
- }
7441
- };
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;
7777
+ manifest: fleetSchema.parse(manifest2),
7778
+ info: {
7779
+ source,
7780
+ loadedFrom: "private-ref",
7781
+ warnings
7782
+ }
7783
+ };
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;
7806
7882
  }
7883
+ function manifestValidate() {
7884
+ return validateManifest(getManifestPath());
7885
+ }
7886
+
7887
+ // src/commands/setup.ts
7888
+ import { homedir as homedir2 } from "os";
7889
+ init_db();
7890
+ init_mutation_approval();
7807
7891
 
7808
7892
  // src/remote.ts
7809
7893
  init_db();
@@ -9810,6 +9894,7 @@ function diffMachines(leftMachineId, rightMachineId) {
9810
9894
  }
9811
9895
 
9812
9896
  // src/commands/apps.ts
9897
+ init_mutation_approval();
9813
9898
  function getPackageName(app) {
9814
9899
  return app.packageName || app.name;
9815
9900
  }
@@ -9950,6 +10035,7 @@ function runAppsPlan(plan, options = {}, runner = runMachineCommand) {
9950
10035
  }
9951
10036
 
9952
10037
  // src/commands/install-claude.ts
10038
+ init_mutation_approval();
9953
10039
  var AI_CLI_PACKAGES = {
9954
10040
  claude: "@anthropic-ai/claude-code",
9955
10041
  codex: "@openai/codex",
@@ -10057,6 +10143,7 @@ function runClaudeInstallPlan(plan, options = {}, runner = runMachineCommand) {
10057
10143
  }
10058
10144
 
10059
10145
  // src/commands/install-tailscale.ts
10146
+ init_mutation_approval();
10060
10147
  function buildInstallSteps2(machine) {
10061
10148
  if (machine.platform === "macos") {
10062
10149
  return [
@@ -10124,6 +10211,7 @@ function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand)
10124
10211
  import { accessSync, constants, existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
10125
10212
  import { delimiter, isAbsolute, join as join7 } from "path";
10126
10213
  init_paths();
10214
+ init_mutation_approval();
10127
10215
  var notificationChannelSchema = exports_external.object({
10128
10216
  id: exports_external.string(),
10129
10217
  type: exports_external.enum(["email", "webhook", "command"]),
@@ -10467,9 +10555,20 @@ import { EventsClient } from "@hasna/events";
10467
10555
  function shellQuote5(value) {
10468
10556
  return `'${value.replace(/'/g, "'\\''")}'`;
10469
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
+ }
10470
10568
  function buildTmuxPaneDiedHookPlan(options = {}) {
10471
10569
  const tmuxCommand = options.tmuxCommand ?? process.env["HASNA_MACHINES_TMUX_BIN"] ?? "tmux";
10472
10570
  const machinesCommand = options.machinesCommand ?? "machines";
10571
+ assertMachinesCommandSafe(machinesCommand);
10473
10572
  const deliver = options.deliver === true;
10474
10573
  const approvalToken = options.approvalToken?.trim();
10475
10574
  const trustedLocalMutation = approvalToken ? false : options.trustedLocalMutation === true;
@@ -10710,6 +10809,7 @@ import { existsSync as existsSync9, lstatSync, readFileSync as readFileSync7, sy
10710
10809
  import { homedir as homedir5 } from "os";
10711
10810
  init_paths();
10712
10811
  init_db();
10812
+ init_mutation_approval();
10713
10813
  function quote4(value) {
10714
10814
  return `'${value.replace(/'/g, `'\\''`)}'`;
10715
10815
  }
@@ -11494,6 +11594,9 @@ function runDoctor(machineId, options = {}) {
11494
11594
  };
11495
11595
  }
11496
11596
 
11597
+ // src/cli/index.ts
11598
+ init_mutation_approval();
11599
+
11497
11600
  // src/commands/daemon.ts
11498
11601
  import { execFileSync } from "child_process";
11499
11602
  import { chmodSync, existsSync as existsSync10, readFileSync as readFileSync8, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync5 } from "fs";
@@ -12053,6 +12156,7 @@ function getAgentStatus(machineId, options = {}) {
12053
12156
  }
12054
12157
 
12055
12158
  // src/commands/serve.ts
12159
+ init_mutation_approval();
12056
12160
  function escapeHtml(value) {
12057
12161
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
12058
12162
  }
@@ -12512,6 +12616,16 @@ function startDashboardServer(options = {}) {
12512
12616
  function check(id, status, summary, detail) {
12513
12617
  return { id, status, summary, detail };
12514
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
+ }
12515
12629
  function runSelfTest() {
12516
12630
  const version = getPackageVersion();
12517
12631
  const status = getStatus();
@@ -12523,19 +12637,21 @@ function runSelfTest() {
12523
12637
  const apps = listApps(machineId);
12524
12638
  const appsDiff = diffApps(machineId);
12525
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
+ ];
12526
12651
  return {
12527
12652
  machineId,
12528
- checks: [
12529
- check("package-version", version === "0.0.0" ? "fail" : "ok", "Package version resolves", version),
12530
- check("status", "ok", "Status loads", JSON.stringify({ machines: status.manifestMachineCount, heartbeats: status.heartbeatCount })),
12531
- check("doctor", doctor.checks.some((entry) => entry.status === "fail") ? "warn" : "ok", "Doctor completed", `${doctor.checks.length} checks`),
12532
- check("serve-info", "ok", "Dashboard info renders", `${serveInfo.url} routes=${serveInfo.routes.length}`),
12533
- check("dashboard-html", html.includes("Machines Dashboard") ? "ok" : "fail", "Dashboard HTML renders", html.slice(0, 80)),
12534
- check("notifications", "ok", "Notifications config loads", `${notifications.channels.length} channels`),
12535
- check("apps", "ok", "Apps manifest loads", `${apps.apps.length} apps`),
12536
- check("apps-diff", appsDiff.missing.length === 0 ? "ok" : "warn", "Apps diff computed", `missing=${appsDiff.missing.length} installed=${appsDiff.installed.length}`),
12537
- check("install-claude-plan", cliPlan.steps.length > 0 ? "ok" : "warn", "Install plan renders", `${cliPlan.steps.length} steps`)
12538
- ]
12653
+ ...summarize(checks),
12654
+ checks
12539
12655
  };
12540
12656
  }
12541
12657
 
@@ -13632,6 +13748,7 @@ function renderDoctorResult(report) {
13632
13748
  function renderSelfTestResult(result) {
13633
13749
  return [
13634
13750
  `machine: ${result.machineId}`,
13751
+ `overall: ${result.overall} ok=${result.counts.ok} warn=${result.counts.warn} fail=${result.counts.fail}`,
13635
13752
  ...result.checks.map((check2) => {
13636
13753
  const status = check2.status === "ok" ? source_default.green(check2.status) : check2.status === "warn" ? source_default.yellow(check2.status) : source_default.red(check2.status);
13637
13754
  return `${check2.id.padEnd(20)} ${status} ${check2.detail}`;
@@ -14765,10 +14882,10 @@ storageCommand.command("status").description("Show storage sync status").option(
14765
14882
  });
14766
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) => {
14767
14884
  try {
14768
- 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));
14769
14886
  const tables = resolveTables2(parseStorageTables2(options.tables));
14770
14887
  requireCliMutation("storage_push", options.approvalToken, { resourceId: cliResourceId("storage-push", tables.join(",")), args: { tables } });
14771
- const results = await storagePush2({ tables });
14888
+ const results = await storagePush3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
14772
14889
  printStorageResults(results, options.json);
14773
14890
  } catch (error) {
14774
14891
  printStorageError(error);
@@ -14776,10 +14893,10 @@ storageCommand.command("push").description("Push local machine runtime data to s
14776
14893
  });
14777
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) => {
14778
14895
  try {
14779
- 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));
14780
14897
  const tables = resolveTables2(parseStorageTables2(options.tables));
14781
14898
  requireCliMutation("storage_pull", options.approvalToken, { resourceId: cliResourceId("storage-pull", tables.join(",")), args: { tables } });
14782
- const results = await storagePull2({ tables });
14899
+ const results = await storagePull3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
14783
14900
  printStorageResults(results, options.json);
14784
14901
  } catch (error) {
14785
14902
  printStorageError(error);
@@ -14787,10 +14904,10 @@ storageCommand.command("pull").description("Pull machine runtime data from stora
14787
14904
  });
14788
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) => {
14789
14906
  try {
14790
- 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));
14791
14908
  const tables = resolveTables2(parseStorageTables2(options.tables));
14792
14909
  requireCliMutation("storage_sync", options.approvalToken, { resourceId: cliResourceId("storage-sync", tables.join(",")), args: { tables } });
14793
- const result = await storageSync2({ tables });
14910
+ const result = await storageSync3({ tables, trustedLocalMutation: createTrustedSdkMutationApproval() });
14794
14911
  if (options.json) {
14795
14912
  console.log(JSON.stringify(result, null, 2));
14796
14913
  return;