@suveren/gateway 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +19 -3
  2. package/bin/suveren-gateway.js +5 -0
  3. package/dist/control-plane/index.mjs +146 -114
  4. package/dist/mcp-server/http.mjs +162 -118
  5. package/dist/ui/assets/{index-CmLqzgqA.js → index-Cg6VaGKb.js} +12 -12
  6. package/dist/ui/index.html +1 -1
  7. package/lib/autostart-templates.mjs +15 -7
  8. package/node_modules/@hap/core/dist/index.d.mts +49 -1
  9. package/node_modules/@hap/core/dist/index.d.ts +49 -1
  10. package/node_modules/@hap/core/dist/index.js +50 -0
  11. package/node_modules/@hap/core/dist/index.mjs +49 -0
  12. package/node_modules/@hap/core/package.json +1 -1
  13. package/node_modules/@hap/core/src/frame.ts +63 -1
  14. package/node_modules/@hap/core/src/gatekeeper.ts +37 -0
  15. package/node_modules/@hap/core/src/types.ts +29 -1
  16. package/node_modules/hono/dist/cjs/hono-base.js +2 -0
  17. package/node_modules/hono/dist/cjs/middleware/language/language.js +1 -1
  18. package/node_modules/hono/dist/cjs/middleware/mount/index.js +71 -0
  19. package/node_modules/hono/dist/cjs/request.js +4 -4
  20. package/node_modules/hono/dist/hono-base.js +2 -0
  21. package/node_modules/hono/dist/middleware/mount/index.js +70 -0
  22. package/node_modules/hono/dist/request.js +4 -4
  23. package/node_modules/hono/dist/types/hono-base.d.ts +2 -0
  24. package/node_modules/hono/dist/types/jsx/base.d.ts +1 -1
  25. package/node_modules/hono/dist/types/middleware/mount/index.d.ts +46 -0
  26. package/node_modules/hono/dist/types/request.d.ts +4 -4
  27. package/node_modules/hono/package.json +9 -1
  28. package/package.json +2 -2
  29. package/profiles/index.json +3 -1
  30. package/profiles/purchase/0.6.profile.json +234 -0
  31. package/profiles/sales/0.3.profile.json +290 -0
  32. package/server.js +6 -3
@@ -2202,7 +2202,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2202
2202
  import { z as z2 } from "zod";
2203
2203
 
2204
2204
  // src/lib/mandate-brief.ts
2205
- import { getProfile } from "@hap/core";
2205
+ import { getProfile as getProfile3 } from "@hap/core";
2206
2206
 
2207
2207
  // src/lib/consumption.ts
2208
2208
  import { boundActionTypes } from "@hap/core";
@@ -2296,87 +2296,16 @@ function getContextForBrief(dataDir2) {
2296
2296
  return { brief: truncated, truncated: true };
2297
2297
  }
2298
2298
 
