@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/cli.mjs CHANGED
@@ -13762,6 +13762,9 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
13762
13762
  ...transcriptPathField,
13763
13763
  ...taintFields,
13764
13764
  ...exposureFields,
13765
+ ...meta?.decidedBy && { decidedBy: meta.decidedBy },
13766
+ ...meta?.channel && { channel: meta.channel },
13767
+ ...meta?.cardId && { cardId: meta.cardId },
13765
13768
  ...testRun,
13766
13769
  agent: meta?.agent,
13767
13770
  mcpServer: meta?.mcpServer,
@@ -16849,8 +16852,204 @@ var init_state = __esm({
16849
16852
  }
16850
16853
  });
16851
16854
 
16855
+ // src/auth/decision-record.ts
16856
+ function personRecord(channel, decision, extra = {}, now = Date.now()) {
16857
+ return {
16858
+ decision,
16859
+ decidedBy: "person",
16860
+ channel,
16861
+ label: extra.redirect ? REDIRECT_LABEL : PERSON_LABELS[channel],
16862
+ ...extra,
16863
+ decidedAt: now
16864
+ };
16865
+ }
16866
+ function timerRecord(timeoutMs, now = Date.now()) {
16867
+ return {
16868
+ decision: "deny",
16869
+ decidedBy: "timer",
16870
+ label: TIMER_LABEL,
16871
+ reason: `No response within ${Math.round(timeoutMs / 1e3)}s: auto-denied by the approval timeout.`,
16872
+ decidedAt: now
16873
+ };
16874
+ }
16875
+ function abandonedRecord(now = Date.now()) {
16876
+ return {
16877
+ decision: "deny",
16878
+ decidedBy: "abandoned",
16879
+ label: ABANDONED_LABEL,
16880
+ reason: "No approver was left to answer: node9 tail was closed.",
16881
+ decidedAt: now
16882
+ };
16883
+ }
16884
+ function boundedString(v, max) {
16885
+ return typeof v === "string" && v.length > 0 ? v.slice(0, max) : void 0;
16886
+ }
16887
+ function parseDecisionRecord(x) {
16888
+ if (!x || typeof x !== "object") return null;
16889
+ const r = x;
16890
+ if (r.decision !== "allow" && r.decision !== "deny") return null;
16891
+ if (typeof r.decidedBy !== "string" || !DECIDED_BY.has(r.decidedBy)) return null;
16892
+ const label3 = boundedString(r.label, 200);
16893
+ if (!label3) return null;
16894
+ const channel = typeof r.channel === "string" && CHANNELS.includes(r.channel) ? r.channel : void 0;
16895
+ if (r.decidedBy === "person" && (!channel || !PERSON_CHANNELS.has(channel))) return null;
16896
+ const out = {
16897
+ decision: r.decision,
16898
+ decidedBy: r.decidedBy,
16899
+ label: label3,
16900
+ decidedAt: typeof r.decidedAt === "number" && Number.isFinite(r.decidedAt) ? r.decidedAt : 0
16901
+ };
16902
+ if (channel) out.channel = channel;
16903
+ const redirect = boundedString(r.redirect, 2e3);
16904
+ if (redirect) out.redirect = redirect;
16905
+ const reason = boundedString(r.reason, 2e3);
16906
+ if (reason) out.reason = reason;
16907
+ const ruleName = boundedString(r.ruleName, 256);
16908
+ if (ruleName) out.ruleName = ruleName;
16909
+ if (typeof r.standing === "string" && STANDING.has(r.standing))
16910
+ out.standing = r.standing;
16911
+ const trustDuration = boundedString(r.trustDuration, 8);
16912
+ if (trustDuration) out.trustDuration = trustDuration;
16913
+ return out;
16914
+ }
16915
+ function legacyWaitFields(record2) {
16916
+ switch (record2.decidedBy) {
16917
+ case "person": {
16918
+ const source = record2.redirect ? "terminal-redirect" : record2.channel === "native" ? "native" : "terminal";
16919
+ const reason = record2.redirect ?? record2.reason;
16920
+ return { decision: record2.decision, source, ...reason && { reason } };
16921
+ }
16922
+ case "timer":
16923
+ return {
16924
+ decision: record2.decision,
16925
+ source: "timeout",
16926
+ ...record2.reason && { reason: record2.reason }
16927
+ };
16928
+ case "cloud":
16929
+ case "policy":
16930
+ return {
16931
+ decision: record2.decision,
16932
+ source: record2.decidedBy,
16933
+ ...record2.reason && { reason: record2.reason }
16934
+ };
16935
+ case "abandoned":
16936
+ return { decision: "abandoned" };
16937
+ }
16938
+ }
16939
+ function recordFromLegacyWait(body, now = Date.now()) {
16940
+ if (body.decision === "abandoned") return abandonedRecord(now);
16941
+ const decision = body.decision;
16942
+ switch (body.source) {
16943
+ case "terminal":
16944
+ case "browser":
16945
+ return personRecord("terminal", decision, body.reason ? { reason: body.reason } : {}, now);
16946
+ case "native":
16947
+ return personRecord("native", decision, {}, now);
16948
+ case "terminal-redirect":
16949
+ return personRecord("terminal", decision, body.reason ? { redirect: body.reason } : {}, now);
16950
+ case "timeout":
16951
+ return { ...timerRecord(0, now), ...body.reason && { reason: body.reason } };
16952
+ case "cloud":
16953
+ return {
16954
+ decision,
16955
+ decidedBy: "cloud",
16956
+ label: CLOUD_LABEL,
16957
+ ...body.reason && { reason: body.reason },
16958
+ decidedAt: now
16959
+ };
16960
+ case "policy":
16961
+ return {
16962
+ decision,
16963
+ decidedBy: "policy",
16964
+ label: "Policy",
16965
+ ...body.reason && { reason: body.reason },
16966
+ decidedAt: now
16967
+ };
16968
+ default:
16969
+ return null;
16970
+ }
16971
+ }
16972
+ function recordFromVerdict(v, now = Date.now()) {
16973
+ const decidedBy = v.checkedBy === "cloud" || v.blockedBy === "team-policy" ? "cloud" : v.blockedBy === "timeout" ? "timer" : "policy";
16974
+ return {
16975
+ decision: v.approved ? "allow" : "deny",
16976
+ decidedBy,
16977
+ label: v.blockedByLabel || (decidedBy === "cloud" ? CLOUD_LABEL : v.approved ? "Allowed by policy" : "Blocked by policy"),
16978
+ ...v.reason && { reason: v.reason },
16979
+ ...v.ruleHit && { ruleName: v.ruleHit },
16980
+ decidedAt: now
16981
+ };
16982
+ }
16983
+ function channelFromDecisionSource(source) {
16984
+ switch (source) {
16985
+ case void 0:
16986
+ case "terminal":
16987
+ case "browser":
16988
+ return { channel: "terminal", redirect: false };
16989
+ case "terminal-redirect":
16990
+ return { channel: "terminal", redirect: true };
16991
+ case "monitor":
16992
+ case "dashboard":
16993
+ return { channel: "monitor", redirect: false };
16994
+ case "native":
16995
+ return { channel: "native", redirect: false };
16996
+ default:
16997
+ return null;
16998
+ }
16999
+ }
17000
+ function recordFromLegacyResolve(decision, source, now = Date.now()) {
17001
+ const person = source === void 0 ? null : channelFromDecisionSource(source);
17002
+ if (person) return personRecord(person.channel, decision, {}, now);
17003
+ if (decision === "allow")
17004
+ return { decision, decidedBy: "cloud", label: CLOUD_LABEL, decidedAt: now };
17005
+ return {
17006
+ decision,
17007
+ decidedBy: "timer",
17008
+ label: "Resolved by the hook",
17009
+ reason: "An older node9 hook ended the approval without naming who decided.",
17010
+ decidedAt: now
17011
+ };
17012
+ }
17013
+ var CHANNELS, LOCAL_UI_CHANNELS, PERSON_CHANNELS, CARD_PROTOCOL, PERSON_LABELS, REDIRECT_LABEL, TIMER_LABEL, CLOUD_LABEL, ABANDONED_LABEL, DECIDED_BY, STANDING;
17014
+ var init_decision_record = __esm({
17015
+ "src/auth/decision-record.ts"() {
17016
+ "use strict";
17017
+ CHANNELS = ["terminal", "monitor", "native", "cloud"];
17018
+ LOCAL_UI_CHANNELS = /* @__PURE__ */ new Set(["terminal", "monitor"]);
17019
+ PERSON_CHANNELS = /* @__PURE__ */ new Set([
17020
+ "terminal",
17021
+ "monitor",
17022
+ "native"
17023
+ ]);
17024
+ CARD_PROTOCOL = 2;
17025
+ PERSON_LABELS = {
17026
+ terminal: "User Decision (Terminal (node9 tail))",
17027
+ monitor: "User Decision (node9 monitor)",
17028
+ native: "User Decision (Native)"
17029
+ };
17030
+ REDIRECT_LABEL = "Steered Redirect (Terminal)";
17031
+ TIMER_LABEL = "Approval Timeout";
17032
+ CLOUD_LABEL = "Organization Policy (SaaS)";
17033
+ ABANDONED_LABEL = "No Approver (node9 tail closed)";
17034
+ DECIDED_BY = /* @__PURE__ */ new Set([
17035
+ "person",
17036
+ "timer",
17037
+ "cloud",
17038
+ "policy",
17039
+ "abandoned"
17040
+ ]);
17041
+ STANDING = /* @__PURE__ */ new Set([
17042
+ "always-allow",
17043
+ "always-deny",
17044
+ "trust",
17045
+ "trust-recipients"
17046
+ ]);
17047
+ }
17048
+ });
17049
+
16852
17050
  // src/auth/daemon.ts
16853
17051
  import fs11 from "fs";
17052
+ import http from "http";
16854
17053
  import net from "net";
16855
17054
  import path11 from "path";
16856
17055
  import os9 from "os";
@@ -16969,7 +17168,7 @@ async function daemonHasInteractiveApprover(timeoutMs = 400) {
16969
17168
  return false;
16970
17169
  }
16971
17170
  }
16972
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId) {
17171
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched, socketActivitySent, sessionId, card) {
16973
17172
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
16974
17173
  const ctrl = new AbortController();
16975
17174
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -16995,19 +17194,43 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
16995
17194
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
16996
17195
  ...viewOnly && { viewOnly: true },
16997
17196
  ...localSmartRuleMatched && { localSmartRuleMatched: true },
16998
- ...sessionId && { sessionId }
17197
+ ...sessionId && { sessionId },
17198
+ ...cardFields(card)
16999
17199
  }),
17000
17200
  signal: ctrl.signal
17001
17201
  });
17002
17202
  if (!res.ok) throw new Error("Daemon fail");
17003
- const { id, allowCount } = await res.json();
17004
- return { id, allowCount: allowCount ?? 1 };
17203
+ return registeredCard(await res.json());
17005
17204
  } finally {
17006
17205
  clearTimeout(timer);
17007
17206
  }
17008
17207
  }
17208
+ function cardFields(card) {
17209
+ return {
17210
+ // A review card: the hook already decided this call needs a person; the
17211
+ // daemon never checks it again (approval-card design, defect 1).
17212
+ kind: "review",
17213
+ // Protocol 2 makes the card's /wait require the internal token: claim it
17214
+ // only when this process can read the token (a daemon under another uid
17215
+ // is running but its pid file probe fails with EPERM), or every wait
17216
+ // would get 403 and the review would be denied unseen.
17217
+ ...getInternalToken() && { protocol: CARD_PROTOCOL },
17218
+ ...card?.channels && { channels: card.channels },
17219
+ ...typeof card?.timeoutMs === "number" && { timeoutMs: card.timeoutMs }
17220
+ };
17221
+ }
17222
+ function registeredCard(body) {
17223
+ const b = body;
17224
+ return {
17225
+ id: b.id,
17226
+ allowCount: b.allowCount ?? 1,
17227
+ ...b.protocol === CARD_PROTOCOL && {
17228
+ protocol: CARD_PROTOCOL,
17229
+ deadline: typeof b.deadline === "number" ? b.deadline : null
17230
+ }
17231
+ };
17232
+ }
17009
17233
  async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
17010
- const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
17011
17234
  const waitCtrl = new AbortController();
17012
17235
  let capped = false;
