@node9/proxy 2.34.0 → 2.35.0

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/index.mjs CHANGED
@@ -12934,6 +12934,9 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
12934
12934
  ...transcriptPathField,
12935
12935
  ...taintFields,
12936
12936
  ...exposureFields,
12937
+ ...meta?.decidedBy && { decidedBy: meta.decidedBy },
12938
+ ...meta?.channel && { channel: meta.channel },
12939
+ ...meta?.cardId && { cardId: meta.cardId },
12937
12940
  ...testRun,
12938
12941
  agent: meta?.agent,
12939
12942
  mcpServer: meta?.mcpServer,
@@ -15310,9 +15313,159 @@ function getPersistentDecision(toolName) {
15310
15313
 
15311
15314
  // src/auth/daemon.ts
15312
15315
  import fs10 from "fs";
15316
+ import http from "http";
15313
15317
  import net from "net";
15314
15318
  import path10 from "path";
15315
15319
  import os8 from "os";
15320
+
15321
+ // src/auth/decision-record.ts
15322
+ var CHANNELS = ["terminal", "monitor", "native", "cloud"];
15323
+ var PERSON_CHANNELS = /* @__PURE__ */ new Set([
15324
+ "terminal",
15325
+ "monitor",
15326
+ "native"
15327
+ ]);
15328
+ var CARD_PROTOCOL = 2;
15329
+ var PERSON_LABELS = {
15330
+ terminal: "User Decision (Terminal (node9 tail))",
15331
+ monitor: "User Decision (node9 monitor)",
15332
+ native: "User Decision (Native)"
15333
+ };
15334
+ var REDIRECT_LABEL = "Steered Redirect (Terminal)";
15335
+ var TIMER_LABEL = "Approval Timeout";
15336
+ var CLOUD_LABEL = "Organization Policy (SaaS)";
15337
+ var ABANDONED_LABEL = "No Approver (node9 tail closed)";
15338
+ function personRecord(channel, decision, extra = {}, now = Date.now()) {
15339
+ return {
15340
+ decision,
15341
+ decidedBy: "person",
15342
+ channel,
15343
+ label: extra.redirect ? REDIRECT_LABEL : PERSON_LABELS[channel],
15344
+ ...extra,
15345
+ decidedAt: now
15346
+ };
15347
+ }
15348
+ function timerRecord(timeoutMs, now = Date.now()) {
15349
+ return {
15350
+ decision: "deny",
15351
+ decidedBy: "timer",
15352
+ label: TIMER_LABEL,
15353
+ reason: `No response within ${Math.round(timeoutMs / 1e3)}s: auto-denied by the approval timeout.`,
15354
+ decidedAt: now
15355
+ };
15356
+ }
15357
+ function abandonedRecord(now = Date.now()) {
15358
+ return {
15359
+ decision: "deny",
15360
+ decidedBy: "abandoned",
15361
+ label: ABANDONED_LABEL,
15362
+ reason: "No approver was left to answer: node9 tail was closed.",
15363
+ decidedAt: now
15364
+ };
15365
+ }
15366
+ var DECIDED_BY = /* @__PURE__ */ new Set([
15367
+ "person",
15368
+ "timer",
15369
+ "cloud",
15370
+ "policy",
15371
+ "abandoned"
15372
+ ]);
15373
+ var STANDING = /* @__PURE__ */ new Set([
15374
+ "always-allow",
15375
+ "always-deny",
15376
+ "trust",
15377
+ "trust-recipients"
15378
+ ]);
15379
+ function boundedString(v, max) {
15380
+ return typeof v === "string" && v.length > 0 ? v.slice(0, max) : void 0;
15381
+ }
15382
+ function parseDecisionRecord(x) {
15383
+ if (!x || typeof x !== "object") return null;
15384
+ const r = x;
15385
+ if (r.decision !== "allow" && r.decision !== "deny") return null;
15386
+ if (typeof r.decidedBy !== "string" || !DECIDED_BY.has(r.decidedBy)) return null;
15387
+ const label2 = boundedString(r.label, 200);
15388
+ if (!label2) return null;
15389
+ const channel = typeof r.channel === "string" && CHANNELS.includes(r.channel) ? r.channel : void 0;
15390
+ if (r.decidedBy === "person" && (!channel || !PERSON_CHANNELS.has(channel))) return null;
15391
+ const out = {
15392
+ decision: r.decision,
15393
+ decidedBy: r.decidedBy,
15394
+ label: label2,
15395
+ decidedAt: typeof r.decidedAt === "number" && Number.isFinite(r.decidedAt) ? r.decidedAt : 0
15396
+ };
15397
+ if (channel) out.channel = channel;
15398
+ const redirect = boundedString(r.redirect, 2e3);
15399
+ if (redirect) out.redirect = redirect;
15400
+ const reason = boundedString(r.reason, 2e3);
15401
+ if (reason) out.reason = reason;
15402
+ const ruleName = boundedString(r.ruleName, 256);
15403
+ if (ruleName) out.ruleName = ruleName;
15404
+ if (typeof r.standing === "string" && STANDING.has(r.standing))
15405
+ out.standing = r.standing;
15406
+ const trustDuration = boundedString(r.trustDuration, 8);
15407
+ if (trustDuration) out.trustDuration = trustDuration;
15408
+ return out;
15409
+ }
15410
+ function legacyWaitFields(record) {
15411
+ switch (record.decidedBy) {
15412
+ case "person": {
15413
+ const source = record.redirect ? "terminal-redirect" : record.channel === "native" ? "native" : "terminal";
15414
+ const reason = record.redirect ?? record.reason;
15415
+ return { decision: record.decision, source, ...reason && { reason } };
15416
+ }
15417
+ case "timer":
15418
+ return {
15419
+ decision: record.decision,
15420
+ source: "timeout",
15421
+ ...record.reason && { reason: record.reason }
15422
+ };
15423
+ case "cloud":
15424
+ case "policy":
15425
+ return {
15426
+ decision: record.decision,
15427
+ source: record.decidedBy,
15428
+ ...record.reason && { reason: record.reason }
15429
+ };
15430
+ case "abandoned":
15431
+ return { decision: "abandoned" };
15432
+ }
15433
+ }
15434
+ function recordFromLegacyWait(body, now = Date.now()) {
15435
+ if (body.decision === "abandoned") return abandonedRecord(now);
15436
+ const decision = body.decision;
15437
+ switch (body.source) {
15438
+ case "terminal":
15439
+ case "browser":
15440
+ return personRecord("terminal", decision, body.reason ? { reason: body.reason } : {}, now);
15441
+ case "native":
15442
+ return personRecord("native", decision, {}, now);
15443
+ case "terminal-redirect":
15444
+ return personRecord("terminal", decision, body.reason ? { redirect: body.reason } : {}, now);
15445
+ case "timeout":
15446
+ return { ...timerRecord(0, now), ...body.reason && { reason: body.reason } };
15447
+ case "cloud":
15448
+ return {
15449
+ decision,
15450
+ decidedBy: "cloud",
15451
+ label: CLOUD_LABEL,
15452
+ ...body.reason && { reason: body.reason },
15453
+ decidedAt: now
15454
+ };
15455
+ case "policy":
15456
+ return {
15457
+ decision,
15458
+ decidedBy: "policy",
15459
+ label: "Policy",
15460
+ ...body.reason && { reason: body.reason },
15461
+ decidedAt: now
15462
+ };
15463
+ default:
15464
+ return null;
15465
+ }
15466
+ }
15467
+
15468
+ // src/auth/daemon.ts
15316
15469
  var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os8.tmpdir(), "node9-activity.sock");
