@suveren/gateway 0.5.0 → 0.6.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.
Files changed (82) hide show
  1. package/bin/suveren-gateway.js +107 -12
  2. package/content/integrations/deploy-github.json +164 -0
  3. package/content/integrations/gmail.json +10 -4
  4. package/content/integrations/index.json +2 -1
  5. package/dist/control-plane/index.mjs +31 -7
  6. package/dist/mcp-server/http.mjs +49 -4
  7. package/dist/ui/assets/index-DecSrutK.js +105 -0
  8. package/dist/ui/index.html +1 -1
  9. package/node_modules/@hap/core/dist/index.d.mts +21 -0
  10. package/node_modules/@hap/core/dist/index.d.ts +21 -0
  11. package/node_modules/@hap/core/dist/index.js +17 -0
  12. package/node_modules/@hap/core/dist/index.mjs +17 -0
  13. package/node_modules/@hap/core/package.json +1 -1
  14. package/node_modules/@hap/core/src/gatekeeper.ts +28 -0
  15. package/node_modules/@hap/core/src/types.ts +21 -0
  16. package/node_modules/fast-uri/index.js +37 -1
  17. package/node_modules/fast-uri/package.json +1 -1
  18. package/node_modules/fast-uri/test/security.test.js +136 -0
  19. package/node_modules/hono/dist/cjs/helper/proxy/index.js +4 -0
  20. package/node_modules/hono/dist/cjs/jsx/base.js +1 -9
  21. package/node_modules/hono/dist/cjs/jsx/hooks/index.js +14 -11
  22. package/node_modules/hono/dist/cjs/middleware/cors/index.js +1 -1
  23. package/node_modules/hono/dist/cjs/middleware/language/language.js +10 -6
  24. package/node_modules/hono/dist/cjs/utils/cookie.js +2 -1
  25. package/node_modules/hono/dist/helper/proxy/index.js +4 -0
  26. package/node_modules/hono/dist/jsx/base.js +1 -9
  27. package/node_modules/hono/dist/jsx/hooks/index.js +14 -11
  28. package/node_modules/hono/dist/middleware/cors/index.js +1 -1
  29. package/node_modules/hono/dist/middleware/language/language.js +10 -6
  30. package/node_modules/hono/dist/utils/cookie.js +2 -1
  31. package/node_modules/hono/package.json +2 -3
  32. package/node_modules/ip-address/README.md +142 -128
  33. package/node_modules/ip-address/dist/common.d.ts +6 -0
  34. package/node_modules/ip-address/dist/common.js +17 -1
  35. package/node_modules/ip-address/dist/common.js.map +1 -1
  36. package/node_modules/ip-address/dist/ipv4.js +3 -12
  37. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  38. package/node_modules/ip-address/dist/ipv6.d.ts +9 -2
  39. package/node_modules/ip-address/dist/ipv6.js +35 -16
  40. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  41. package/node_modules/ip-address/package.json +5 -3
  42. package/node_modules/jose/dist/types/types.d.ts +7 -9
  43. package/node_modules/jose/dist/webapi/jwe/compact/decrypt.js +2 -2
  44. package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +12 -12
  45. package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +50 -51
  46. package/node_modules/jose/dist/webapi/jwks/local.js +10 -31
  47. package/node_modules/jose/dist/webapi/jwks/remote.js +17 -26
  48. package/node_modules/jose/dist/webapi/jws/compact/verify.js +2 -2
  49. package/node_modules/jose/dist/webapi/jws/flattened/sign.js +1 -9
  50. package/node_modules/jose/dist/webapi/jws/general/sign.js +12 -21
  51. package/node_modules/jose/dist/webapi/jws/general/verify.js +13 -11
  52. package/node_modules/jose/dist/webapi/jwt/decrypt.js +3 -3
  53. package/node_modules/jose/dist/webapi/jwt/verify.js +4 -4
  54. package/node_modules/jose/dist/webapi/key/export.js +3 -3
  55. package/node_modules/jose/dist/webapi/key/generate_key_pair.js +7 -6
  56. package/node_modules/jose/dist/webapi/lib/asn1.js +29 -62
  57. package/node_modules/jose/dist/webapi/lib/content_encryption.js +16 -34
  58. package/node_modules/jose/dist/webapi/lib/crypto_key.js +6 -0
  59. package/node_modules/jose/dist/webapi/lib/invalid_key_input.js +0 -1
  60. package/node_modules/jose/dist/webapi/lib/jwe_algorithms.js +44 -71
  61. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +46 -41
  62. package/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +14 -13
  63. package/node_modules/jose/dist/webapi/lib/jwk_to_key.js +5 -10
  64. package/node_modules/jose/dist/webapi/lib/jws_algorithms.js +20 -18
  65. package/node_modules/jose/dist/webapi/lib/jws_sign.js +12 -6
  66. package/node_modules/jose/dist/webapi/lib/jws_verify.js +26 -27
  67. package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +60 -102
  68. package/node_modules/jose/dist/webapi/lib/key.js +61 -99
  69. package/node_modules/jose/dist/webapi/lib/key_algorithm.js +2 -8
  70. package/node_modules/jose/dist/webapi/lib/key_descriptor.js +1 -1
  71. package/node_modules/jose/dist/webapi/lib/key_management.js +74 -161
  72. package/node_modules/jose/dist/webapi/lib/options.js +11 -14
  73. package/node_modules/jose/dist/webapi/lib/signing.js +6 -16
  74. package/node_modules/jose/dist/webapi/lib/type_checks.js +13 -18
  75. package/node_modules/jose/dist/webapi/util/base64url.js +4 -3
  76. package/node_modules/jose/package.json +1 -1
  77. package/package.json +2 -2
  78. package/profiles/deploy/0.6.profile.json +168 -0
  79. package/profiles/deploy/0.7.profile.json +168 -0
  80. package/profiles/email/0.4.profile.json +7 -1
  81. package/profiles/index.json +3 -1
  82. package/dist/ui/assets/index-DedRWpBs.js +0 -105
