@suveren/gateway 0.25.0 → 0.25.1

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.
@@ -15,6 +15,7 @@ import {
15
15
  computeMetric,
16
16
  fingerprintOf,
17
17
  formatDateTime,
18
+ getIdentityDirectory,
18
19
  glossaryUsage,
19
20
  issuerFromPublicKeyHex,
20
21
  mandateFields,
@@ -31,7 +32,7 @@ import {
31
32
  sanitizeReport,
32
33
  suggestedFilename,
33
34
  ticketFields
34
- } from "./chunk-APIQIEUA.mjs";
35
+ } from "./chunk-PU4VQEEA.mjs";
35
36
 
36
37
  // bin/http.ts
37
38
  import { randomUUID as randomUUID2 } from "crypto";
@@ -987,6 +988,19 @@ var SPClient = class {
987
988
  const data = await res.json();
988
989
  return data.proposals;
989
990
  }
991
+ /**
992
+ * RJ4 — the same proposals a human just rejected, sent back for a change,
993
+ * or replaced with their own mandate (all three outcomes live under
994
+ * `status: 'rejected'`, see `ProposalOutcome`). Polled on the SAME tick as
995
+ * {@link getCommittedProposals} (bin/http.ts) so the outcome outbox learns
996
+ * about them without a second AS polling loop.
997
+ */
998
+ async getRejectedProposals() {
999
+ const res = await this.fetch("/api/proposals?status=rejected");
1000
+ if (!res.ok) throw new Error(`SP rejected proposals request failed: ${res.status}`);
1001
+ const data = await res.json();
1002
+ return data.proposals;
1003
+ }
990
1004
  /**
991
1005
  * Look up ONE proposal by id, whatever its status. `null` when the AS has no
992
1006
  * such proposal (or the caller may not see it).
@@ -2132,6 +2146,163 @@ var PreviewLog = class {
2132
2146
  }
2133
2147
  };
2134
2148
 
2149
+ // src/lib/outcome-outbox.ts
2150
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, unlinkSync as unlinkSync5 } from "fs";
2151
+ import { homedir as homedir8 } from "os";
2152
+ import { dirname as dirname9, join as join11 } from "path";
2153
+ import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
2154
+ var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
2155
+ var MAX_AGE_MS3 = 7 * 24 * 60 * 60 * 1e3;
2156
+ function outcomeEntryFor(proposal, submittedHere, who, nowMs) {
2157
+ if (!submittedHere) return null;
2158
+ const by = proposal.approverRejectedBy ?? proposal.rejectedBy;
2159
+ if (!by || typeof by.at !== "number") return null;
2160
+ const decidedAt = by.at * 1e3;
2161
+ if (nowMs - decidedAt > MAX_AGE_MS3) return null;
2162
+ return { proposalId: proposal.id, outcome: by.outcome ?? "rejected", note: by.note, who, decidedAt };
2163
+ }
2164
+ var OutcomeOutbox = class {
2165
+ entries = [];
2166
+ baseDir;
2167
+ vaultKey = null;
2168
+ constructor(filePath) {
2169
+ this.baseDir = filePath && filePath.endsWith(".json") ? dirname9(filePath) : filePath ?? DEFAULT_DIR8;
2170
+ this.loadPlaintext();
2171
+ }
2172
+ /** Mirrors DenialLog.setVaultKey — migrates plaintext -> encrypted once, in place. */
2173
+ setVaultKey(key) {
2174
+ this.vaultKey = key;
2175
+ if (existsSync11(this.encryptedFilePath)) {
2176
+ this.loadEncrypted();
2177
+ } else if (this.entries.length > 0) {
2178
+ this.persistEncrypted();
2179
+ if (existsSync11(this.plaintextFilePath)) {
2180
+ try {
2181
+ unlinkSync5(this.plaintextFilePath);
2182
+ } catch {
2183
+ }
2184
+ }
2185
+ }
2186
+ }
2187
+ /**
2188
+ * Record a resolved outcome, UNLESS this proposal is already known here
2189
+ * (delivered or not) — the poller rechecks the AS's rejected list on
2190
+ * every tick, and a proposal stays in that list well past the moment this
2191
+ * gateway first learned about it, so without this guard the same outcome
2192
+ * would be queued again right after it was delivered.
2193
+ */
2194
+ recordIfNew(entry) {
2195
+ if (this.entries.some((e) => e.proposalId === entry.proposalId)) return;
2196
+ this.entries.push({ ...entry });
2197
+ this.prune(Date.now());
2198
+ this.persist();
2199
+ }
2200
+ /** Undelivered entries, oldest first — what the next tool reply should carry. */
2201
+ getUndelivered() {
2202
+ return this.entries.filter((e) => e.deliveredAt === void 0).sort((a, b) => a.decidedAt - b.decidedAt);
2203
+ }
2204
+ /** Mark these proposals' entries delivered (idempotent). Always persists from disk state. */
2205
+ markDelivered(proposalIds) {
2206
+ if (proposalIds.length === 0) return;
2207
+ const ids = new Set(proposalIds);
2208
+ const now = Date.now();
2209
+ let changed = false;
2210
+ for (const e of this.entries) {
2211
+ if (ids.has(e.proposalId) && e.deliveredAt === void 0) {
2212
+ e.deliveredAt = now;
2213
+ changed = true;
2214
+ }
2215
+ }
2216
+ if (changed) this.persist();
2217
+ }
2218
+ /** Diagnostic / tests. */
2219
+ get size() {
2220
+ return this.entries.length;
2221
+ }
2222
+ // ─── Retention ────────────────────────────────────────────────────────────
2223
+ prune(now) {
2224
+ const cutoff = now - MAX_AGE_MS3;
2225
+ this.entries = this.entries.filter((e) => e.decidedAt >= cutoff);
2226
+ }
2227
+ // ─── Encryption (mirrors DenialLog / ExecutionLog) ─────────────────────────
2228
+ encrypt(plaintext) {
2229
+ if (!this.vaultKey) throw new Error("No vault key");
2230
+ const iv = randomBytes5(12);
2231
+ const cipher = createCipheriv5("aes-256-gcm", this.vaultKey, iv);
2232
+ const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2233
+ return { iv: iv.toString("hex"), ciphertext: enc.toString("hex"), tag: cipher.getAuthTag().toString("hex") };
2234
+ }
2235
+ decrypt(blob) {
2236
+ if (!this.vaultKey) throw new Error("No vault key");
2237
+ const decipher = createDecipheriv5("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
2238
+ decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
2239
+ return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
2240
+ }
2241
+ get plaintextFilePath() {
2242
+ return join11(this.baseDir, "outcome-outbox.json");
2243
+ }
2244
+ get encryptedFilePath() {
2245
+ return join11(this.baseDir, "outcome-outbox.enc.json");
2246
+ }
2247
+ loadPlaintext() {
2248
+ if (!existsSync11(this.plaintextFilePath)) {
2249
+ mkdirSync9(this.baseDir, { recursive: true });
2250
+ return;
2251
+ }
2252
+ try {
2253
+ const data = JSON.parse(readFileSync12(this.plaintextFilePath, "utf-8"));
2254
+ this.entries = data.entries ?? [];
2255
+ this.prune(Date.now());
2256
+ } catch {
2257
+ console.error(`[OutcomeOutbox] Could not parse ${this.plaintextFilePath}, starting fresh`);
2258
+ this.entries = [];
2259
+ }
2260
+ }
2261
+ loadEncrypted() {
2262
+ if (!existsSync11(this.encryptedFilePath)) return;
2263
+ try {
2264
+ const data = JSON.parse(readFileSync12(this.encryptedFilePath, "utf-8"));
2265
+ const storeFile = JSON.parse(this.decrypt(data.blob));
2266
+ this.entries = storeFile.entries ?? [];
2267
+ this.prune(Date.now());
2268
+ } catch (err) {
2269
+ console.error(`[OutcomeOutbox] Could not decrypt ${this.encryptedFilePath}:`, err);
2270
+ this.entries = [];
2271
+ }
2272
+ }
2273
+ persist() {
2274
+ if (this.vaultKey) this.persistEncrypted();
2275
+ else this.persistPlaintext();
2276
+ }
2277
+ persistPlaintext() {
2278
+ const data = { version: 1, entries: this.entries };
2279
+ mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
2280
+ writeFileSync9(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2281
+ }
2282
+ persistEncrypted() {
2283
+ const storeFile = { version: 1, entries: this.entries };
2284
+ const data = { version: 1, blob: this.encrypt(JSON.stringify(storeFile)) };
2285
+ mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
2286
+ writeFileSync9(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2287
+ }
2288
+ };
2289
+ function formatOutcomeLine(e) {
2290
+ const what = e.outcome === "change_needed" ? `SENT BACK FOR A CHANGE by ${e.who}: "${e.note ?? ""}"` : e.outcome === "replaced" ? `REPLACED by the person's own mandate` : `REJECTED by ${e.who}`;
2291
+ return `[Suveren gateway] Since your last call: proposal ${e.proposalId} was ${what}.`;
2292
+ }
2293
+ function wrapWithOutcomeOutbox(outbox, handler) {
2294
+ return async (...args) => {
2295
+ const result = await handler(...args);
2296
+ if (!outbox) return result;
2297
+ const pending = outbox.getUndelivered();
2298
+ if (pending.length === 0) return result;
2299
+ if (!result || !Array.isArray(result.content)) return result;
2300
+ outbox.markDelivered(pending.map((e) => e.proposalId));
2301
+ const note = pending.map(formatOutcomeLine).join("\n");
2302
+ return { ...result, content: [...result.content, { type: "text", text: note }] };
2303
+ };
2304
+ }
2305
+
2135
2306
  // src/lib/gatekeeper.ts
2136
2307
  import { verify } from "@hap/core";
2137
2308
  var MCPGatekeeper = class {
@@ -2224,10 +2395,10 @@ var MCPGatekeeper = class {
2224
2395
  };
2225
2396
 
2226
2397
  // src/lib/report/report-store.ts
2227
- import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, unlinkSync as unlinkSync5 } from "fs";
2228
- import { homedir as homedir8 } from "os";
2229
- import { dirname as dirname9, join as join11 } from "path";
2230
- import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
2398
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync12, unlinkSync as unlinkSync6 } from "fs";
2399
+ import { homedir as homedir9 } from "os";
2400
+ import { dirname as dirname10, join as join12 } from "path";
2401
+ import { createCipheriv as createCipheriv6, createDecipheriv as createDecipheriv6, randomBytes as randomBytes6 } from "crypto";
2231
2402
 
2232
2403
  // src/lib/report/record-resolvers.ts
2233
2404
  function findById(rows, id) {
@@ -2535,29 +2706,29 @@ async function buildCoverage(caseElements, getExport, archive, referenced, windo
2535
2706
  }
2536
2707
 
2537
2708
  // src/lib/report/report-store.ts
2538
- var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
2709
+ var DEFAULT_DIR9 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
2539
2710
  var ReportStore = class {
2540
2711
  report = null;
2541
2712
  baseDir;
2542
2713
  vaultKey = null;
2543
2714
  constructor(filePath) {
2544
2715
  if (filePath && filePath.endsWith(".json")) {
2545
- this.baseDir = dirname9(filePath);
2716
+ this.baseDir = dirname10(filePath);
2546
2717
  } else {
2547
- this.baseDir = filePath ?? DEFAULT_DIR8;
2718
+ this.baseDir = filePath ?? DEFAULT_DIR9;
2548
2719
  }
2549
2720
  this.loadPlaintext();
2550
2721
  }
2551
2722
  /** Set the vault key for encryption. Triggers migration from plaintext if needed. */
2552
2723
  setVaultKey(key) {
2553
2724
  this.vaultKey = key;
2554
- if (existsSync11(this.encryptedFilePath)) {
2725
+ if (existsSync12(this.encryptedFilePath)) {
2555
2726
  this.loadEncrypted();
2556
2727
  } else if (this.report !== null) {
2557
2728
  this.persistEncrypted();
2558
- if (existsSync11(this.plaintextFilePath)) {
2729
+ if (existsSync12(this.plaintextFilePath)) {
2559
2730
  try {
2560
- unlinkSync5(this.plaintextFilePath);
2731
+ unlinkSync6(this.plaintextFilePath);
2561
2732
  } catch {
2562
2733
  }
2563
2734
  }
@@ -2570,7 +2741,7 @@ var ReportStore = class {
2570
2741
  * "the report exists but is locked".
2571
2742
  */
2572
2743
  isLocked() {
2573
- return this.vaultKey === null && existsSync11(this.encryptedFilePath);
2744
+ return this.vaultKey === null && existsSync12(this.encryptedFilePath);
2574
2745
  }
2575
2746
  /** The stored, already-verified report — or null if none has been saved
2576
2747
  * (and the store is not locked; check `isLocked()` first). */
@@ -2604,49 +2775,49 @@ var ReportStore = class {
2604
2775
  // ─── Encryption helpers ─────────────────────────────────────────────────
2605
2776
  encrypt(plaintext) {
2606
2777
  if (!this.vaultKey) throw new Error("No vault key");
2607
- const iv = randomBytes5(12);
2608
- const cipher = createCipheriv5("aes-256-gcm", this.vaultKey, iv);
2778
+ const iv = randomBytes6(12);
2779
+ const cipher = createCipheriv6("aes-256-gcm", this.vaultKey, iv);
2609
2780
  const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
2610
2781
  const tag = cipher.getAuthTag();
2611
2782
  return { iv: iv.toString("hex"), ciphertext: encrypted.toString("hex"), tag: tag.toString("hex") };
2612
2783
  }
2613
2784
  decrypt(blob) {
2614
2785
  if (!this.vaultKey) throw new Error("No vault key");
2615
- const decipher = createDecipheriv5("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
2786
+ const decipher = createDecipheriv6("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
2616
2787
  decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
2617
2788
  const decrypted = Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]);
2618
2789
  return decrypted.toString("utf8");
2619
2790
  }
2620
2791
  // ─── File paths ─────────────────────────────────────────────────────────
2621
2792
  get plaintextFilePath() {
2622
- return join11(this.baseDir, "report.json");
2793
+ return join12(this.baseDir, "report.json");
2623
2794
  }
2624
2795
  get encryptedFilePath() {
2625
- return join11(this.baseDir, "report.enc.json");
2796
+ return join12(this.baseDir, "report.enc.json");
2626
2797
  }
2627
2798
  // ─── Load / Persist ─────────────────────────────────────────────────────
2628
2799
  loadPlaintext() {
2629
- if (!existsSync11(this.plaintextFilePath)) {
2630
- mkdirSync9(this.baseDir, { recursive: true });
2800
+ if (!existsSync12(this.plaintextFilePath)) {
2801
+ mkdirSync10(this.baseDir, { recursive: true });
2631
2802
  return;
2632
2803
  }
2633
2804
  try {
2634
- const raw = readFileSync12(this.plaintextFilePath, "utf-8");
2805
+ const raw = readFileSync13(this.plaintextFilePath, "utf-8");
2635
2806
  const parsed = JSON.parse(raw);
2636
2807
  this.report = parsed.report ?? null;
2637
2808
  } catch {
2638
2809
  console.error(`[ReportStore] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
2639
2810
  try {
2640
- writeFileSync9(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync12(this.plaintextFilePath));
2811
+ writeFileSync10(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync13(this.plaintextFilePath));
2641
2812
  } catch {
2642
2813
  }
2643
2814
  this.report = null;
2644
2815
  }
2645
2816
  }
2646
2817
  loadEncrypted() {
2647
- if (!existsSync11(this.encryptedFilePath)) return;
2818
+ if (!existsSync12(this.encryptedFilePath)) return;
2648
2819
  try {
2649
- const raw = readFileSync12(this.encryptedFilePath, "utf-8");
2820
+ const raw = readFileSync13(this.encryptedFilePath, "utf-8");
2650
2821
  const data = JSON.parse(raw);
2651
2822
  const decrypted = this.decrypt(data.blob);
2652
2823
  const parsed = JSON.parse(decrypted);
@@ -2654,7 +2825,7 @@ var ReportStore = class {
2654
2825
  } catch (err) {
2655
2826
  console.error(`[ReportStore] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
2656
2827
  try {
2657
- writeFileSync9(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync12(this.encryptedFilePath));
2828
+ writeFileSync10(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync13(this.encryptedFilePath));
2658
2829
  } catch {
2659
2830
  }
2660
2831
  this.report = null;
@@ -2669,16 +2840,16 @@ var ReportStore = class {
2669
2840
  }
2670
2841
  persistPlaintext() {
2671
2842
  const data = { version: 1, report: this.report };
2672
- mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
2673
- writeFileSync9(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2843
+ mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
2844
+ writeFileSync10(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2674
2845
  }
2675
2846
  persistEncrypted() {
2676
2847
  const data = {
2677
2848
  version: 1,
2678
2849
  blob: this.encrypt(JSON.stringify({ version: 1, report: this.report }))
2679
2850
  };
2680
- mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
2681
- writeFileSync9(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2851
+ mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
2852
+ writeFileSync10(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
2682
2853
  }
2683
2854
  };
2684
2855
 
@@ -2701,6 +2872,9 @@ var SharedState = class {
2701
2872
  proposalSubmissions;
2702
2873
  /** AU3 — local, content-free record of preview reads. See preview-log.ts. */
2703
2874
  previewLog;
2875
+ /** RJ4 — rejection/change-needed/replaced outcomes awaiting one-time
2876
+ * delivery to the AI on its next tool call. See outcome-outbox.ts. */
2877
+ outcomeOutbox;
2704
2878
  gatekeeper;
2705
2879
  /**
2706
2880
  * V7 — set by {@link checkAsCompat} (called once at process start, and
@@ -2735,6 +2909,7 @@ var SharedState = class {
2735
2909
  this.executionJournal = new ExecutionJournal(gateStorePath);
2736
2910
  this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
2737
2911
  this.previewLog = new PreviewLog(gateStorePath);
2912
+ this.outcomeOutbox = new OutcomeOutbox(gateStorePath);
2738
2913
  this.gatekeeper = new MCPGatekeeper(this.cache);
2739
2914
  }
2740
2915
  /**
@@ -2923,29 +3098,29 @@ function formatConsumptionFull(entries) {
2923
3098
  }
2924
3099
 
2925
3100
  // src/lib/context-loader.ts
2926
- import { readFileSync as readFileSync13, existsSync as existsSync12, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10, renameSync as renameSync4 } from "fs";
2927
- import { homedir as homedir9 } from "os";
2928
- import { join as join12, dirname as dirname10 } from "path";
2929
- var DEFAULT_DIR9 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
3101
+ import { readFileSync as readFileSync14, existsSync as existsSync13, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11, renameSync as renameSync4 } from "fs";
3102
+ import { homedir as homedir10 } from "os";
3103
+ import { join as join13, dirname as dirname11 } from "path";
3104
+ var DEFAULT_DIR10 = process.env.SUVEREN_DATA_DIR ?? join13(homedir10(), ".suveren");
2930
3105
  var CONTEXT_MAX_BYTES = 16 * 1024;
2931
3106
  function contextFilePath(dataDir2) {
2932
- return join12(dataDir2 ?? DEFAULT_DIR9, "context.md");
3107
+ return join13(dataDir2 ?? DEFAULT_DIR10, "context.md");
2933
3108
  }
2934
3109
  function writeContextFile(content, dataDir2) {
2935
3110
  const bytes = Buffer.byteLength(content, "utf-8");
2936
3111
  if (bytes > CONTEXT_MAX_BYTES) throw new Error(`the brief is ${bytes} bytes; the limit is ${CONTEXT_MAX_BYTES}.`);
2937
3112
  const filePath = contextFilePath(dataDir2);
2938
- mkdirSync10(dirname10(filePath), { recursive: true });
3113
+ mkdirSync11(dirname11(filePath), { recursive: true });
2939
3114
  const tmpPath = `${filePath}.tmp`;
2940
- writeFileSync10(tmpPath, content, "utf-8");
3115
+ writeFileSync11(tmpPath, content, "utf-8");
2941
3116
  renameSync4(tmpPath, filePath);
2942
3117
  }
2943
3118
  var BRIEF_MAX_CHARS = 16 * 1024;
2944
3119
  function readContextFile(dataDir2) {
2945
3120
  const filePath = contextFilePath(dataDir2);
2946
- if (!existsSync12(filePath)) return null;
3121
+ if (!existsSync13(filePath)) return null;
2947
3122
  try {
2948
- const content = readFileSync13(filePath, "utf-8").trim();
3123
+ const content = readFileSync14(filePath, "utf-8").trim();
2949
3124
  return content || null;
2950
3125
  } catch {
2951
3126
  return null;
@@ -3135,8 +3310,8 @@ function attachTicketId(tool, args, ticketId) {
3135
3310
  import { computeContentHash as computeContentHash2 } from "@hap/core";
3136
3311
 
3137
3312
  // src/lib/manifest-loader.ts
3138
- import { readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
3139
- import { join as join13, resolve } from "path";
3313
+ import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
3314
+ import { join as join14, resolve } from "path";
3140
3315
  var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
3141
3316
  function isExactSemver(value) {
3142
3317
  return typeof value === "string" && EXACT_SEMVER_RE.test(value);
@@ -3205,25 +3380,25 @@ function invalidManifestPreviewReason(manifest) {
3205
3380
  var manifests = /* @__PURE__ */ new Map();
3206
3381
  function loadManifests(integrationsDir) {
3207
3382
  const dir = resolve(
3208
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join13(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3383
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3209
3384
  );
3210
- const indexPath = join13(dir, "index.json");
3211
- if (!existsSync13(indexPath)) {
3385
+ const indexPath = join14(dir, "index.json");
3386
+ if (!existsSync14(indexPath)) {
3212
3387
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
3213
3388
  return 0;
3214
3389
  }
3215
3390
  let index;
3216
3391
  try {
3217
- index = JSON.parse(readFileSync14(indexPath, "utf-8"));
3392
+ index = JSON.parse(readFileSync15(indexPath, "utf-8"));
3218
3393
  } catch (err) {
3219
3394
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
3220
3395
  return 0;
3221
3396
  }
3222
3397
  let loaded = 0;
3223
3398
  for (const [id, relativePath] of Object.entries(index.integrations)) {
3224
- const manifestPath = join13(dir, relativePath);
3399
+ const manifestPath = join14(dir, relativePath);
3225
3400
  try {
3226
- const manifest = JSON.parse(readFileSync14(manifestPath, "utf-8"));
3401
+ const manifest = JSON.parse(readFileSync15(manifestPath, "utf-8"));
3227
3402
  const pinReason = invalidNpmPinReason(manifest);
3228
3403
  if (pinReason) {
3229
3404
  console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
@@ -5134,6 +5309,22 @@ function listIntegrationsHandler(state2, integrationManager2) {
5134
5309
 
5135
5310
  // src/tools/commitments.ts
5136
5311
  import { ContentBindingError as ContentBindingError2 } from "@hap/core";
5312
+
5313
+ // src/lib/who-label.ts
5314
+ var UNKNOWN_DECIDER = "the person";
5315
+ async function resolveWhoLabel(archive, domainKey, userId) {
5316
+ if (!archive) return UNKNOWN_DECIDER;
5317
+ try {
5318
+ const dir = await getIdentityDirectory(archive);
5319
+ const did = dir.domainDids.get(domainKey) ?? dir.domainDids.get(userId);
5320
+ const name = did ? dir.names.get(did) : void 0;
5321
+ return name ?? UNKNOWN_DECIDER;
5322
+ } catch {
5323
+ return UNKNOWN_DECIDER;
5324
+ }
5325
+ }
5326
+
5327
+ // src/tools/commitments.ts
5137
5328
  function buildSkippedProposalNote(proposalId) {
5138
5329
  return `Proposal ${proposalId} was approved, but was submitted from another device/installation of this operator (or one whose local record is gone) \u2014 this gateway will not execute it. Re-run the same request here to execute it from this gateway.`;
5139
5330
  }
@@ -5432,11 +5623,34 @@ Result: ${JSON.stringify(match.executionResult, null, 2)}` : " The action ran; t
5432
5623
  }
5433
5624
  if (match.status === "rejected") {
5434
5625
  const by = match.approverRejectedBy ?? match.rejectedBy;
5435
- const reason = match.approverRejectedBy?.reason;
5626
+ const outcome = by?.outcome ?? "rejected";
5627
+ const legacyReason = match.approverRejectedBy?.reason;
5628
+ const who = by ? await resolveWhoLabel(
5629
+ state2.receiptArchive,
5630
+ "domain" in by ? by.domain : by.userId,
5631
+ by.userId
5632
+ ) : "a person";
5633
+ if (outcome === "change_needed") {
5634
+ return {
5635
+ content: [{
5636
+ type: "text",
5637
+ text: `Proposal ${match.id} was SENT BACK FOR A CHANGE by ${who}: "${by?.note ?? ""}". The action did not run. You may adjust it and propose again \u2014 a new proposal the person approves again. Do not resend the same values.`
5638
+ }]
5639
+ };
5640
+ }
5641
+ if (outcome === "replaced") {
5642
+ return {
5643
+ content: [{
5644
+ type: "text",
5645
+ text: `Proposal ${match.id} was REPLACED: the person signed their own version of this mandate on the mandate screen. Nothing was created from this proposal. Use the person's mandate \u2014 check list-authorizations.`
5646
+ }]
5647
+ };
5648
+ }
5649
+ const reasonSuffix = legacyReason ? `: "${legacyReason}".` : ".";
5436
5650
  return {
5437
5651
  content: [{
5438
5652
  type: "text",
5439
- text: `Proposal ${match.id} was REJECTED${by ? ` by ${by.userId}` : ""}${reason ? `: ${reason}` : "."} The action did not run and must not be retried \u2014 a decision owner declined it. Ask before proposing anything similar.`
5653
+ text: `Proposal ${match.id} was REJECTED by ${who}${reasonSuffix} The action did not run and must not be retried. Ask the person before proposing anything similar.`
5440
5654
  }]
5441
5655
  };
5442
5656
  }
@@ -5585,6 +5799,7 @@ function createMcpServer(state2, integrationManager2) {
5585
5799
  { name: "suveren-gateway", version: "0.1.0" },
5586
5800
  { instructions }
5587
5801
  );
5802
+ const withOutcomeOutbox = (handler) => wrapWithOutcomeOutbox(state2.outcomeOutbox, handler);
5588
5803
  server.registerTool(
5589
5804
  "list-authorizations",
5590
5805
  {
@@ -5593,7 +5808,7 @@ function createMcpServer(state2, integrationManager2) {
5593
5808
  domain: z2.string().optional().describe('Profile domain to show full details for (e.g., "charge", "deploy"). Omit for compact overview.')
5594
5809
  }
5595
5810
  },
5596
- listAuthorizationsHandler(state2, integrationManager2)
5811
+ withOutcomeOutbox(listAuthorizationsHandler(state2, integrationManager2))
5597
5812
  );
5598
5813
  server.registerTool(
5599
5814
  "check-pending-mandates",
@@ -5603,7 +5818,7 @@ function createMcpServer(state2, integrationManager2) {
5603
5818
  domain: z2.string().describe(`The owner's domain (e.g., "compliance")`)
5604
5819
  }
5605
5820
  },
5606
- checkPendingHandler(cache)
5821
+ withOutcomeOutbox(checkPendingHandler(cache))
5607
5822
  );
5608
5823
  server.registerTool(
5609
5824
  "list-integrations",
@@ -5611,7 +5826,7 @@ function createMcpServer(state2, integrationManager2) {
5611
5826
  description: "List all running integrations and their authorization status. Returns a compact overview \u2014 use list-authorizations(domain) for full details on a specific profile.",
5612
5827
  inputSchema: {}
5613
5828
  },
5614
- listIntegrationsHandler(state2, integrationManager2)
5829
+ withOutcomeOutbox(listIntegrationsHandler(state2, integrationManager2))
5615
5830
  );
5616
5831
  server.registerTool(
5617
5832
  "check-pending-commitments",
@@ -5621,7 +5836,7 @@ function createMcpServer(state2, integrationManager2) {
5621
5836
  proposal_id: z2.string().optional().describe("Specific proposal ID to check. Omit to see all.")
5622
5837
  }
5623
5838
  },
5624
- checkPendingCommitmentsHandler(state2, integrationManager2)
5839
+ withOutcomeOutbox(checkPendingCommitmentsHandler(state2, integrationManager2))
5625
5840
  );
5626
5841
  const proxiedTools = /* @__PURE__ */ new Map();
5627
5842
  function registerProxiedTools() {
@@ -5655,7 +5870,10 @@ function createMcpServer(state2, integrationManager2) {
5655
5870
  description,
5656
5871
  ...Object.keys(zodShape).length > 0 ? { inputSchema: zodShape } : {}
5657
5872
  },
5658
- handler
5873
+ // Built-in tools (reports, setup) never carry the outcome line: a
5874
+ // report's output is evidence and must not mix in a notice (nor the
5875
+ // decider's identity); setup tools are not the working agent's.
5876
+ integrationManager2?.isBuiltin?.(tool.integrationId) ? handler : withOutcomeOutbox(handler)
5659
5877
  );
5660
5878
  proxiedTools.set(tool.namespacedName, { tool, registered });
5661
5879
  } catch (err) {
@@ -5688,21 +5906,21 @@ function createMcpServer(state2, integrationManager2) {
5688
5906
  }
5689
5907
 
5690
5908
  // src/lib/agent-contact.ts
5691
- import { existsSync as existsSync14, mkdirSync as mkdirSync11, readFileSync as readFileSync15, renameSync as renameSync5, writeFileSync as writeFileSync11 } from "fs";
5692
- import { dirname as dirname11, join as join14 } from "path";
5909
+ import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync16, renameSync as renameSync5, writeFileSync as writeFileSync12 } from "fs";
5910
+ import { dirname as dirname12, join as join15 } from "path";
5693
5911
  var FILE = "agent-contact.json";
5694
5912
  var REWRITE_AFTER_MS = 6e4;
5695
5913
  var AgentContactStore = class {
5696
5914
  path;
5697
5915
  current;
5698
5916
  constructor(dataDir2) {
5699
- this.path = join14(dataDir2, FILE);
5917
+ this.path = join15(dataDir2, FILE);
5700
5918
  this.current = this.load();
5701
5919
  }
5702
5920
  load() {
5703
- if (!existsSync14(this.path)) return null;
5921
+ if (!existsSync15(this.path)) return null;
5704
5922
  try {
5705
- const v = JSON.parse(readFileSync15(this.path, "utf8"));
5923
+ const v = JSON.parse(readFileSync16(this.path, "utf8"));
5706
5924
  if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
5707
5925
  return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
5708
5926
  } catch {
@@ -5725,9 +5943,9 @@ var AgentContactStore = class {
5725
5943
  };
5726
5944
  if (!due) return;
5727
5945
  try {
5728
- mkdirSync11(dirname11(this.path), { recursive: true });
5946
+ mkdirSync12(dirname12(this.path), { recursive: true });
5729
5947
  const tmp = `${this.path}.tmp`;
5730
- writeFileSync11(tmp, JSON.stringify(this.current, null, 2));
5948
+ writeFileSync12(tmp, JSON.stringify(this.current, null, 2));
5731
5949
  renameSync5(tmp, this.path);
5732
5950
  } catch (err) {
5733
5951
  console.error("[Suveren MCP] Could not save agent contact:", err);
@@ -5762,16 +5980,16 @@ function verifyGateContentHashes(content, auth) {
5762
5980
  }
5763
5981
 
5764
5982
  // src/lib/integration-registry.ts
5765
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync12, existsSync as existsSync15 } from "fs";
5766
- import { homedir as homedir10 } from "os";
5767
- import { dirname as dirname12, join as join15 } from "path";
5768
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join15(homedir10(), ".suveren");
5983
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync17, writeFileSync as writeFileSync13, existsSync as existsSync16 } from "fs";
5984
+ import { homedir as homedir11 } from "os";
5985
+ import { dirname as dirname13, join as join16 } from "path";
5986
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join16(homedir11(), ".suveren");
5769
5987
  var IntegrationRegistry = class {
5770
5988
  integrations = /* @__PURE__ */ new Map();
5771
5989
  filePath;
5772
5990
  constructor(dataDir2) {
5773
5991
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
5774
- this.filePath = join15(dir, "integrations.json");
5992
+ this.filePath = join16(dir, "integrations.json");
5775
5993
  this.load();
5776
5994
  }
5777
5995
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -5802,12 +6020,12 @@ var IntegrationRegistry = class {
5802
6020
  }
5803
6021
  // ─── Load / Save ────────────────────────────────────────────────────────
5804
6022
  load() {
5805
- if (!existsSync15(this.filePath)) {
5806
- mkdirSync12(dirname12(this.filePath), { recursive: true });
6023
+ if (!existsSync16(this.filePath)) {
6024
+ mkdirSync13(dirname13(this.filePath), { recursive: true });
5807
6025
  return;
5808
6026
  }
5809
6027
  try {
5810
- const raw = readFileSync16(this.filePath, "utf-8");
6028
+ const raw = readFileSync17(this.filePath, "utf-8");
5811
6029
  const data = JSON.parse(raw);
5812
6030
  const migrated = data.integrations.map((i) => {
5813
6031
  if ("toolGating" in i && !("profile" in i)) {
@@ -5836,15 +6054,15 @@ var IntegrationRegistry = class {
5836
6054
  version: 1,
5837
6055
  integrations: Array.from(this.integrations.values())
5838
6056
  };
5839
- mkdirSync12(dirname12(this.filePath), { recursive: true });
5840
- writeFileSync12(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
6057
+ mkdirSync13(dirname13(this.filePath), { recursive: true });
6058
+ writeFileSync13(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
5841
6059
  }
5842
6060
  };
5843
6061
 
5844
6062
  // src/lib/integration-manager.ts
5845
- import { homedir as homedir11 } from "os";
5846
- import { join as join16, delimiter } from "path";
5847
- import { existsSync as existsSync16, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync17, rmSync } from "fs";
6063
+ import { homedir as homedir12 } from "os";
6064
+ import { join as join17, delimiter } from "path";
6065
+ import { existsSync as existsSync17, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync18, rmSync } from "fs";
5848
6066
  import { execFile } from "child_process";
5849
6067
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5850
6068
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -5974,8 +6192,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
5974
6192
  }
5975
6193
 
5976
6194
  // src/lib/integration-manager.ts
5977
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join16(homedir11(), ".suveren");
5978
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join16(DEFAULT_DATA_DIR2, "integrations");
6195
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join17(homedir12(), ".suveren");
6196
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join17(DEFAULT_DATA_DIR2, "integrations");
5979
6197
  var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
5980
6198
  var installLock = Promise.resolve();
5981
6199
  function withInstallLock(task) {
@@ -5983,7 +6201,7 @@ function withInstallLock(task) {
5983
6201
  installLock = run.then(() => void 0, () => void 0);
5984
6202
  return run;
5985
6203
  }
5986
- var INTEGRATIONS_BIN = join16(INTEGRATIONS_DIR, "node_modules", ".bin");
6204
+ var INTEGRATIONS_BIN = join17(INTEGRATIONS_DIR, "node_modules", ".bin");
5987
6205
  function getIntegrationsBinDir() {
5988
6206
  return INTEGRATIONS_BIN;
5989
6207
  }
@@ -5992,12 +6210,12 @@ function buildPath() {
5992
6210
  return [INTEGRATIONS_BIN, base].join(delimiter);
5993
6211
  }
5994
6212
  function ensureIntegrationsDir() {
5995
- if (!existsSync16(INTEGRATIONS_DIR)) {
5996
- mkdirSync13(INTEGRATIONS_DIR, { recursive: true });
6213
+ if (!existsSync17(INTEGRATIONS_DIR)) {
6214
+ mkdirSync14(INTEGRATIONS_DIR, { recursive: true });
5997
6215
  }
5998
- const pkgPath = join16(INTEGRATIONS_DIR, "package.json");
5999
- if (!existsSync16(pkgPath)) {
6000
- writeFileSync13(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
6216
+ const pkgPath = join17(INTEGRATIONS_DIR, "package.json");
6217
+ if (!existsSync17(pkgPath)) {
6218
+ writeFileSync14(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
6001
6219
  }
6002
6220
  }
6003
6221
  function readAgeOf(config) {
@@ -6086,19 +6304,19 @@ var IntegrationManager = class {
6086
6304
  * exist, and treat anything else as not-installed (and reinstallable).
6087
6305
  */
6088
6306
  isUsableInstall(npmPackage) {
6089
- const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
6090
- const pkgJson = join16(pkgDir, "package.json");
6091
- if (!existsSync16(pkgDir) || !existsSync16(pkgJson)) return false;
6307
+ const pkgDir = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
6308
+ const pkgJson = join17(pkgDir, "package.json");
6309
+ if (!existsSync17(pkgDir) || !existsSync17(pkgJson)) return false;
6092
6310
  try {
6093
- const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
6311
+ const pkg = JSON.parse(readFileSync18(pkgJson, "utf8"));
6094
6312
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
6095
6313
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
6096
6314
  const binEntries = Object.entries(binMap);
6097
6315
  for (const [binName, target] of binEntries) {
6098
- if (!existsSync16(join16(pkgDir, target))) return false;
6316
+ if (!existsSync17(join17(pkgDir, target))) return false;
6099
6317
  if (!this.binShimExists(binName)) return false;
6100
6318
  }
6101
- if (binEntries.length === 0 && pkg.main && !existsSync16(join16(pkgDir, pkg.main))) return false;
6319
+ if (binEntries.length === 0 && pkg.main && !existsSync17(join17(pkgDir, pkg.main))) return false;
6102
6320
  return true;
6103
6321
  } catch {
6104
6322
  return false;
@@ -6113,9 +6331,9 @@ var IntegrationManager = class {
6113
6331
  */
6114
6332
  readInstalledVersion(npmPackage) {
6115
6333
  if (!this.isUsableInstall(npmPackage)) return null;
6116
- const pkgJson = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
6334
+ const pkgJson = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
6117
6335
  try {
6118
- const pkg = JSON.parse(readFileSync17(pkgJson, "utf8"));
6336
+ const pkg = JSON.parse(readFileSync18(pkgJson, "utf8"));
6119
6337
  return typeof pkg.version === "string" ? pkg.version : null;
6120
6338
  } catch {
6121
6339
  return null;
@@ -6129,7 +6347,7 @@ var IntegrationManager = class {
6129
6347
  binShimExists(binName) {
6130
6348
  if (!binName) return false;
6131
6349
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
6132
- return candidates.some((c) => existsSync16(join16(INTEGRATIONS_BIN, c)));
6350
+ return candidates.some((c) => existsSync17(join17(INTEGRATIONS_BIN, c)));
6133
6351
  }
6134
6352
  /**
6135
6353
  * The exact version THIS gateway's CURRENTLY LOADED manifest pins
@@ -6219,8 +6437,8 @@ var IntegrationManager = class {
6219
6437
  * nothing at all.
6220
6438
  */
6221
6439
  async installNow(npmPackage, exactVersion, cleanFirst = true) {
6222
- const pkgDir = join16(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
6223
- if (cleanFirst && existsSync16(pkgDir)) {
6440
+ const pkgDir = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
6441
+ if (cleanFirst && existsSync17(pkgDir)) {
6224
6442
  console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
6225
6443
  try {
6226
6444
  rmSync(pkgDir, { recursive: true, force: true });
@@ -7160,28 +7378,28 @@ function agentAccess(token, opensSession) {
7160
7378
  }
7161
7379
 
7162
7380
  // src/lib/profile-loader.ts
7163
- import { readFileSync as readFileSync18, existsSync as existsSync17 } from "fs";
7164
- import { join as join17, resolve as resolve2 } from "path";
7381
+ import { readFileSync as readFileSync19, existsSync as existsSync18 } from "fs";
7382
+ import { join as join18, resolve as resolve2 } from "path";
7165
7383
  import { registerProfile } from "@hap/core";
7166
7384
  function loadProfiles(profilesDir) {
7167
- const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join17(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
7168
- const indexPath = join17(dir, "index.json");
7169
- if (!existsSync17(indexPath)) {
7385
+ const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join18(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
7386
+ const indexPath = join18(dir, "index.json");
7387
+ if (!existsSync18(indexPath)) {
7170
7388
  warnNoProfiles(`No index.json at ${indexPath}`);
7171
7389
  return 0;
7172
7390
  }
7173
7391
  let index;
7174
7392
  try {
7175
- index = JSON.parse(readFileSync18(indexPath, "utf-8"));
7393
+ index = JSON.parse(readFileSync19(indexPath, "utf-8"));
7176
7394
  } catch (err) {
7177
7395
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
7178
7396
  return 0;
7179
7397
  }
7180
7398
  let loaded = 0;
7181
7399
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
7182
- const profilePath = join17(dir, relativePath);
7400
+ const profilePath = join18(dir, relativePath);
7183
7401
  try {
7184
- const profile = JSON.parse(readFileSync18(profilePath, "utf-8"));
7402
+ const profile = JSON.parse(readFileSync19(profilePath, "utf-8"));
7185
7403
  registerProfile(profileId, profile);
7186
7404
  const shortName2 = profileId.split("/").pop()?.replace(/@.*$/, "");
7187
7405
  if (shortName2 && shortName2 !== profileId) {
@@ -7522,20 +7740,20 @@ async function controlPlaneMandate(dryRun, request) {
7522
7740
  }
7523
7741
 
7524
7742
  // src/lib/guides.ts
7525
- import { existsSync as existsSync18, readdirSync, readFileSync as readFileSync19 } from "fs";
7526
- import { dirname as dirname13, join as join18 } from "path";
7743
+ import { existsSync as existsSync19, readdirSync, readFileSync as readFileSync20 } from "fs";
7744
+ import { dirname as dirname14, join as join19 } from "path";
7527
7745
  var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
7528
7746
  function builtinGuidesDir() {
7529
7747
  if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
7530
7748
  let dir = import.meta.dirname ?? __dirname;
7531
7749
  for (let i = 0; i < 8; i++) {
7532
- const candidate = join18(dir, "content", "guides");
7533
- if (existsSync18(candidate)) return candidate;
7534
- const parent = dirname13(dir);
7750
+ const candidate = join19(dir, "content", "guides");
7751
+ if (existsSync19(candidate)) return candidate;
7752
+ const parent = dirname14(dir);
7535
7753
  if (parent === dir) break;
7536
7754
  dir = parent;
7537
7755
  }
7538
- return join18(import.meta.dirname ?? __dirname, "content", "guides");
7756
+ return join19(import.meta.dirname ?? __dirname, "content", "guides");
7539
7757
  }
7540
7758
  function parse(body) {
7541
7759
  const lines = body.split(/\r?\n/);
@@ -7546,14 +7764,14 @@ function parse(body) {
7546
7764
  return { title, summary };
7547
7765
  }
7548
7766
  function readDir(dir, source) {
7549
- if (!dir || !existsSync18(dir)) return [];
7767
+ if (!dir || !existsSync19(dir)) return [];
7550
7768
  const out = [];
7551
7769
  for (const file of readdirSync(dir)) {
7552
7770
  const m = FILE_RE.exec(file);
7553
7771
  if (!m) continue;
7554
7772
  let body;
7555
7773
  try {
7556
- body = readFileSync19(join18(dir, file), "utf8");
7774
+ body = readFileSync20(join19(dir, file), "utf8");
7557
7775
  } catch {
7558
7776
  continue;
7559
7777
  }
@@ -8104,8 +8322,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
8104
8322
  import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
8105
8323
 
8106
8324
  // src/lib/committed-executor.ts
8107
- import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync20, unlinkSync as unlinkSync6, writeFileSync as writeFileSync14 } from "fs";
8108
- import { dirname as dirname14, join as join19 } from "path";
8325
+ import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync21, unlinkSync as unlinkSync7, writeFileSync as writeFileSync15 } from "fs";
8326
+ import { dirname as dirname15, join as join20 } from "path";
8109
8327
  var CommittedExecutor = class {
8110
8328
  constructor(run) {
8111
8329
  this.run = run;
@@ -8133,7 +8351,7 @@ var ExecutorLock = class {
8133
8351
  path;
8134
8352
  held = false;
8135
8353
  constructor(dataDir2) {
8136
- this.path = join19(dataDir2, "executor.lock");
8354
+ this.path = join20(dataDir2, "executor.lock");
8137
8355
  }
8138
8356
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
8139
8357
  acquire() {
@@ -8141,9 +8359,9 @@ var ExecutorLock = class {
8141
8359
  if (current && current.pid !== process.pid && isAlive(current.pid)) {
8142
8360
  return { held: false, holderPid: current.pid };
8143
8361
  }
8144
- mkdirSync14(dirname14(this.path), { recursive: true, mode: 448 });
8362
+ mkdirSync15(dirname15(this.path), { recursive: true, mode: 448 });
8145
8363
  const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
8146
- writeFileSync14(this.path, JSON.stringify(lock), { mode: 384 });
8364
+ writeFileSync15(this.path, JSON.stringify(lock), { mode: 384 });
8147
8365
  this.held = true;
8148
8366
  return { held: true };
8149
8367
  }
@@ -8159,15 +8377,15 @@ var ExecutorLock = class {
8159
8377
  const current = this.read();
8160
8378
  if (current?.pid === process.pid) {
8161
8379
  try {
8162
- unlinkSync6(this.path);
8380
+ unlinkSync7(this.path);
8163
8381
  } catch {
8164
8382
  }
8165
8383
  }
8166
8384
  }
8167
8385
  read() {
8168
- if (!existsSync19(this.path)) return null;
8386
+ if (!existsSync20(this.path)) return null;
8169
8387
  try {
8170
- const parsed = JSON.parse(readFileSync20(this.path, "utf8"));
8388
+ const parsed = JSON.parse(readFileSync21(this.path, "utf8"));
8171
8389
  return typeof parsed?.pid === "number" ? parsed : null;
8172
8390
  } catch {
8173
8391
  return null;
@@ -8184,20 +8402,20 @@ function isAlive(pid) {
8184
8402
  }
8185
8403
 
8186
8404
  // bin/http.ts
8187
- import { homedir as homedir12 } from "os";
8188
- import { join as join21 } from "path";
8405
+ import { homedir as homedir13 } from "os";
8406
+ import { join as join22 } from "path";
8189
8407
 
8190
8408
  // src/lib/mcp-as-tracker.ts
8191
- import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync15 } from "fs";
8192
- import { dirname as dirname15, join as join20 } from "path";
8409
+ import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync16 } from "fs";
8410
+ import { dirname as dirname16, join as join21 } from "path";
8193
8411
  function trackerPath(dataDir2) {
8194
- return join20(dataDir2, "mcp-as-pairing.json");
8412
+ return join21(dataDir2, "mcp-as-pairing.json");
8195
8413
  }
8196
8414
  function readMcpPairedAsUrl(dataDir2) {
8197
8415
  const path = trackerPath(dataDir2);
8198
- if (!existsSync20(path)) return null;
8416
+ if (!existsSync21(path)) return null;
8199
8417
  try {
8200
- const data = JSON.parse(readFileSync21(path, "utf-8"));
8418
+ const data = JSON.parse(readFileSync22(path, "utf-8"));
8201
8419
  return typeof data.asUrl === "string" ? data.asUrl : null;
8202
8420
  } catch {
8203
8421
  return null;
@@ -8205,12 +8423,12 @@ function readMcpPairedAsUrl(dataDir2) {
8205
8423
  }
8206
8424
  function writeMcpPairedAsUrl(dataDir2, asUrl) {
8207
8425
  const path = trackerPath(dataDir2);
8208
- mkdirSync15(dirname15(path), { recursive: true, mode: 448 });
8209
- writeFileSync15(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
8426
+ mkdirSync16(dirname16(path), { recursive: true, mode: 448 });
8427
+ writeFileSync16(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
8210
8428
  }
8211
8429
 
8212
8430
  // bin/http.ts
8213
- var dataDir = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
8431
+ var dataDir = process.env.SUVEREN_DATA_DIR ?? join22(homedir13(), ".suveren");
8214
8432
  var spUrl = resolveAsUrl(dataDir);
8215
8433
  validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
8216
8434
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
@@ -8295,7 +8513,8 @@ app.post("/internal/configure", internalOnly, (req, res) => {
8295
8513
  state.denialLog.setVaultKey(key);
8296
8514
  state.receiptArchive.setVaultKey(key);
8297
8515
  state.reportStore.setVaultKey(key);
8298
- console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log + receipt archive + report store encryption active");
8516
+ state.outcomeOutbox.setVaultKey(key);
8517
+ console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log + receipt archive + report store + outcome outbox encryption active");
8299
8518
  }
8300
8519
  if (apiKey) {
8301
8520
  state.spClient.setApiKey(apiKey);
@@ -8936,7 +9155,7 @@ app.listen(port, bindHost, () => {
8936
9155
  (proposal) => executeCommitted(proposal, state, integrationManager)
8937
9156
  );
8938
9157
  installCommittedExecutor(committedExecutor);
8939
- const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join21(homedir12(), ".suveren");
9158
+ const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join22(homedir13(), ".suveren");
8940
9159
  const executorLock = new ExecutorLock(dataDir2);
8941
9160
  const lock = executorLock.acquire();
8942
9161
  if (!lock.held) {
@@ -8947,8 +9166,28 @@ app.listen(port, bindHost, () => {
8947
9166
  for (const sig of ["SIGINT", "SIGTERM", "exit"]) {
8948
9167
  process.once(sig, () => executorLock.release());
8949
9168
  }
9169
+ async function pollRejectedOutcomes() {
9170
+ try {
9171
+ const rejected = await state.spClient.getRejectedProposals();
9172
+ for (const proposal of rejected) {
9173
+ const by = proposal.approverRejectedBy ?? proposal.rejectedBy;
9174
+ if (!by) continue;
9175
+ const submittedHere = state.proposalSubmissions.get(proposal.id) !== void 0;
9176
+ if (!outcomeEntryFor(proposal, submittedHere, by.userId, Date.now())) continue;
9177
+ const who = await resolveWhoLabel(
9178
+ state.receiptArchive,
9179
+ "domain" in by ? by.domain : by.userId,
9180
+ by.userId
9181
+ );
9182
+ const entry = outcomeEntryFor(proposal, submittedHere, who, Date.now());
9183
+ if (entry) state.outcomeOutbox.recordIfNew(entry);
9184
+ }
9185
+ } catch {
9186
+ }
9187
+ }
8950
9188
  let draining = false;
8951
9189
  async function executeCommittedProposals() {
9190
+ void pollRejectedOutcomes();
8952
9191
  if (!executorLock.isHeld()) return;
8953
9192
  if (draining) return;
8954
9193
  draining = true;