15317
15470
  function notifyActivitySocket(data) {
15318
15471
  return new Promise((resolve) => {
@@ -15410,7 +15563,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
15410
15563
  return false;
15411
15564
  }
15412
15565
  }
15413
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
15566
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId, card) {
15414
15567
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
15415
15568
  const ctrl = new AbortController();
15416
15569
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -15436,19 +15589,43 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
15436
15589
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
15437
15590
  ...viewOnly && { viewOnly: true },
15438
15591
  ...localSmartRuleMatched && { localSmartRuleMatched: true },
15439
- ...sessionId && { sessionId }
15592
+ ...sessionId && { sessionId },
15593
+ ...cardFields(card)
15440
15594
  }),
15441
15595
  signal: ctrl.signal
15442
15596
  });
15443
15597
  if (!res.ok) throw new Error("Daemon fail");
15444
- const { id, allowCount } = await res.json();
15445
- return { id, allowCount: allowCount ?? 1 };
15598
+ return registeredCard(await res.json());
15446
15599
  } finally {
15447
15600
  clearTimeout(timer);
15448
15601
  }
15449
15602
  }
15603
+ function cardFields(card) {
15604
+ return {
15605
+ // A review card: the hook already decided this call needs a person; the
15606
+ // daemon never checks it again (approval-card design, defect 1).
15607
+ kind: "review",
15608
+ // Protocol 2 makes the card's /wait require the internal token: claim it
15609
+ // only when this process can read the token (a daemon under another uid
15610
+ // is running but its pid file probe fails with EPERM), or every wait
15611
+ // would get 403 and the review would be denied unseen.
15612
+ ...getInternalToken() && { protocol: CARD_PROTOCOL },
15613
+ ...card?.channels && { channels: card.channels },
15614
+ ...typeof card?.timeoutMs === "number" && { timeoutMs: card.timeoutMs }
15615
+ };
15616
+ }
15617
+ function registeredCard(body) {
15618
+ const b = body;
15619
+ return {
15620
+ id: b.id,
15621
+ allowCount: b.allowCount ?? 1,
15622
+ ...b.protocol === CARD_PROTOCOL && {
15623
+ protocol: CARD_PROTOCOL,
15624
+ deadline: typeof b.deadline === "number" ? b.deadline : null
15625
+ }
15626
+ };
15627
+ }
15450
15628
  async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
