@suveren/gateway 0.2.7 → 0.2.9

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.
@@ -904,6 +904,54 @@ function buildMandateBrief(opts) {
904
904
  // src/tools/authorizations.ts
905
905
  import { getProfile as getProfile2 } from "@hap/core";
906
906
 
907
+ // src/lib/receipt-footer.ts
908
+ var AS_BASE = (process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai").replace(/\/$/, "");
909
+ var FOOTER_PROFILES = /* @__PURE__ */ new Set(["email", "calendar", "publish"]);
910
+ var CONTENT_FIELD_CANDIDATES = ["body", "text", "description"];
911
+ var FOOTER_MARKER = "\u2014 Sent by an AI agent via Suveren";
912
+ function shouldAttachFooter() {
913
+ return true;
914
+ }
915
+ function verifyUrl(receiptId) {
916
+ return `${AS_BASE}/r/${receiptId}`;
917
+ }
918
+ function footerText(receiptId) {
919
+ return `
920
+
921
+ ${FOOTER_MARKER}. Verify: ${verifyUrl(receiptId)}`;
922
+ }
923
+ function isStringType(t) {
924
+ return t === "string" || Array.isArray(t) && t.includes("string");
925
+ }
926
+ function detectContentField(tool) {
927
+ const schema = tool.inputSchema;
928
+ const props = schema?.properties ?? {};
929
+ for (const candidate of CONTENT_FIELD_CANDIDATES) {
930
+ const prop = props[candidate];
931
+ if (prop && isStringType(prop.type)) return candidate;
932
+ }
933
+ return null;
934
+ }
935
+ function stripFooter(value) {
936
+ const i = value.indexOf(FOOTER_MARKER);
937
+ if (i === -1) return value;
938
+ return value.slice(0, i).replace(/\n+$/, "");
939
+ }
940
+ function appendVerificationFooter(tool, args, receiptId) {
941
+ const profile = tool.gating?.profile;
942
+ if (!profile || !FOOTER_PROFILES.has(profile)) return args;
943
+ if (typeof args.raw === "string" && args.raw.trim().length > 0) {
944
+ console.error(
945
+ `[Suveren MCP] ${tool.namespacedName}: 'raw' message present \u2014 skipping verification footer.`
946
+ );
947
+ return args;
948
+ }
949
+ const field = detectContentField(tool);
950
+ if (!field) return args;
951
+ const current = typeof args[field] === "string" ? args[field] : "";
952
+ return { ...args, [field]: stripFooter(current) + footerText(receiptId) };
953
+ }
954
+
907
955
  // src/lib/tool-proxy.ts
908
956
  import { readFile } from "fs/promises";
909
957
  import { extname } from "path";
@@ -1064,6 +1112,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
1064
1112
  };
1065
1113
  }
1066
1114
  }
1115
+ let receiptId;
1067
1116
  try {
1068
1117
  const actionType = typeof execution.action_type === "string" ? execution.action_type : void 0;
1069
1118
  if (!actionType) {
@@ -1071,7 +1120,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
1071
1120
  `[Suveren MCP] Warning: tool ${tool.namespacedName} has no action_type in staticExecution. Bounds check may be skipped. Fix the integration manifest.`
1072
1121
  );
1073
1122
  }
1074
- await state2.spClient.postReceipt({
1123
+ const { receipt } = await state2.spClient.postReceipt({
1075
1124
  // v0.5: send the bare content address; the AS reconstructs the
1076
1125
  // per-user storage key. Fall back to frameHash only for legacy
1077
1126
  // (pre-v0.4) records that predate bounds_hash.
@@ -1083,6 +1132,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
1083
1132
  amount: typeof execution.amount === "number" ? execution.amount : void 0,
1084
1133
  idempotencyKey: randomUUID()
1085
1134
  });
1135
+ receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
1086
1136
  } catch (err) {
1087
1137
  if (err instanceof SPReceiptError && err.statusCode === 409) {
1088
1138
  const spBody = err.body;
@@ -1160,7 +1210,8 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
1160
1210
  execution: { ...execution },
1161
1211
  timestamp: Math.floor(Date.now() / 1e3)
1162
1212
  });
1163
- return integrationManager2.callTool(tool.integrationId, tool.originalName, args);
1213
+ const outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId) : args;
1214
+ return integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
1164
1215
  }