@@ -69,13 +69,22 @@ async function start(args) {
69
69
  if (await isPortListening(SUVEREN_PORT)) {
70
70
  console.error(`Port ${SUVEREN_PORT} is already in use.`);
71
71
  console.error(``);
72
- console.error(`Something is already serving there — most likely a gateway started by the`);
73
- console.error(`login service or from another terminal, which leaves no PID file for this`);
74
- console.error(`CLI to find.`);
75
- console.error(``);
76
- console.error(` Check what it is: suveren-gateway status`);
77
- console.error(` Stop a login service: suveren-gateway service uninstall`);
78
- console.error(` Or use another port: SUVEREN_CP_PORT=3410 suveren-gateway start`);
72
+ if (serviceRunning()) {
73
+ // Do not suggest uninstalling autostart: that removes a feature the user
74
+ // asked for in order to solve a problem that needs a restart. After an
75
+ // upgrade the files on disk are new and the running process is not.
76
+ console.error(`The login service is already running the gateway, so there is nothing to start.`);
77
+ console.error(``);
78
+ console.error(` Pick up an update: suveren-gateway restart`);
79
+ console.error(` Check what is running: suveren-gateway status`);
80
+ console.error(` Remove autostart: suveren-gateway service uninstall`);
81
+ } else {
82
+ console.error(`Something is already serving there — most likely a gateway started from`);
83
+ console.error(`another terminal, which leaves no PID file for this CLI to find.`);
84
+ console.error(``);
85
+ console.error(` Check what it is: suveren-gateway status`);
86
+ console.error(` Or use another port: SUVEREN_CP_PORT=3410 suveren-gateway start`);
87
+ }
79
88
  process.exit(1);
80
89
  }
81
90
 
@@ -134,6 +143,16 @@ async function start(args) {
134
143
 
135
144
  async function stop() {
136
145
  const pid = readPid();
146
+ // A service-managed gateway writes no PID file. Reporting "not running" while
147
+ // it serves requests is the most misleading thing this CLI can say, and it is
148
+ // what sent an upgrade into a port conflict it could not explain.
149
+ if (!pid && serviceRunning()) {
150
+ console.error('suveren-gateway is running under the login service, which this command does not manage.');
151
+ console.error('');
152
+ console.error(' To pick up an update: suveren-gateway restart');
153
+ console.error(' To stop it for good: suveren-gateway service uninstall');
154
+ process.exit(1);
155
+ }
137
156
  if (!pid) {
138
157
  console.error('suveren-gateway is not running (no PID file).');
139
158
  process.exit(1);
@@ -242,6 +261,14 @@ async function status() {
242
261
  }
243
262
 
244
263
  async function restart() {
264
+ // The service manager owns the process when autostart is installed, and it
265
+ // leaves no PID file — so this must be asked FIRST or the checks below all
266
+ // conclude, wrongly, that nothing is running.
267
+ if (serviceRestart()) {
268
+ console.log('suveren-gateway: restarted via the login service.');
269
+ console.log(` The vault is locked after a restart — unlock at http://localhost:${SUVEREN_PORT}`);
270
+ return;
271
+ }
245
272
  if (readPid() && isPidAlive(readPid())) {
246
273
  await stop();
247
274
  }
@@ -279,6 +306,52 @@ async function logs(args) {
279
306
 
280
307
  const LAUNCH_AGENT_LABEL = 'ai.suveren.gateway';
281
308
 
309
+ /**
310
+ * Is the login service currently running the gateway?
311
+ *
312
+ * `stop`, `start` and `restart` were all written before autostart existed and
313
+ * all reason from the PID file — which a service-managed gateway never writes.
314
+ * The result was that installing autostart silently broke the update path:
315
+ * `stop` reported "not running", and `start` then hit a port held by a process
316
+ * it could not see. Every command has to know about the service the same CLI
317
+ * installs.
318
+ */
319
+ function serviceRunning() {
320
+ const os = platform();
321
+ if (os === 'darwin') {
322
+ const p = runQuiet('launchctl', ['print', `gui/${process.getuid()}/${LAUNCH_AGENT_LABEL}`]);
323
+ return p.status === 0 && /state = running/.test(p.stdout || '');
324
+ }
325
+ if (os === 'win32') {
326
+ const p = runQuiet('schtasks', ['/Query', '/TN', WIN_TASK_NAME, '/FO', 'LIST']);
327
+ return p.status === 0 && /Status:\s+Running/i.test(p.stdout || '');
328
+ }
329
+ const p = runQuiet('systemctl', ['--user', 'is-active', SYSTEMD_UNIT]);
330
+ return (p.stdout || '').trim() === 'active';
331
+ }
332
+
333
+ /**
334
+ * Restart through whichever service manager owns the process.
335
+ *
336
+ * Returns false when no service is running, so callers fall back to the manual
337
+ * path. Restarting is what an upgrade needs: npm replaces the files on disk,
338
+ * but the running process loaded them once at start and keeps serving the old
339
+ * code until it is replaced.
340
+ */
341
+ function serviceRestart() {
342
+ if (!serviceRunning()) return false;
343
+ const os = platform();
344
+ if (os === 'darwin') {
345
+ runQuiet('launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${LAUNCH_AGENT_LABEL}`]);
346
+ } else if (os === 'win32') {
347
+ runQuiet('schtasks', ['/End', '/TN', WIN_TASK_NAME]);
348
+ runQuiet('schtasks', ['/Run', '/TN', WIN_TASK_NAME]);
349
+ } else {
350
+ runQuiet('systemctl', ['--user', 'restart', SYSTEMD_UNIT]);
351
+ }
352
+ return true;
353
+ }
354
+
282
355
  async function service(args) {
283
356
  const sub = args[0] ?? 'help';
284
357
  const os = platform();
@@ -350,14 +423,30 @@ async function serviceInstallMac() {
350
423
 
351
424
  const uid = process.getuid();
352
425
  // Clear any prior "disabled" mark from a previous uninstall, so the agent is
353
- // eligible again — but do NOT bootstrap it now: that would start a second
354
- // gateway alongside the running one and fight over the port.
426
+ // eligible again.
355
427
  runQuiet('launchctl', ['enable', `gui/${uid}/${LAUNCH_AGENT_LABEL}`]);
356
428
 
429
+ // Bootstrap NOW only when nothing is already serving. With a gateway running,
430
+ // loading the agent starts a second one that loses the port race and then
431
+ // crash-loops under KeepAlive — so in that case the agent waits for the next
432
+ // login, when the manual instance is gone. With nothing running there is no
433
+ // conflict, and waiting would leave the user with a "service" that isn't
434
+ // running and no obvious reason why.
435
+ const running = await isAlreadyRunning();
436
+ let loadedNow = false;
437
+ if (!running) {
438
+ const r = runQuiet('launchctl', ['bootstrap', `gui/${uid}`, plistPath]);
439
+ loadedNow = r.status === 0;
440
+ }
441
+
357
442
  console.log(`✓ Suveren gateway installed as a login service.`);
358
443
  console.log(``);
359
- console.log(` It starts automatically from your NEXT login, and restarts if it crashes.`);
360
- console.log(` Your current gateway keeps running nothing was interrupted.`);
444
+ if (loadedNow) {
445
+ console.log(` It is running now, starts automatically at login, and restarts if it crashes.`);
446
+ } else {
447
+ console.log(` It starts automatically from your NEXT login, and restarts if it crashes.`);
448
+ if (running) console.log(` Your current gateway keeps running — nothing was interrupted.`);
449
+ }
361
450
  console.log(` After a reboot it comes up LOCKED — open http://localhost:${SUVEREN_PORT} and`);
362
451
  console.log(` enter your Suveren API key once to unlock it (your key is never stored).`);
363
452
  console.log(``);
@@ -395,7 +484,13 @@ async function serviceStatusMac() {
395
484
  const state = /state = (\w+)/.exec(p.stdout || '');
396
485
  console.log(` launchd: loaded${state ? ` (${state[1]})` : ''}`);
397
486
  } else if (installed) {
398
- console.log(` launchd: not loaded (run \`suveren-gateway service install\` to (re)load)`);
487
+ // Do NOT point at `service install` here: install deliberately does not
488
+ // bootstrap while a gateway is already running (two would fight over the
489
+ // port), so re-running it would change nothing and the advice would be a
490
+ // dead end. Say plainly when it becomes active, and give the command that
491
+ // actually loads it now.
492
+ console.log(` launchd: not loaded — starts at your next login`);
493
+ console.log(` to activate now: suveren-gateway stop && launchctl bootstrap gui/$(id -u) "${plistPath}"`);
399
494
  }
400
495
  console.log('');
401
496
  await status(); // process/health/vault line
@@ -0,0 +1,164 @@
1
+ {
2
+ "id": "deploy-github",
3
+ "name": "Deploy (GitHub)",
4
+ "version": "1.0.0",
5
+ "description": "Make an already-built version live, under signed authority",
6
+ "icon": "wrench",
7
+ "profile": "deploy",
8
+ "npmPackage": "@humanagencyp/deploy-mcp",
9
+ "mcp": {
10
+ "command": "npx",
11
+ "args": [
12
+ "-y",
13
+ "@humanagencyp/deploy-mcp@latest"
14
+ ]
15
+ },
16
+ "credentials": {
17
+ "fields": [
18
+ {
19
+ "key": "githubToken",
20
+ "label": "GitHub token",
21
+ "type": "password",
22
+ "placeholder": "github_pat_..."
23
+ }
24
+ ],
25
+ "envMapping": {
26
+ "GITHUB_TOKEN": "githubToken"
27
+ }
28
+ },
29
+ "oauth": null,
30
+ "toolGating": {
31
+ "default": {
32
+ "executionMapping": {},
33
+ "staticExecution": {},
34
+ "category": "disabled"
35
+ },
36
+ "overrides": {
37
+ "list_deployments": {
38
+ "category": "read",
39
+ "readGovernance": "none",
40
+ "readGovernanceReason": "Returns build metadata and the host's own deployment URLs. No source, no build output, no user content. Scoped to permitted repositories.",
41
+ "read": {
42
+ "resourceBound": "allowed_repos",
43
+ "resourceArg": "repo"
44
+ }
45
+ },
46
+ "get_deployment": {
47
+ "category": "read",
48
+ "readGovernance": "none",
49
+ "readGovernanceReason": "Returns metadata for one build — state, commit, URL. No build output.",
50
+ "read": {
51
+ "resourceBound": "allowed_repos",
52
+ "resourceArg": "repo"
53
+ }
54
+ },
55
+ "resolve_ref": {
56
+ "category": "read",
57
+ "readGovernance": "none",
58
+ "readGovernanceReason": "Returns a commit pointer only. Repository metadata, not user content.",
59
+ "read": {
60
+ "resourceBound": "allowed_repos",
61
+ "resourceArg": "repo"
62
+ }
63
+ },
64
+ "list_environments": {
65
+ "category": "read",
66
+ "readGovernance": "none",
67
+ "readGovernanceReason": "Returns environment names only. Repository configuration, needed so the owner can pick a target that exists.",
68
+ "read": {
69
+ "resourceBound": "allowed_repos",
70
+ "resourceArg": "repo"
71
+ }
72
+ },
73
+ "release": {
74
+ "executionMapping": {
75
+ "repo": "allowed_repos",
76
+ "environment": "allowed_environments",
77
+ "workflow": "allowed_workflows"
78
+ },
79
+ "staticExecution": {
80
+ "action_type": "release"
81
+ },
82
+ "contentField": "deployment_url"
83
+ }
84
+ }
85
+ },
86
+ "templates": [
87
+ {
88
+ "name": "Watch deployments",
89
+ "description": "See builds and their URLs. Cannot make anything live.",
90
+ "risk": "low",
91
+ "mode": "automatic",
92
+ "bounds": {
93
+ "release_daily_max": "0",
94
+ "rollback_allowed": "no"
95
+ },
96
+ "context": {
97
+ "allowed_repos": "",
98
+ "allowed_environments": "",
99
+ "allowed_workflows": "",
100
+ "allowed_branches": ""
101
+ },
102
+ "intent": "Let the agent see what has been built and whether it succeeded. It may not make anything live: a daily maximum of zero is a real limit, not an unset one. Fill in allowed_repos with the repositories it may look at.",
103
+ "ttl": 2592000,
104
+ "tags": [
105
+ "read-only",
106
+ "30 days"
107
+ ]
108
+ },
109
+ {
110
+ "name": "Release to production, review",
111
+ "description": "Make an existing build live after approval. Three a day, no rollback.",
112
+ "risk": "high",
113
+ "mode": "review",
114
+ "bounds": {
115
+ "release_daily_max": "3",
116
+ "rollback_allowed": "no"
117
+ },
118
+ "context": {
119
+ "allowed_repos": "",
120
+ "allowed_environments": "production",
121
+ "allowed_workflows": "deploy-website.yml",
122
+ "allowed_branches": "main"
123
+ },
124
+ "intent": "The agent may propose making a specific build live; nothing runs until I approve it. Open the build URL in the proposal and look at it before approving — that is the point of reviewing a release rather than a commit. Fill in allowed_repos, and check allowed_environments matches what the host actually calls it.",
125
+ "ttl": 2592000,
126
+ "tags": [
127
+ "review",
128
+ "production",
129
+ "30 days"
130
+ ]
131
+ }
132
+ ],
133
+ "setupHint": "Use a fine-grained token, scoped to one repository.",
134
+ "setupGuide": [
135
+ {
136
+ "title": "Create a fine-grained token",
137
+ "description": "GitHub Settings, Developer settings, Personal access tokens, Fine-grained. Not a classic token — those reach everything you own.",
138
+ "link": "https://github.com/settings/personal-access-tokens/new"
139
+ },
140
+ {
141
+ "title": "Scope it to one repository",
142
+ "description": "Under Repository access pick Only select repositories, then the repo to deploy."
143
+ },
144
+ {
145
+ "title": "Grant three permissions",
146
+ "description": "Actions: Read and write. Environments: Read. Contents: Read. Not Contents: Write — the agent ships commits, it never writes them."
147
+ },
148
+ {
149
+ "title": "Paste the token below",
150
+ "description": "Encrypted in your local vault. The agent never sees it."
151
+ }
152
+ ],
153
+ "proposalLinks": [
154
+ {
155
+ "label": "Open this build",
156
+ "template": "{deployment_url}",
157
+ "description": "Look at exactly what would go live"
158
+ },
159
+ {
160
+ "label": "View repository",
161
+ "template": "https://github.com/{repo}"
162
+ }
163
+ ]
164
+ }
@@ -83,7 +83,10 @@
83
83
  },
84
84
  "staticExecution": {
85
85
  "action_type": "send"
86
- }
86
+ },
87
+ "blockedArgs": [
88
+ "raw"
89
+ ]
87
90
  },
88
91
  "create_draft": {
89
92
  "executionMapping": {
@@ -104,7 +107,10 @@
104
107
  },
105
108
  "staticExecution": {
106
109
  "action_type": "send"
107
- }
110
+ },
111
+ "blockedArgs": [
112
+ "raw"
113
+ ]
108
114
  },
109
115
  "send_draft": {
110
116
  "executionMapping": {},
@@ -187,7 +193,7 @@
187
193
  "get_profile": {
188
194
  "category": "read",
189
195
  "readGovernance": "none",
190
- "readGovernanceReason": "Returns the account's own address and message counts the connector's own identity, no correspondence content."
196
+ "readGovernanceReason": "Returns the account's own address and message counts \u2014 the connector's own identity, no correspondence content."
191
197
  }
192
198
  }
193
199
  },
@@ -230,7 +236,7 @@
230
236
  "allowed_recipients": "",
231
237
  "allowed_domains": ""
232
238
  },
233
- "intent": "Read my inbox and compose emails. Every send requires my review I see the recipient, subject, and body before it goes out.",
239
+ "intent": "Read my inbox and compose emails. Every send requires my review \u2014 I see the recipient, subject, and body before it goes out.",
234
240
  "ttl": 2592000,
235
241
  "tags": [
236
242
  "5 recipients/email",
@@ -5,6 +5,7 @@
5
5
  "crm": "crm.json",
6
6
  "records": "records.json",
7
7
  "linkedin": "linkedin.json",
8
- "calendar": "calendar.json"
8
+ "calendar": "calendar.json",
9
+ "deploy-github": "deploy-github.json"
9
10
  }
10
11
  }
@@ -1600,16 +1600,23 @@ function createAutostartRouter() {
1600
1600
  }
1601
1601
 
1602
1602
  // src/lib/desktop-notify.ts
1603
- import { spawn } from "child_process";
1603
+ import { spawn, spawnSync } from "child_process";
1604
1604
  function osaQuote(value) {
1605
1605
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1606
1606
  }
1607
1607
  function psQuote(value) {
1608
1608
  return `'${value.replace(/'/g, "''")}'`;
1609
1609
  }
1610
- function buildNotifyCommand(platform, title, message) {
1610
+ function buildNotifyCommand(platform, title, message, options = {}) {
1611
+ const { url, hasTerminalNotifier: hasTerminalNotifier2 = false } = options;
1611
1612
  switch (platform) {
1612
1613
  case "darwin":
1614
+ if (url && hasTerminalNotifier2) {
1615
+ return {
1616
+ cmd: "terminal-notifier",
1617
+ args: ["-title", title, "-message", message, "-open", url, "-group", "ai.suveren.gateway"]
1618
+ };
1619
+ }
1613
1620
  return {
1614
1621
  cmd: "osascript",
1615
1622
  args: ["-e", `display notification ${osaQuote(message)} with title ${osaQuote(title)}`]
@@ -1631,13 +1638,30 @@ function buildNotifyCommand(platform, title, message) {
1631
1638
  }
1632
1639
  }
1633
1640
  function lockedNotification(port2) {
1641
+ const url = `http://localhost:${port2}`;
1634
1642
  return {
1635
1643
  title: "Suveren is running but locked",
1636
- message: `Your agent has no authority until you unlock it. Open http://localhost:${port2} and enter your API key.`
1644
+ message: `Your agent has no authority until you unlock it. Open ${url} and enter your API key.`,
1645
+ url
1637
1646
  };
1638
1647
  }
1639
- function notify(title, message, platform = process.platform) {
1640
- const command = buildNotifyCommand(platform, title, message);
1648
+ var terminalNotifierCache = null;
1649
+ function hasTerminalNotifier() {
1650
+ if (terminalNotifierCache !== null) return terminalNotifierCache;
1651
+ try {
1652
+ const probe = spawnSync("terminal-notifier", ["-help"], { stdio: "ignore" });
1653
+ terminalNotifierCache = probe.error === void 0;
1654
+ } catch {
1655
+ terminalNotifierCache = false;
1656
+ }
1657
+ return terminalNotifierCache;
1658
+ }
1659
+ function notify(title, message, platform = process.platform, url) {
1660
+ const command = buildNotifyCommand(platform, title, message, {
1661
+ url,
1662
+ // Only probe when a URL could actually be attached.
1663
+ hasTerminalNotifier: url !== void 0 && platform === "darwin" ? hasTerminalNotifier() : false
1664
+ });
1641
1665
  if (!command) return;
1642
1666
  try {
1643
1667
  const child = spawn(command.cmd, command.args, { detached: true, stdio: "ignore" });
@@ -2546,8 +2570,8 @@ if (existsSync5(UI_DIST)) {
2546
2570
  app.listen(port, "0.0.0.0", () => {
2547
2571
  console.error(`[Control Plane] Listening on http://0.0.0.0:${port}`);
2548
2572
  if (process.env.SUVEREN_AUTOSTART === "1" && !vault.isUnlocked()) {
2549
- const { title, message } = lockedNotification(port);
2550
- notify(title, message);
2573
+ const { title, message, url } = lockedNotification(port);
2574
+ notify(title, message, process.platform, url);
2551
2575
  }
2552
2576
  console.error(`[Control Plane] SP proxy: ${SP_URL2}`);
2553
2577
  console.error(`[Control Plane] UI dist: ${UI_DIST}`);
@@ -1157,9 +1157,11 @@ function computeContentBinding(profileId, tool, toolArgs) {
1157
1157
  if (binding.kind === "jcs") {
1158
1158
  contentHash = computeContentHash(binding, toolArgs);
1159
1159
  } else {
1160
- const field = tool ? detectContentField(tool) : null;
1160
+ const declared = tool?.gating?.contentField;
1161
+ const field = declared ?? (tool ? detectContentField(tool) : null);
1161
1162
  if (!field) return void 0;
1162
1163
  const raw = typeof toolArgs[field] === "string" ? toolArgs[field] : "";
1164
+ if (raw === "") return void 0;
1163
1165
  contentHash = computeContentHash(binding, raw);
1164
1166
  }
1165
1167
  return {
@@ -1445,6 +1447,22 @@ function denyRead(state2, tool, reason, detail, target) {
1445
1447
  return { content: [{ type: "text", text: `Read blocked by Gatekeeper: ${detail}` }], isError: true };
1446
1448
  }
1447
1449
  function createGatedToolHandler(tool, integrationManager2, state2) {
1450
+ const inner = createGatedToolHandlerInner(tool, integrationManager2, state2);
1451
+ const blocked = tool.gating?.blockedArgs ?? [];
1452
+ if (blocked.length === 0) return inner;
1453
+ return async (args) => {
1454
+ const present = blocked.filter((a) => a in (args ?? {}));
1455
+ if (present.length === 0) return inner(args);
1456
+ return {
1457
+ content: [{
1458
+ type: "text",
1459
+ text: `Refused: "${tool.namespacedName}" was called with ${present.map((b) => `"${b}"`).join(", ")}, which the integration manifest blocks. That argument carries the whole request in one opaque value, so the recipients, the content and the limits on this authorization cannot be read from it \u2014 the call could not be shown to stay in scope, and could not be shown to a human for approval in any form they could judge. Use the individual arguments instead.`
1460
+ }],
1461
+ isError: true
1462
+ };
1463
+ };
1464
+ }
1465
+ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
1448
1466
  if (!tool.gating || !tool.gating.profile) {
1449
1467
  return async () => {
1450
1468
  return {
@@ -2611,6 +2629,19 @@ function readAgeOf(config) {
2611
2629
  const days = config.readAgeDays;
2612
2630
  return typeof days === "number" && Number.isFinite(days) && days >= 0 ? days : null;
2613
2631
  }
2632
+ function withoutBlockedArgs(schema, blocked) {
2633
+ if (!blocked?.length) return schema;
2634
+ const props = schema.properties;
2635
+ if (!props) return schema;
2636
+ const kept = Object.fromEntries(Object.entries(props).filter(([k]) => !blocked.includes(k)));
2637
+ const out = { ...schema, properties: kept };
2638
+ if (Array.isArray(schema.required)) {
2639
+ out.required = schema.required.filter(
2640
+ (r) => typeof r !== "string" || !blocked.includes(r)
2641
+ );
2642
+ }
2643
+ return out;
2644
+ }
2614
2645
  var RESPAWN_DELAYS = [2e3, 4e3, 6e3];
2615
2646
  var MAX_RESPAWN_ATTEMPTS = 3;
2616
2647
  var CONNECT_TIMEOUT_MS = 3e4;
@@ -2806,7 +2837,14 @@ var IntegrationManager = class {
2806
2837
  namespacedName: `${config.id}__${tool.name}`,
2807
2838
  integrationId: config.id,
2808
2839
  description: tool.description ?? "",
2809
- inputSchema: tool.inputSchema ?? {},
2840
+ // A downstream schema otherwise reaches the agent verbatim, so a
2841
+ // connector can offer a control-bypassing argument and we would pass it
2842
+ // straight on. Anything the manifest blocks is removed here, before the
2843
+ // agent ever sees it exists.
2844
+ inputSchema: withoutBlockedArgs(
2845
+ tool.inputSchema ?? {},
2846
+ gating?.blockedArgs
2847
+ ),
2810
2848
  gating
2811
2849
  };
2812
2850
  });
@@ -3011,13 +3049,20 @@ var IntegrationManager = class {
3011
3049
  requiredValue: ext.requiredValue,
3012
3050
  read: ext.read,
3013
3051
  readGovernance: ext.readGovernance,
3014
- readGovernanceReason: ext.readGovernanceReason
3052
+ readGovernanceReason: ext.readGovernanceReason,
3053
+ blockedArgs: ext.blockedArgs
3015
3054
  };
3016
3055
  }
3017
3056
  return {
3018
3057
  profile: profileId,
3019
3058
  executionMapping: override.executionMapping,
3020
- staticExecution: override.staticExecution
3059
+ staticExecution: override.staticExecution,
3060
+ // Which argument the receipt binds to. This resolver is an explicit
3061
+ // whitelist, so a field added to the manifest and the type but not
3062
+ // copied here is silently dropped — the receipt is still issued and
3063
+ // simply carries no binding, which only surfaces when a verifier asks.
3064
+ contentField: ext.contentField,
3065
+ blockedArgs: ext.blockedArgs
3021
3066
  };
3022
3067
  }
3023
3068
  return {