15451
- const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
15452
15629
  const waitCtrl = new AbortController();
15453
15630
  let capped = false;
15454
15631
  const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
@@ -15458,14 +15635,18 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
15458
15635
  const onAbort = () => waitCtrl.abort();
15459
15636
  if (signal) signal.addEventListener("abort", onAbort);
15460
15637
  try {
15461
- const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal }).catch(
15638
+ const { status, body } = await daemonGet(`/wait/${id}`, waitCtrl.signal).catch(
15462
15639
  (err) => {
15463
15640
  if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
15464
15641
  throw err;
15465
15642
  }
15466
15643
  );
15467
- if (!waitRes.ok) return { decision: "deny" };
15468
- const { decision, source, reason } = await waitRes.json();
15644
+ if (status === 409) throw new Error("node9 card was withdrawn");
15645
+ if (status !== 200) return { decision: "deny" };
15646
+ const parsed = JSON.parse(body);
15647
+ const record = parseDecisionRecord(parsed.record);
15648
+ if (record) return { ...legacyWaitFields(record), record };
15649
+ const { decision, source, reason } = parsed;
15469
15650
  if (decision === "allow") return { decision: "allow", source };
15470
15651
  if (decision === "abandoned") return { decision: "abandoned", source };
15471
15652
  return { decision: "deny", source, reason };
@@ -15474,7 +15655,29 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
15474
15655
  if (signal) signal.removeEventListener("abort", onAbort);
15475
15656
  }
15476
15657
  }
15477
- async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent) {
15658
+ function daemonGet(urlPath, signal, token = getInternalToken()) {
15659
+ return new Promise((resolve, reject) => {
15660
+ const req = http.get(
15661
+ {
15662
+ host: DAEMON_HOST,
15663
+ port: DAEMON_PORT,
15664
+ path: urlPath,
15665
+ signal,
15666
+ agent: false,
15667
+ headers: token ? { "X-Node9-Internal": token } : {}
15668
+ },
15669
+ (res) => {
15670
+ let body = "";
15671
+ res.setEncoding("utf8");
15672
+ res.on("data", (chunk) => body += chunk);
15673
+ res.on("end", () => resolve({ status: res.statusCode ?? 0, body }));
15674
+ res.on("error", reject);
15675
+ }
15676
+ );
15677
+ req.on("error", reject);
15678
+ });
15679
+ }
15680
+ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent, extra) {
15478
15681
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
15479
15682
  const res = await fetch(`${base2}/check`, {
15480
15683
  method: "POST",
@@ -15490,13 +15693,16 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
15490
15693
  // event via socket. Same contract as registerDaemonEntry — without
15491
15694
  // it the daemon double-emits 'activity' for cloud-enforced flows.
15492
15695
  fromCLI: socketActivitySent !== false,
15493
- activityId
15696
+ activityId,
15697
+ ...extra?.cwd && { cwd: extra.cwd },
15698
+ ...extra?.recoveryCommand && { recoveryCommand: extra.recoveryCommand },
15699
+ ...extra?.sessionId && { sessionId: extra.sessionId },
15700
+ ...cardFields(extra?.card)
15494
15701
  }),
15495
15702
  signal: AbortSignal.timeout(3e3)
15496
15703
  });