1165
1216
  const reasons = result.errors.map((e) => {
1166
1217
  if (e.code === "BOUND_EXCEEDED") {
@@ -2193,7 +2244,7 @@ function loadProfiles(profilesDir) {
2193
2244
  const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join6(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
2194
2245
  const indexPath = join6(dir, "index.json");
2195
2246
  if (!existsSync6(indexPath)) {
2196
- console.error(`[ProfileLoader] No index.json found at ${indexPath}, skipping profile loading`);
2247
+ warnNoProfiles(`No index.json at ${indexPath}`);
2197
2248
  return 0;
2198
2249
  }
2199
2250
  let index;
@@ -2218,9 +2269,26 @@ function loadProfiles(profilesDir) {
2218
2269
  console.error(`[ProfileLoader] Failed to load profile ${profileId} from ${profilePath}:`, err);
2219
2270
  }
2220
2271
  }
2221
- console.error(`[ProfileLoader] Loaded ${loaded} profile(s) from ${dir}`);
2272
+ if (loaded === 0) {
2273
+ warnNoProfiles(`index.json at ${indexPath} registered 0 profiles`);
2274
+ } else {
2275
+ console.error(`[ProfileLoader] Loaded ${loaded} profile(s) from ${dir}`);
2276
+ }
2222
2277
  return loaded;
2223
2278
  }
2279
+ function warnNoProfiles(reason) {
2280
+ console.error(
2281
+ `
2282
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
2283
+ \u2551 \u26A0 NO PROFILES LOADED \u2014 every gated action will be REJECTED with \u2551
2284
+ \u2551 "Unknown profile". This gateway is misconfigured. \u2551
2285
+ \u2551 Reason: ${reason}
2286
+ \u2551 Fix: install a build that bundles profiles, or set \u2551
2287
+ \u2551 SUVEREN_PROFILES_DIR to a hap-profiles checkout, then restart. \u2551
2288
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
2289
+ `
2290
+ );
2291
+ }
2224
2292
 
2225
2293
  // src/lib/manifest-loader.ts
2226
2294
  import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
@@ -2554,11 +2622,13 @@ app.all("/mcp", async (req, res) => {
2554
2622
  res.status(405).json({ error: "Method not allowed" });
2555
2623
  }
2556
2624
  });
2625
+ var profilesLoaded = 0;
2557
2626
  app.get("/health", (_req, res) => {
2558
2627
  res.json({
2559
2628
  status: "ok",
2560
2629
  transports: ["sse", "streamable-http"],
2561
2630
  sp: spUrl,
2631
+ profilesLoaded,
2562
2632
  activeSessions: activeSessions.size,
2563
2633
  storedGates: state.gateStore.getAll().length,
2564
2634
  serviceCredentials: Array.from(serviceCredentials.keys()),
@@ -2648,7 +2718,7 @@ app.listen(port, "0.0.0.0", () => {
2648
2718
  console.error(`[Suveren MCP] SSE: http://0.0.0.0:${port}/sse`);
2649
2719
  console.error(`[Suveren MCP] Streamable: http://0.0.0.0:${port}/mcp`);
2650
2720
  console.error(`[Suveren MCP] SP server: ${spUrl}`);
2651
- loadProfiles();
2721
+ profilesLoaded = loadProfiles();
2652
2722
  loadManifests();
2653
2723
  if (integrationRegistry.getEnabled().length === 0) {
2654
2724
  const personalManifests = getAllManifests().filter((m) => m.personalDefault);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suveren/gateway",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Suveren gateway — local agent gateway built in compliance with the Human Agency Protocol (HAP). Runs the UI, control plane, and MCP server in one Node process.",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -0,0 +1,209 @@
1
+ # HAP Authority Profiles
2
+
3
+ Authorization templates for the [Human Agency Protocol](https://humanagencyprotocol.org). Each profile defines what an AI agent is allowed to do within a specific domain — the bounds a human sets, and the Gatekeeper enforces.
4
+
5
+ > **Version 0.4** — March 2026
6
+
7
+ ---
8
+
9
+ ## What Profiles Define
10
+
11
+ A profile is a complete authorization schema. It specifies:
12
+
13
+ - **Bounds schema** — the limits a human commits to (e.g., max amount, allowed currencies)
14
+ - **Context schema** — local parameters that stay encrypted (e.g., allowed services, environment)
15
+ - **Execution context** — what the Gatekeeper checks at runtime, including cumulative limits
16
+ - **Execution paths** — governance tiers with required domain owners and TTLs
17
+ - **Gates** — the structured questions a human must answer before authorization
18
+
19
+ Profiles are referenced by ID (e.g., `charge@0.4`) and are immutable once published.
20
+
21
+ ---
22
+
23
+ ## Profiles
24
+
25
+ ### charge — Charge Authority
26
+
27
+ Governs charging customers: payments, refunds, subscriptions.
28
+
29
+ | Bound | Type | Purpose |
30
+ |-------|------|---------|
31
+ | `amount_max` | per-transaction | Maximum monetary amount |
32
+ | `amount_daily_max` | cumulative | Daily charge cap |
33
+ | `amount_monthly_max` | cumulative | Monthly charge cap |
34
+ | `transaction_count_daily_max` | cumulative | Daily transaction limit |
35
+
36
+ | Context | Type | Purpose |
37
+ |---------|------|---------|
38
+ | `currency` | enum | Permitted currencies |
39
+ | `action_type` | enum | charge, refund, subscribe |
40
+
41
+ | Path | Default TTL |
42
+ |------|-------------|
43
+ | `charge-routine` | 24h |
44
+ | `charge-reviewed` | 4h |
45
+
46
+ ---
47
+
48
+ ### purchase — Purchase Authority
49
+
50
+ Governs spending company money: subscriptions, supplies, services, advertising.
51
+
52
+ | Bound | Type | Purpose |
53
+ |-------|------|---------|
54
+ | `spend_max` | per-transaction | Maximum spend amount |
55
+ | `spend_daily_max` | cumulative | Daily spend cap |
56
+ | `spend_monthly_max` | cumulative | Monthly spend cap |
57
+ | `transaction_count_daily_max` | cumulative | Daily transaction limit |
58
+
59
+ | Context | Type | Purpose |
60
+ |---------|------|---------|
61
+ | `currency` | enum | Permitted currencies |
62
+ | `category` | enum | subscription, supply, service, advertising |
63
+ | `allowed_vendors` | subset | Approved vendor names |
64
+
65
+ | Path | Default TTL |
66
+ |------|-------------|
67
+ | `purchase-routine` | 24h |
68
+ | `purchase-reviewed` | 4h |
69
+
70
+ ---
71
+
72
+ ### email — Email Authority
73
+
74
+ Governs sending, drafting, and reading email via Gmail.
75
+
76
+ | Bound | Type | Purpose |
77
+ |-------|------|---------|
78
+ | `recipient_max` | per-email | Maximum recipients |
79
+ | `send_daily_max` | cumulative | Daily send/draft limit |
80
+ | `read_max_age_days` | per-query | Max email age for search |
81
+ | `read_daily_max` | cumulative | Daily read limit |
82
+
83
+ | Context | Type | Purpose |
84
+ |---------|------|---------|
85
+ | `allowed_recipients` | subset | Permitted email addresses |
86
+ | `allowed_domains` | subset | Permitted recipient domains |
87
+
88
+ | Path | Default TTL |
89
+ |------|-------------|
90
+ | `email-draft` | 24h |
91
+ | `email-send` | 4h |
92
+ | `email-read` | 24h |
93
+
94
+ ---
95
+
96
+ ### customers — Customer Management
97
+
98
+ Governs CRM operations: contacts, activities, deals, tasks.
99
+
100
+ | Bound | Type | Purpose |
101
+ |-------|------|---------|
102
+ | `write_daily_max` | cumulative | Daily write operations |
103
+
104
+ | Context | Type | Purpose |
105
+ |---------|------|---------|
106
+ | `contact_type` | subset | customer, lead, partner, vendor |
107
+
108
+ | Path | Default TTL |
109
+ |------|-------------|
110
+ | `customers-read` | 24h |
111
+ | `customers-write` | 8h |
112
+ | `customers-delete` | 2h |
113
+
114
+ ---
115
+
116
+ ### schedule — Scheduling Authority
117
+
118
+ Governs calendar access: reading, drafting, and booking events.
119
+
120
+ | Bound | Type | Purpose |
121
+ |-------|------|---------|
122
+ | `booking_daily_max` | cumulative | Daily new bookings |
123
+ | `booking_duration_max` | per-event | Max event duration (minutes) |
124
+ | `lookahead_days_max` | per-event | Max days into the future |
125
+
126
+ | Context | Type | Purpose |
127
+ |---------|------|---------|
128
+ | `allowed_calendars` | subset | Permitted calendar names |
129
+ | `allowed_attendees` | subset | Permitted attendee emails |
130
+ | `allowed_domains` | subset | Permitted attendee domains |
131
+
132
+ | Path | Default TTL |
133
+ |------|-------------|
134
+ | `schedule-read` | 24h |
135
+ | `schedule-draft` | 24h |
136
+ | `schedule-book` | 4h |
137
+
138
+ ---
139
+
140
+ ### publish — Content Publishing
141
+
142
+ Governs posting public content to social media, blogs, and other platforms.
143
+
144
+ | Bound | Type | Purpose |
145
+ |-------|------|---------|
146
+ | `post_daily_max` | cumulative | Daily post limit |
147
+ | `post_monthly_max` | cumulative | Monthly post limit |
148
+
149
+ | Context | Type | Purpose |
150
+ |---------|------|---------|
151
+ | `allowed_platforms` | subset | twitter, linkedin, instagram, blog, medium |
152
+ | `content_type` | enum | text, image, link, thread |
153
+ | `audience` | enum | public, followers, connections |
154
+
155
+ | Path | Default TTL |
156
+ |------|-------------|
157
+ | `publish-draft` | 24h |
158
+ | `publish-post` | 4h |
159
+
160
+ ---
161
+
162
+ ### records — Records Authority
163
+
164
+ Governs accessing and modifying personal structured data: queries, schema changes, exports. Renamed from `data` — for personal databases and spreadsheet replacements, not shared CRM data.
165
+
166
+ | Bound | Type | Purpose |
167
+ |-------|------|---------|
168
+ | `row_limit_max` | per-query | Maximum rows returned |
169
+ | `query_count_daily_max` | cumulative | Daily query limit |
170
+ | `export_row_count_daily_max` | cumulative | Daily exported row limit |
171
+
172
+ | Context | Type | Purpose |
173
+ |---------|------|---------|
174
+ | `access_level` | enum | read, write, admin |
175
+ | `data_scope` | enum | public, internal, pii |
176
+ | `scope` | enum | external (production), internal (sandbox) |
177
+
178
+ | Path | Default TTL |
179
+ |------|-------------|
180
+ | `records-read` | 8h |
181
+ | `records-write` | 2h |
182
+ | `records-export` | 1h |
183
+
184
+ ---
185
+
186
+ ## How Profiles Work
187
+
188
+ A human creates an authorization by selecting a profile and execution path, setting the bounds, and answering the gate questions. Domain owners cryptographically attest to the bounds. The Gatekeeper then enforces those bounds on every tool call:
189
+
190
+ ```
191
+ Human sets bounds Agent requests execution Gatekeeper checks
192
+ amount_max: 80 EUR -> amount: 5 EUR -> 5 <= 80, approved
193
+ action_type: [charge] action_type: "charge" "charge" in [charge], approved
194
+ amount_daily_max: 500 amount_daily: 423 (from log) 423 + 5 <= 500, approved
195
+ ```
196
+
197
+ If any bound is exceeded, the Gatekeeper blocks execution.
198
+
199
+ All profiles require the same six gates: bounds, problem, objective, tradeoff, commitment, and decision owner.
200
+
201
+ ---
202
+
203
+ ## Community Profiles
204
+
205
+ Anyone can create and publish profiles through the [HAP Service Provider](https://humanagencyprotocol.com). Published profiles are immutable and versioned. There is no approval process — trust decisions are local.
206
+
207
+ ---
208
+
209
+ See [humanagencyprotocol.org](https://humanagencyprotocol.org) for the full protocol specification.
@@ -0,0 +1,183 @@
1
+ {
2
+ "id": "github.com/humanagencyprotocol/hap-profiles/calendar@0.4",
3
+ "name": "Calendar",
4
+ "version": "0.4",
5
+ "description": "Calendar authority \u2014 governs calendar access: reading, drafting, and booking events",
6
+ "boundsSchema": {
7
+ "keyOrder": [
8
+ "profile",
9
+ "booking_daily_max",
10
+ "booking_duration_max",
11
+ "lookahead_days_max"
12
+ ],
13
+ "fields": {
14
+ "profile": {
15
+ "type": "string",
16
+ "required": true
17
+ },
18
+ "booking_daily_max": {
19
+ "type": "number",
20
+ "required": true,
21
+ "displayName": "Daily booking limit",
22
+ "description": "Maximum new events booked per day",
23
+ "unit": "count",
24
+ "boundType": {
25
+ "kind": "cumulative_count",
26
+ "window": "daily"
27
+ }
28
+ },
29
+ "booking_duration_max": {
30
+ "type": "number",
31
+ "required": true,
32
+ "displayName": "Max event duration",
33
+ "description": "Maximum duration of a single event",
34
+ "unit": "minutes",
35
+ "boundType": {
36
+ "kind": "per_transaction",
37
+ "of": "duration"
38
+ }
39
+ },
40
+ "lookahead_days_max": {
41
+ "type": "number",
42
+ "required": true,
43
+ "displayName": "Max lookahead",
44
+ "description": "Maximum number of days into the future an event can be booked",
45
+ "unit": "days",
46
+ "boundType": {
47
+ "kind": "per_transaction",
48
+ "of": "lookahead"
49
+ }
50
+ }
51
+ }
52
+ },
53
+ "contextSchema": {
54
+ "keyOrder": [
55
+ "allowed_calendars",
56
+ "allowed_attendees",
57
+ "allowed_domains"
58
+ ],
59
+ "fields": {
60
+ "allowed_calendars": {
61
+ "type": "string",
62
+ "required": false,
63
+ "displayName": "Allowed calendars",
64
+ "description": "Comma-separated list of calendar names the agent may access",
65
+ "constraint": {
66
+ "type": "string",
67
+ "enforceable": [
68
+ "subset"
69
+ ]
70
+ }
71
+ },
72
+ "allowed_attendees": {
73
+ "type": "string",
74
+ "required": false,
75
+ "displayName": "Allowed attendees",
76
+ "format": "email",
77
+ "description": "Comma-separated list of allowed attendee email addresses",
78
+ "constraint": {
79
+ "type": "string",
80
+ "enforceable": [
81
+ "subset"
82
+ ]
83
+ }
84
+ },
85
+ "allowed_domains": {
86
+ "type": "string",
87
+ "required": false,
88
+ "displayName": "Allowed domains",
89
+ "format": "domain",
90
+ "description": "Comma-separated list of allowed attendee domains (e.g. acme.com,partner.org)",
91
+ "constraint": {
92
+ "type": "string",
93
+ "enforceable": [
94
+ "subset"
95
+ ]
96
+ }
97
+ }
98
+ }
99
+ },
100
+ "executionContextSchema": {
101
+ "fields": {
102
+ "duration": {
103
+ "source": "declared",
104
+ "description": "Duration of the event in minutes",
105
+ "required": true,
106
+ "constraint": {
107
+ "type": "number",
108
+ "enforceable": [
109
+ "max"
110
+ ]
111
+ }
112
+ },
113
+ "lookahead": {
114
+ "source": "declared",
115
+ "description": "Days from now the event is scheduled",
116
+ "required": true,
117
+ "constraint": {
118
+ "type": "number",
119
+ "enforceable": [
120
+ "max"
121
+ ]
122
+ }
123
+ },
124
+ "allowed_calendars": {
125
+ "source": "declared",
126
+ "description": "Calendar being used (checked against authorized calendars)",
127
+ "required": false,
128
+ "constraint": {
129
+ "type": "string",
130
+ "enforceable": [
131
+ "subset"
132
+ ]
133
+ }
134
+ },
135
+ "allowed_attendees": {
136
+ "source": "declared",
137
+ "description": "Attendee addresses from this event (checked against authorized attendees)",
138
+ "required": false,
139
+ "constraint": {
140
+ "type": "string",
141
+ "enforceable": [
142
+ "subset"
143
+ ]
144
+ }
145
+ },
146
+ "allowed_domains": {
147
+ "source": "declared",
148
+ "description": "Unique attendee domains from this event (checked against authorized domains)",
149
+ "required": false,
150
+ "constraint": {
151
+ "type": "string",
152
+ "enforceable": [
153
+ "subset"
154
+ ]
155
+ }
156
+ },
157
+ "booking_count_daily": {
158
+ "source": "cumulative",
159
+ "cumulativeField": "_count",
160
+ "window": "daily",
161
+ "description": "Running daily booking count (resolved from execution log)",
162
+ "required": true,
163
+ "constraint": {
164
+ "type": "number",
165
+ "enforceable": [
166
+ "max"
167
+ ]
168
+ }
169
+ }
170
+ }
171
+ },
172
+ "requiredGates": [
173
+ "bounds",
174
+ "intent",
175
+ "commitment",
176
+ "decision_owner"
177
+ ],
178
+ "ttl": {
179
+ "default": 86400,
180
+ "max": 31536000
181
+ },
182
+ "retention_minimum": 7776000
183
+ }