17013
17236
  const waitTimer = maxWaitMs > 0 ? setTimeout(() => {
@@ -17017,14 +17240,18 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
17017
17240
  const onAbort = () => waitCtrl.abort();
17018
17241
  if (signal) signal.addEventListener("abort", onAbort);
17019
17242
  try {
17020
- const waitRes = await fetch(`${base2}/wait/${id}`, { signal: waitCtrl.signal }).catch(
17243
+ const { status, body } = await daemonGet(`/wait/${id}`, waitCtrl.signal).catch(
17021
17244
  (err2) => {
17022
17245
  if (capped && !signal?.aborted) throw new Error("node9 daemon did not answer in time");
17023
17246
  throw err2;
17024
17247
  }
17025
17248
  );
17026
- if (!waitRes.ok) return { decision: "deny" };
17027
- const { decision, source, reason } = await waitRes.json();
17249
+ if (status === 409) throw new Error("node9 card was withdrawn");
17250
+ if (status !== 200) return { decision: "deny" };
17251
+ const parsed = JSON.parse(body);
17252
+ const record2 = parseDecisionRecord(parsed.record);
17253
+ if (record2) return { ...legacyWaitFields(record2), record: record2 };
17254
+ const { decision, source, reason } = parsed;
17028
17255
  if (decision === "allow") return { decision: "allow", source };
17029
17256
  if (decision === "abandoned") return { decision: "abandoned", source };
17030
17257
  return { decision: "deny", source, reason };
@@ -17033,7 +17260,29 @@ async function waitForDaemonDecision(id, signal, maxWaitMs = 12e4) {
17033
17260
  if (signal) signal.removeEventListener("abort", onAbort);
17034
17261
  }
17035
17262
  }
17036
- async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent) {
17263
+ function daemonGet(urlPath, signal, token = getInternalToken()) {
17264
+ return new Promise((resolve2, reject) => {
17265
+ const req = http.get(
17266
+ {
17267
+ host: DAEMON_HOST,
17268
+ port: DAEMON_PORT,
17269
+ path: urlPath,
17270
+ signal,
17271
+ agent: false,
17272
+ headers: token ? { "X-Node9-Internal": token } : {}
17273
+ },
17274
+ (res) => {
17275
+ let body = "";
17276
+ res.setEncoding("utf8");
17277
+ res.on("data", (chunk2) => body += chunk2);
17278
+ res.on("end", () => resolve2({ status: res.statusCode ?? 0, body }));
17279
+ res.on("error", reject);
17280
+ }
17281
+ );
17282
+ req.on("error", reject);
17283
+ });
17284
+ }
17285
+ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId, socketActivitySent, extra) {
17037
17286
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
17038
17287
  const res = await fetch(`${base2}/check`, {
17039
17288
  method: "POST",
@@ -17049,13 +17298,16 @@ async function notifyDaemonViewer(toolName, args, meta, riskMetadata, activityId
17049
17298
  // event via socket. Same contract as registerDaemonEntry — without
17050
17299
  // it the daemon double-emits 'activity' for cloud-enforced flows.
17051
17300
  fromCLI: socketActivitySent !== false,
17052
- activityId
17301
+ activityId,
17302
+ ...extra?.cwd && { cwd: extra.cwd },
17303
+ ...extra?.recoveryCommand && { recoveryCommand: extra.recoveryCommand },
17304
+ ...extra?.sessionId && { sessionId: extra.sessionId },
17305
+ ...cardFields(extra?.card)
17053
17306
  }),
17054
17307
  signal: AbortSignal.timeout(3e3)
17055
17308
  });
17056
17309
  if (!res.ok) throw new Error("Daemon unreachable");
17057
- const { id, allowCount } = await res.json();
17058
- return { id, allowCount: allowCount ?? 1 };
17310
+ return registeredCard(await res.json());
17059
17311
  }
17060
17312
  async function notifyTaint(filePath, source, fromEid) {
17061
17313
  if (!isDaemonRunning()) return;
@@ -17239,19 +17491,24 @@ async function clearSessionExposure(opts) {
17239
17491
  return { ok: false, cleared: 0, daemonUnavailable: true };
17240
17492
  }
17241
17493
  }
17242
- async function resolveViaDaemon(id, decision, internalToken, source) {
17494
+ async function resolveViaDaemon(id, decision, internalToken, source, record2) {
17243
17495
  const base2 = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
17244
- await fetch(`${base2}/resolve/${id}`, {
17496
+ const res = await fetch(`${base2}/resolve/${id}`, {
17245
17497
  method: "POST",
17246
17498
  headers: { "Content-Type": "application/json", "X-Node9-Internal": internalToken },
17247
- body: JSON.stringify({ decision, ...source && { source } }),
17499
+ body: JSON.stringify({ decision, ...source && { source }, ...record2 && { record: record2 } }),
17248
17500
  signal: AbortSignal.timeout(3e3)
17249
17501
  });
17502
+ if (!res.ok) return null;
17503
+ const body = await res.json().catch(() => null);
17504
+ const winner = parseDecisionRecord(body?.record);
17505
+ return winner && typeof body?.won === "boolean" ? { won: body.won, record: winner } : null;
17250
17506
  }
17251
17507
  var ACTIVITY_SOCKET_PATH, DEFAULT_DAEMON_PORT, DAEMON_PORT, DAEMON_HOST;
17252
17508
  var init_daemon = __esm({
17253
17509
  "src/auth/daemon.ts"() {
17254
17510
  "use strict";
17511
+ init_decision_record();
17255
17512
  ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path11.join(os9.tmpdir(), "node9-activity.sock");
17256
17513
  DEFAULT_DAEMON_PORT = 7391;
17257
17514
  DAEMON_PORT = resolveDaemonPort();
@@ -19524,6 +19781,13 @@ var init_check = __esm({
19524
19781
 
19525
19782
  // src/policy/negotiation.ts
19526
19783
  function buildNegotiationMessage(blockedByLabel, isHumanDecision2, humanReason, recoveryCommand, context) {
19784
+ if (isHumanDecision2 && context?.redirect && humanReason) {
19785
+ return `NODE9: The user stopped this action and gave you an instruction.
19786
+ INSTRUCTION FROM THE USER: ${humanReason}
19787
+ INSTRUCTIONS:
19788
+ - Follow the user's instruction above.
19789
+ - Retry the original action only if the instruction says you may.`;
19790
+ }
19527
19791
  if (isHumanDecision2) {
19528
19792
  return `NODE9: The human user rejected this action.
19529
19793
  REASON: ${humanReason || "No specific reason provided."}
@@ -19614,7 +19878,8 @@ function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
19614
19878
  const sentence2 = /[.!?\u2026]$/.test(trimmed) ? trimmed : `${trimmed}.`;
19615
19879
  return `Node9 flagged this for your review: ${sentence2} Approve to proceed, or deny to cancel.`;
19616
19880
  }
19617
- function isHumanDecision(label3, blockedBy) {
19881
+ function isHumanDecision(label3, blockedBy, decidedBy) {
19882
+ if (decidedBy !== void 0) return decidedBy === "person";
19618
19883
  if (blockedBy !== void 0 && blockedBy !== "local-decision") return false;
19619
19884
  const l = label3.toLowerCase();
19620
19885
  return l.includes("user") || l.includes("daemon") || l.includes("decision");
@@ -19646,26 +19911,103 @@ var init_negotiation = __esm({
19646
19911
  }
19647
19912
  });
19648
19913
 
19649
- // src/auth/decision-sources.ts
19650
- function backgroundPassSource(result) {
19651
- if (result.checkedBy === "cloud" || result.blockedBy === "team-policy") return "cloud";
19652
- if (result.blockedBy === "timeout") return "timeout";
19914
+ // src/auth/card-result.ts
19915
+ function authResultFromRecord(r, ctx = {}) {
19916
+ const who = { decidedBy: r.decidedBy, ...r.channel && { channel: r.channel } };
19917
+ switch (r.decidedBy) {
19918
+ case "person": {
19919
+ const channel = r.channel ?? "terminal";
19920
+ const decisionSource = channel === "native" ? "native" : "terminal";
19921
+ if (r.decision === "allow") {
19922
+ return { approved: true, checkedBy: "daemon", decisionSource, ...who };
19923
+ }
19924
+ return {
19925
+ approved: false,
19926
+ reason: r.redirect || `The human user rejected this action via the Node9 ${CHANNEL_NAMES[channel]}.`,
19927
+ blockedBy: "local-decision",
19928
+ blockedByLabel: r.label,
19929
+ decisionSource,
19930
+ ...who,
19931
+ ...r.redirect && { redirect: r.redirect }
19932
+ };
19933
+ }
19934
+ case "timer":
19935
+ if (r.decision === "deny" && ctx.downgradedBlock)
19936
+ return { ...ctx.downgradedBlock, blockedBy: "timeout", ...who };
19937
+ return r.decision === "allow" ? { approved: true, ...who } : {
19938
+ approved: false,
19939
+ reason: ctx.timeoutReason || r.reason || "No response: auto-denied after the approval timeout.",
19940
+ blockedBy: "timeout",
19941
+ blockedByLabel: TIMER_LABEL,
19942
+ ...who
19943
+ };
19944
+ case "cloud":
19945
+ return r.decision === "allow" ? { approved: true, checkedBy: "cloud", decisionSource: "cloud", ...who } : {
19946
+ approved: false,
19947
+ reason: r.reason || "Action rejected by organization administrator via Slack.",
19948
+ blockedBy: "team-policy",
19949
+ blockedByLabel: CLOUD_LABEL,
19950
+ decisionSource: "cloud",
19951
+ ...who
19952
+ };
19953
+ case "policy":
19954
+ return r.decision === "allow" ? { approved: true, checkedBy: "local-policy", ...who } : {
19955
+ approved: false,
19956
+ reason: r.reason || "Action blocked by policy.",
19957
+ blockedBy: "local-config",
19958
+ blockedByLabel: r.label,
19959
+ ...r.ruleName && { ruleHit: r.ruleName },
19960
+ ...who
19961
+ };
19962
+ case "abandoned":
19963
+ return {
19964
+ approved: false,
19965
+ reason: r.reason || "No approver was left to answer.",
19966
+ blockedBy: "no-approval-mechanism",
19967
+ blockedByLabel: ABANDONED_LABEL,
19968
+ ...who
19969
+ };
19970
+ }
19971
+ }
19972
+ function approvalUnavailable(reason) {
19973
+ return {
19974
+ approved: false,
19975
+ reason: reason || "node9 could not get an approval decision for this call.",
19976
+ blockedBy: "no-approval-mechanism",
19977
+ blockedByLabel: "Approval Unavailable"
19978
+ };
19979
+ }
19980
+ function decidedByOf(res) {
19981
+ if (res.decidedBy) return res.decidedBy;
19982
+ if (res.blockedBy === "timeout") return "timer";
19983
+ if (res.checkedBy === "cloud" || res.blockedBy === "team-policy") return "cloud";
19653
19984
  return "policy";
19654
19985
  }
19655
- var HUMAN_DECISION_SOURCES, REDIRECT_SOURCE, DECISION_ROUTE_SOURCES;
19656
- var init_decision_sources = __esm({
19657
- "src/auth/decision-sources.ts"() {
19986
+ function recordFromAuthResult(res, now = Date.now()) {
19987
+ const decidedBy = decidedByOf(res);
19988
+ const channel = res.channel ?? (decidedBy === "cloud" ? "cloud" : decidedBy === "person" ? "native" : void 0);
19989
+ return {
19990
+ decision: res.approved ? "allow" : "deny",
19991
+ decidedBy,
19992
+ ...channel && { channel },
19993
+ label: res.blockedByLabel || (decidedBy === "cloud" ? CLOUD_LABEL : decidedBy === "timer" ? TIMER_LABEL : res.approved ? "Allowed" : "Blocked"),
19994
+ ...res.reason && { reason: res.reason.slice(0, 2e3) },
19995
+ ...res.ruleHit && { ruleName: res.ruleHit },
19996
+ ...res.redirect && { redirect: res.redirect },
19997
+ decidedAt: now
19998
+ };
19999
+ }
20000
+ var CHANNEL_NAMES;
20001
+ var init_card_result = __esm({
20002
+ "src/auth/card-result.ts"() {
19658
20003
  "use strict";
19659
- HUMAN_DECISION_SOURCES = /* @__PURE__ */ new Set([
19660
- "terminal",
19661
- "browser",
19662
- "native"
19663
- ]);
19664
- REDIRECT_SOURCE = "terminal-redirect";
19665
- DECISION_ROUTE_SOURCES = /* @__PURE__ */ new Set([
19666
- ...HUMAN_DECISION_SOURCES,
19667
- REDIRECT_SOURCE
19668
- ]);
20004
+ init_decision_record();
20005
+ CHANNEL_NAMES = {
20006
+ terminal: "Terminal (node9 tail)",
20007
+ monitor: "node9 monitor",
20008
+ native: "system dialog window",
20009
+ cloud: "organization"
20010
+ };
19669
20011
  }
19670
20012
  });
19671
20013
 
@@ -20696,28 +21038,22 @@ ${packageReview}`;
20696
21038
  const { signal } = abortController;
20697
21039
  const racePromises = [];
20698
21040
  const approvalTimeoutMs = config.settings.approvalTimeoutMs ?? 0;
20699
- if (approvalTimeoutMs > 0) {
20700
- racePromises.push(
20701
- new Promise((resolve2, reject) => {
20702
- const timer = setTimeout(() => {
20703
- resolve2({
20704
- approved: false,
20705
- reason: approvalTimeoutReason(approvalTimeoutMs),
20706
- blockedBy: "timeout",
20707
- blockedByLabel: "Approval Timeout"
20708
- });
20709
- }, approvalTimeoutMs);
20710
- signal.addEventListener("abort", () => {
20711
- clearTimeout(timer);
20712
- reject(new Error("Aborted"));
20713
- });
20714
- })
20715
- );
20716
- }
20717
21041
  let viewerId = null;
20718
21042
  const internalToken = getInternalToken();
20719
21043
  let daemonEntryId = null;
20720
21044
  let daemonAllowCount = 1;
21045
+ let cardDeadline;
21046
+ const cardOptions = {
21047
+ channels: [
21048
+ "terminal",
21049
+ "monitor",
21050
+ // Only when the popup can show: a card listing a channel nobody can
21051
+ // use is never abandoned when node9 tail closes, and waits unanswered.
21052
+ ...approvers.native && !isManual && !options?.calledFromDaemon && (process.platform === "darwin" || process.platform === "win32" || !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY)) ? ["native"] : [],
21053
+ ...cloudEnforced && cloudRequestId && cloudMayVote ? ["cloud"] : []
21054
+ ],
21055
+ timeoutMs: approvalTimeoutMs
21056
+ };
20721
21057
  if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
20722
21058
  if (cloudEnforced && cloudRequestId) {
20723
21059
  const viewer = await notifyDaemonViewer(
@@ -20726,11 +21062,20 @@ ${packageReview}`;
20726
21062
  meta,
20727
21063
  riskMetadata,
20728
21064
  options?.activityId,
20729
- options?.socketActivitySent
21065
+ options?.socketActivitySent,
21066
+ {
21067
+ cwd: options?.cwd,
21068
+ recoveryCommand: statefulRecoveryCommand,
21069
+ sessionId: meta?.sessionId,
21070
+ card: cardOptions
21071
+ }
20730
21072
  ).catch(() => null);
20731
21073
  viewerId = viewer?.id ?? null;
20732
21074
  daemonEntryId = viewerId;
20733
- if (viewer) daemonAllowCount = viewer.allowCount;
21075
+ if (viewer) {
21076
+ daemonAllowCount = viewer.allowCount;
21077
+ cardDeadline = viewer.deadline;
21078
+ }
20734
21079
  } else {
20735
21080
  try {
20736
21081
  const entry = await registerDaemonEntry(
@@ -20754,14 +21099,35 @@ ${packageReview}`;
20754
21099
  void 0,
20755
21100
  localSmartRuleMatched || options?.localSmartRuleMatched,
20756
21101
  options?.socketActivitySent,
20757
- meta?.sessionId
21102
+ meta?.sessionId,
21103
+ cardOptions
20758
21104
  );
20759
21105
  daemonEntryId = entry.id;
20760
21106
  daemonAllowCount = entry.allowCount;
21107
+ cardDeadline = entry.deadline;
20761
21108
  } catch {
20762
21109
  }
20763
21110
  }
20764
21111
  }
21112
+ if (approvalTimeoutMs > 0 && cardDeadline === void 0) {
21113
+ racePromises.push(
21114
+ new Promise((resolve2, reject) => {
21115
+ const timer = setTimeout(() => {
21116
+ resolve2({
21117
+ approved: false,
21118
+ reason: approvalTimeoutReason(approvalTimeoutMs),
21119
+ blockedBy: "timeout",
21120
+ blockedByLabel: "Approval Timeout",
21121
+ decidedBy: "timer"
21122
+ });
21123
+ }, approvalTimeoutMs);
21124
+ signal.addEventListener("abort", () => {
21125
+ clearTimeout(timer);
21126
+ reject(new Error("Aborted"));
21127
+ });
21128
+ })
21129
+ );
21130
+ }
20765
21131
  if (cloudEnforced && cloudRequestId && cloudMayVote) {
20766
21132
  racePromises.push(
20767
21133
  (async () => {
@@ -20772,7 +21138,9 @@ ${packageReview}`;
20772
21138
  reason: cloudResult.approved ? void 0 : cloudResult.reason || "Action rejected by organization administrator via Slack.",
20773
21139
  checkedBy: cloudResult.approved ? "cloud" : void 0,
20774
21140
  blockedBy: cloudResult.approved ? void 0 : "team-policy",
20775
- blockedByLabel: "Organization Policy (SaaS)"
21141
+ blockedByLabel: "Organization Policy (SaaS)",
21142
+ decidedBy: "cloud",
21143
+ channel: "cloud"
20776
21144
  };
20777
21145
  } catch (err2) {
20778
21146
  const error = err2;
@@ -20802,14 +21170,31 @@ ${packageReview}`;
20802
21170
  if (config.policySource !== "workspace") {
20803
21171
  for (const d of exposureDestinations) addTrustedRecipient(d, "approval");
20804
21172
  }
20805
- return { approved: true, checkedBy: "daemon", decisionSource: "native" };
21173
+ return {
21174
+ approved: true,
21175
+ checkedBy: "daemon",
21176
+ decisionSource: "native",
21177
+ decidedBy: "person",
21178
+ channel: "native"
21179
+ };
20806
21180
  }
20807
21181
  if (decision === "always_allow") {
20808
21182
  if (standingTrustApplies()) {
20809
21183
  writeTrustSession(toolName, 36e5, args);
20810
- return { approved: true, checkedBy: "trust" };
21184
+ return {
21185
+ approved: true,
21186
+ checkedBy: "trust",
21187
+ decidedBy: "person",
21188
+ channel: "native"
21189
+ };
20811
21190
  }
20812
- return { approved: true, checkedBy: "daemon", decisionSource: "native" };
21191
+ return {
21192
+ approved: true,
21193
+ checkedBy: "daemon",
21194
+ decisionSource: "native",
21195
+ decidedBy: "person",
21196
+ channel: "native"
21197
+ };
20813
21198
  }
20814
21199
  const isApproved = decision === "allow";
20815
21200
  return {
@@ -20818,75 +21203,32 @@ ${packageReview}`;
20818
21203
  checkedBy: isApproved ? "daemon" : void 0,
20819
21204
  blockedBy: isApproved ? void 0 : "local-decision",
20820
21205
  blockedByLabel: "User Decision (Native)",
20821
- decisionSource: "native"
21206
+ decisionSource: "native",
21207
+ decidedBy: "person",
21208
+ channel: "native"
20822
21209
  };
20823
21210
  })()
20824
21211
  );
20825
21212
  }
21213
+ const fromCard = /* @__PURE__ */ new WeakSet();
21214
+ const timeoutReason = approvalTimeoutMs > 0 ? approvalTimeoutReason(approvalTimeoutMs) : void 0;
20826
21215
  if (daemonEntryId && approvers.terminal) {
20827
21216
  racePromises.push(
20828
21217
  (async () => {
20829
- const {
20830
- decision: daemonDecision,
20831
- source: decisionSource,
20832
- reason: daemonReason
20833
- } = await waitForDaemonDecision(
20834
- daemonEntryId,
20835
- signal,
20836
- daemonWaitCapMs(config.settings.approvalTimeoutMs)
20837
- );
20838
- if (daemonDecision === "abandoned") throw new Error("Abandoned");
20839
- const isApproved = daemonDecision === "allow";
20840
- const isRedirect = decisionSource === REDIRECT_SOURCE;
20841
- if (!isApproved && !isRedirect && !(decisionSource && HUMAN_DECISION_SOURCES.has(decisionSource))) {
20842
- if (decisionSource === "cloud")
20843
- return {
20844
- approved: false,
20845
- reason: daemonReason || "Action rejected by organization administrator via Slack.",
20846
- blockedBy: "team-policy",
20847
- blockedByLabel: "Organization Policy (SaaS)",
20848
- decisionSource: "cloud"
20849
- };
20850
- const timedOut = decisionSource === "timeout";
20851
- if (downgradedBlock)
20852
- return {
20853
- ...downgradedBlock,
20854
- blockedBy: timedOut ? "timeout" : "local-config"
20855
- };
20856
- if (timedOut)
20857
- return {
20858
- approved: false,
20859
- reason: daemonReason || "No response: auto-denied after the approval timeout.",
20860
- blockedBy: "timeout",
20861
- blockedByLabel: "Approval Timeout"
20862
- };
20863
- if (decisionSource === "policy")
20864
- return {
20865
- approved: false,
20866
- reason: daemonReason || "Action blocked by policy.",
20867
- blockedBy: "local-config",
20868
- blockedByLabel: explainableLabel
20869
- };
20870
- return {
20871
- approved: false,
20872
- reason: daemonReason || "node9 could not get an approval decision for this call.",
20873
- blockedBy: "no-approval-mechanism",
20874
- blockedByLabel: "Approval Unavailable"
20875
- };
20876
- }
20877
- if (isApproved && decisionSource === "cloud")
20878
- return { approved: true, checkedBy: "cloud", decisionSource: "cloud" };
20879
- if (isApproved && decisionSource === "policy")
20880
- return { approved: true, checkedBy: "local-policy" };
20881
- const via = "Terminal (node9 tail)";
20882
- return {
20883
- approved: isApproved,
20884
- reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
20885
- checkedBy: isApproved ? "daemon" : void 0,
20886
- blockedBy: isApproved ? void 0 : "local-decision",
20887
- blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
20888
- decisionSource: "terminal"
20889
- };
21218
+ const cap = cardDeadline === void 0 ? daemonWaitCapMs(config.settings.approvalTimeoutMs) : cardDeadline === null ? 0 : Math.max(cardDeadline - Date.now(), 0) + 5e3;
21219
+ const outcome = await waitForDaemonDecision(daemonEntryId, signal, cap);
21220
+ const legacy = !outcome.record;
21221
+ const record2 = outcome.record ?? recordFromLegacyWait(outcome);
21222
+ if (downgradedBlock && (!record2 || legacy && record2.decidedBy === "policy" && record2.decision === "deny")) {
21223
+ const softened = { ...downgradedBlock, blockedBy: "local-config" };
21224
+ fromCard.add(softened);
21225
+ return softened;
21226
+ }
21227
+ if (!record2) return approvalUnavailable(outcome.reason);
21228
+ if (legacy && record2.decidedBy === "policy") record2.label = explainableLabel;
21229
+ const result = authResultFromRecord(record2, { downgradedBlock, timeoutReason });
21230
+ fromCard.add(result);
21231
+ return result;
20890
21232
  })()
20891
21233
  );
20892
21234
  }
@@ -20918,7 +21260,7 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
20918
21260
  blockedByLabel: explainableLabel
20919
21261
  };
20920
21262
  }
20921
- const finalResult = await new Promise((resolve2) => {
21263
+ const raceResult = await new Promise((resolve2) => {
20922
21264
  let resolved = false;
20923
21265
  let failures = 0;
20924
21266
  const total = racePromises.length;
@@ -20926,14 +21268,6 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
20926
21268
  if (!resolved) {
20927
21269
  resolved = true;
20928
21270
  abortController.abort();
20929
- if (daemonEntryId && internalToken) {
20930
- resolveViaDaemon(
20931
- daemonEntryId,
20932
- res.approved ? "allow" : "deny",
20933
- internalToken,
20934
- res.decisionSource
20935
- ).catch(() => null);
20936
- }
20937
21271
  resolve2(res);
20938
21272
  }
20939
21273
  };
@@ -20941,26 +21275,29 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
20941
21275
  p.then(finish).catch((err2) => {
20942
21276
  if (err2.name === "AbortError" || err2.message?.includes("canceled") || err2.message?.includes("Aborted"))
20943
21277
  return;
20944
- if (err2.message === "Abandoned") {
20945
- finish({
20946
- approved: false,
20947
- reason: "Browser dashboard closed without making a decision.",
20948
- blockedBy: "local-decision",
20949
- blockedByLabel: "Browser Dashboard (Abandoned)"
20950
- });
20951
- return;
20952
- }
20953
21278
  failures++;
20954
21279
  if (failures === total && !resolved) {
20955
- finish({ approved: false, reason: "All approval channels failed or disconnected." });
21280
+ finish(approvalUnavailable("All approval channels failed or disconnected."));
20956
21281
  }
20957
21282
  });
20958
21283
  }
20959
21284
  });
21285
+ let finalResult = raceResult;
21286
+ if (daemonEntryId && internalToken && !fromCard.has(raceResult)) {
21287
+ const reply = await resolveViaDaemon(
21288
+ daemonEntryId,
21289
+ raceResult.approved ? "allow" : "deny",
21290
+ internalToken,
21291
+ raceResult.decisionSource,
21292
+ recordFromAuthResult(raceResult)
21293
+ ).catch(() => null);
21294
+ if (reply && !reply.won)
21295
+ finalResult = authResultFromRecord(reply.record, { downgradedBlock, timeoutReason });
21296
+ }
20960
21297
  if (finalResult.approved && exposureKeys.length > 0 && meta?.sessionId && ucPolicy?.afterExposure === "ask-once") {
20961
21298
  await approveSessionExposure(meta.sessionId, exposureKeys);
20962
21299
  }
20963
- if (cloudRequestId && creds && finalResult.checkedBy !== "cloud") {
21300
+ if (cloudRequestId && creds && finalResult.checkedBy !== "cloud" && finalResult.decidedBy !== "cloud") {
20964
21301
  await resolveNode9SaaS(
20965
21302
  cloudRequestId,
20966
21303
  creds,
@@ -20984,7 +21321,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
20984
21321
  {
20985
21322
  ...ownRowMeta(finalResult.approved),
20986
21323
  ...cloudRequestId && { cloudRequestId },
20987
- ...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` }
21324
+ ...appPermReview && { ruleName: `app-permission:${appPermReviewTool}` },
21325
+ // Who decided, from the decision record, and the card: the daemon
21326
+ // writes its own row for the same card, and the report counts one.
21327
+ ...finalResult.decidedBy && { decidedBy: finalResult.decidedBy },
21328
+ ...finalResult.channel && { channel: finalResult.channel },
21329
+ ...daemonEntryId && { cardId: daemonEntryId }
20988
21330
  },
20989
21331
  hashAuditArgs
20990
21332
  );
@@ -21036,7 +21378,8 @@ var init_orchestrator = __esm({
21036
21378
  init_jail();
21037
21379
  init_safe_text();
21038
21380
  init_negotiation();
21039
- init_decision_sources();
21381
+ init_decision_record();
21382
+ init_card_result();
21040
21383
  SENSITIVE_PATH_PATTERN = sensitivePathMatch("").patternName;
21041
21384
  WRITE_TOOLS = /* @__PURE__ */ new Set([
21042
21385
  "write",
@@ -30673,12 +31016,206 @@ var init_session_history = __esm({
30673
31016
  }
30674
31017
  });
30675
31018
 
31019
+ // src/daemon/cards.ts
31020
+ import { randomUUID as randomUUID5 } from "crypto";
31021
+ var CARD_RETENTION_MS, MAX_CARD_TIMEOUT_MS, CardStore;
31022
+ var init_cards = __esm({
31023
+ "src/daemon/cards.ts"() {
31024
+ "use strict";
31025
+ init_decision_record();
31026
+ CARD_RETENTION_MS = 6e4;
31027
+ MAX_CARD_TIMEOUT_MS = 24 * 60 * 6e4;
31028
+ CardStore = class {
31029
+ slots = /* @__PURE__ */ new Map();
31030
+ now;
31031
+ setTimer;
31032
+ clearTimer;
31033
+ deps;
31034
+ constructor(deps = {}) {
31035
+ this.deps = deps;
31036
+ this.now = deps.now ?? Date.now;
31037
+ this.setTimer = deps.setTimer ?? ((fn, ms) => {
31038
+ const t = setTimeout(fn, ms);
31039
+ t.unref?.();
31040
+ return t;
31041
+ });
31042
+ this.clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
31043
+ }
31044
+ register(request2, opts) {
31045
+ const id = opts.id && !this.slots.has(opts.id) ? opts.id : randomUUID5();
31046
+ const createdAt = this.now();
31047
+ const timeoutMs = opts.timeoutMs && opts.timeoutMs > 0 ? Math.min(opts.timeoutMs, MAX_CARD_TIMEOUT_MS) : null;
31048
+ const card = {
31049
+ id,
31050
+ kind: opts.kind,
31051
+ state: "open",
31052
+ request: request2,
31053
+ channels: [...opts.channels],
31054
+ createdAt,
31055
+ deadline: timeoutMs === null ? null : createdAt + timeoutMs
31056
+ };
31057
+ const slot = { card, waiters: /* @__PURE__ */ new Set(), everWaited: false };
31058
+ if (timeoutMs !== null) {
31059
+ slot.timer = this.setTimer(() => {
31060
+ slot.timer = void 0;
31061
+ this.decide(id, timerRecord(timeoutMs, this.now()));
31062
+ }, timeoutMs);
31063
+ }
31064
+ this.slots.set(id, slot);
31065
+ return card;
31066
+ }
31067
+ get(id) {
31068
+ return this.slots.get(id)?.card;
31069
+ }
31070
+ /** Cards still waiting for a decision, oldest first. */
31071
+ open() {
31072
+ return [...this.slots.values()].filter((s) => s.card.state === "open").map((s) => s.card);
31073
+ }
31074
+ /**
31075
+ * The ONLY way a card gets a decision. The first call wins; a later call
31076
+ * gets the winning record back with `won: false`. A withdrawn card can
31077
+ * still be decided (its asker, an older hook, may report its own winner
31078
+ * after closing its wait). Null when the card does not exist.
31079
+ */
31080
+ decide(id, record2) {
31081
+ const slot = this.slots.get(id);
31082
+ if (!slot) return null;
31083
+ const { card } = slot;
31084
+ if (card.state === "decided") return { record: card.record, won: false };
31085
+ if (slot.timer !== void 0) {
31086
+ this.clearTimer(slot.timer);
31087
+ slot.timer = void 0;
31088
+ }
31089
+ card.state = "decided";
31090
+ card.record = record2;
31091
+ card.closedAt = this.now();
31092
+ this.deps.onDecided?.(card);
31093
+ if (slot.waiters.size > 0) {
31094
+ const waiters = [...slot.waiters];
31095
+ slot.waiters.clear();
31096
+ for (const w of waiters) w({ kind: "decided", record: record2 });
31097
+ this.slots.delete(id);
31098
+ }
31099
+ return { record: record2, won: true };
31100
+ }
31101
+ /** The asker left before any decision. False when the card was not open. */
31102
+ withdraw(id) {
31103
+ const slot = this.slots.get(id);
31104
+ if (!slot || slot.card.state !== "open") return false;
31105
+ if (slot.timer !== void 0) {
31106
+ this.clearTimer(slot.timer);
31107
+ slot.timer = void 0;
31108
+ }
31109
+ slot.card.state = "withdrawn";
31110
+ slot.card.closedAt = this.now();
31111
+ const waiters = [...slot.waiters];
31112
+ slot.waiters.clear();
31113
+ for (const w of waiters) w({ kind: "withdrawn" });
31114
+ this.deps.onWithdrawn?.(slot.card);
31115
+ return true;
31116
+ }
31117
+ /**
31118
+ * Wait for the card's decision. Any number of waiters. A decided card
31119
+ * answers at once, and is removed once read. When the last waiter of an
31120
+ * open card goes away (its signal aborts), the card is withdrawn: nobody is
31121
+ * left to act on a decision.
31122
+ */
31123
+ wait(id, signal) {
31124
+ const slot = this.slots.get(id);
31125
+ if (!slot) return Promise.resolve({ kind: "missing" });
31126
+ const { card } = slot;
31127
+ if (card.state === "decided") {
31128
+ this.slots.delete(id);
31129
+ return Promise.resolve({ kind: "decided", record: card.record });
31130
+ }
31131
+ if (card.state === "withdrawn") return Promise.resolve({ kind: "withdrawn" });
31132
+ slot.everWaited = true;
31133
+ return new Promise((resolve2) => {
31134
+ const waiter = (o) => {
31135
+ signal?.removeEventListener("abort", onAbort);
31136
+ resolve2(o);
31137
+ };
31138
+ const onAbort = () => {
31139
+ if (!slot.waiters.delete(waiter)) return;
31140
+ resolve2({ kind: "withdrawn" });
31141
+ if (slot.waiters.size === 0 && card.state === "open") this.withdraw(id);
31142
+ };
31143
+ slot.waiters.add(waiter);
31144
+ if (signal) {
31145
+ if (signal.aborted) onAbort();
31146
+ else signal.addEventListener("abort", onAbort, { once: true });
31147
+ }
31148
+ });
31149
+ }
31150
+ /**
31151
+ * The daemon's local UIs all went away. Open cards that only they could
31152
+ * answer are decided as abandoned; a card that Slack or the native popup
31153
+ * can still answer stays open. Returns how many were abandoned.
31154
+ */
31155
+ abandonLocalOnly() {
31156
+ let n = 0;
31157
+ for (const card of this.open()) {
31158
+ const localOnly = card.channels.length > 0 && card.channels.every((c) => LOCAL_UI_CHANNELS.has(c));
31159
+ if (!localOnly) continue;
31160
+ if (this.decide(card.id, abandonedRecord(this.now()))?.won) n++;
31161
+ }
31162
+ return n;
31163
+ }
31164
+ /**
31165
+ * Housekeeping, run every few seconds:
31166
+ * - a decided card nobody read and a withdrawn card are removed after the
31167
+ * retention;
31168
+ * - an asked card that nobody ever waited on (its asker died between
31169
+ * registering and waiting) is withdrawn after the retention.
31170
+ */
31171
+ sweep() {
31172
+ const now = this.now();
31173
+ for (const [id, slot] of this.slots) {
31174
+ const { card } = slot;
31175
+ if (card.state !== "open") {
31176
+ if (now - (card.closedAt ?? card.createdAt) >= CARD_RETENTION_MS) this.slots.delete(id);
31177
+ continue;
31178
+ }
31179
+ if (card.kind !== "discovery" && !slot.everWaited && now - card.createdAt >= CARD_RETENTION_MS)
31180
+ this.withdraw(id);
31181
+ }
31182
+ }
31183
+ /** Cards in any state (tests and diagnostics). */
31184
+ size() {
31185
+ return this.slots.size;
31186
+ }
31187
+ };
31188
+ }
31189
+ });
31190
+
30676
31191
  // src/daemon/state.ts
30677
31192
  import net2 from "net";
30678
31193
  import fs49 from "fs";
30679
31194
  import path49 from "path";
30680
31195
  import os40 from "os";
30681
- import { randomUUID as randomUUID5 } from "crypto";
31196
+ import { randomUUID as randomUUID6 } from "crypto";
31197
+ function onCardDecided(card) {
31198
+ const r = card.record;
31199
+ broadcast("remove", { id: card.id, decision: r.decision, decidedBy: r.decidedBy });
31200
+ if (card.kind === "discovery") return;
31201
+ appendAuditLog({
31202
+ toolName: card.request.toolName,
31203
+ args: card.request.args,
31204
+ decision: r.decidedBy === "timer" ? "auto-deny" : r.standing === "trust" && r.trustDuration ? `trust:${r.trustDuration}` : r.decision,
31205
+ decidedBy: r.decidedBy,
31206
+ ...r.channel && { channel: r.channel },
31207
+ cardId: card.id
31208
+ });
31209
+ if (r.decidedBy !== "person") return;
31210
+ const tool = card.request.toolName;
31211
+ if (r.decision === "allow" && r.standing !== "always-allow" && r.standing !== "trust") {
31212
+ insightCounts.set(tool, (insightCounts.get(tool) ?? 0) + 1);
31213
+ saveInsightCounts();
31214
+ } else if (r.decision === "deny") {
31215
+ insightCounts.delete(tool);
31216
+ saveInsightCounts();
31217
+ }
31218
+ }
30682
31219
  function loadInsightCounts() {
30683
31220
  try {
30684
31221
  if (!fs49.existsSync(INSIGHT_COUNTS_FILE)) return;
@@ -30745,7 +31282,10 @@ function appendAuditLog(data) {
30745
31282
  tool: data.toolName,
30746
31283
  args: maskPersonalArgs(redactArgs(data.args)),
30747
31284
  decision: data.decision,
30748
- source: "daemon"
31285
+ source: "daemon",
31286
+ ...data.decidedBy && { decidedBy: data.decidedBy },
31287
+ ...data.channel && { channel: data.channel },
31288
+ ...data.cardId && { cardId: data.cardId }
30749
31289
  };
30750
31290
  const dir2 = path49.dirname(AUDIT_LOG_FILE);
30751
31291
  if (!fs49.existsSync(dir2)) fs49.mkdirSync(dir2, { recursive: true });
@@ -30878,7 +31418,7 @@ function broadcastForensic(finding) {
30878
31418
  const severity = CRITICAL_FORENSIC_CATEGORIES.has(finding.type) ? "critical" : "warning";
30879
31419
  const event = {
30880
31420
  type: "forensic",
30881
- id: `fnd_${randomUUID5()}`,
31421
+ id: `fnd_${randomUUID6()}`,
30882
31422
  ts: Date.now(),
30883
31423
  sessionId: finding.sessionId,
30884
31424
  category: finding.type,
@@ -30889,14 +31429,8 @@ function broadcastForensic(finding) {
30889
31429
  }
30890
31430
  function abandonPending() {
30891
31431
  setAbandonTimer(null);
30892
- pending.forEach((entry, id) => {
30893
- clearTimeout(entry.timer);
30894
- if (entry.waiter) entry.waiter("abandoned");
30895
- else entry.earlyDecision = "abandoned";
30896
- pending.delete(id);
30897
- broadcast("remove", { id });
30898
- });
30899
- if (autoStarted) {
31432
+ cards.abandonLocalOnly();
31433
+ if (autoStarted && cards.open().length === 0) {
30900
31434
  try {
30901
31435
  fs49.unlinkSync(DAEMON_PID_FILE);
30902
31436
  } catch {
@@ -31060,7 +31594,7 @@ function bindActivitySocket() {
31060
31594
  });
31061
31595
  activitySocketServer = unixServer;
31062
31596
  }
31063
- var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, pending, sseClients, suggestionTracker, taintStore, sessionTaintStore, sessionExposureStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, CRITICAL_FORENSIC_CATEGORIES, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
31597
+ var homeDir, DAEMON_PID_FILE, DECISIONS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, INSIGHT_COUNTS_FILE, sseClients, cards, suggestionTracker, taintStore, sessionTaintStore, sessionExposureStore, insightCounts, _abandonTimer, _hadBrowserClient, _daemonServer, daemonRejectionHandlerRegistered, AUTO_DENY_MS, TRUST_DURATIONS, autoStarted, ACTIVITY_SOCKET_PATH2, ACTIVITY_RING_SIZE, activityRing, LARGE_RESPONSE_RING_SIZE, largeResponseRing, cachedScanResult, cachedScanTs, SCAN_CACHE_TTL_MS, SECRET_KEY_RE, INPUT_PRICE_PER_1M, OUTPUT_PRICE_PER_1M, BYTES_PER_TOKEN, CRITICAL_FORENSIC_CATEGORIES, WRITE_TOOL_NAMES, ACTIVITY_REBIND_MAX_ATTEMPTS, ACTIVITY_REBIND_WINDOW_MS, ACTIVITY_HEALTH_PROBE_MS, activitySocketServer, activityHealthInterval, activityRebindAttempts, activityCircuitTripped;
31064
31598
  var init_state2 = __esm({
31065
31599
  "src/daemon/state.ts"() {
31066
31600
  "use strict";
@@ -31073,6 +31607,7 @@ var init_state2 = __esm({
31073
31607
  init_session_counters();
31074
31608
  init_session_history();
31075
31609
  init_atomic_write();
31610
+ init_cards();
31076
31611
  homeDir = os40.homedir();
31077
31612
  DAEMON_PID_FILE = path49.join(homeDir, ".node9", "daemon.pid");
31078
31613
  DECISIONS_FILE = path49.join(homeDir, ".node9", "decisions.json");
@@ -31081,8 +31616,11 @@ var init_state2 = __esm({
31081
31616
  GLOBAL_CONFIG_FILE = path49.join(homeDir, ".node9", "config.json");
31082
31617
  CREDENTIALS_FILE = path49.join(homeDir, ".node9", "credentials.json");
31083
31618
  INSIGHT_COUNTS_FILE = path49.join(homeDir, ".node9", "insight-counts.json");
31084
- pending = /* @__PURE__ */ new Map();
31085
31619
  sseClients = /* @__PURE__ */ new Set();
31620
+ cards = new CardStore({
31621
+ onDecided: onCardDecided,
31622
+ onWithdrawn: (card) => broadcast("remove", { id: card.id })
31623
+ });
31086
31624
  suggestionTracker = new SuggestionTracker(3);
31087
31625
  taintStore = new TaintStore();
31088
31626
  sessionTaintStore = new SessionTaintStore();
@@ -32395,7 +32933,7 @@ var init_posture = __esm({
32395
32933
  });
32396
32934
 
32397
32935
  // src/posture/ship.ts
32398
- import http from "http";
32936
+ import http2 from "http";
32399
32937
  import https2 from "https";
32400
32938
  import { URL as URL2 } from "url";
32401
32939
  function buildShipBody(result) {
@@ -32436,7 +32974,7 @@ async function shipPosture(result, creds) {
32436
32974
  if (!url) return false;
32437
32975
  const body = JSON.stringify(buildShipBody(result));
32438
32976
  const parsed = new URL2(url);
32439
- const transport = parsed.protocol === "http:" ? http : https2;
32977
+ const transport = parsed.protocol === "http:" ? http2 : https2;
32440
32978
  return new Promise((resolve2) => {
32441
32979
  const req = transport.request(
32442
32980
  {
@@ -32635,7 +33173,7 @@ var init_mcp_status = __esm({
32635
33173
  });
32636
33174
 
32637
33175
  // src/policy-snapshot/ship.ts
32638
- import http2 from "http";
33176
+ import http3 from "http";
32639
33177
  import https3 from "https";
32640
33178
  import { URL as URL3 } from "url";
32641
33179
  function policySnapshotUrlFrom(apiUrl) {
@@ -32646,7 +33184,7 @@ async function shipPolicySnapshot(body, creds) {
32646
33184
  if (!url) return false;
32647
33185
  const payload = JSON.stringify(body);
32648
33186
  const parsed = new URL3(url);
32649
- const transport = parsed.protocol === "http:" ? http2 : https3;
33187
+ const transport = parsed.protocol === "http:" ? http3 : https3;
32650
33188
  return new Promise((resolve2) => {
32651
33189
  const req = transport.request(
32652
33190
  {
@@ -32714,17 +33252,17 @@ var init_machine_id = __esm({
32714
33252
  // src/daemon/sync-trigger.ts
32715
33253
  function createSyncTrigger(sync) {
32716
33254
  let running2;
32717
- let pending2 = false;
33255
+ let pending = false;
32718
33256
  return (invalidate = true) => {
32719
33257
  if (running2) {
32720
- if (invalidate) pending2 = true;
33258
+ if (invalidate) pending = true;
32721
33259
  return running2;
32722
33260
  }
32723
33261
  running2 = (async () => {
32724
33262
  do {
32725
- pending2 = false;
33263
+ pending = false;
32726
33264
  await sync();
32727
- } while (pending2);
33265
+ } while (pending);
32728
33266
  })().finally(() => {
32729
33267
  running2 = void 0;
32730
33268
  });
@@ -33632,7 +34170,11 @@ function buildWireRows(chunk2) {
33632
34170
  ...typeof parsed.taintFromEid === "string" ? { taintFromEid: parsed.taintFromEid } : {},
33633
34171
  ...typeof parsed.taintSource === "string" ? { taintSource: parsed.taintSource } : {},
33634
34172
  ...exposureOf(parsed.exposure) ? { exposure: exposureOf(parsed.exposure) } : {},
33635
- ...typeof parsed.exposureFromEid === "string" ? { exposureFromEid: parsed.exposureFromEid } : {}
34173
+ ...typeof parsed.exposureFromEid === "string" ? { exposureFromEid: parsed.exposureFromEid } : {},
34174
+ // Approval cards. Only the known values: the ingest's schema is a
34175
+ // closed vocabulary and rejects the whole batch for anything else.
34176
+ ...typeof parsed.decidedBy === "string" && DECIDED_BY_VALUES.has(parsed.decidedBy) ? { decidedBy: parsed.decidedBy } : {},
34177
+ ...typeof parsed.channel === "string" && CHANNELS.includes(parsed.channel) ? { channel: parsed.channel } : {}
33636
34178
  });
33637
34179
  }
33638
34180
  return { rows, consumed: lastNl + 1 };
@@ -33784,15 +34326,23 @@ function outboxBacklog(auditLogPath = LOCAL_AUDIT_LOG, watermarkPath = AUDIT_SHI
33784
34326
  };
33785
34327
  }
33786
34328
  }
33787
- var AUDIT_SHIP_WATERMARK, DEFAULT_INTERVAL_MS, MAX_BATCH, MAX_CHUNK_BYTES, MAX_CHUNKS_PER_TICK, FETCH_TIMEOUT_MS, SKIP_CHECKED_BY, shipperStarted;
34329
+ var DECIDED_BY_VALUES, AUDIT_SHIP_WATERMARK, DEFAULT_INTERVAL_MS, MAX_BATCH, MAX_CHUNK_BYTES, MAX_CHUNKS_PER_TICK, FETCH_TIMEOUT_MS, SKIP_CHECKED_BY, shipperStarted;
33788
34330
  var init_audit_shipper = __esm({
33789
34331
  "src/daemon/audit-shipper.ts"() {
33790
34332
  "use strict";
33791
34333
  init_audit();
34334
+ init_decision_record();
33792
34335
  init_config();
33793
34336
  init_sync();
33794
34337
  init_cloud();
33795
34338
  init_dist();
34339
+ DECIDED_BY_VALUES = /* @__PURE__ */ new Set([
34340
+ "person",
34341
+ "timer",
34342
+ "cloud",
34343
+ "policy",
34344
+ "abandoned"
34345
+ ]);
33796
34346
  AUDIT_SHIP_WATERMARK = path55.join(os47.homedir(), ".node9", "audit-ship.json");
33797
34347
  DEFAULT_INTERVAL_MS = 2e4;
33798
34348
  MAX_BATCH = 500;
@@ -33817,6 +34367,18 @@ function classifyDecision(a, b) {
33817
34367
  }
33818
34368
  if (d === "dlp") return { outcome: "info", label: "Finding", raw };
33819
34369
  if (d === "mcp-discovered") return { outcome: "info", label: "Info", raw };
34370
+ const decidedBy = isRow ? a.decidedBy : void 0;
34371
+ if (typeof decidedBy === "string") {
34372
+ const allowed = d === "allow" || d === "allowed" || d.startsWith("trust:");
34373
+ const denied = d === "deny" || d === "auto-deny" || d === "block";
34374
+ if (allowed || denied) {
34375
+ if (decidedBy === "person")
34376
+ return allowed ? { outcome: "allow", label: "Approved", raw } : { outcome: "deny", label: "Denied", raw };
34377
+ if (decidedBy === "timer" || decidedBy === "abandoned")
34378
+ return { outcome: "deny", label: "Timed out", raw };
34379
+ return allowed ? { outcome: "allow", label: "Auto-allowed", raw } : { outcome: "deny", label: "Blocked", raw };
34380
+ }
34381
+ }
33820
34382
  if (d === "allow" || d === "allowed") {
33821
34383
  if (src === "post-hook") return { outcome: "allow", label: "Ran", raw };
33822
34384
  if (HUMAN_SOURCES.has(src)) {
@@ -35243,11 +35805,11 @@ var init_heartbeat = __esm({
35243
35805
  });
35244
35806
 
35245
35807
  // src/daemon/server.ts
35246
- import http3 from "http";
35808
+ import http4 from "http";
35247
35809
  import fs63 from "fs";
35248
35810
  import path61 from "path";
35249
35811
  import os52 from "os";
35250
- import { randomUUID as randomUUID7 } from "crypto";
35812
+ import { randomUUID as randomUUID8 } from "crypto";
35251
35813
  import { spawnSync as spawnSync3 } from "child_process";
35252
35814
  import chalk7 from "chalk";
35253
35815
  function buildDaemonReport(allEntries, period, now) {
@@ -35320,14 +35882,40 @@ function buildDaemonReport(allEntries, period, now) {
35320
35882
  byAgent: [...agentMap.values()].sort((a, b) => b.total - a.total)
35321
35883
  };
35322
35884
  }
35323
- function autoDenyMs() {
35324
- const ms = getConfig().settings.approvalTimeoutMs;
35325
- if (ms === 0) return null;
35885
+ function configTimeoutMs(config) {
35886
+ const ms = config.settings.approvalTimeoutMs;
35326
35887
  return ms ?? AUTO_DENY_MS;
35327
35888
  }
35328
- function armAutoDeny(fire) {
35329
- const ms = autoDenyMs();
35330
- return ms === null ? void 0 : setTimeout(fire, ms);
35889
+ function parseChannels(v) {
35890
+ if (!Array.isArray(v)) return null;
35891
+ const out = v.filter((c) => CHANNELS.includes(c));
35892
+ return out.length > 0 ? [...new Set(out)] : null;
35893
+ }
35894
+ function inferChannels(kind, slackDelegated) {
35895
+ if (kind === "evaluate") return ["terminal", "monitor"];
35896
+ return slackDelegated ? ["terminal", "monitor", "cloud"] : ["terminal", "monitor", "native", "cloud"];
35897
+ }
35898
+ function cardView(card) {
35899
+ const r = card.request;
35900
+ return {
35901
+ id: card.id,
35902
+ toolName: r.toolName,
35903
+ args: r.args,
35904
+ riskMetadata: r.riskMetadata,
35905
+ ...r.recoveryCommand && { recoveryCommand: r.recoveryCommand },
35906
+ ...r.viewOnly && { viewOnly: true },
35907
+ slackDelegated: card.channels.includes("cloud") && !card.channels.includes("native"),
35908
+ timestamp: card.createdAt,
35909
+ deadline: card.deadline,
35910
+ agent: r.agent,
35911
+ mcpServer: r.mcpServer,
35912
+ ...card.kind === "discovery" && {
35913
+ type: "mcp-discovery",
35914
+ serverKey: r.serverKey,
35915
+ mcpTools: r.mcpTools
35916
+ },
35917
+ allowCount: (insightCounts.get(r.toolName) ?? 0) + 1
35918
+ };
35331
35919
  }
35332
35920
  function startDaemon() {
35333
35921
  try {
@@ -35349,7 +35937,7 @@ function startDaemon() {
35349
35937
  recordStartupState("failed", "startup-throw", err2 instanceof Error ? err2.message : String(err2));
35350
35938
  process.exit(1);
35351
35939
  }
35352
- const internalToken = randomUUID7();
35940
+ const internalToken = randomUUID8();
35353
35941
  const applyTrustRecipients = (entry) => {
35354
35942
  const dests = entry.riskMetadata?.exposureDestinations ?? [];
35355
35943
  if (dests.length === 0) return;
@@ -35394,8 +35982,9 @@ function startDaemon() {
35394
35982
  idleTimer.unref();
35395
35983
  }
35396
35984
  resetIdleTimer();
35985
+ setInterval(() => cards.sweep(), 1e4).unref();
35397
35986
  const allowedHosts = /* @__PURE__ */ new Set([`127.0.0.1:${DAEMON_PORT}`, `localhost:${DAEMON_PORT}`]);
35398
- const server = http3.createServer(async (req, res) => {
35987
+ const server = http4.createServer(async (req, res) => {
35399
35988
  const host2 = req.headers.host ?? "";
35400
35989
  if (!allowedHosts.has(host2)) {
35401
35990
  res.writeHead(421, { "Content-Type": "text/plain" });
@@ -35425,21 +36014,8 @@ function startDaemon() {
35425
36014
  res.write(
35426
36015
  `event: init
35427
36016
  data: ${JSON.stringify({
35428
- requests: Array.from(pending.values()).map((e) => ({
35429
- id: e.id,
35430
- toolName: e.toolName,
35431
- args: e.args,
35432
- riskMetadata: e.riskMetadata,
35433
- ...e.recoveryCommand && { recoveryCommand: e.recoveryCommand },
35434
- ...e.viewOnly && { viewOnly: true },
35435
- slackDelegated: e.slackDelegated,
35436
- timestamp: e.timestamp,
35437
- agent: e.agent,
35438
- mcpServer: e.mcpServer,
35439
- allowCount: (insightCounts.get(e.toolName) ?? 0) + 1
35440
- })),
35441
- orgName: getOrgName(),
35442
- autoDenyMs: autoDenyMs()
36017
+ requests: cards.open().map(cardView),
36018
+ orgName: getOrgName()
35443
36019
  })}
35444
36020
 
35445
36021
  `
@@ -35460,7 +36036,7 @@ data: ${JSON.stringify(item.data)}
35460
36036
  }
35461
36037
  return req.on("close", () => {
35462
36038
  sseClients.delete(sseClient);
35463
- if (sseClients.size === 0 && pending.size > 0) {
36039
+ if (sseClients.size === 0 && cards.open().length > 0) {
35464
36040
  setAbandonTimer(setTimeout(abandonPending, getHadBrowserClient() ? 1e4 : 15e3));
35465
36041
  }
35466
36042
  });
@@ -35471,137 +36047,81 @@ data: ${JSON.stringify(item.data)}
35471
36047
  _resetConfigCache();
35472
36048
  const body = await readBody(req);
35473
36049
  if (body.length > 65536) return res.writeHead(413).end();
35474
- const {
35475
- toolName,
35476
- args,
35477
- slackDelegated = false,
35478
- agent,
35479
- mcpServer,
35480
- riskMetadata,
35481
- recoveryCommand,
35482
- skipBackgroundAuth = false,
35483
- viewOnly = false,
35484
- fromCLI = false,
35485
- activityId,
35486
- cwd,
35487
- localSmartRuleMatched = false,
35488
- sessionId
35489
- } = JSON.parse(body);
35490
- const id = fromCLI && typeof activityId === "string" && activityId || randomUUID7();
35491
- const entry = {
35492
- id,
36050
+ const parsed = JSON.parse(body);
36051
+ const { toolName, args, agent, mcpServer, riskMetadata, recoveryCommand, activityId, cwd } = parsed;
36052
+ if (typeof toolName !== "string" || !toolName) return res.writeHead(400).end();
36053
+ const fromCLI = parsed.fromCLI === true;
36054
+ const kind = parsed.kind === "review" || parsed.kind === "evaluate" ? parsed.kind : "fromCLI" in parsed ? "review" : "evaluate";
36055
+ const projectCwd = typeof cwd === "string" && path61.isAbsolute(cwd) ? cwd : void 0;
36056
+ const projectConfig = getConfig(projectCwd);
36057
+ const timeoutMs = typeof parsed.timeoutMs === "number" && Number.isFinite(parsed.timeoutMs) && parsed.timeoutMs >= 0 ? parsed.timeoutMs : configTimeoutMs(projectConfig);
36058
+ const sessionId = parsed.sessionId;
36059
+ const request2 = {
35493
36060
  toolName,
35494
36061
  args,
35495
- riskMetadata: riskMetadata ?? void 0,
35496
- ...typeof sessionId === "string" && sessionId.length <= 256 && { sessionId },
36062
+ ...riskMetadata && typeof riskMetadata === "object" ? { riskMetadata } : {},
35497
36063
  ...typeof recoveryCommand === "string" && recoveryCommand && { recoveryCommand },
35498
- ...viewOnly && { viewOnly: true },
35499
- agent: typeof agent === "string" ? agent : void 0,
35500
- mcpServer: typeof mcpServer === "string" ? mcpServer : void 0,
35501
- slackDelegated: !!slackDelegated,
35502
- timestamp: Date.now(),
35503
- earlyDecision: null,
35504
- waiter: null,
35505
- timer: armAutoDeny(() => {
35506
- if (pending.has(id)) {
35507
- const e = pending.get(id);
35508
- appendAuditLog({
35509
- toolName: e.toolName,
35510
- args: e.args,
35511
- decision: "auto-deny"
35512
- });
35513
- e.decisionSource = "timeout";
35514
- if (e.waiter) e.waiter("deny", "No response \u2014 auto-denied after timeout");
35515
- else {
35516
- e.earlyDecision = "deny";
35517
- e.earlyReason = "No response \u2014 auto-denied after timeout";
35518
- }
35519
- pending.delete(id);
35520
- broadcast("remove", { id, decision: "deny" });
35521
- }
35522
- })
36064
+ ...parsed.viewOnly === true && { viewOnly: true },
36065
+ ...typeof agent === "string" && { agent },
36066
+ ...typeof mcpServer === "string" && { mcpServer },
36067
+ ...typeof sessionId === "string" && sessionId.length <= 256 && { sessionId },
36068
+ ...projectCwd && { cwd: projectCwd },
36069
+ ...parsed.protocol === CARD_PROTOCOL && { protocol: CARD_PROTOCOL }
35523
36070
  };
35524
- pending.set(id, entry);
36071
+ const card = cards.register(request2, {
36072
+ id: fromCLI && typeof activityId === "string" && activityId ? activityId : void 0,
36073
+ kind,
36074
+ channels: parseChannels(parsed.channels) ?? inferChannels(kind, parsed.slackDelegated === true),
36075
+ timeoutMs
36076
+ });
35525
36077
  if (!fromCLI) {
35526
36078
  broadcast("activity", {
35527
- id,
35528
- ts: entry.timestamp,
36079
+ id: card.id,
36080
+ ts: card.createdAt,
35529
36081
  tool: toolName,
35530
36082
  args: redactArgs(args),
35531
36083
  status: "pending",
35532
- agent: entry.agent,
35533
- mcpServer: entry.mcpServer
36084
+ agent: request2.agent,
36085
+ mcpServer: request2.mcpServer
35534
36086
  });
35535
36087
  }
35536
- const projectCwd = typeof cwd === "string" && path61.isAbsolute(cwd) ? cwd : void 0;
35537
- const projectConfig = getConfig(projectCwd);
35538
36088
  const browserEnabled = projectConfig.settings.approvers?.browser !== false;
35539
36089
  const terminalEnabled = projectConfig.settings.approvers?.terminal !== false;
35540
36090
  if (browserEnabled || terminalEnabled) {
35541
- broadcast("add", {
35542
- id,
35543
- toolName,
35544
- args,
35545
- riskMetadata: entry.riskMetadata,
35546
- ...entry.recoveryCommand && { recoveryCommand: entry.recoveryCommand },
35547
- ...entry.viewOnly && { viewOnly: true },
35548
- slackDelegated: entry.slackDelegated,
35549
- agent: entry.agent,
35550
- mcpServer: entry.mcpServer,
35551
- interactive: terminalEnabled,
35552
- // allowCount = what this count will be if the user allows.
35553
- // Terminal uses this to show the 💡 insight line on the Nth consecutive approval.
35554
- allowCount: (insightCounts.get(toolName) ?? 0) + 1
35555
- });
36091
+ broadcast("add", { ...cardView(card), interactive: terminalEnabled });
35556
36092
  }
35557
36093
  res.writeHead(200, { "Content-Type": "application/json" });
35558
- res.end(JSON.stringify({ id, allowCount: (insightCounts.get(toolName) ?? 0) + 1 }));
35559
- if (slackDelegated || skipBackgroundAuth) return;
36094
+ res.end(
36095
+ JSON.stringify({
36096
+ id: card.id,
36097
+ allowCount: (insightCounts.get(toolName) ?? 0) + 1,
36098
+ protocol: CARD_PROTOCOL,
36099
+ deadline: card.deadline
36100
+ })
36101
+ );
36102
+ if (kind !== "evaluate") return;
35560
36103
  authorizeHeadless(
35561
36104
  toolName,
35562
36105
  args,
36106
+ { agent: request2.agent, mcpServer: request2.mcpServer },
35563
36107
  {
35564
- agent: typeof agent === "string" ? agent : void 0,
35565
- mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
35566
- },
35567
- { calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
36108
+ calledFromDaemon: true,
36109
+ ...projectCwd && { cwd: projectCwd },
36110
+ localSmartRuleMatched: parsed.localSmartRuleMatched === true
36111
+ }
35568
36112
  ).then((result) => {
35569
- const e = pending.get(id);
35570
- if (!e) return;
35571
- if (result.noApprovalMechanism) return;
35572
- if (result.checkedBy === "audit") return;
35573
- if (e.earlyDecision !== null) return;
36113
+ if (cards.get(card.id)?.state !== "open") return;
36114
+ if (result.noApprovalMechanism || result.checkedBy === "audit") return;
35574
36115
  broadcast("activity-result", {
35575
- id,
36116
+ id: card.id,
35576
36117
  status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
35577
36118
  label: result.blockedByLabel,
35578
- agent: e.agent,
35579
- mcpServer: e.mcpServer
36119
+ agent: request2.agent,
36120
+ mcpServer: request2.mcpServer
35580
36121
  });
35581
- clearTimeout(e.timer);
35582
- const decision = result.approved ? "allow" : "deny";
35583
- appendAuditLog({ toolName: e.toolName, args: e.args, decision });
35584
- e.decisionSource = backgroundPassSource(result);
35585
- if (e.waiter) {
35586
- e.waiter(decision, result.reason);
35587
- pending.delete(id);
35588
- broadcast("remove", { id, decision });
35589
- } else {
35590
- e.earlyDecision = decision;
35591
- e.earlyReason = result.reason;
35592
- }
36122
+ cards.decide(card.id, recordFromVerdict(result));
35593
36123
  }).catch((err2) => {
35594
- const e = pending.get(id);
35595
- if (!e) return;
35596
- clearTimeout(e.timer);
35597
- const reason = err2?.reason || "No response \u2014 request timed out";
35598
- if (e.waiter) e.waiter("deny", reason);
35599
- else {
35600
- e.earlyDecision = "deny";
35601
- e.earlyReason = reason;
35602
- }
35603
- pending.delete(id);
35604
- broadcast("remove", { id, decision: "deny" });
36124
+ console.error(chalk7.red("[node9 daemon] check of an evaluate card failed:"), err2);
35605
36125
  });
35606
36126
  return;
35607
36127
  } catch {
@@ -35610,36 +36130,33 @@ data: ${JSON.stringify(item.data)}
35610
36130
  }
35611
36131
  if (req.method === "GET" && pathname.startsWith("/wait/")) {
35612
36132
  const id = pathname.split("/").pop();
35613
- const entry = pending.get(id);
35614
- if (!entry) return res.writeHead(404).end();
35615
- if (entry.earlyDecision) {
35616
- clearTimeout(entry.timer);
35617
- const source = entry.decisionSource;
35618
- pending.delete(id);
35619
- res.writeHead(200, { "Content-Type": "application/json" });
35620
- const body = {
35621
- decision: entry.earlyDecision
35622
- };
35623
- if (entry.earlyReason) body.reason = entry.earlyReason;
35624
- if (source) body.source = source;
35625
- return res.end(JSON.stringify(body));
35626
- }
35627
- entry.waiter = (d, reason) => {
35628
- res.writeHead(200, { "Content-Type": "application/json" });
35629
- const body = { decision: d };
35630
- if (reason) body.reason = reason;
35631
- if (entry.decisionSource) body.source = entry.decisionSource;
35632
- res.end(JSON.stringify(body));
35633
- };
35634
- req.on("close", () => {
35635
- setTimeout(() => {
35636
- const e = pending.get(id);
35637
- if (e && e.waiter && e.earlyDecision === null) {
35638
- clearTimeout(e.timer);
35639
- pending.delete(id);
35640
- broadcast("remove", { id });
35641
- }
35642
- }, 200);
36133
+ const card = cards.get(id);
36134
+ if (!card) return res.writeHead(404).end();
36135
+ if (card.request.protocol === CARD_PROTOCOL && !validToken(req))
36136
+ return res.writeHead(403).end();
36137
+ const ctrl = new AbortController();
36138
+ let answered = false;
36139
+ res.on("close", () => {
36140
+ if (!answered) ctrl.abort();
36141
+ });
36142
+ void cards.wait(id, ctrl.signal).then((outcome) => {
36143
+ answered = true;
36144
+ if (res.writableEnded || res.destroyed) return;
36145
+ if (outcome.kind === "decided") {
36146
+ res.writeHead(200, { "Content-Type": "application/json" });
36147
+ res.end(
36148
+ JSON.stringify({
36149
+ protocol: CARD_PROTOCOL,
36150
+ record: outcome.record,
36151
+ ...legacyWaitFields(outcome.record)
36152
+ })
36153
+ );
36154
+ } else if (outcome.kind === "withdrawn") {
36155
+ res.writeHead(409, { "Content-Type": "application/json" });
36156
+ res.end(JSON.stringify({ protocol: CARD_PROTOCOL, withdrawn: true }));
36157
+ } else {
36158
+ res.writeHead(404).end();
36159
+ }
35643
36160
  });
35644
36161
  return;
35645
36162
  }
@@ -35647,68 +36164,63 @@ data: ${JSON.stringify(item.data)}
35647
36164
  if (!validToken(req)) return res.writeHead(403).end();
35648
36165
  try {
35649
36166
  const id = pathname.split("/").pop();
35650
- const entry = pending.get(id);
35651
- if (!entry) return res.writeHead(404).end();
35652
- if (entry.earlyDecision !== null) {
36167
+ const card = cards.get(id);
36168
+ if (!card) return res.writeHead(404).end();
36169
+ if (card.state === "decided") {
35653
36170
  res.writeHead(409, { "Content-Type": "application/json" });
35654
- return res.end(JSON.stringify({ conflict: true, decision: entry.earlyDecision }));
36171
+ return res.end(
36172
+ JSON.stringify({ conflict: true, decision: card.record.decision, record: card.record })
36173
+ );
35655
36174
  }
36175
+ if (card.request.viewOnly) return res.writeHead(409).end();
35656
36176
  const { decision, persist, trustDuration, reason, source, trustRecipients } = JSON.parse(
35657
36177
  await readBody(req)
35658
36178
  );
35659
- if (decision === "allow" && trustRecipients === true) {
35660
- applyTrustRecipients(entry);
36179
+ const via = channelFromDecisionSource(source);
36180
+ if (!via) return res.writeHead(400).end();
36181
+ const isTrust = decision === "trust";
36182
+ if (decision !== "allow" && decision !== "deny" && !isTrust)
36183
+ return res.writeHead(400).end();
36184
+ if (isTrust && (typeof trustDuration !== "string" || !(trustDuration in TRUST_DURATIONS)))
36185
+ return res.writeHead(400).end();
36186
+ if (persist === true && isTrust) return res.writeHead(400).end();
36187
+ const verdict = isTrust ? "allow" : decision;
36188
+ const redirect = via.redirect && typeof reason === "string" && reason ? reason.slice(0, 2e3) : void 0;
36189
+ const record2 = personRecord(via.channel, verdict, {
36190
+ ...redirect && { redirect },
36191
+ ...isTrust ? { standing: "trust", trustDuration } : persist === true ? {
36192
+ standing: verdict === "allow" ? "always-allow" : "always-deny"
36193
+ } : verdict === "allow" && trustRecipients === true ? { standing: "trust-recipients" } : {}
36194
+ });
36195
+ const outcome = cards.decide(id, record2);
36196
+ if (!outcome) return res.writeHead(404).end();
36197
+ if (!outcome.won) {
36198
+ res.writeHead(409, { "Content-Type": "application/json" });
36199
+ return res.end(
36200
+ JSON.stringify({
36201
+ conflict: true,
36202
+ decision: outcome.record.decision,
36203
+ record: outcome.record
36204
+ })
36205
+ );
35661
36206
  }
35662
- if (decision === "trust" && trustDuration) {
35663
- const ms = TRUST_DURATIONS[trustDuration] ?? 60 * 6e4;
35664
- const commandPattern = extractCommandPattern(entry.toolName, entry.args);
35665
- writeTrustEntry(entry.toolName, ms, commandPattern);
35666
- appendAuditLog({
35667
- toolName: entry.toolName,
35668
- args: entry.args,
35669
- decision: `trust:${trustDuration}`
35670
- });
35671
- clearTimeout(entry.timer);
35672
- if (entry.waiter) {
35673
- entry.waiter("allow");
35674
- pending.delete(id);
35675
- broadcast("remove", { id, decision: "allow" });
35676
- } else {
35677
- entry.earlyDecision = "allow";
35678
- broadcast("remove", { id, decision: "allow" });
35679
- entry.timer = setTimeout(() => pending.delete(id), 3e4);
35680
- }
35681
- res.writeHead(200);
35682
- return res.end(JSON.stringify({ ok: true }));
36207
+ if (isTrust) {
36208
+ const ms = TRUST_DURATIONS[trustDuration];
36209
+ writeTrustEntry(
36210
+ card.request.toolName,
36211
+ ms,
36212
+ extractCommandPattern(card.request.toolName, card.request.args)
36213
+ );
36214
+ } else if (persist === true) {
36215
+ writePersistentDecision(card.request.toolName, verdict);
36216
+ } else if (verdict === "allow" && trustRecipients === true) {
36217
+ applyTrustRecipients(card.request);
35683
36218
  }
35684
- const resolvedDecision = decision === "allow" || decision === "deny" ? decision : "deny";
35685
- if (persist) writePersistentDecision(entry.toolName, resolvedDecision);
35686
- appendAuditLog({
35687
- toolName: entry.toolName,
35688
- args: entry.args,
35689
- decision: resolvedDecision
35690
- });
35691
- clearTimeout(entry.timer);
35692
- if (resolvedDecision === "allow" && !persist) {
35693
- insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
35694
- saveInsightCounts();
35695
- } else if (resolvedDecision === "deny") {
35696
- insightCounts.delete(entry.toolName);
35697
- saveInsightCounts();
35698
- }
35699
- if (source && DECISION_ROUTE_SOURCES.has(source)) entry.decisionSource = source;
35700
- if (entry.waiter) {
35701
- entry.waiter(resolvedDecision, reason);
35702
- pending.delete(id);
35703
- broadcast("remove", { id, decision: resolvedDecision });
35704
- } else {
35705
- entry.earlyDecision = resolvedDecision;
35706
- entry.earlyReason = reason;
35707
- broadcast("remove", { id, decision: resolvedDecision });
35708
- entry.timer = setTimeout(() => pending.delete(id), 3e4);
36219
+ if (card.kind === "discovery" && verdict === "allow" && card.request.serverKey) {
36220
+ approveServer(card.request.serverKey, []);
35709
36221
  }
35710
- res.writeHead(200);
35711
- return res.end(JSON.stringify({ ok: true }));
36222
+ res.writeHead(200, { "Content-Type": "application/json" });
36223
+ return res.end(JSON.stringify({ ok: true, record: outcome.record }));
35712
36224
  } catch {
35713
36225
  res.writeHead(400).end();
35714
36226
  }
@@ -35813,34 +36325,18 @@ data: ${JSON.stringify(item.data)}
35813
36325
  }
35814
36326
  }
35815
36327
  if (req.method === "POST" && pathname.startsWith("/resolve/")) {
35816
- const internalAuth = req.headers["x-node9-internal"];
35817
- if (internalAuth !== internalToken) return res.writeHead(403).end();
36328
+ if (!validToken(req)) return res.writeHead(403).end();
35818
36329
  try {
35819
36330
  const id = pathname.split("/").pop();
35820
- const entry = pending.get(id);
35821
- if (!entry) return res.writeHead(404).end();
35822
- const { decision, source } = JSON.parse(await readBody(req));
35823
- const resolvedResolveDecision = decision === "allow" ? "allow" : "deny";
35824
- appendAuditLog({
35825
- toolName: entry.toolName,
35826
- args: entry.args,
35827
- decision: resolvedResolveDecision
35828
- });
35829
- clearTimeout(entry.timer);
35830
- if (resolvedResolveDecision === "allow") {
35831
- insightCounts.set(entry.toolName, (insightCounts.get(entry.toolName) ?? 0) + 1);
35832
- saveInsightCounts();
35833
- } else {
35834
- insightCounts.delete(entry.toolName);
35835
- saveInsightCounts();
35836
- }
35837
- if (source && HUMAN_DECISION_SOURCES.has(source)) entry.decisionSource = source;
35838
- if (entry.waiter) entry.waiter(resolvedResolveDecision);
35839
- else entry.earlyDecision = resolvedResolveDecision;
35840
- pending.delete(id);
35841
- broadcast("remove", { id, decision: resolvedResolveDecision });
35842
- res.writeHead(200);
35843
- return res.end(JSON.stringify({ ok: true }));
36331
+ if (!cards.get(id)) return res.writeHead(404).end();
36332
+ const body = JSON.parse(await readBody(req));
36333
+ const record2 = parseDecisionRecord(body.record) ?? recordFromLegacyResolve(body.decision === "allow" ? "allow" : "deny", body.source);
36334
+ const outcome = cards.decide(id, record2);
36335
+ if (!outcome) return res.writeHead(404).end();
36336
+ res.writeHead(200, { "Content-Type": "application/json" });
36337
+ return res.end(
36338
+ JSON.stringify({ protocol: CARD_PROTOCOL, won: outcome.won, record: outcome.record })
36339
+ );
35844
36340
  } catch {
35845
36341
  res.writeHead(400).end();
35846
36342
  }
@@ -36185,35 +36681,16 @@ data: ${JSON.stringify(item.data)}
36185
36681
  args: { toolCount: tools.length, status },
36186
36682
  decision: "mcp-discovered"
36187
36683
  });
36188
- const id = randomUUID7();
36189
- const entry = {
36190
- id,
36191
- type: "mcp-discovery",
36192
- serverKey,
36193
- mcpTools: tools,
36194
- toolName: "mcp-discovery",
36195
- args: { serverKey, tools: tools.length },
36196
- timestamp: Date.now(),
36197
- slackDelegated: false,
36198
- timer: setTimeout(() => {
36199
- if (pending.has(id)) {
36200
- pending.delete(id);
36201
- broadcast("remove", { id, decision: "deny" });
36202
- }
36203
- }, 6e4),
36204
- // 60s timeout for discovery
36205
- waiter: null,
36206
- earlyDecision: null
36207
- };
36208
- pending.set(id, entry);
36209
- broadcast("add", {
36210
- id,
36211
- type: "mcp-discovery",
36212
- serverKey,
36213
- mcpTools: tools,
36214
- toolName: "mcp-discovery",
36215
- args: { serverKey, toolCount: tools.length }
36216
- });
36684
+ const card = cards.register(
36685
+ {
36686
+ toolName: "mcp-discovery",
36687
+ args: { serverKey, toolCount: tools.length },
36688
+ serverKey,
36689
+ mcpTools: tools
36690
+ },
36691
+ { kind: "discovery", channels: ["terminal", "monitor"], timeoutMs: 6e4 }
36692
+ );
36693
+ broadcast("add", cardView(card));
36217
36694
  }
36218
36695
  res.writeHead(200).end();
36219
36696
  return;
@@ -36246,20 +36723,22 @@ data: ${JSON.stringify(item.data)}
36246
36723
  if (!validToken(req)) return res.writeHead(403).end();
36247
36724
  try {
36248
36725
  const { id, serverKey, disabledTools } = JSON.parse(await readBody(req));
36249
- const entry = pending.get(id);
36250
- if (!entry || entry.type !== "mcp-discovery" || entry.serverKey !== serverKey) {
36726
+ const card = typeof id === "string" ? cards.get(id) : void 0;
36727
+ if (!card || card.kind !== "discovery" || card.request.serverKey !== serverKey) {
36251
36728
  res.writeHead(404).end();
36252
36729
  return;
36253
36730
  }
36254
- clearTimeout(entry.timer);
36731
+ const outcome = cards.decide(card.id, personRecord("terminal", "allow"));
36732
+ if (!outcome?.won) {
36733
+ res.writeHead(409).end();
36734
+ return;
36735
+ }
36255
36736
  approveServer(serverKey, disabledTools);
36256
36737
  appendAuditLog({
36257
36738
  toolName: `mcp-server:${serverKey}`,
36258
36739
  args: { disabledTools },
36259
36740
  decision: "allow"
36260
36741
  });
36261
- pending.delete(id);
36262
- broadcast("remove", { id, decision: "allow" });
36263
36742
  res.writeHead(200).end();
36264
36743
  return;
36265
36744
  } catch {
@@ -36492,7 +36971,7 @@ var init_server = __esm({
36492
36971
  "use strict";
36493
36972
  init_migrate();
36494
36973
  init_trusted_recipients();
36495
- init_decision_sources();
36974
+ init_decision_record();
36496
36975
  init_core();
36497
36976
  init_keyed_guard();
36498
36977
  init_scan();
@@ -36883,14 +37362,14 @@ var init_open_browser = __esm({
36883
37362
  });
36884
37363
 
36885
37364
  // src/utils/post-json.ts
36886
- import * as http4 from "http";
37365
+ import * as http5 from "http";
36887
37366
  import * as https5 from "https";
36888
37367
  import { URL as URL4 } from "url";
36889
37368
  function postJson2(url, body, bearer) {
36890
37369
  return new Promise((resolve2, reject) => {
36891
37370
  const payload = JSON.stringify(body);
36892
37371
  const u = new URL4(url);
36893
- const lib = u.protocol === "http:" ? http4 : https5;
37372
+ const lib = u.protocol === "http:" ? http5 : https5;
36894
37373
  const req = lib.request(
36895
37374
  {
36896
37375
  method: "POST",
@@ -46289,7 +46768,7 @@ var require_client = __commonJS({
46289
46768
  "use strict";
46290
46769
  var assert = __require("assert");
46291
46770
  var net3 = __require("net");
46292
- var http8 = __require("http");
46771
+ var http9 = __require("http");
46293
46772
  var util = require_util();
46294
46773
  var { ClientStats } = require_stats();
46295
46774
  var { channels } = require_diagnostics();
@@ -46347,7 +46826,7 @@ var require_client = __commonJS({
46347
46826
  var connectH1 = require_client_h1();
46348
46827
  var connectH2 = require_client_h2();
46349
46828
  var kClosedResolve = /* @__PURE__ */ Symbol("kClosedResolve");
46350
- var getDefaultNodeMaxHeaderSize = http8 && http8.maxHeaderSize && Number.isInteger(http8.maxHeaderSize) && http8.maxHeaderSize > 0 ? () => http8.maxHeaderSize : () => {
46829
+ var getDefaultNodeMaxHeaderSize = http9 && http9.maxHeaderSize && Number.isInteger(http9.maxHeaderSize) && http9.maxHeaderSize > 0 ? () => http9.maxHeaderSize : () => {
46351
46830
  throw new InvalidArgumentError("http module not available or http.maxHeaderSize invalid");
46352
46831
  };
46353
46832
  var noop = () => {
@@ -46964,8 +47443,8 @@ var require_pool_base = __commonJS({
46964
47443
  }
46965
47444
  get [kPending]() {
46966
47445
  let ret = this[kQueued];
46967
- for (const { [kPending]: pending2 } of this[kClients]) {
46968
- ret += pending2;
47446
+ for (const { [kPending]: pending } of this[kClients]) {
47447
+ ret += pending;
46969
47448
  }
46970
47449
  return ret;
46971
47450
  }
@@ -51394,19 +51873,19 @@ var require_mock_agent = __commonJS({
51394
51873
  }
51395
51874
  pendingInterceptors() {
51396
51875
  const mockAgentClients = this[kClients];
51397
- return Array.from(mockAgentClients.entries()).flatMap(([origin, result]) => result.dispatcher[kDispatches].map((dispatch) => ({ ...dispatch, origin }))).filter(({ pending: pending2 }) => pending2);
51876
+ return Array.from(mockAgentClients.entries()).flatMap(([origin, result]) => result.dispatcher[kDispatches].map((dispatch) => ({ ...dispatch, origin }))).filter(({ pending }) => pending);
51398
51877
  }
51399
51878
  assertNoPendingInterceptors({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) {
51400
- const pending2 = this.pendingInterceptors();
51401
- if (pending2.length === 0) {
51879
+ const pending = this.pendingInterceptors();
51880
+ if (pending.length === 0) {
51402
51881
  return;
51403
51882
  }
51404
51883
  throw new UndiciError(
51405
- pending2.length === 1 ? `1 interceptor is pending:
51884
+ pending.length === 1 ? `1 interceptor is pending:
51406
51885
 
51407
- ${pendingInterceptorsFormatter.format(pending2)}`.trim() : `${pending2.length} interceptors are pending:
51886
+ ${pendingInterceptorsFormatter.format(pending)}`.trim() : `${pending.length} interceptors are pending:
51408
51887
 
51409
- ${pendingInterceptorsFormatter.format(pending2)}`.trim()
51888
+ ${pendingInterceptorsFormatter.format(pending)}`.trim()
51410
51889
  );
51411
51890
  }
51412
51891
  };
@@ -63715,12 +64194,13 @@ var init_first_run = __esm({
63715
64194
  var tail_exports = {};
63716
64195
  __export(tail_exports, {
63717
64196
  agentLabel: () => agentLabel,
64197
+ deadlineLine: () => deadlineLine,
63718
64198
  eventsUrl: () => eventsUrl,
63719
64199
  sessionTag: () => sessionTag,
63720
64200
  shortenPathSummary: () => shortenPathSummary,
63721
64201
  startTail: () => startTail
63722
64202
  });
63723
- import http6 from "http";
64203
+ import http7 from "http";
63724
64204
  import chalk47 from "chalk";
63725
64205
  import fs97 from "fs";
63726
64206
  import os80 from "os";
@@ -63932,7 +64412,7 @@ function postDecisionHttp(id, decision, authToken, port, opts) {
63932
64412
  if (opts?.trustRecipients) bodyObj.trustRecipients = true;
63933
64413
  if (opts?.reason) bodyObj.reason = opts.reason;
63934
64414
  const body = JSON.stringify(bodyObj);
63935
- const req = http6.request(
64415
+ const req = http7.request(
63936
64416
  {
63937
64417
  hostname: "127.0.0.1",
63938
64418
  port,
@@ -63948,9 +64428,21 @@ function postDecisionHttp(id, decision, authToken, port, opts) {
63948
64428
  }
63949
64429
  },
63950
64430
  (res) => {
63951
- res.resume();
63952
- if (res.statusCode === 200 || res.statusCode === 409) resolve2();
63953
- else reject(new Error(`POST /decision returned ${res.statusCode}`));
64431
+ let text = "";
64432
+ res.setEncoding("utf8");
64433
+ res.on("data", (c) => text += c);
64434
+ res.on("end", () => {
64435
+ if (res.statusCode === 200) return resolve2(null);
64436
+ if (res.statusCode === 409) {
64437
+ try {
64438
+ const r = JSON.parse(text).record;
64439
+ if (r?.decision && r.label) return resolve2({ decision: r.decision, label: r.label });
64440
+ } catch {
64441
+ }
64442
+ return resolve2(null);
64443
+ }
64444
+ reject(new Error(`POST /decision returned ${res.statusCode}`));
64445
+ });
63954
64446
  }
63955
64447
  );
63956
64448
  req.on("error", reject);
@@ -63979,6 +64471,15 @@ function cleanBlockedBy(raw) {
63979
64471
  if (smartMatch) return smartMatch[1];
63980
64472
  return raw;
63981
64473
  }
64474
+ function deadlineLine(deadline, now = Date.now()) {
64475
+ if (deadline === void 0) return null;
64476
+ if (deadline === null) return "No timeout: waits for your answer";
64477
+ const left = Math.max(0, Math.round((deadline - now) / 1e3));
64478
+ const m = Math.floor(left / 60);
64479
+ const sec = left % 60;
64480
+ const at = new Date(deadline).toLocaleTimeString([], { hour12: false });
64481
+ return `Auto-denies in ${m > 0 ? `${m}m ` : ""}${sec}s (at ${at}) if nobody answers`;
64482
+ }
63982
64483
  function buildCardLines(req, localCount = 0) {
63983
64484
  if (req.recoveryCommand) {
63984
64485
  return buildRecoveryCardLines(req);
@@ -64018,8 +64519,10 @@ function buildCardLines(req, localCount = 0) {
64018
64519
  `${CYAN}\u2551${RESET} ${YELLOW}\u{1F4A1}${RESET} Approved ${localCount}\xD7 before \u2014 ${BOLD}[a]${RESET}${YELLOW} creates a permanent rule${RESET}`
64019
64520
  );
64020
64521
  }
64522
+ const timing = deadlineLine(req.deadline);
64021
64523
  lines.push(
64022
64524
  `${CYAN}\u255A${RESET}`,
64525
+ ...timing ? [` ${GRAY}${timing}${RESET}`] : [],
64023
64526
  ``,
64024
64527
  exposure.length > 0 ? ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${CYAN}[s]${RESET} Allow + trust ${exposure.length === 1 ? "this address" : "these addresses"}` : ` ${BOLD}${GREEN}[\u21B5/y]${RESET} Allow ${BOLD}${RED}[n]${RESET} Deny ${BOLD}${YELLOW}[a]${RESET} Always Allow ${BOLD}${CYAN}[t]${RESET} Trust 30m`,
64025
64528
  ``
@@ -64036,7 +64539,7 @@ function buildRecoveryCardLines(req) {
64036
64539
  ` ${BOLD}${YELLOW}[2]${RESET} Redirect AI: "Run '${recoveryCommand}' first, then retry"`,
64037
64540
  ` ${BOLD}${RED}[3]${RESET} Deny & stop ${GRAY}(hard block)${RESET}`,
64038
64541
  ``,
64039
- ` ${GRAY}[Timeout: auto-deny]${RESET}`,
64542
+ ` ${GRAY}${deadlineLine(req.deadline) ?? "Auto-denies at the approval timeout if nobody answers"}${RESET}`,
64040
64543
  ` Select [1-3]: `
64041
64544
  ];
64042
64545
  return [
@@ -64083,7 +64586,7 @@ async function startTail(options = {}) {
64083
64586
  const port = await ensureDaemon();
64084
64587
  if (options.clear) {
64085
64588
  const result = await new Promise((resolve2) => {
64086
- const req2 = http6.request(
64589
+ const req2 = http7.request(
64087
64590
  { method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
64088
64591
  (res) => {
64089
64592
  const status = res.statusCode ?? 0;
@@ -64246,7 +64749,13 @@ async function startTail(options = {}) {
64246
64749
  } else {
64247
64750
  httpDecision = action;
64248
64751
  }
64249
- postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
64752
+ postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).then((won) => {
64753
+ if (won && !cardActive)
64754
+ process.stdout.write(
64755
+ ` ${YELLOW}\u21B3 Already decided before your answer: ${won.decision.toUpperCase()} (${safeMessage(won.label, 120)})${RESET}
64756
+ `
64757
+ );
64758
+ }).catch((err2) => {
64250
64759
  try {
64251
64760
  fs97.appendFileSync(
64252
64761
  path94.join(os80.homedir(), ".node9", "hook-debug.log"),
@@ -64371,7 +64880,7 @@ async function startTail(options = {}) {
64371
64880
  }, STALL_THRESHOLD_MS / 2);
64372
64881
  stallWatchdog.unref();
64373
64882
  const sseUrl = eventsUrl(port, canApprove);
64374
- const req = http6.get(
64883
+ const req = http7.get(
64375
64884
  sseUrl,
64376
64885
  {
64377
64886
  headers: authToken ? { "X-Node9-Internal": authToken } : {}
@@ -64589,7 +65098,7 @@ __export(hud_exports, {
64589
65098
  import fs98 from "fs";
64590
65099
  import path95 from "path";
64591
65100
  import os81 from "os";
64592
- import http7 from "http";
65101
+ import http8 from "http";
64593
65102
  async function readStdin() {
64594
65103
  const chunks = [];
64595
65104
  for await (const chunk2 of process.stdin) {
@@ -64607,7 +65116,7 @@ function queryDaemon() {
64607
65116
  return new Promise((resolve2) => {
64608
65117
  const timeout = setTimeout(() => resolve2(null), 50);
64609
65118
  try {
64610
- const req = http7.get(
65119
+ const req = http8.get(
64611
65120
  `http://${DAEMON_HOST}:${DAEMON_PORT}/status`,
64612
65121
  { timeout: 50 },
64613
65122
  (res) => {
@@ -65078,7 +65587,7 @@ async function runProxy(targetCommand) {
65078
65587
  console.error(chalk9.gray(` Reason: ${result.reason || "Security Policy"}
65079
65588
  `));
65080
65589
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
65081
- const isHuman = isHumanDecision(blockedByLabel, result.blockedBy);
65590
+ const isHuman = isHumanDecision(blockedByLabel, result.blockedBy, result.decidedBy);
65082
65591
  const aiInstruction = buildNegotiationMessage(
65083
65592
  blockedByLabel,
65084
65593
  isHuman,
@@ -65086,7 +65595,8 @@ async function runProxy(targetCommand) {
65086
65595
  void 0,
65087
65596
  {
65088
65597
  kind: result.blockKind,
65089
- path: result.blockedPath
65598
+ path: result.blockedPath,
65599
+ redirect: !!result.redirect
65090
65600
  }
65091
65601
  );
65092
65602
  const errorResponse = {
@@ -65760,7 +66270,11 @@ RAW: ${raw}
65760
66270
  const mcpServer = mcpMatch?.[1];
65761
66271
  const sendBlock = (msg, result2) => {
65762
66272
  const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || (isKeyedForPolicy() ? "Workspace Policy" : "Local Security Policy");
65763
- const humanDecision = isHumanDecision(blockedByContext, result2?.blockedBy);
66273
+ const humanDecision = isHumanDecision(
66274
+ blockedByContext,
66275
+ result2?.blockedBy,
66276
+ result2?.decidedBy
66277
+ );
65764
66278
  let ttyFd = null;
65765
66279
  try {
65766
66280
  ttyFd = fs70.openSync("/dev/tty", "w");
@@ -65800,7 +66314,7 @@ RAW: ${raw}
65800
66314
  humanDecision,
65801
66315
  msg,
65802
66316
  result2?.recoveryCommand,
65803
- { kind: result2?.blockKind, path: result2?.blockedPath }
66317
+ { kind: result2?.blockKind, path: result2?.blockedPath, redirect: !!result2?.redirect }
65804
66318
  );
65805
66319
  if (agent === "Antigravity") {
65806
66320
  process.stdout.write(
@@ -68243,11 +68757,15 @@ function aggregateReportFromAudit(period, opts = {}) {
68243
68757
  let dimAppsBlocked = 0;
68244
68758
  let dimPinMismatch = 0;
68245
68759
  let dimInjectionBlocked = 0;
68760
+ const hookCardIds = new Set(
68761
+ entries.filter((e) => e.source !== "daemon" && e.cardId).map((e) => e.cardId)
68762
+ );
68246
68763
  for (const e of entries) {
68764
+ if (e.source === "daemon" && e.cardId && hookCardIds.has(e.cardId)) continue;
68247
68765
  if (superseded.has(supersedeKey(e))) continue;
68248
68766
  const view = viewOf(e);
68249
68767
  const dateKey = e.ts.slice(0, 10);
68250
- const userInteracted = e.source === "daemon";
68768
+ const userInteracted = e.decidedBy ? e.decidedBy === "person" : e.source === "daemon";
68251
68769
  if (view.observed) {
68252
68770
  if (e.checkedBy === "observe-mode-dlp-would-block") observeDlp++;
68253
68771
  } else if (view.info) {
@@ -68255,7 +68773,8 @@ function aggregateReportFromAudit(period, opts = {}) {
68255
68773
  if (view.ran) userApproved++;
68256
68774
  else userDenied++;
68257
68775
  } else if (view.blocked) {
68258
- if (e.checkedBy === "timeout") timedOut++;
68776
+ if (e.checkedBy === "timeout" || e.decidedBy === "timer" || e.decidedBy === "abandoned")
68777
+ timedOut++;
68259
68778
  else if (isDlp(e.checkedBy)) dlpBlocked++;
68260
68779
  else if (e.checkedBy === "local-decision") userDenied++;
68261
68780
  else if (e.checkedBy !== "loop-detected") hardBlocked++;
@@ -69245,7 +69764,7 @@ function registerHealCommand(program2) {
69245
69764
  init_onboarding();
69246
69765
  init_cloud_endpoints();
69247
69766
  init_safe_text();
69248
- import http5 from "http";
69767
+ import http6 from "http";
69249
69768
  import https8 from "https";
69250
69769
  import { URL as URL5 } from "url";
69251
69770
  import chalk22 from "chalk";
@@ -69256,7 +69775,7 @@ function postConnect(url, token) {
69256
69775
  return new Promise((resolve2, reject) => {
69257
69776
  const body = JSON.stringify({ token });
69258
69777
  const u = new URL5(url);
69259
- const lib = u.protocol === "http:" ? http5 : https8;
69778
+ const lib = u.protocol === "http:" ? http6 : https8;
69260
69779
  const req = lib.request(
69261
69780
  {
69262
69781
  method: "POST",
@@ -69395,7 +69914,7 @@ init_safe_text();
69395
69914
  import readline3 from "readline";
69396
69915
  import chalk24 from "chalk";
69397
69916
  import { spawn as spawn8 } from "child_process";
69398
- import { randomUUID as randomUUID8 } from "crypto";
69917
+ import { randomUUID as randomUUID9 } from "crypto";
69399
69918
  import { execa as execa2 } from "execa";
69400
69919
 
69401
69920
  // src/mcp-gateway/result-scan.ts
@@ -69661,7 +70180,7 @@ async function runMcpGateway(upstreamCommand, configName) {
69661
70180
  return false;
69662
70181
  }
69663
70182
  };
69664
- const gatewaySessionId = `mcp-gateway-${randomUUID8()}`;
70183
+ const gatewaySessionId = `mcp-gateway-${randomUUID9()}`;
69665
70184
  const readOnlyTools = /* @__PURE__ */ new Set();
69666
70185
  let clientName;
69667
70186
  const agentIn = readline3.createInterface({ input: process.stdin, terminal: false });
@@ -69760,13 +70279,13 @@ async function runMcpGateway(upstreamCommand, configName) {
69760
70279
  console.error(chalk24.gray(` Reason: ${result.reason ?? "Security Policy"}
69761
70280
  `));
69762
70281
  const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
69763
- const humanDecision = isHumanDecision(blockedByLabel, result.blockedBy);
70282
+ const humanDecision = isHumanDecision(blockedByLabel, result.blockedBy, result.decidedBy);
69764
70283
  const aiInstruction = buildNegotiationMessage(
69765
70284
  blockedByLabel,
69766
70285
  humanDecision,
69767
70286
  result.reason,
69768
70287
  void 0,
69769
- { kind: result.blockKind, path: result.blockedPath }
70288
+ { kind: result.blockKind, path: result.blockedPath, redirect: !!result.redirect }
69770
70289
  );
69771
70290
  const errorResponse = {
69772
70291
  jsonrpc: "2.0",
@@ -71033,7 +71552,7 @@ function registerMcpGatewayCommand2(mcp) {
71033
71552
  }
71034
71553
  const ungoverned = rows.filter((e) => e.connection === "ungoverned").length;
71035
71554
  const unlaunchable = rows.filter((e) => e.connection === "unlaunchable").length;
71036
- const pending2 = rows.filter((e) => e.connection === "pending-launch").length;
71555
+ const pending = rows.filter((e) => e.connection === "pending-launch").length;
71037
71556
  if (ungoverned > 0) {
71038
71557
  console.error(
71039
71558
  chalk26.yellow(
@@ -71042,9 +71561,9 @@ ${ungoverned} ungoverned \u2014 run ` + chalk26.bold("node9 mcp gateway --all")
71042
71561
  )
71043
71562
  );
71044
71563
  }
71045
- if (pending2 > 0) {
71564
+ if (pending > 0) {
71046
71565
  console.error(
71047
- chalk26.cyan(`${pending2} governed but not yet connected \u2014 restart the agent to activate.`)
71566
+ chalk26.cyan(`${pending} governed but not yet connected \u2014 restart the agent to activate.`)
71048
71567
  );
71049
71568
  }
71050
71569
  if (unlaunchable > 0) {