15497
15704
  if (!res.ok) throw new Error("Daemon unreachable");
15498
- const { id, allowCount } = await res.json();
15499
- return { id, allowCount: allowCount ?? 1 };
15705
+ return registeredCard(await res.json());
15500
15706
  }
15501
15707
  async function notifyTaint(filePath, source, fromEid) {
15502
15708
  if (!isDaemonRunning()) return;
@@ -15589,14 +15795,18 @@ async function approveSessionExposure(sessionId, keys) {
15589
15795
  } catch {
15590
15796
  }
15591
15797
  }
15592
- async function resolveViaDaemon(id, decision, internalToken, source) {
15798
+ async function resolveViaDaemon(id, decision, internalToken, source, record) {
15593
15799
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
15594
- await fetch(`${base2}/resolve/${id}`, {
15800
+ const res = await fetch(`${base2}/resolve/${id}`, {
15595
15801
  method: "POST",
15596
15802
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
15597
- body: JSON.stringify({ decision, ...source && { source } }),
15803
+ body: JSON.stringify({ decision, ...source && { source }, ...record && { record } }),
15598
15804
  signal: AbortSignal.timeout(3e3)
15599
15805
  });
15806
+ if (!res.ok) return null;
15807
+ const body = await res.json().catch(() => null);
15808
+ const winner = parseDecisionRecord(body?.record);
15809
+ return winner && typeof body?.won === "boolean" ? { won: body.won, record: winner } : null;
15600
15810
  }
15601
15811
 
15602
15812
  // src/auth/orchestrator.ts
@@ -17190,17 +17400,98 @@ function asApprovalRequest(description) {
17190
17400
  return description;
17191
17401
  }
17192
17402
 
17193
- // src/auth/decision-sources.ts
17194
- var HUMAN_DECISION_SOURCES = /* @__PURE__ */ new Set([
17195
- "terminal",
17196
- "browser",
17197
- "native"
17198
- ]);
17199
- var REDIRECT_SOURCE = "terminal-redirect";
17200
- var DECISION_ROUTE_SOURCES = /* @__PURE__ */ new Set([
17201
- ...HUMAN_DECISION_SOURCES,
17202
- REDIRECT_SOURCE
17203
- ]);
17403
+ // src/auth/card-result.ts
17404
+ var CHANNEL_NAMES = {
17405
+ terminal: "Terminal (node9 tail)",
17406
+ monitor: "node9 monitor",
17407
+ native: "system dialog window",
17408
+ cloud: "organization"
17409
+ };
17410
+ function authResultFromRecord(r, ctx = {}) {
17411
+ const who = { decidedBy: r.decidedBy, ...r.channel && { channel: r.channel } };
17412
+ switch (r.decidedBy) {
17413
+ case "person": {
17414
+ const channel = r.channel ?? "terminal";
17415
+ const decisionSource = channel === "native" ? "native" : "terminal";
17416
+ if (r.decision === "allow") {
17417
+ return { approved: true, checkedBy: "daemon", decisionSource, ...who };
17418
+ }
17419
+ return {
17420
+ approved: false,
17421
+ reason: r.redirect || `The human user rejected this action via the Node9 ${CHANNEL_NAMES[channel]}.`,
17422
+ blockedBy: "local-decision",
17423
+ blockedByLabel: r.label,
17424
+ decisionSource,
17425
+ ...who,
17426
+ ...r.redirect && { redirect: r.redirect }
17427
+ };
17428
+ }
17429
+ case "timer":
17430
+ if (r.decision === "deny" && ctx.downgradedBlock)
17431
+ return { ...ctx.downgradedBlock, blockedBy: "timeout", ...who };
17432
+ return r.decision === "allow" ? { approved: true, ...who } : {
17433
+ approved: false,
17434
+ reason: ctx.timeoutReason || r.reason || "No response: auto-denied after the approval timeout.",
17435
+ blockedBy: "timeout",
17436
+ blockedByLabel: TIMER_LABEL,
17437
+ ...who
17438
+ };
17439
+ case "cloud":
17440
+ return r.decision === "allow" ? { approved: true, checkedBy: "cloud", decisionSource: "cloud", ...who } : {
17441
+ approved: false,
17442
+ reason: r.reason || "Action rejected by organization administrator via Slack.",
17443
+ blockedBy: "team-policy",
17444
+ blockedByLabel: CLOUD_LABEL,
17445
+ decisionSource: "cloud",
17446
+ ...who
17447
+ };
17448
+ case "policy":
17449
+ return r.decision === "allow" ? { approved: true, checkedBy: "local-policy", ...who } : {
17450
+ approved: false,
17451
+ reason: r.reason || "Action blocked by policy.",
17452
+ blockedBy: "local-config",
17453
+ blockedByLabel: r.label,
17454
+ ...r.ruleName && { ruleHit: r.ruleName },
17455
+ ...who
17456
+ };
17457
+ case "abandoned":
17458
+ return {
17459
+ approved: false,
17460
+ reason: r.reason || "No approver was left to answer.",
17461
+ blockedBy: "no-approval-mechanism",
17462
+ blockedByLabel: ABANDONED_LABEL,
17463
+ ...who
17464
+ };
17465
+ }
17466
+ }
17467
+ function approvalUnavailable(reason) {
17468
+ return {
17469
+ approved: false,
17470
+ reason: reason || "node9 could not get an approval decision for this call.",
17471
+ blockedBy: "no-approval-mechanism",
17472
+ blockedByLabel: "Approval Unavailable"
17473
+ };
17474
+ }
17475
+ function decidedByOf(res) {
17476
+ if (res.decidedBy) return res.decidedBy;
17477
+ if (res.blockedBy === "timeout") return "timer";
17478
+ if (res.checkedBy === "cloud" || res.blockedBy === "team-policy") return "cloud";
17479
+ return "policy";
17480
+ }
17481
+ function recordFromAuthResult(res, now = Date.now()) {
17482
+ const decidedBy = decidedByOf(res);
17483
+ const channel = res.channel ?? (decidedBy === "cloud" ? "cloud" : decidedBy === "person" ? "native" : void 0);
17484
+ return {
17485
+ decision: res.approved ? "allow" : "deny",
17486
+ decidedBy,
17487
+ ...channel && { channel },
17488
+ label: res.blockedByLabel || (decidedBy === "cloud" ? CLOUD_LABEL : decidedBy === "timer" ? TIMER_LABEL : res.approved ? "Allowed" : "Blocked"),
17489
+ ...res.reason && { reason: res.reason.slice(0, 2e3) },
17490
+ ...res.ruleHit && { ruleName: res.ruleHit },
17491
+ ...res.redirect && { redirect: res.redirect },
17492
+ decidedAt: now
17493
+ };
17494
+ }
17204
17495
 
17205
17496
  // src/auth/orchestrator.ts
17206
17497
  var SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
@@ -18240,28 +18531,22 @@ ${packageReview}`;
18240
18531
  const { signal } = abortController;
18241
18532
  const racePromises = [];
18242
18533
  const approvalTimeoutMs = config.settings.approvalTimeoutMs ?? 0;
18243
- if (approvalTimeoutMs > 0) {
18244
- racePromises.push(
18245
- new Promise((resolve, reject) => {
18246
- const timer = setTimeout(() => {
18247
- resolve({
18248
- approved: false,
18249
- reason: approvalTimeoutReason(approvalTimeoutMs),
18250
- blockedBy: "timeout",
18251
- blockedByLabel: "Approval Timeout"
18252
- });
18253
- }, approvalTimeoutMs);
18254
- signal.addEventListener("abort", () => {
18255
- clearTimeout(timer);
18256
- reject(new Error("Aborted"));
18257
- });
18258
- })
18259
- );
18260
- }
18261
18534
  let viewerId = null;
18262
18535
  const internalToken = getInternalToken();
18263
18536
  let daemonEntryId = null;
18264
18537
  let daemonAllowCount = 1;
18538
+ let cardDeadline;
18539
+ const cardOptions = {
18540
+ channels: [
18541
+ "terminal",
18542
+ "monitor",
18543
+ // Only when the popup can show: a card listing a channel nobody can
18544
+ // use is never abandoned when node9 tail closes, and waits unanswered.
18545
+ ...approvers.native && !isManual && !options?.calledFromDaemon && (process.platform === "darwin" || process.platform === "win32" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) ? ["native"] : [],
18546
+ ...cloudEnforced && cloudRequestId && cloudMayVote ? ["cloud"] : []
18547
+ ],
18548
+ timeoutMs: approvalTimeoutMs
18549
+ };
18265
18550
  if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
18266
18551
  if (cloudEnforced && cloudRequestId) {
18267
18552
  const viewer = await notifyDaemonViewer(
@@ -18270,11 +18555,20 @@ ${packageReview}`;
18270
18555
  meta,
18271
18556
  riskMetadata,
18272
18557
  options?.activityId,
18273
- options?.socketActivitySent
18558
+ options?.socketActivitySent,
18559
+ {
18560
+ cwd: options?.cwd,
18561
+ recoveryCommand: statefulRecoveryCommand,
18562
+ sessionId: meta?.sessionId,
18563
+ card: cardOptions
18564
+ }
18274
18565
  ).catch(() => null);
18275
18566
  viewerId = viewer?.id ?? null;
18276
18567
  daemonEntryId = viewerId;
18277
- if (viewer) daemonAllowCount = viewer.allowCount;
18568
+ if (viewer) {
18569
+ daemonAllowCount = viewer.allowCount;
18570
+ cardDeadline = viewer.deadline;
18571
+ }
18278
18572
  } else {
18279
18573
  try {
18280
18574
  const entry = await registerDaemonEntry(
@@ -18298,14 +18592,35 @@ ${packageReview}`;
18298
18592
  void 0,
18299
18593
  localSmartRuleMatched || options?.localSmartRuleMatched,
18300
18594
  options?.socketActivitySent,
18301
- meta?.sessionId
18595
+ meta?.sessionId,
18596
+ cardOptions
18302
18597
  );
18303
18598
  daemonEntryId = entry.id;
18304
18599
  daemonAllowCount = entry.allowCount;
18600
+ cardDeadline = entry.deadline;
18305
18601
  } catch {
18306
18602
  }
18307
18603
  }
18308
18604
  }
18605
+ if (approvalTimeoutMs > 0 && cardDeadline === void 0) {
18606
+ racePromises.push(
18607
+ new Promise((resolve, reject) => {
18608
+ const timer = setTimeout(() => {
18609
+ resolve({
18610
+ approved: false,
18611
+ reason: approvalTimeoutReason(approvalTimeoutMs),
18612
+ blockedBy: "timeout",
18613
+ blockedByLabel: "Approval Timeout",
18614
+ decidedBy: "timer"
18615
+ });
18616
+ }, approvalTimeoutMs);
18617
+ signal.addEventListener("abort", () => {
18618
+ clearTimeout(timer);
18619
+ reject(new Error("Aborted"));
18620
+ });
18621
+ })
18622
+ );
18623
+ }
18309
18624
  if (cloudEnforced && cloudRequestId && cloudMayVote) {
18310
18625
  racePromises.push(
18311
18626
  (async () => {
@@ -18316,7 +18631,9 @@ ${packageReview}`;
18316
18631
  reason: cloudResult.approved ? void 0 : cloudResult.reason || "Action rejected by organization administrator via Slack.",
18317
18632
  checkedBy: cloudResult.approved ? "cloud" : void 0,
18318
18633
  blockedBy: cloudResult.approved ? void 0 : "team-policy",
18319
- blockedByLabel: "Organization Policy (SaaS)"
18634
+ blockedByLabel: "Organization Policy (SaaS)",
18635
+ decidedBy: "cloud",
18636
+ channel: "cloud"
18320
18637
  };
18321
18638
  } catch (err) {
18322
18639
  const error = err;
@@ -18346,14 +18663,31 @@ ${packageReview}`;
18346
18663
  if (config.policySource !== "workspace") {
18347
18664
  for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
18348
18665
  }
18349
- return { approved: true, checkedBy: "daemon", decisionSource: "native" };
18666
+ return {
18667
+ approved: true,
18668
+ checkedBy: "daemon",
18669
+ decisionSource: "native",
18670
+ decidedBy: "person",
18671
+ channel: "native"
18672
+ };
18350
18673
  }
18351
18674
  if (decision === "always_allow") {
18352
18675
  if (standingTrustApplies()) {
18353
18676
  writeTrustSession(toolName, 36e5, args);
18354
- return { approved: true, checkedBy: "trust" };
18677
+ return {
18678
+ approved: true,
18679
+ checkedBy: "trust",
18680
+ decidedBy: "person",
18681
+ channel: "native"
18682
+ };
18355
18683
  }
18356
- return { approved: true, checkedBy: "daemon", decisionSource: "native" };
18684
+ return {
18685
+ approved: true,
18686
+ checkedBy: "daemon",
18687
+ decisionSource: "native",
18688
+ decidedBy: "person",
18689
+ channel: "native"
18690
+ };
18357
18691
  }
18358
18692
  const isApproved = decision === "allow";
18359
18693
  return {
@@ -18362,75 +18696,32 @@ ${packageReview}`;
18362
18696
  checkedBy: isApproved ? "daemon" : void 0,
18363
18697
  blockedBy: isApproved ? void 0 : "local-decision",
18364
18698
  blockedByLabel: "User Decision (Native)",
18365
- decisionSource: "native"
18699
+ decisionSource: "native",
18700
+ decidedBy: "person",
18701
+ channel: "native"
18366
18702
  };
18367
18703
  })()
