@suveren/gateway 0.31.0 → 0.31.2

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.
@@ -216,6 +216,7 @@
216
216
  },
217
217
  "log_activity": {
218
218
  "summary": "Log {type}: {summary|short}",
219
+ "actionLabel": "Activity logged",
219
220
  "executionMapping": {
220
221
  "contact_type": "contact_type"
221
222
  },
@@ -236,6 +237,7 @@
236
237
  },
237
238
  "create_deal": {
238
239
  "summary": "Create deal: {title|short}[ — {value|money}]",
240
+ "actionLabel": "Deal created",
239
241
  "executionMapping": {
240
242
  "contact_type": "contact_type"
241
243
  },
@@ -256,6 +258,7 @@
256
258
  },
257
259
  "update_deal": {
258
260
  "summary": "Update deal[: {title|short}][ — {value|money}]",
261
+ "actionLabel": "Deal updated",
259
262
  "executionMapping": {
260
263
  "contact_type": "contact_type"
261
264
  },
@@ -285,6 +288,7 @@
285
288
  },
286
289
  "create_task": {
287
290
  "summary": "Create task: {title|short}[ (due {due_date|date})]",
291
+ "actionLabel": "Task created",
288
292
  "executionMapping": {
289
293
  "contact_type": "contact_type"
290
294
  },
@@ -305,6 +309,7 @@
305
309
  },