2299
- // src/lib/mandate-brief.ts
2300
- function shortProfileName(profileId) {
2301
- const withoutVersion = profileId.replace(/@.*$/, "");
2302
- const parts = withoutVersion.split("/");
2303
- return parts[parts.length - 1];
2304
- }
2305
- function countToolsByGating(profileId, integrationManager2) {
2306
- if (!integrationManager2) return { gated: 0, readOnly: 0 };
2307
- const allTools = integrationManager2.getAllTools();
2308
- let gated = 0;
2309
- let readOnly = 0;
2310
- for (const tool of allTools) {
2311
- if (!tool.gating || !tool.gating.profile) {
2312
- readOnly++;
2313
- } else if (tool.gating.profile === profileId || tool.gating.profile === shortProfileName(profileId)) {
2314
- if (tool.gating.staticExecution?.action_type === "read" && Object.keys(tool.gating.executionMapping ?? {}).length === 0) {
2315
- readOnly++;
2316
- } else {
2317
- gated++;
2318
- }
2319
- }
2320
- }
2321
- return { gated, readOnly };
2299
+ // src/lib/simulation-mode.ts
2300
+ function isSimulationMode() {
2301
+ const policySimulation = readPolicy().policy.simulation;
2302
+ if (policySimulation !== void 0) return policySimulation;
2303
+ return process.env.SUVEREN_SIMULATION === "1";
2322
2304
  }
2323
- function buildMandateBrief(opts) {
2324
- const { authorizations, executionLog, integrationManager: integrationManager2, contextDir } = opts;
2325
- const lines = [
2326
- "You are connected to Suveren \u2014 the gateway that gates every privileged tool call you make.",
2327
- "Suveren implements the bounded-authority model from the open Human Agency Protocol (HAP).",
2328
- "You have bounded authorities granted by human decision owners.",
2329
- "You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them."
2330
- ];
2331
- const { brief: contextBrief } = getContextForBrief(contextDir);
2332
- if (contextBrief) {
2333
- lines.push("");
2334
- lines.push("=== CONTEXT ===");
2335
- lines.push("");
2336
- lines.push(contextBrief);
2337
- }
2338
- const active = authorizations.filter((a) => a.complete);
2339
- const pending = authorizations.filter((a) => !a.complete);
2340
- const now = Math.floor(Date.now() / 1e3);
2341
- if (active.length > 0) {
2342
- lines.push("");
2343
- lines.push("=== ACTIVE AUTHORITIES ===");
2344
- lines.push("");
2345
- for (const auth of active) {
2346
- const earliestExpiry = Math.min(...auth.attestations.map((a) => a.expiresAt));
2347
- const remainingMin = Math.max(0, Math.round((earliestExpiry - now) / 60));
2348
- const shortName = shortProfileName(auth.profileId);
2349
- const version = auth.profileId.match(/@(.+)$/)?.[1] ?? "";
2350
- const shortId = version ? `${shortName}@${version}` : shortName;
2351
- const boundsDesc = Object.entries(auth.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}:${value}`).join(" \xB7 ");
2352
- const parts = [];
2353
- if (boundsDesc) parts.push(boundsDesc);
2354
- if (executionLog) {
2355
- const profile = getProfile(auth.profileId) ?? getProfile(shortName);
2356
- const consumption = getConsumptionState(auth, executionLog, profile);
2357
- const compact = formatConsumptionCompact(consumption);
2358
- if (compact) parts.push(compact);
2359
- }
2360
- const { gated, readOnly } = countToolsByGating(auth.profileId, integrationManager2);
2361
- if (gated > 0 || readOnly > 0) parts.push(`${gated} gated tools`);
2362
- parts.push(`${remainingMin} min remaining`);
2363
- lines.push(`[${shortId}] ${parts.join(" \xB7 ")}`);
2364
- lines.push(` \u2192 list-authorizations(domain: "${shortName}") for intent & details`);
2365
- lines.push("");
2366
- }
2367
- }
2368
- if (pending.length > 0) {
2369
- lines.push("=== PENDING (awaiting attestations) ===");
2370
- lines.push("");
2371
- for (const auth of pending) {
2372
- const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
2373
- lines.push(`[${auth.path}] ${auth.profileId} \u2014 needs: ${missing.join(", ")}`);
2374
- }
2375
- lines.push("");
2376
- }
2377
- lines.push("When you receive a task, call list-authorizations(domain) to load full details for the relevant domain.");
2378
- return lines.join("\n");
2305
+ function manifestIsSimulated(manifest) {
2306
+ return Boolean(manifest?.simulation);
2379
2307
  }
2308
+ var SIMULATION_BLOCK_REASON = "blocked: simulation mode \u2014 real systems are off";
2380
2309
 
2381
2310
  // src/lib/locked-notice.ts
2382
2311
  function uiUrl() {
@@ -2426,9 +2355,6 @@ function lockedNotice(action, reason = "restart") {
2426
2355
  }
2427
2356
  }
2428
2357
 
2429
- // src/tools/authorizations.ts
2430
- import { getProfile as getProfile4 } from "@hap/core";
2431
-
2432
2358
  // src/lib/receipt-footer.ts
2433
2359
  import { deriveIdentityLine } from "@hap/core";
2434
2360
  var AS_BASE = (process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai").replace(/\/$/, "");
@@ -2490,10 +2416,10 @@ import {
2490
2416
  selectBoundFields,
2491
2417
  isFieldBinding,
2492
2418
  bindingAppliesTo,
2493
- getProfile as getProfile2
2419
+ getProfile
2494
2420
  } from "@hap/core";
2495
2421
  function getContentBinding(profileId) {
2496
- return getProfile2(profileId)?.content_binding;
2422
+ return getProfile(profileId)?.content_binding;
2497
2423
  }
2498
2424
  function computeContentBinding(profileId, tool, toolArgs, actionType) {
2499
2425
  const binding = getContentBinding(profileId);
@@ -2913,7 +2839,7 @@ function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
2913
2839
  }
2914
2840
 
2915
2841
  // src/lib/tool-proxy.ts
2916
- import { getProfile as getProfile3, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
2842
+ import { getProfile as getProfile2, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
2917
2843
  import { readFile } from "fs/promises";
2918
2844
  import { extname } from "path";
2919
2845
  import { randomUUID } from "crypto";
@@ -2978,17 +2904,6 @@ function getAllManifests() {
2978
2904
  return Array.from(manifests.values());
2979
2905
  }
2980
2906
 
2981
- // src/lib/simulation-mode.ts
2982
- function isSimulationMode() {
2983
- const policySimulation = readPolicy().policy.simulation;
2984
- if (policySimulation !== void 0) return policySimulation;
2985
- return process.env.SUVEREN_SIMULATION === "1";
2986
- }
2987
- function manifestIsSimulated(manifest) {
2988
- return Boolean(manifest?.simulation);
2989
- }
2990
- var SIMULATION_BLOCK_REASON = "blocked: simulation mode \u2014 real systems are off";
2991
-
2992
2907
  // src/lib/tool-proxy.ts
2993
2908
  var IMAGE_MIME = {
2994
2909
  ".jpg": "image/jpeg",
@@ -3050,7 +2965,7 @@ function profileMatches(profileId, shortName) {
3050
2965
  return profileId === shortName || profileId.includes("/" + shortName + "@") || profileId.endsWith("/" + shortName);
3051
2966
  }
3052
2967
  function countBoundsFor(profileId, actionType) {
3053
- const fields = getProfile3(profileId)?.boundsSchema?.fields;
2968
+ const fields = getProfile2(profileId)?.boundsSchema?.fields;
3054
2969
  if (!fields) return null;
3055
2970
  return Object.entries(fields).filter(([, def]) => def.boundType?.kind === "cumulative_count").filter(([name, def]) => {
3056
2971
  const applies = boundActionTypes2(name, def);
@@ -3110,7 +3025,7 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
3110
3025
  return async () => ({
3111
3026
  content: [{
3112
3027
  type: "text",
3113
- text: `Refused: "${tool.namespacedName}" talks to a real system, but this gateway is running in simulation mode (real systems are off). No ticket was requested.`
3028
+ text: `Refused: "${tool.namespacedName}" is not available. No ticket was requested.`
3114
3029
  }],
3115
3030
  isError: true
3116
3031
  });
@@ -3131,6 +3046,17 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
3131
3046
  };
3132
3047
  };
3133
3048
  }
3049
+ function isStaleMandateRefusal(err) {
3050
+ const errors = err.body?.errors;
3051
+ const code = errors?.[0]?.code;
3052
+ if (err.statusCode === 403 && (code === "ATTESTATION_REVOKED" || code === "ATTESTATION_EXPIRED")) {
3053
+ return true;
3054
+ }
3055
+ if (err.statusCode === 404 && code === "ATTESTATION_NOT_FOUND") {
3056
+ return true;
3057
+ }
3058
+ return false;
3059
+ }
3134
3060
  function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3135
3061
  if (!tool.gating || !tool.gating.profile) {
3136
3062
  return async () => {
@@ -3202,7 +3128,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3202
3128
  );
3203
3129
  }
3204
3130
  }
3205
- const readProfile = readAdapter ? getProfile3(matchingAuths[0].profileId) : void 0;
3131
+ const readProfile = readAdapter ? getProfile2(matchingAuths[0].profileId) : void 0;
3206
3132
  const boundsSchema = readProfile?.boundsSchema;
3207
3133
  const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
3208
3134
  const hasAgeDimension = Boolean(readAdapter?.ageField);
@@ -3358,7 +3284,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3358
3284
  };
3359
3285
  }
3360
3286
  {
3361
- const profileDef = getProfile3(profile);
3287
+ const profileDef = getProfile2(profile);
3362
3288
  const registry = profileDef?.boundsSchema?.actionTypes;
3363
3289
  if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
3364
3290
  return {
@@ -3415,23 +3341,24 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
3415
3341
  isError: true
3416
3342
  };
3417
3343
  }
3418
- const contextKeys = getProfile3(passers[0].profileId)?.contextSchema?.keyOrder ?? [];
3419
- const selection = selectAuthorization(
3420
- contextKeys,
3421
- passers.map((a) => ({
3422
- id: a.authorizationId,
3423
- auth: a,
3424
- context: a.context ?? {},
3425
- requiresApproval: (a.deferredCommitmentDomains ?? []).length > 0
3426
- }))
3427
- );
3428
- const auth = selection.chosen.auth;
3429
- if (selection.superseded.length > 0) {
3430
- console.error(
3431
- `[Suveren MCP] selection(${tool.namespacedName}): chose ${auth.authorizationId} (${selection.reason}) over [${selection.superseded.map((s) => s.id).join(", ")}]`
3344
+ const contextKeys = getProfile2(passers[0].profileId)?.contextSchema?.keyOrder ?? [];
3345
+ return attemptWithCandidates(passers);
3346
+ async function attemptWithCandidates(candidates) {
3347
+ const selection = selectAuthorization(
3348
+ contextKeys,
3349
+ candidates.map((a) => ({
3350
+ id: a.authorizationId,
3351
+ auth: a,
3352
+ context: a.context ?? {},
3353
+ requiresApproval: (a.deferredCommitmentDomains ?? []).length > 0
3354
+ }))
3432
3355
  );
3433
- }
3434
- {
3356
+ const auth = selection.chosen.auth;
3357
+ if (selection.superseded.length > 0) {
3358
+ console.error(
3359
+ `[Suveren MCP] selection(${tool.namespacedName}): chose ${auth.authorizationId} (${selection.reason}) over [${selection.superseded.map((s) => s.id).join(", ")}]`
3360
+ );
3361
+ }
3435
3362
  const authzId = auth.authorizationId;
3436
3363
  if (isCommitmentDowngrade(auth)) {
3437
3364
  return {
@@ -3544,6 +3471,24 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
3544
3471
  isError: true
3545
3472
  };
3546
3473
  }
3474
+ if (err instanceof SPReceiptError && isStaleMandateRefusal(err)) {
3475
+ state2.cache.invalidate(auth.authorizationId);
3476
+ const code = err.body?.errors?.[0]?.code ?? "unknown";
3477
+ const remaining = candidates.filter((c) => c.authorizationId !== auth.authorizationId);
3478
+ if (remaining.length === 0) {
3479
+ console.error(
3480
+ `[Suveren MCP] fallback(${tool.namespacedName}): mandate ${auth.authorizationId} invalid (${code}) \u2014 no remaining candidates`
3481
+ );
3482
+ return {
3483
+ content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
3484
+ isError: true
3485
+ };
3486
+ }
3487
+ console.error(
3488
+ `[Suveren MCP] fallback(${tool.namespacedName}): mandate ${auth.authorizationId} invalid (${code}) \u2192 retrying with [${remaining.map((c) => c.authorizationId).join(", ")}]`
3489
+ );
3490
+ return attemptWithCandidates(remaining);
3491
+ }
3547
3492
  if (err instanceof SPReceiptError && err.statusCode === 409) {
3548
3493
  const spBody = err.body;
3549
3494
  let pendingApprovers = spBody.approvers ?? [];
@@ -3700,7 +3645,106 @@ function buildProxiedToolDescription(tool, state2) {
3700
3645
  return `[Suveren: ${profile} \u2014 ${tag}] ${tool.description}`;
3701
3646
  }
3702
3647
 
3648
+ // src/lib/agent-visibility.ts
3649
+ function agentVisibleAuthorizations(authorizations, integrationManager2) {
3650
+ if (!isSimulationMode()) return authorizations;
3651
+ if (!integrationManager2) return [];
3652
+ const runningProfiles = /* @__PURE__ */ new Set();
3653
+ for (const tool of integrationManager2.getAllTools()) {
3654
+ const profile = tool.gating?.profile;
3655
+ if (profile) runningProfiles.add(profile);
3656
+ }
3657
+ if (runningProfiles.size === 0) return [];
3658
+ return authorizations.filter(
3659
+ (auth) => [...runningProfiles].some((profile) => profileMatches(auth.profileId, profile))
3660
+ );
3661
+ }
3662
+
3663
+ // src/lib/mandate-brief.ts
3664
+ function shortProfileName(profileId) {
3665
+ const withoutVersion = profileId.replace(/@.*$/, "");
3666
+ const parts = withoutVersion.split("/");
3667
+ return parts[parts.length - 1];
3668
+ }
3669
+ function countToolsByGating(profileId, integrationManager2) {
3670
+ if (!integrationManager2) return { gated: 0, readOnly: 0 };
3671
+ const allTools = integrationManager2.getAllTools();
3672
+ let gated = 0;
3673
+ let readOnly = 0;
3674
+ for (const tool of allTools) {
3675
+ if (!tool.gating || !tool.gating.profile) {
3676
+ readOnly++;
3677
+ } else if (tool.gating.profile === profileId || tool.gating.profile === shortProfileName(profileId)) {
3678
+ if (tool.gating.staticExecution?.action_type === "read" && Object.keys(tool.gating.executionMapping ?? {}).length === 0) {
3679
+ readOnly++;
3680
+ } else {
3681
+ gated++;
3682
+ }
3683
+ }
3684
+ }
3685
+ return { gated, readOnly };
3686
+ }
3687
+ function buildMandateBrief(opts) {
3688
+ const { executionLog, integrationManager: integrationManager2, contextDir } = opts;
3689
+ const authorizations = agentVisibleAuthorizations(opts.authorizations, integrationManager2);
3690
+ const lines = [
3691
+ "You are connected to Suveren \u2014 the gateway that gates every privileged tool call you make.",
3692
+ "Suveren implements the bounded-authority model from the open Human Agency Protocol (HAP).",
3693
+ "You have bounded authorities granted by human decision owners.",
3694
+ "You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them."
3695
+ ];
3696
+ const { brief: contextBrief } = getContextForBrief(contextDir);
3697
+ if (contextBrief) {
3698
+ lines.push("");
3699
+ lines.push("=== CONTEXT ===");
3700
+ lines.push("");
3701
+ lines.push(contextBrief);
3702
+ }
3703
+ const active = authorizations.filter((a) => a.complete);
3704
+ const pending = authorizations.filter((a) => !a.complete);
3705
+ const now = Math.floor(Date.now() / 1e3);
3706
+ if (active.length > 0) {
3707
+ lines.push("");
3708
+ lines.push("=== ACTIVE AUTHORITIES ===");
3709
+ lines.push("");
3710
+ for (const auth of active) {
3711
+ const earliestExpiry = Math.min(...auth.attestations.map((a) => a.expiresAt));
3712
+ const remainingMin = Math.max(0, Math.round((earliestExpiry - now) / 60));
3713
+ const shortName = shortProfileName(auth.profileId);
3714
+ const version = auth.profileId.match(/@(.+)$/)?.[1] ?? "";
3715
+ const shortId = version ? `${shortName}@${version}` : shortName;
3716
+ const boundsDesc = Object.entries(auth.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}:${value}`).join(" \xB7 ");
3717
+ const parts = [];
3718
+ if (boundsDesc) parts.push(boundsDesc);
3719
+ if (executionLog) {
3720
+ const profile = getProfile3(auth.profileId) ?? getProfile3(shortName);
3721
+ const consumption = getConsumptionState(auth, executionLog, profile);
3722
+ const compact = formatConsumptionCompact(consumption);
3723
+ if (compact) parts.push(compact);
3724
+ }
3725
+ const { gated, readOnly } = countToolsByGating(auth.profileId, integrationManager2);
3726
+ if (gated > 0 || readOnly > 0) parts.push(`${gated} gated tools`);
3727
+ parts.push(`${remainingMin} min remaining`);
3728
+ lines.push(`[${shortId}] ${parts.join(" \xB7 ")}`);
3729
+ lines.push(` \u2192 list-authorizations(domain: "${shortName}") for intent & details`);
3730
+ lines.push("");
3731
+ }
3732
+ }
3733
+ if (pending.length > 0) {
3734
+ lines.push("=== PENDING (awaiting attestations) ===");
3735
+ lines.push("");
3736
+ for (const auth of pending) {
3737
+ const missing = auth.requiredDomains.filter((d) => !auth.attestedDomains.includes(d));
3738
+ lines.push(`[${auth.path}] ${auth.profileId} \u2014 needs: ${missing.join(", ")}`);
3739
+ }
3740
+ lines.push("");
3741
+ }
3742
+ lines.push("When you receive a task, call list-authorizations(domain) to load full details for the relevant domain.");
3743
+ return lines.join("\n");
3744
+ }
3745
+
3703
3746
  // src/tools/authorizations.ts
3747
+ import { getProfile as getProfile4 } from "@hap/core";
3704
3748
  function shortProfileName2(profileId) {
3705
3749
  const withoutVersion = profileId.replace(/@.*$/, "");
3706
3750
  const parts = withoutVersion.split("/");
@@ -3761,7 +3805,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
3761
3805
  if (!state2.spClient.isUnlocked()) {
3762
3806
  return { content: [{ type: "text", text: lockedNotice("list authorizations", state2.spClient.getLockReason() ?? "restart") }] };
3763
3807
  }
3764
- const authorizations = state2.getEnrichedAuthorizations();
3808
+ const authorizations = agentVisibleAuthorizations(state2.getEnrichedAuthorizations(), integrationManager2);
3765
3809
  const now = Math.floor(Date.now() / 1e3);
3766
3810
  const domain = args?.domain;
3767
3811
  if (authorizations.length === 0) {