18368
18704
  );
18369
18705
  }
18706
+ const fromCard = /* @__PURE__ */ new WeakSet();
18707
+ const timeoutReason = approvalTimeoutMs > 0 ? approvalTimeoutReason(approvalTimeoutMs) : void 0;
18370
18708
  if (daemonEntryId && approvers.terminal) {
18371
18709
  racePromises.push(
18372
18710
  (async () => {
18373
- const {
18374
- decision: daemonDecision,
18375
- source: decisionSource,
18376
- reason: daemonReason
18377
- } = await waitForDaemonDecision(
18378
- daemonEntryId,
18379
- signal,
18380
- daemonWaitCapMs(config.settings.approvalTimeoutMs)
18381
- );
18382
- if (daemonDecision === "abandoned") throw new Error("Abandoned");
18383
- const isApproved = daemonDecision === "allow";
18384
- const isRedirect = decisionSource === REDIRECT_SOURCE;
18385
- if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
18386
- if (decisionSource === "cloud")
18387
- return {
18388
- approved: false,
18389
- reason: daemonReason || "Action rejected by organization administrator via Slack.",
18390
- blockedBy: "team-policy",
18391
- blockedByLabel: "Organization Policy (SaaS)",
18392
- decisionSource: "cloud"
18393
- };
18394
- const timedOut = decisionSource === "timeout";
18395
- if (downgradedBlock)
18396
- return {
18397
- ...downgradedBlock,
18398
- blockedBy: timedOut ? "timeout" : "local-config"
18399
- };
18400
- if (timedOut)
18401
- return {
18402
- approved: false,
18403
- reason: daemonReason || "No response: auto-denied after the approval timeout.",
18404
- blockedBy: "timeout",
18405
- blockedByLabel: "Approval Timeout"
18406
- };
18407
- if (decisionSource === "policy")
18408
- return {
18409
- approved: false,
18410
- reason: daemonReason || "Action blocked by policy.",
18411
- blockedBy: "local-config",
18412
- blockedByLabel: explainableLabel
18413
- };
18414
- return {
18415
- approved: false,
18416
- reason: daemonReason || "node9 could not get an approval decision for this call.",
18417
- blockedBy: "no-approval-mechanism",
18418
- blockedByLabel: "Approval Unavailable"
18419
- };
18711
+ const cap = cardDeadline === void 0 ? daemonWaitCapMs(config.settings.approvalTimeoutMs) : cardDeadline === null ? 0 : Math.max(cardDeadline - Date.now(), 0) + 5e3;
18712
+ const outcome = await waitForDaemonDecision(daemonEntryId, signal, cap);
18713
+ const legacy = !outcome.record;
18714
+ const record = outcome.record ?? recordFromLegacyWait(outcome);
18715
+ if (downgradedBlock && (!record || legacy && record.decidedBy === "policy" && record.decision === "deny")) {
18716
+ const softened = { ...downgradedBlock, blockedBy: "local-config" };
18717
+ fromCard.add(softened);
18718
+ return softened;
18420
18719
  }
18421
- if (isApproved && decisionSource === "cloud")
18422
- return { approved: true, checkedBy: "cloud", decisionSource: "cloud" };
18423
- if (isApproved && decisionSource === "policy")
18424
- return { approved: true, checkedBy: "local-policy" };
18425
- const via = "Terminal (node9 tail)";
18426
- return {
18427
- approved: isApproved,
18428
- reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
18429
- checkedBy: isApproved ? "daemon" : void 0,
18430
- blockedBy: isApproved ? void 0 : "local-decision",
18431
- blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
18432
- decisionSource: "terminal"
18433
- };
18720
+ if (!record) return approvalUnavailable(outcome.reason);
18721
+ if (legacy && record.decidedBy === "policy") record.label = explainableLabel;
18722
+ const result = authResultFromRecord(record, { downgradedBlock, timeoutReason });
18723
+ fromCard.add(result);
18724
+ return result;
18434
18725
  })()
18435
18726
  );
18436
18727
  }