306
310
  "complete_task": {
307
311
  "summary": "Complete a task",
312
+ "actionLabel": "Task completed",
308
313
  "executionMapping": {
309
314
  "contact_type": "contact_type"
310
315
  },
@@ -39,6 +39,7 @@
39
39
  "overrides": {
40
40
  "CreatePayment": {
41
41
  "summary": "Payment of {amount|money}: {description|short}",
42
+ "actionLabel": "Payment created",
42
43
  "executionMapping": {
43
44
  "amount": "amount",
44
45
  "currency": "currency"
@@ -58,6 +59,7 @@
58
59
  },
59
60
  "CreatePaymentLink": {
60
61
  "summary": "Payment link[ of {amount|money}]: {description|short}",
62
+ "actionLabel": "Payment link created",
61
63
  "executionMapping": {
62
64
  "amount": "amount",
63
65
  "currency": "currency"
@@ -92,6 +94,7 @@
92
94
  },
93
95
  "CancelSubscription": {
94
96
  "summary": "Cancel a subscription",
97
+ "actionLabel": "Subscription cancelled",
95
98
  "executionMapping": {},
96
99
  "staticExecution": {
97
100
  "action_type": "subscribe"
@@ -99,6 +102,7 @@
99
102
  },
100
103
  "CreateCustomer": {
101
104
  "summary": "Create payment customer[ {name}]",
105
+ "actionLabel": "Payment customer created",
102
106
  "executionMapping": {},
103
107
  "staticExecution": {
104
108
  "action_type": "charge"
@@ -118,6 +122,7 @@
118
122
  },
119
123
  "RevokeMandate": {
120
124
  "summary": "Revoke a payment mandate",
125
+ "actionLabel": "Payment mandate revoked",
121
126
  "executionMapping": {},
122
127
  "staticExecution": {
123
128
  "action_type": "charge"
@@ -5,9 +5,9 @@ import {
5
5
 
6
6
  // src/index.ts
7
7
  import { randomBytes as randomBytes3 } from "crypto";
8
- import { existsSync as existsSync13, readFileSync as readFileSync17, renameSync as renameSync5 } from "fs";
9
- import { dirname as dirname12, join as join17 } from "path";
10
- import { homedir as homedir10 } from "os";
8
+ import { existsSync as existsSync13, readFileSync as readFileSync18, renameSync as renameSync6 } from "fs";
9
+ import { dirname as dirname13, join as join18 } from "path";
10
+ import { homedir as homedir11 } from "os";
11
11
  import express from "express";
12
12
  import { createProxyMiddleware } from "http-proxy-middleware";
13
13
 
@@ -3384,15 +3384,15 @@ import { promisify } from "util";
3384
3384
  var run = promisify(execFile);
3385
3385
  var SUPPORTED = /* @__PURE__ */ new Set(["darwin", "win32", "linux"]);
3386
3386
  var MODULE_DIR = pathDirname(fileURLToPath(import.meta.url));
3387
- function findCli(dirname13 = MODULE_DIR) {
3387
+ function findCli(dirname14 = MODULE_DIR) {
3388
3388
  const candidates = [
3389
3389
  // Bundled (the shipped layout): dist/control-plane → dist → root → bin
3390
- resolve(dirname13, "..", "..", "bin", "suveren-gateway.js"),
3390
+ resolve(dirname14, "..", "..", "bin", "suveren-gateway.js"),
3391
3391
  // Bundled, were the routes/ directory ever preserved.
3392
- resolve(dirname13, "..", "..", "..", "bin", "suveren-gateway.js"),
3392
+ resolve(dirname14, "..", "..", "..", "bin", "suveren-gateway.js"),
3393
3393
  // Dev, running from source: the repo's own bundle directory.
3394
- resolve(dirname13, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
3395
- resolve(dirname13, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
3394
+ resolve(dirname14, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
3395
+ resolve(dirname14, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
3396
3396
  // Deliberately NO process.cwd() fallback: it makes the result depend on
3397
3397
  // where the process happened to be launched from, so the same install
3398
3398
  // resolves differently between a shell and a login service — and it would
@@ -4724,15 +4724,63 @@ function createTicketsSeenRouter(dataDir) {
4724
4724
  return router;
4725
4725
  }
4726
4726
 
4727
- // src/routes/internal-events.ts
4727
+ // src/routes/report-seen.ts
4728
4728
  import { Router as Router21 } from "express";
4729
+
4730
+ // src/lib/report-seen-store.ts
4731
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, renameSync as renameSync5 } from "fs";
4732
+ import { homedir as homedir10 } from "os";
4733
+ import { join as join16, dirname as dirname12 } from "path";
4734
+ function storePath4(dataDir) {
4735
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join16(homedir10(), ".suveren");
4736
+ return join16(dir, "report-seen.json");
4737
+ }
4738
+ function readLastSeenReportAt(dataDir) {
4739
+ try {
4740
+ const raw = JSON.parse(readFileSync16(storePath4(dataDir), "utf8"));
4741
+ return typeof raw.lastSeenReportAt === "number" && Number.isFinite(raw.lastSeenReportAt) ? raw.lastSeenReportAt : 0;
4742
+ } catch {
4743
+ return 0;
4744
+ }
4745
+ }
4746
+ function markReportSeen(ts, dataDir) {
4747
+ const safeTs = typeof ts === "number" && Number.isFinite(ts) ? ts : Date.now();
4748
+ const path = storePath4(dataDir);
4749
+ mkdirSync11(dirname12(path), { recursive: true });
4750
+ const tmp = `${path}.tmp`;
4751
+ writeFileSync11(tmp, JSON.stringify({ lastSeenReportAt: safeTs }, null, 2) + "\n", "utf8");
4752
+ renameSync5(tmp, path);
4753
+ return safeTs;
4754
+ }
4755
+
4756
+ // src/routes/report-seen.ts
4757
+ function createReportSeenRouter(dataDir) {
4758
+ const router = Router21();
4759
+ router.get("/", (_req, res) => {
4760
+ res.json({ lastSeenReportAt: readLastSeenReportAt(dataDir) });
4761
+ });
4762
+ router.post("/", (req, res) => {
4763
+ const body = req.body;
4764
+ const ts = typeof body?.ts === "number" && Number.isFinite(body.ts) ? body.ts : Date.now();
4765
+ try {
4766
+ res.json({ lastSeenReportAt: markReportSeen(ts, dataDir) });
4767
+ } catch (err) {
4768
+ res.status(500).json({ error: `Could not save: ${err.message}` });
4769
+ }
4770
+ });
4771
+ return router;
4772
+ }
4773
+
4774
+ // src/routes/internal-events.ts
4775
+ import { Router as Router22 } from "express";
4729
4776
  import { timingSafeEqual as timingSafeEqual3 } from "crypto";
4730
4777
  var ALLOWED = [
4731
4778
  "proposal-added",
4732
4779
  "action-approval-needed",
4733
4780
  "execution-failed",
4734
4781
  "denial-recorded",
4735
- "ticket-recorded"
4782
+ "ticket-recorded",
4783
+ "report-written"
4736
4784
  ];
4737
4785
  var SESSION_EXPIRED = "session-expired";
4738
4786
  var AS_KEY_MISMATCH = "as-key-mismatch";
@@ -4746,7 +4794,7 @@ function secretMatches2(provided, expected) {
4746
4794
  return timingSafeEqual3(a, b);
4747
4795
  }
4748
4796
  function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch, onAsTlsMismatch) {
4749
- const router = Router21();
4797
+ const router = Router22();
4750
4798
  router.post("/event", (req, res) => {
4751
4799
  if (!secretMatches2(req.headers["x-internal-secret"], getSecret())) {
4752
4800
  res.status(401).json({ error: "Unauthorized" });
@@ -4780,7 +4828,7 @@ function createInternalEventsRouter(getSecret, onSessionExpired, onAsKeyMismatch
4780
4828
  }
4781
4829
 
4782
4830
  // src/routes/internal-mandate.ts
4783
- import { Router as Router22 } from "express";
4831
+ import { Router as Router23 } from "express";
4784
4832
  import { timingSafeEqual as timingSafeEqual4, randomUUID } from "crypto";
4785
4833
 
4786
4834
  // src/lib/mandate-ceremony.ts
@@ -4962,7 +5010,7 @@ function secretMatches3(provided, expected) {
4962
5010
  return a.length === b.length && timingSafeEqual4(a, b);
4963
5011
  }
4964
5012
  function createInternalMandateRouter(getSecret, deps) {
4965
- const router = Router22();
5013
+ const router = Router23();
4966
5014
  router.post("/mandate", async (req, res) => {
4967
5015
  if (!secretMatches3(req.header("X-Internal-Secret") ?? void 0, getSecret())) {
4968
5016
  res.status(401).json({ error: "unauthorized" });
@@ -5165,17 +5213,17 @@ function buildSessionHealth(vault2) {
5165
5213
  }
5166
5214
 
5167
5215
  // src/lib/denials-reader.ts
5168
- import { existsSync as existsSync12, readFileSync as readFileSync16 } from "fs";
5169
- import { join as join16 } from "path";
5216
+ import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
5217
+ import { join as join17 } from "path";
5170
5218
  function loadDenials(dataDir, decrypt) {
5171
- const encPath = join16(dataDir, "denials.enc.json");
5172
- const plainPath = join16(dataDir, "denials.json");
5219
+ const encPath = join17(dataDir, "denials.enc.json");
5220
+ const plainPath = join17(dataDir, "denials.json");
5173
5221
  if (existsSync12(encPath)) {
5174
- const { blob } = JSON.parse(readFileSync16(encPath, "utf-8"));
5222
+ const { blob } = JSON.parse(readFileSync17(encPath, "utf-8"));
5175
5223
  return JSON.parse(decrypt(blob)).records ?? [];
5176
5224
  }
5177
5225
  if (existsSync12(plainPath)) {
5178
- return JSON.parse(readFileSync16(plainPath, "utf-8")).records ?? [];
5226
+ return JSON.parse(readFileSync17(plainPath, "utf-8")).records ?? [];
5179
5227
  }
5180
5228
  return [];
5181
5229
  }
@@ -5199,11 +5247,11 @@ function isSimulationMode() {
5199
5247
  // src/index.ts
5200
5248
  (function migrateDataDir() {
5201
5249
  if (process.env.SUVEREN_DATA_DIR) return;
5202
- const oldDir = join17(homedir10(), ".hap");
5203
- const newDir = join17(homedir10(), ".suveren");
5250
+ const oldDir = join18(homedir11(), ".hap");
5251
+ const newDir = join18(homedir11(), ".suveren");
5204
5252
  if (!existsSync13(newDir) && existsSync13(oldDir)) {
5205
5253
  try {
5206
- renameSync5(oldDir, newDir);
5254
+ renameSync6(oldDir, newDir);
5207
5255
  console.error(`[Control Plane] Migrated data directory: ${oldDir} \u2192 ${newDir}`);
5208
5256
  } catch (err) {
5209
5257
  console.error(`[Control Plane] FATAL: could not migrate data directory ${oldDir} \u2192 ${newDir}:`, err);
@@ -5211,7 +5259,7 @@ function isSimulationMode() {
5211
5259
  }
5212
5260
  }
5213
5261
  })();
5214
- var DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
5262
+ var DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join18(homedir11(), ".suveren");
5215
5263
  var SP_URL = resolveAsUrl(DATA_DIR);
5216
5264
  validatePinTlsForUrl(resolvePinTls(DATA_DIR), SP_URL);
5217
5265
  var POLICY_LOCKED = [...readPolicy().locked];
@@ -5219,7 +5267,7 @@ var POLICY_LOCKED_INSTALL_METHOD = readPolicy().policy.installMethod;
5219
5267
  var POLICY_LOCKED_TEAM = readPolicy().policy.team;
5220
5268
  var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
5221
5269
  var HAP_MODE = process.env.HAP_MODE ?? "personal";
5222
- var UI_DIST = process.env.HAP_UI_DIST ?? join17(import.meta.dirname ?? __dirname, "../../ui/dist");
5270
+ var UI_DIST = process.env.HAP_UI_DIST ?? join18(import.meta.dirname ?? __dirname, "../../ui/dist");
5223
5271
  var vault = new Vault(DATA_DIR);
5224
5272
  var shownSnapshotStore = new ShownSnapshotStore(DATA_DIR);
5225
5273
  (function repairIfAsUrlChanged() {
@@ -5530,6 +5578,7 @@ app.use("/api/gateway-settings", jsonParser, authGuard, createGatewaySettingsRou
5530
5578
  app.use("/api/archived-mandates", jsonParser, authGuard, createArchivedMandatesRouter());
5531
5579
  app.use("/api/blocked-seen", jsonParser, authGuard, createBlockedSeenRouter());
5532
5580
  app.use("/api/tickets-seen", jsonParser, authGuard, createTicketsSeenRouter());
5581
+ app.use("/api/report-seen", jsonParser, authGuard, createReportSeenRouter());
5533
5582
  app.use("/api/encrypt-intent", jsonParser, authGuard, createEncryptIntentRouter());
5534
5583
  app.use("/api/evidence-export", authGuard, createEvidenceExportRouter(SP_URL, vault, DATA_DIR, lockAsTlsMismatch));
5535
5584
  app.use("/proposal-status", jsonParser, authGuard, createProposalStatusRouter(SP_URL, vault, DATA_DIR, lockAsTlsMismatch, shownSnapshotStore));
@@ -5706,7 +5755,7 @@ app.get("/as-pairing", authGuard, (_req, res) => {
5706
5755
  });
5707
5756
  app.get("/denials", authGuard, (req, res) => {
5708
5757
  try {
5709
- const dataDir = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
5758
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join18(homedir11(), ".suveren");
5710
5759
  const all = loadDenials(dataDir, (blob) => vault.decrypt(blob));
5711
5760
  const since = Number(req.query.since);
5712
5761
  const limit = Number(req.query.limit);
@@ -5765,12 +5814,12 @@ app.put("/agent-brief/context", jsonParser, authGuard, async (req, res) => {
5765
5814
  res.status(413).json({ error: `Context exceeds ${AGENT_CONTEXT_MAX_BYTES}-byte cap` });
5766
5815
  return;
5767
5816
  }
5768
- const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync11, renameSync: renameSync6 } = await import("fs");
5817
+ const { mkdirSync: mkdirSync12, writeFileSync: writeFileSync12, renameSync: renameSync7 } = await import("fs");
5769
5818
  const filePath = agentBriefPath();
5770
- mkdirSync11(dirname12(filePath), { recursive: true });
5819
+ mkdirSync12(dirname13(filePath), { recursive: true });
5771
5820
  const tmpPath = `${filePath}.tmp`;
5772
- writeFileSync11(tmpPath, content, "utf-8");
5773
- renameSync6(tmpPath, filePath);
5821
+ writeFileSync12(tmpPath, content, "utf-8");
5822
+ renameSync7(tmpPath, filePath);
5774
5823
  res.json({ ok: true });
5775
5824
  } catch (err) {
5776
5825
  console.error("[Control Plane] agent-brief/context PUT failed:", err);
@@ -5862,10 +5911,10 @@ function detectRunningVersion() {
5862
5911
  return process.env.HAP_BUILD_SHA ?? "dev";
5863
5912
  }
5864
5913
  const dir = import.meta.dirname ?? __dirname;
5865
- const bundlePkg = join17(dir, "..", "..", "package.json");
5914
+ const bundlePkg = join18(dir, "..", "..", "package.json");
5866
5915
  if (existsSync13(bundlePkg)) {
5867
5916
  try {
5868
- const pkg = JSON.parse(readFileSync17(bundlePkg, "utf8"));
5917
+ const pkg = JSON.parse(readFileSync18(bundlePkg, "utf8"));
5869
5918
  if (pkg.name === "@suveren/gateway" && typeof pkg.version === "string") {
5870
5919
  return pkg.version;
5871
5920
  }
@@ -5917,7 +5966,7 @@ if (existsSync13(UI_DIST)) {
5917
5966
  }));
5918
5967
  app.get("*", (_req, res) => {
5919
5968
  res.setHeader("Cache-Control", "no-store, must-revalidate");
5920
- res.sendFile(join17(UI_DIST, "index.html"));
5969
+ res.sendFile(join18(UI_DIST, "index.html"));
5921
5970
  });
5922
5971
  } else {
5923
5972
  app.get("/", (_req, res) => {
@@ -3113,6 +3113,7 @@ var ReportStore = class {
3113
3113
  const result = await verifyReport(html, sources);
3114
3114
  this.report = { html: result.html, savedAt: now, checkedAt: now, result };
3115
3115
  this.persist();
3116
+ void notifyControlPlane("report-written");
3116
3117
  return this.report;
3117
3118
  }
3118
3119
  /**
@@ -5373,10 +5374,10 @@ function zeroCappedBound(profileId, bounds, actionType) {
5373
5374
  }
5374
5375
  return null;
5375
5376
  }
5376
- function toolAlwaysListed(tool, isBuiltin) {
5377
+ function toolAlwaysListed(tool, isBuiltin, builtinAlwaysListed = () => false) {
5377
5378
  if (tool.internal || !tool.gating?.profile) return false;
5378
5379
  if (tool.gating.category === "disabled") return false;
5379
- if (isBuiltin(tool.integrationId)) return false;
5380
+ if (isBuiltin(tool.integrationId) && !builtinAlwaysListed(tool.integrationId)) return false;
5380
5381
  if (!tool.gating.hideUnlessAuthorized) return true;
5381
5382
  return Boolean(tool.gating.simulationOnly) && isSimulationMode();
5382
5383
  }
@@ -6441,9 +6442,10 @@ function buildCapabilityMap(profileId, integrationManager2) {
6441
6442
  function listedWithoutMandate(authorizations, integrationManager2) {
6442
6443
  if (!integrationManager2) return [];
6443
6444
  const isBuiltin = (id) => integrationManager2.isBuiltin?.(id) ?? false;
6445
+ const builtinAlwaysListed = (id) => integrationManager2.builtinAlwaysListed?.(id) ?? false;
6444
6446
  const byProfile = /* @__PURE__ */ new Map();
6445
6447
  for (const tool of integrationManager2.getAllTools()) {
6446
- if (!toolAlwaysListed(tool, isBuiltin)) continue;
6448
+ if (!toolAlwaysListed(tool, isBuiltin, builtinAlwaysListed)) continue;
6447
6449
  const profile = tool.gating.profile;
6448
6450
  if (authorizations.some((a) => a.complete && profileMatches(a.profileId, profile))) continue;
6449
6451
  const short = shortProfileName2(profile);
@@ -7418,8 +7420,9 @@ function createMcpServer(state2, integrationManager2) {
7418
7420
  function refreshTools() {
7419
7421
  const auths = state2.getEnrichedAuthorizations();
7420
7422
  const isBuiltin = (id) => integrationManager2?.isBuiltin?.(id) ?? false;
7423
+ const builtinAlwaysListed = (id) => integrationManager2?.builtinAlwaysListed?.(id) ?? false;
7421
7424
  for (const [, { tool, registered }] of proxiedTools) {
7422
- if (toolAlwaysListed(tool, isBuiltin)) {
7425
+ if (toolAlwaysListed(tool, isBuiltin, builtinAlwaysListed)) {
7423
7426
  registered.enable();
7424
7427
  } else if (tool.gating?.profile) {
7425
7428
  const matchingAuths = auths.filter(
@@ -8268,6 +8271,25 @@ var IntegrationManager = class {
8268
8271
  isBuiltin(id) {
8269
8272
  return this.builtins.has(id);
8270
8273
  }
8274
+ /**
8275
+ * Names only, for the UI's labels ("Report written", system "Reports"):
8276
+ * id, display name and per-tool action labels. Kept apart from the
8277
+ * connector manifests so a built-in never shows up as a connector card.
8278
+ */
8279
+ getBuiltinLabels() {
8280
+ return [...this.builtins.values()].map(({ def }) => {
8281
+ const overrides = {};
8282
+ for (const [tool, o] of Object.entries(def.toolGating?.overrides ?? {})) {
8283
+ const label = o?.actionLabel;
8284
+ if (typeof label === "string") overrides[tool] = { actionLabel: label };
8285
+ }
8286
+ return { id: def.id, name: def.name, toolGating: { overrides } };
8287
+ });
8288
+ }
8289
+ /** A built-in that declares `alwaysListed` (see builtin-integration.ts). */
8290
+ builtinAlwaysListed(id) {
8291
+ return this.builtins.get(id)?.def.alwaysListed === true;
8292
+ }
8271
8293
  /**
8272
8294
  * Whether `id` may run while simulation mode is on: a connector whose manifest
8273
8295
  * carries the `simulation` marker, or a built-in that declares `simulation: true`.
@@ -9191,6 +9213,14 @@ function writeReportRefusal(args) {
9191
9213
  if (bytes > MAX_REPORT_HTML_BYTES) {
9192
9214
  return `the report is ${bytes} bytes, over the ${MAX_REPORT_HTML_BYTES}-byte limit. Shorten it (the gateway draws the verifiable elements for you \u2014 you do not need to inline their data) and write again.`;
9193
9215
  }
9216
+ const { html: kept, notes } = sanitizeReport(html);
9217
+ if (!/<sv-(ai|ticket|approval|mandate|record|case|metric|row)\b/i.test(kept)) {
9218
+ const lost = [];
9219
+ if (notes.droppedBlocks > 0) lost.push(`${notes.droppedBlocks} block(s) outside sv-ai`);
9220
+ if (notes.droppedSvInsideAi > 0) lost.push(`${notes.droppedSvInsideAi} sv-* element(s) inside sv-ai`);
9221
+ if (notes.droppedStyles > 0) lost.push(`${notes.droppedStyles} <style> block(s)`);
9222
+ return `nothing in this report would be shown${lost.length ? ` (dropped: ${lost.join(", ")})` : ""}. A report is a flat sequence of top-level blocks: your own content inside <sv-ai>\u2026</sv-ai>, gateway elements (sv-ticket, sv-approval, sv-mandate, sv-record, sv-case, sv-metric, optionally in sv-row) and at most one sv-glossary. Write it again in that form; nothing was stored and no ticket was used.`;
9223
+ }
9194
9224
  return void 0;
9195
9225
  }
9196
9226
  function writeReportTool(deps) {
@@ -9244,6 +9274,11 @@ var reportBuiltin = (deps) => ({
9244
9274
  // read-only export CLI; writes only the local, vault-encrypted report file)
9245
9275
  // — safe during a simulation-mode test.
9246
9276
  simulation: true,
9277
+ // Listed without a mandate, like connector tools: an AI client that ignores
9278
+ // tools/list_changed (ChatGPT) otherwise never sees report__* after the
9279
+ // reporting mandate is signed mid-chat (simulation finding, 2026-10-16).
9280
+ // Listing authorizes nothing — the gate still refuses a call without one.
9281
+ alwaysListed: true,
9247
9282
  // The same predicate the reporting window uses to refuse a mandate
9248
9283
  // (reporting@0.1, or no window set): the gate never selects such a mandate
9249
9284
  // for ANY report tool — so write_report is never ticketed under (and never
@@ -9255,7 +9290,7 @@ var reportBuiltin = (deps) => ({
9255
9290
  get_ticket: READ_GATE,
9256
9291
  list_cases: READ_GATE,
9257
9292
  get_records: READ_GATE,
9258
- write_report: { executionMapping: {}, staticExecution: { action_type: "report" } }
9293
+ write_report: { executionMapping: {}, staticExecution: { action_type: "report" }, actionLabel: "Report written" }
9259
9294
  }
9260
9295
  },
9261
9296
  tools: [
@@ -9654,6 +9689,7 @@ function setupBuiltin(deps) {
9654
9689
  set_agent_brief: {
9655
9690
  executionMapping: {},
9656
9691
  staticExecution: { action_type: "brief" },
9692
+ actionLabel: "Agent brief set",
9657
9693
  hideUnlessAuthorized: true,
9658
9694
  approvalView: { content: { label: "New agent brief", kind: "markdown" } },
9659
9695
  // AU3 — the approval card's preview of this pending proposal (DL1
@@ -9668,6 +9704,7 @@ function setupBuiltin(deps) {
9668
9704
  create_mandate: {
9669
9705
  executionMapping: { profile: "allowed_profiles", mode: "allowed_modes" },
9670
9706
  staticExecution: { action_type: "mandate" },
9707
+ actionLabel: "Mandate proposed",
9671
9708
  hideUnlessAuthorized: true,
9672
9709
  // Shown like the mandate screen: limits and scope by the profile's own names.
9673
9710
  approvalView: {
@@ -10437,7 +10474,7 @@ app.get("/internal/authorizations", internalOnly, (_req, res) => {
10437
10474
  res.json({ authorizations });
10438
10475
  });
10439
10476
  app.get("/internal/manifests", internalOnly, (_req, res) => {
10440
- res.json({ manifests: getAllManifests() });
10477
+ res.json({ manifests: getAllManifests(), builtins: integrationManager?.getBuiltinLabels?.() ?? [] });
10441
10478
  });
10442
10479
  app.get("/internal/skipped-commitments", internalOnly, async (_req, res) => {
10443
10480
  try {