@@ -18462,7 +18753,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
18462
18753
  blockedByLabel: explainableLabel
18463
18754
  };
18464
18755
  }
18465
- const finalResult = await new Promise((resolve) => {
18756
+ const raceResult = await new Promise((resolve) => {
18466
18757
  let resolved = false;
18467
18758
  let failures = 0;
18468
18759
  const total = racePromises.length;
@@ -18470,14 +18761,6 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
18470
18761
  if (!resolved) {
18471
18762
  resolved = true;
18472
18763
  abortController.abort();
18473
- if (daemonEntryId && internalToken) {
18474
- resolveViaDaemon(
18475
- daemonEntryId,
18476
- res.approved ? "allow" : "deny",
18477
- internalToken,
18478
- res.decisionSource
18479
- ).catch(() => null);
18480
- }
18481
18764
  resolve(res);
18482
18765
  }
18483
18766
  };
@@ -18485,26 +18768,29 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
18485
18768
  p.then(finish).catch((err) => {
18486
18769
  if (err.name === "AbortError" || err.message?.includes("canceled") || err.message?.includes("Aborted"))
18487
18770
  return;
18488
- if (err.message === "Abandoned") {
18489
- finish({
18490
- approved: false,
18491
- reason: "Browser dashboard closed without making a decision.",
18492
- blockedBy: "local-decision",
18493
- blockedByLabel: "Browser Dashboard (Abandoned)"
18494
- });
18495
- return;
18496
- }
18497
18771
  failures++;
18498
18772
  if (failures === total && !resolved) {
18499
- finish({ approved: false, reason: "All approval channels failed or disconnected." });
18773
+ finish(approvalUnavailable("All approval channels failed or disconnected."));
18500
18774
  }
18501
18775
  });
18502
18776
  }
18503
18777
  });
18778
+ let finalResult = raceResult;
18779
+ if (daemonEntryId && internalToken && !fromCard.has(raceResult)) {
18780
+ const reply = await resolveViaDaemon(
18781
+ daemonEntryId,
18782
+ raceResult.approved ? "allow" : "deny",
18783
+ internalToken,
18784
+ raceResult.decisionSource,
18785
+ recordFromAuthResult(raceResult)
18786
+ ).catch(() => null);
18787
+ if (reply && !reply.won)
18788
+ finalResult = authResultFromRecord(reply.record, { downgradedBlock, timeoutReason });
18789
+ }
18504
18790
  if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
18505
18791
  await approveSessionExposure(meta.sessionId, exposureKeys);
18506
18792
  }
18507
- if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
18793
+ if (cloudRequestId && creds && finalResult.checkedBy !== "cloud" && finalResult.decidedBy !== "cloud") {
18508
18794
  await resolveNode9SaaS(
18509
18795
  cloudRequestId,
18510
18796
  creds,
@@ -18528,7 +18814,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
18528
18814
  {
18529
18815
  ...ownRowMeta(finalResult.approved),
18530
18816
  ...cloudRequestId && { cloudRequestId },
18531
- ...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` }
18817
+ ...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` },
18818
+ // Who decided, from the decision record, and the card: the daemon
18819
+ // writes its own row for the same card, and the report counts one.
18820
+ ...finalResult.decidedBy && { decidedBy: finalResult.decidedBy },
18821
+ ...finalResult.channel && { channel: finalResult.channel },
18822
+ ...daemonEntryId && { cardId: daemonEntryId }
18532
18823
  },
18533
18824
  hashAuditArgs
18534
18825
  );