@suveren/gateway 0.25.0 → 0.26.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.
- package/bin/suveren-gateway.js +10 -1
- package/content/guides/3-mandates.md +1 -1
- package/dist/control-plane/index.mjs +368 -247
- package/dist/mcp-server/{chunk-APIQIEUA.mjs → chunk-PU4VQEEA.mjs} +1 -0
- package/dist/mcp-server/http.mjs +478 -137
- package/dist/mcp-server/report-verify-cli.mjs +1 -1
- package/dist/ui/assets/index-46UkxmPE.css +1 -0
- package/dist/ui/assets/index-BC5hwcsC.js +120 -0
- package/dist/ui/index.html +2 -2
- package/lib/policy.d.mts +3 -1
- package/lib/policy.mjs +28 -2
- package/node_modules/ip-address/README.md +70 -70
- package/node_modules/ip-address/dist/ipv6.js +7 -4
- package/node_modules/ip-address/dist/ipv6.js.map +1 -1
- package/node_modules/ip-address/dist/v6/constants.d.ts +3 -0
- package/node_modules/ip-address/dist/v6/constants.js +3 -0
- package/node_modules/ip-address/dist/v6/constants.js.map +1 -1
- package/node_modules/ip-address/package.json +2 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-C604QQYJ.css +0 -1
- package/dist/ui/assets/index-D6Bhqe3_.js +0 -120
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
computeMetric,
|
|
16
16
|
fingerprintOf,
|
|
17
17
|
formatDateTime,
|
|
18
|
+
getIdentityDirectory,
|
|
18
19
|
glossaryUsage,
|
|
19
20
|
issuerFromPublicKeyHex,
|
|
20
21
|
mandateFields,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
sanitizeReport,
|
|
32
33
|
suggestedFilename,
|
|
33
34
|
ticketFields
|
|
34
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-PU4VQEEA.mjs";
|
|
35
36
|
|
|
36
37
|
// bin/http.ts
|
|
37
38
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -153,7 +154,7 @@ import { dirname, join as join3 } from "path";
|
|
|
153
154
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
154
155
|
import { join as join2 } from "path";
|
|
155
156
|
import { spawnSync } from "child_process";
|
|
156
|
-
var POLICY_KEYS = ["AsUrl", "CaFile", "PinTls", "Proxy", "NoProxy", "Simulation", "InstallMethod"];
|
|
157
|
+
var POLICY_KEYS = ["AsUrl", "CaFile", "PinTls", "Proxy", "NoProxy", "Simulation", "InstallMethod", "Team"];
|
|
157
158
|
var DEFAULT_REGISTRY_BASE_KEY = "SOFTWARE\\Policies\\Suveren\\Gateway";
|
|
158
159
|
function registryKeyPath(hive) {
|
|
159
160
|
const baseKey = process.env.SUVEREN_POLICY_REGISTRY_KEY || DEFAULT_REGISTRY_BASE_KEY;
|
|
@@ -189,6 +190,11 @@ function validateCaFile(candidate) {
|
|
|
189
190
|
if (!existsSync2(trimmed)) return { ok: false, error: `No file at "${trimmed}".` };
|
|
190
191
|
return { ok: true, path: trimmed };
|
|
191
192
|
}
|
|
193
|
+
function validateTeamId(candidate) {
|
|
194
|
+
const trimmed = candidate.trim();
|
|
195
|
+
if (!trimmed) return { ok: false, error: "The team id is empty." };
|
|
196
|
+
return { ok: true, id: trimmed };
|
|
197
|
+
}
|
|
192
198
|
function validateProxyUrl(candidate) {
|
|
193
199
|
const trimmed = candidate.trim();
|
|
194
200
|
if (!trimmed) return { ok: false, error: "The proxy URL is empty." };
|
|
@@ -362,6 +368,14 @@ function readPolicy() {
|
|
|
362
368
|
policy.installMethod = "managed";
|
|
363
369
|
locked.add("installMethod");
|
|
364
370
|
}
|
|
371
|
+
if (merged.Team !== void 0) {
|
|
372
|
+
const v = validateTeamId(String(merged.Team));
|
|
373
|
+
if (!v.ok) {
|
|
374
|
+
throw new Error(`Invalid policy Team ("${String(merged.Team)}") from ${sourceOf.Team}: ${v.error}`);
|
|
375
|
+
}
|
|
376
|
+
policy.team = v.id;
|
|
377
|
+
locked.add("team");
|
|
378
|
+
}
|
|
365
379
|
cached = { policy, locked };
|
|
366
380
|
return cached;
|
|
367
381
|
}
|
|
@@ -860,6 +874,32 @@ var SPClient = class {
|
|
|
860
874
|
if (!res.ok) throw new Error(`SP compat request failed: ${res.status}`);
|
|
861
875
|
return res.json();
|
|
862
876
|
}
|
|
877
|
+
/**
|
|
878
|
+
* WS3 defence-in-depth (work-plan "Added 2026-10-11 — an account is
|
|
879
|
+
* Personal OR in one team"): the account's active TEAM id, or null when
|
|
880
|
+
* it currently has none (Personal mode) — `GET /api/groups/me`. Used only
|
|
881
|
+
* by AttestationCache.refreshActiveWorkspace, never on the hot tool-call
|
|
882
|
+
* path (it makes a live AS call).
|
|
883
|
+
*/
|
|
884
|
+
async getActiveTeamGroupId() {
|
|
885
|
+
const res = await this.fetch("/api/groups/me");
|
|
886
|
+
if (!res.ok) throw new Error(`SP groups/me request failed: ${res.status}`);
|
|
887
|
+
const data = await res.json();
|
|
888
|
+
return data.group?.id ?? null;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* The account's own Personal group id — `GET /api/groups`, the entry
|
|
892
|
+
* with `isPersonal`. Same caller/purpose as {@link getActiveTeamGroupId}:
|
|
893
|
+
* together they resolve "the active workspace" (the active team if any,
|
|
894
|
+
* else Personal) without the gateway ever guessing at an id shape.
|
|
895
|
+
*/
|
|
896
|
+
async getPersonalGroupId() {
|
|
897
|
+
const res = await this.fetch("/api/groups");
|
|
898
|
+
if (!res.ok) throw new Error(`SP groups request failed: ${res.status}`);
|
|
899
|
+
const data = await res.json();
|
|
900
|
+
const personal = (data.groups ?? []).find((g) => g.isPersonal);
|
|
901
|
+
return personal?.id ?? null;
|
|
902
|
+
}
|
|
863
903
|
/**
|
|
864
904
|
* Get all mandates for an authorization id.
|
|
865
905
|
*
|
|
@@ -987,6 +1027,19 @@ var SPClient = class {
|
|
|
987
1027
|
const data = await res.json();
|
|
988
1028
|
return data.proposals;
|
|
989
1029
|
}
|
|
1030
|
+
/**
|
|
1031
|
+
* RJ4 — the same proposals a human just rejected, sent back for a change,
|
|
1032
|
+
* or replaced with their own mandate (all three outcomes live under
|
|
1033
|
+
* `status: 'rejected'`, see `ProposalOutcome`). Polled on the SAME tick as
|
|
1034
|
+
* {@link getCommittedProposals} (bin/http.ts) so the outcome outbox learns
|
|
1035
|
+
* about them without a second AS polling loop.
|
|
1036
|
+
*/
|
|
1037
|
+
async getRejectedProposals() {
|
|
1038
|
+
const res = await this.fetch("/api/proposals?status=rejected");
|
|
1039
|
+
if (!res.ok) throw new Error(`SP rejected proposals request failed: ${res.status}`);
|
|
1040
|
+
const data = await res.json();
|
|
1041
|
+
return data.proposals;
|
|
1042
|
+
}
|
|
990
1043
|
/**
|
|
991
1044
|
* Look up ONE proposal by id, whatever its status. `null` when the AS has no
|
|
992
1045
|
* such proposal (or the caller may not see it).
|
|
@@ -1052,6 +1105,48 @@ var AttestationCache = class {
|
|
|
1052
1105
|
* this fact to begin with.
|
|
1053
1106
|
*/
|
|
1054
1107
|
reapprovalNeeded = /* @__PURE__ */ new Set();
|
|
1108
|
+
/**
|
|
1109
|
+
* WS3 defence-in-depth (work-plan "Added 2026-10-11 — an account is
|
|
1110
|
+
* Personal OR in one team"): the account's current active workspace's
|
|
1111
|
+
* group id — its active team, else its Personal group. `null` = not yet
|
|
1112
|
+
* known (before the first {@link refreshActiveWorkspace}, or every
|
|
1113
|
+
* lookup it tried failed) — filtering is then SKIPPED rather than
|
|
1114
|
+
* refusing everything, because the Authority Server itself is the
|
|
1115
|
+
* fail-closed authority at ticket time (decision 6, WS2); this is a
|
|
1116
|
+
* second, narrower check for a window a buggy/stale cache entry could
|
|
1117
|
+
* otherwise slip through in, not a replacement for that enforcement.
|
|
1118
|
+
*/
|
|
1119
|
+
activeWorkspaceGroupId = null;
|
|
1120
|
+
/**
|
|
1121
|
+
* Re-resolve {@link activeWorkspaceGroupId} from the Authority Server.
|
|
1122
|
+
* Called by the resync-gates flow (http.ts's `/internal/resync-gates`),
|
|
1123
|
+
* never on the hot tool-call path — it makes up to two AS calls. Leaves
|
|
1124
|
+
* the previous value in place on any failure (network error, unexpected
|
|
1125
|
+
* shape): losing a known-good value on a transient blip would turn this
|
|
1126
|
+
* defence-in-depth check into a false refusal of every cached mandate,
|
|
1127
|
+
* which is worse than leaving it momentarily stale.
|
|
1128
|
+
*/
|
|
1129
|
+
async refreshActiveWorkspace() {
|
|
1130
|
+
try {
|
|
1131
|
+
const activeTeam = await this.spClient.getActiveTeamGroupId();
|
|
1132
|
+
if (activeTeam) {
|
|
1133
|
+
this.activeWorkspaceGroupId = activeTeam;
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const personalId = await this.spClient.getPersonalGroupId();
|
|
1137
|
+
if (personalId) this.activeWorkspaceGroupId = personalId;
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* True only when `groupId` is KNOWN (not undefined) and the active
|
|
1143
|
+
* workspace is ALSO known (not null) and they differ. Either side
|
|
1144
|
+
* unknown ⇒ false: this check only ever narrows what gets selected, it
|
|
1145
|
+
* never substitutes for the Authority Server's own enforcement.
|
|
1146
|
+
*/
|
|
1147
|
+
isForeignWorkspace(groupId) {
|
|
1148
|
+
return groupId !== void 0 && this.activeWorkspaceGroupId !== null && groupId !== this.activeWorkspaceGroupId;
|
|
1149
|
+
}
|
|
1055
1150
|
/**
|
|
1056
1151
|
* Get the SP public key, fetching from SP if not cached or expired, then —
|
|
1057
1152
|
* when a pin exists for this exact AS URL — verifying the live key still
|
|
@@ -1100,6 +1195,10 @@ var AttestationCache = class {
|
|
|
1100
1195
|
getAuthorization(path) {
|
|
1101
1196
|
const auth = this.authorizations.get(path);
|
|
1102
1197
|
if (!auth) return null;
|
|
1198
|
+
if (this.isForeignWorkspace(auth.groupId)) {
|
|
1199
|
+
this.authorizations.delete(path);
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1103
1202
|
const now = Math.floor(Date.now() / 1e3);
|
|
1104
1203
|
const hasValid = auth.attestations.some((a) => a.expiresAt > now);
|
|
1105
1204
|
if (!hasValid) {
|
|
@@ -1124,6 +1223,13 @@ var AttestationCache = class {
|
|
|
1124
1223
|
);
|
|
1125
1224
|
}
|
|
1126
1225
|
const bounds = result.bounds ?? result.frame ?? {};
|
|
1226
|
+
let groupId;
|
|
1227
|
+
try {
|
|
1228
|
+
const summary = await this.spClient.getAuthorizationSummary(authorizationId);
|
|
1229
|
+
groupId = summary?.group_id ?? void 0;
|
|
1230
|
+
} catch {
|
|
1231
|
+
groupId = void 0;
|
|
1232
|
+
}
|
|
1127
1233
|
let signedCommitmentMode;
|
|
1128
1234
|
let subjects;
|
|
1129
1235
|
const firstBlob = result.attestations[0]?.blob;
|
|
@@ -1158,7 +1264,8 @@ var AttestationCache = class {
|
|
|
1158
1264
|
deferredCommitmentDomains: result.deferred_commitment_domains ?? [],
|
|
1159
1265
|
signedCommitmentMode,
|
|
1160
1266
|
subjects,
|
|
1161
|
-
complete: result.complete
|
|
1267
|
+
complete: result.complete,
|
|
1268
|
+
groupId
|
|
1162
1269
|
};
|
|
1163
1270
|
this.authorizations.set(auth.authorizationId, auth);
|
|
1164
1271
|
return auth;
|
|
@@ -1170,6 +1277,10 @@ var AttestationCache = class {
|
|
|
1170
1277
|
const now = Math.floor(Date.now() / 1e3);
|
|
1171
1278
|
const results = [];
|
|
1172
1279
|
for (const [path, auth] of this.authorizations) {
|
|
1280
|
+
if (this.isForeignWorkspace(auth.groupId)) {
|
|
1281
|
+
this.authorizations.delete(path);
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1173
1284
|
const hasValid = auth.attestations.some((a) => a.expiresAt > now);
|
|
1174
1285
|
if (hasValid) {
|
|
1175
1286
|
results.push(auth);
|
|
@@ -2132,6 +2243,163 @@ var PreviewLog = class {
|
|
|
2132
2243
|
}
|
|
2133
2244
|
};
|
|
2134
2245
|
|
|
2246
|
+
// src/lib/outcome-outbox.ts
|
|
2247
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, unlinkSync as unlinkSync5 } from "fs";
|
|
2248
|
+
import { homedir as homedir8 } from "os";
|
|
2249
|
+
import { dirname as dirname9, join as join11 } from "path";
|
|
2250
|
+
import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
|
|
2251
|
+
var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
|
|
2252
|
+
var MAX_AGE_MS3 = 7 * 24 * 60 * 60 * 1e3;
|
|
2253
|
+
function outcomeEntryFor(proposal, submittedHere, who, nowMs) {
|
|
2254
|
+
if (!submittedHere) return null;
|
|
2255
|
+
const by = proposal.approverRejectedBy ?? proposal.rejectedBy;
|
|
2256
|
+
if (!by || typeof by.at !== "number") return null;
|
|
2257
|
+
const decidedAt = by.at * 1e3;
|
|
2258
|
+
if (nowMs - decidedAt > MAX_AGE_MS3) return null;
|
|
2259
|
+
return { proposalId: proposal.id, outcome: by.outcome ?? "rejected", note: by.note, who, decidedAt };
|
|
2260
|
+
}
|
|
2261
|
+
var OutcomeOutbox = class {
|
|
2262
|
+
entries = [];
|
|
2263
|
+
baseDir;
|
|
2264
|
+
vaultKey = null;
|
|
2265
|
+
constructor(filePath) {
|
|
2266
|
+
this.baseDir = filePath && filePath.endsWith(".json") ? dirname9(filePath) : filePath ?? DEFAULT_DIR8;
|
|
2267
|
+
this.loadPlaintext();
|
|
2268
|
+
}
|
|
2269
|
+
/** Mirrors DenialLog.setVaultKey — migrates plaintext -> encrypted once, in place. */
|
|
2270
|
+
setVaultKey(key) {
|
|
2271
|
+
this.vaultKey = key;
|
|
2272
|
+
if (existsSync11(this.encryptedFilePath)) {
|
|
2273
|
+
this.loadEncrypted();
|
|
2274
|
+
} else if (this.entries.length > 0) {
|
|
2275
|
+
this.persistEncrypted();
|
|
2276
|
+
if (existsSync11(this.plaintextFilePath)) {
|
|
2277
|
+
try {
|
|
2278
|
+
unlinkSync5(this.plaintextFilePath);
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Record a resolved outcome, UNLESS this proposal is already known here
|
|
2286
|
+
* (delivered or not) — the poller rechecks the AS's rejected list on
|
|
2287
|
+
* every tick, and a proposal stays in that list well past the moment this
|
|
2288
|
+
* gateway first learned about it, so without this guard the same outcome
|
|
2289
|
+
* would be queued again right after it was delivered.
|
|
2290
|
+
*/
|
|
2291
|
+
recordIfNew(entry) {
|
|
2292
|
+
if (this.entries.some((e) => e.proposalId === entry.proposalId)) return;
|
|
2293
|
+
this.entries.push({ ...entry });
|
|
2294
|
+
this.prune(Date.now());
|
|
2295
|
+
this.persist();
|
|
2296
|
+
}
|
|
2297
|
+
/** Undelivered entries, oldest first — what the next tool reply should carry. */
|
|
2298
|
+
getUndelivered() {
|
|
2299
|
+
return this.entries.filter((e) => e.deliveredAt === void 0).sort((a, b) => a.decidedAt - b.decidedAt);
|
|
2300
|
+
}
|
|
2301
|
+
/** Mark these proposals' entries delivered (idempotent). Always persists from disk state. */
|
|
2302
|
+
markDelivered(proposalIds) {
|
|
2303
|
+
if (proposalIds.length === 0) return;
|
|
2304
|
+
const ids = new Set(proposalIds);
|
|
2305
|
+
const now = Date.now();
|
|
2306
|
+
let changed = false;
|
|
2307
|
+
for (const e of this.entries) {
|
|
2308
|
+
if (ids.has(e.proposalId) && e.deliveredAt === void 0) {
|
|
2309
|
+
e.deliveredAt = now;
|
|
2310
|
+
changed = true;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (changed) this.persist();
|
|
2314
|
+
}
|
|
2315
|
+
/** Diagnostic / tests. */
|
|
2316
|
+
get size() {
|
|
2317
|
+
return this.entries.length;
|
|
2318
|
+
}
|
|
2319
|
+
// ─── Retention ────────────────────────────────────────────────────────────
|
|
2320
|
+
prune(now) {
|
|
2321
|
+
const cutoff = now - MAX_AGE_MS3;
|
|
2322
|
+
this.entries = this.entries.filter((e) => e.decidedAt >= cutoff);
|
|
2323
|
+
}
|
|
2324
|
+
// ─── Encryption (mirrors DenialLog / ExecutionLog) ─────────────────────────
|
|
2325
|
+
encrypt(plaintext) {
|
|
2326
|
+
if (!this.vaultKey) throw new Error("No vault key");
|
|
2327
|
+
const iv = randomBytes5(12);
|
|
2328
|
+
const cipher = createCipheriv5("aes-256-gcm", this.vaultKey, iv);
|
|
2329
|
+
const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2330
|
+
return { iv: iv.toString("hex"), ciphertext: enc.toString("hex"), tag: cipher.getAuthTag().toString("hex") };
|
|
2331
|
+
}
|
|
2332
|
+
decrypt(blob) {
|
|
2333
|
+
if (!this.vaultKey) throw new Error("No vault key");
|
|
2334
|
+
const decipher = createDecipheriv5("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
|
|
2335
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
2336
|
+
return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
|
|
2337
|
+
}
|
|
2338
|
+
get plaintextFilePath() {
|
|
2339
|
+
return join11(this.baseDir, "outcome-outbox.json");
|
|
2340
|
+
}
|
|
2341
|
+
get encryptedFilePath() {
|
|
2342
|
+
return join11(this.baseDir, "outcome-outbox.enc.json");
|
|
2343
|
+
}
|
|
2344
|
+
loadPlaintext() {
|
|
2345
|
+
if (!existsSync11(this.plaintextFilePath)) {
|
|
2346
|
+
mkdirSync9(this.baseDir, { recursive: true });
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
try {
|
|
2350
|
+
const data = JSON.parse(readFileSync12(this.plaintextFilePath, "utf-8"));
|
|
2351
|
+
this.entries = data.entries ?? [];
|
|
2352
|
+
this.prune(Date.now());
|
|
2353
|
+
} catch {
|
|
2354
|
+
console.error(`[OutcomeOutbox] Could not parse ${this.plaintextFilePath}, starting fresh`);
|
|
2355
|
+
this.entries = [];
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
loadEncrypted() {
|
|
2359
|
+
if (!existsSync11(this.encryptedFilePath)) return;
|
|
2360
|
+
try {
|
|
2361
|
+
const data = JSON.parse(readFileSync12(this.encryptedFilePath, "utf-8"));
|
|
2362
|
+
const storeFile = JSON.parse(this.decrypt(data.blob));
|
|
2363
|
+
this.entries = storeFile.entries ?? [];
|
|
2364
|
+
this.prune(Date.now());
|
|
2365
|
+
} catch (err) {
|
|
2366
|
+
console.error(`[OutcomeOutbox] Could not decrypt ${this.encryptedFilePath}:`, err);
|
|
2367
|
+
this.entries = [];
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
persist() {
|
|
2371
|
+
if (this.vaultKey) this.persistEncrypted();
|
|
2372
|
+
else this.persistPlaintext();
|
|
2373
|
+
}
|
|
2374
|
+
persistPlaintext() {
|
|
2375
|
+
const data = { version: 1, entries: this.entries };
|
|
2376
|
+
mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
|
|
2377
|
+
writeFileSync9(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2378
|
+
}
|
|
2379
|
+
persistEncrypted() {
|
|
2380
|
+
const storeFile = { version: 1, entries: this.entries };
|
|
2381
|
+
const data = { version: 1, blob: this.encrypt(JSON.stringify(storeFile)) };
|
|
2382
|
+
mkdirSync9(this.baseDir, { recursive: true, mode: 448 });
|
|
2383
|
+
writeFileSync9(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
function formatOutcomeLine(e) {
|
|
2387
|
+
const what = e.outcome === "change_needed" ? `SENT BACK FOR A CHANGE by ${e.who}: "${e.note ?? ""}"` : e.outcome === "replaced" ? `REPLACED by the person's own mandate` : `REJECTED by ${e.who}`;
|
|
2388
|
+
return `[Suveren gateway] Since your last call: proposal ${e.proposalId} was ${what}.`;
|
|
2389
|
+
}
|
|
2390
|
+
function wrapWithOutcomeOutbox(outbox, handler) {
|
|
2391
|
+
return async (...args) => {
|
|
2392
|
+
const result = await handler(...args);
|
|
2393
|
+
if (!outbox) return result;
|
|
2394
|
+
const pending = outbox.getUndelivered();
|
|
2395
|
+
if (pending.length === 0) return result;
|
|
2396
|
+
if (!result || !Array.isArray(result.content)) return result;
|
|
2397
|
+
outbox.markDelivered(pending.map((e) => e.proposalId));
|
|
2398
|
+
const note = pending.map(formatOutcomeLine).join("\n");
|
|
2399
|
+
return { ...result, content: [...result.content, { type: "text", text: note }] };
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2135
2403
|
// src/lib/gatekeeper.ts
|
|
2136
2404
|
import { verify } from "@hap/core";
|
|
2137
2405
|
var MCPGatekeeper = class {
|
|
@@ -2224,10 +2492,10 @@ var MCPGatekeeper = class {
|
|
|
2224
2492
|
};
|
|
2225
2493
|
|
|
2226
2494
|
// src/lib/report/report-store.ts
|
|
2227
|
-
import { mkdirSync as
|
|
2228
|
-
import { homedir as
|
|
2229
|
-
import { dirname as
|
|
2230
|
-
import { createCipheriv as
|
|
2495
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync12, unlinkSync as unlinkSync6 } from "fs";
|
|
2496
|
+
import { homedir as homedir9 } from "os";
|
|
2497
|
+
import { dirname as dirname10, join as join12 } from "path";
|
|
2498
|
+
import { createCipheriv as createCipheriv6, createDecipheriv as createDecipheriv6, randomBytes as randomBytes6 } from "crypto";
|
|
2231
2499
|
|
|
2232
2500
|
// src/lib/report/record-resolvers.ts
|
|
2233
2501
|
function findById(rows, id) {
|
|
@@ -2535,29 +2803,29 @@ async function buildCoverage(caseElements, getExport, archive, referenced, windo
|
|
|
2535
2803
|
}
|
|
2536
2804
|
|
|
2537
2805
|
// src/lib/report/report-store.ts
|
|
2538
|
-
var
|
|
2806
|
+
var DEFAULT_DIR9 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
|
|
2539
2807
|
var ReportStore = class {
|
|
2540
2808
|
report = null;
|
|
2541
2809
|
baseDir;
|
|
2542
2810
|
vaultKey = null;
|
|
2543
2811
|
constructor(filePath) {
|
|
2544
2812
|
if (filePath && filePath.endsWith(".json")) {
|
|
2545
|
-
this.baseDir =
|
|
2813
|
+
this.baseDir = dirname10(filePath);
|
|
2546
2814
|
} else {
|
|
2547
|
-
this.baseDir = filePath ??
|
|
2815
|
+
this.baseDir = filePath ?? DEFAULT_DIR9;
|
|
2548
2816
|
}
|
|
2549
2817
|
this.loadPlaintext();
|
|
2550
2818
|
}
|
|
2551
2819
|
/** Set the vault key for encryption. Triggers migration from plaintext if needed. */
|
|
2552
2820
|
setVaultKey(key) {
|
|
2553
2821
|
this.vaultKey = key;
|
|
2554
|
-
if (
|
|
2822
|
+
if (existsSync12(this.encryptedFilePath)) {
|
|
2555
2823
|
this.loadEncrypted();
|
|
2556
2824
|
} else if (this.report !== null) {
|
|
2557
2825
|
this.persistEncrypted();
|
|
2558
|
-
if (
|
|
2826
|
+
if (existsSync12(this.plaintextFilePath)) {
|
|
2559
2827
|
try {
|
|
2560
|
-
|
|
2828
|
+
unlinkSync6(this.plaintextFilePath);
|
|
2561
2829
|
} catch {
|
|
2562
2830
|
}
|
|
2563
2831
|
}
|
|
@@ -2570,7 +2838,7 @@ var ReportStore = class {
|
|
|
2570
2838
|
* "the report exists but is locked".
|
|
2571
2839
|
*/
|
|
2572
2840
|
isLocked() {
|
|
2573
|
-
return this.vaultKey === null &&
|
|
2841
|
+
return this.vaultKey === null && existsSync12(this.encryptedFilePath);
|
|
2574
2842
|
}
|
|
2575
2843
|
/** The stored, already-verified report — or null if none has been saved
|
|
2576
2844
|
* (and the store is not locked; check `isLocked()` first). */
|
|
@@ -2604,49 +2872,49 @@ var ReportStore = class {
|
|
|
2604
2872
|
// ─── Encryption helpers ─────────────────────────────────────────────────
|
|
2605
2873
|
encrypt(plaintext) {
|
|
2606
2874
|
if (!this.vaultKey) throw new Error("No vault key");
|
|
2607
|
-
const iv =
|
|
2608
|
-
const cipher =
|
|
2875
|
+
const iv = randomBytes6(12);
|
|
2876
|
+
const cipher = createCipheriv6("aes-256-gcm", this.vaultKey, iv);
|
|
2609
2877
|
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2610
2878
|
const tag = cipher.getAuthTag();
|
|
2611
2879
|
return { iv: iv.toString("hex"), ciphertext: encrypted.toString("hex"), tag: tag.toString("hex") };
|
|
2612
2880
|
}
|
|
2613
2881
|
decrypt(blob) {
|
|
2614
2882
|
if (!this.vaultKey) throw new Error("No vault key");
|
|
2615
|
-
const decipher =
|
|
2883
|
+
const decipher = createDecipheriv6("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
|
|
2616
2884
|
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
2617
2885
|
const decrypted = Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]);
|
|
2618
2886
|
return decrypted.toString("utf8");
|
|
2619
2887
|
}
|
|
2620
2888
|
// ─── File paths ─────────────────────────────────────────────────────────
|
|
2621
2889
|
get plaintextFilePath() {
|
|
2622
|
-
return
|
|
2890
|
+
return join12(this.baseDir, "report.json");
|
|
2623
2891
|
}
|
|
2624
2892
|
get encryptedFilePath() {
|
|
2625
|
-
return
|
|
2893
|
+
return join12(this.baseDir, "report.enc.json");
|
|
2626
2894
|
}
|
|
2627
2895
|
// ─── Load / Persist ─────────────────────────────────────────────────────
|
|
2628
2896
|
loadPlaintext() {
|
|
2629
|
-
if (!
|
|
2630
|
-
|
|
2897
|
+
if (!existsSync12(this.plaintextFilePath)) {
|
|
2898
|
+
mkdirSync10(this.baseDir, { recursive: true });
|
|
2631
2899
|
return;
|
|
2632
2900
|
}
|
|
2633
2901
|
try {
|
|
2634
|
-
const raw =
|
|
2902
|
+
const raw = readFileSync13(this.plaintextFilePath, "utf-8");
|
|
2635
2903
|
const parsed = JSON.parse(raw);
|
|
2636
2904
|
this.report = parsed.report ?? null;
|
|
2637
2905
|
} catch {
|
|
2638
2906
|
console.error(`[ReportStore] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
|
|
2639
2907
|
try {
|
|
2640
|
-
|
|
2908
|
+
writeFileSync10(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync13(this.plaintextFilePath));
|
|
2641
2909
|
} catch {
|
|
2642
2910
|
}
|
|
2643
2911
|
this.report = null;
|
|
2644
2912
|
}
|
|
2645
2913
|
}
|
|
2646
2914
|
loadEncrypted() {
|
|
2647
|
-
if (!
|
|
2915
|
+
if (!existsSync12(this.encryptedFilePath)) return;
|
|
2648
2916
|
try {
|
|
2649
|
-
const raw =
|
|
2917
|
+
const raw = readFileSync13(this.encryptedFilePath, "utf-8");
|
|
2650
2918
|
const data = JSON.parse(raw);
|
|
2651
2919
|
const decrypted = this.decrypt(data.blob);
|
|
2652
2920
|
const parsed = JSON.parse(decrypted);
|
|
@@ -2654,7 +2922,7 @@ var ReportStore = class {
|
|
|
2654
2922
|
} catch (err) {
|
|
2655
2923
|
console.error(`[ReportStore] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
|
|
2656
2924
|
try {
|
|
2657
|
-
|
|
2925
|
+
writeFileSync10(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync13(this.encryptedFilePath));
|
|
2658
2926
|
} catch {
|
|
2659
2927
|
}
|
|
2660
2928
|
this.report = null;
|
|
@@ -2669,16 +2937,16 @@ var ReportStore = class {
|
|
|
2669
2937
|
}
|
|
2670
2938
|
persistPlaintext() {
|
|
2671
2939
|
const data = { version: 1, report: this.report };
|
|
2672
|
-
|
|
2673
|
-
|
|
2940
|
+
mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
|
|
2941
|
+
writeFileSync10(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2674
2942
|
}
|
|
2675
2943
|
persistEncrypted() {
|
|
2676
2944
|
const data = {
|
|
2677
2945
|
version: 1,
|
|
2678
2946
|
blob: this.encrypt(JSON.stringify({ version: 1, report: this.report }))
|
|
2679
2947
|
};
|
|
2680
|
-
|
|
2681
|
-
|
|
2948
|
+
mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
|
|
2949
|
+
writeFileSync10(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2682
2950
|
}
|
|
2683
2951
|
};
|
|
2684
2952
|
|
|
@@ -2701,6 +2969,9 @@ var SharedState = class {
|
|
|
2701
2969
|
proposalSubmissions;
|
|
2702
2970
|
/** AU3 — local, content-free record of preview reads. See preview-log.ts. */
|
|
2703
2971
|
previewLog;
|
|
2972
|
+
/** RJ4 — rejection/change-needed/replaced outcomes awaiting one-time
|
|
2973
|
+
* delivery to the AI on its next tool call. See outcome-outbox.ts. */
|
|
2974
|
+
outcomeOutbox;
|
|
2704
2975
|
gatekeeper;
|
|
2705
2976
|
/**
|
|
2706
2977
|
* V7 — set by {@link checkAsCompat} (called once at process start, and
|
|
@@ -2735,6 +3006,7 @@ var SharedState = class {
|
|
|
2735
3006
|
this.executionJournal = new ExecutionJournal(gateStorePath);
|
|
2736
3007
|
this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
|
|
2737
3008
|
this.previewLog = new PreviewLog(gateStorePath);
|
|
3009
|
+
this.outcomeOutbox = new OutcomeOutbox(gateStorePath);
|
|
2738
3010
|
this.gatekeeper = new MCPGatekeeper(this.cache);
|
|
2739
3011
|
}
|
|
2740
3012
|
/**
|
|
@@ -2923,29 +3195,29 @@ function formatConsumptionFull(entries) {
|
|
|
2923
3195
|
}
|
|
2924
3196
|
|
|
2925
3197
|
// src/lib/context-loader.ts
|
|
2926
|
-
import { readFileSync as
|
|
2927
|
-
import { homedir as
|
|
2928
|
-
import { join as
|
|
2929
|
-
var
|
|
3198
|
+
import { readFileSync as readFileSync14, existsSync as existsSync13, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11, renameSync as renameSync4 } from "fs";
|
|
3199
|
+
import { homedir as homedir10 } from "os";
|
|
3200
|
+
import { join as join13, dirname as dirname11 } from "path";
|
|
3201
|
+
var DEFAULT_DIR10 = process.env.SUVEREN_DATA_DIR ?? join13(homedir10(), ".suveren");
|
|
2930
3202
|
var CONTEXT_MAX_BYTES = 16 * 1024;
|
|
2931
3203
|
function contextFilePath(dataDir2) {
|
|
2932
|
-
return
|
|
3204
|
+
return join13(dataDir2 ?? DEFAULT_DIR10, "context.md");
|
|
2933
3205
|
}
|
|
2934
3206
|
function writeContextFile(content, dataDir2) {
|
|
2935
3207
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
2936
3208
|
if (bytes > CONTEXT_MAX_BYTES) throw new Error(`the brief is ${bytes} bytes; the limit is ${CONTEXT_MAX_BYTES}.`);
|
|
2937
3209
|
const filePath = contextFilePath(dataDir2);
|
|
2938
|
-
|
|
3210
|
+
mkdirSync11(dirname11(filePath), { recursive: true });
|
|
2939
3211
|
const tmpPath = `${filePath}.tmp`;
|
|
2940
|
-
|
|
3212
|
+
writeFileSync11(tmpPath, content, "utf-8");
|
|
2941
3213
|
renameSync4(tmpPath, filePath);
|
|
2942
3214
|
}
|
|
2943
3215
|
var BRIEF_MAX_CHARS = 16 * 1024;
|
|
2944
3216
|
function readContextFile(dataDir2) {
|
|
2945
3217
|
const filePath = contextFilePath(dataDir2);
|
|
2946
|
-
if (!
|
|
3218
|
+
if (!existsSync13(filePath)) return null;
|
|
2947
3219
|
try {
|
|
2948
|
-
const content =
|
|
3220
|
+
const content = readFileSync14(filePath, "utf-8").trim();
|
|
2949
3221
|
return content || null;
|
|
2950
3222
|
} catch {
|
|
2951
3223
|
return null;
|
|
@@ -3004,6 +3276,11 @@ function asUrlChangedNotice(what) {
|
|
|
3004
3276
|
|
|
3005
3277
|
TELL THE USER: open ${uiUrl()} and sign in again to pair with the new Authority Server.`;
|
|
3006
3278
|
}
|
|
3279
|
+
function managedWrongTeamNotice(what) {
|
|
3280
|
+
return `${what} Suveren gateway is LOCKED because it is set up by IT for one specific team, and the signed-in account is no longer in that team (it left, or was removed). This is NOT an ended sign-in \u2014 signing in again will be refused until the account is back in that team.
|
|
3281
|
+
|
|
3282
|
+
TELL THE USER: ask your IT administrator, or sign in with an account that is in the team this gateway is set up for, at ${uiUrl()}. Nothing ran while unsigned.`;
|
|
3283
|
+
}
|
|
3007
3284
|
function lockedNotice(action, reason = "restart") {
|
|
3008
3285
|
const what = action ? `Cannot ${action}: the` : "The";
|
|
3009
3286
|
switch (reason) {
|
|
@@ -3015,6 +3292,8 @@ function lockedNotice(action, reason = "restart") {
|
|
|
3015
3292
|
return asTlsMismatchNotice(what);
|
|
3016
3293
|
case "as-url-changed":
|
|
3017
3294
|
return asUrlChangedNotice(what);
|
|
3295
|
+
case "managed-wrong-team":
|
|
3296
|
+
return managedWrongTeamNotice(what);
|
|
3018
3297
|
default:
|
|
3019
3298
|
return bootLockedNotice(what);
|
|
3020
3299
|
}
|
|
@@ -3135,8 +3414,8 @@ function attachTicketId(tool, args, ticketId) {
|
|
|
3135
3414
|
import { computeContentHash as computeContentHash2 } from "@hap/core";
|
|
3136
3415
|
|
|
3137
3416
|
// src/lib/manifest-loader.ts
|
|
3138
|
-
import { readFileSync as
|
|
3139
|
-
import { join as
|
|
3417
|
+
import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
|
|
3418
|
+
import { join as join14, resolve } from "path";
|
|
3140
3419
|
var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
3141
3420
|
function isExactSemver(value) {
|
|
3142
3421
|
return typeof value === "string" && EXACT_SEMVER_RE.test(value);
|
|
@@ -3205,25 +3484,25 @@ function invalidManifestPreviewReason(manifest) {
|
|
|
3205
3484
|
var manifests = /* @__PURE__ */ new Map();
|
|
3206
3485
|
function loadManifests(integrationsDir) {
|
|
3207
3486
|
const dir = resolve(
|
|
3208
|
-
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ??
|
|
3487
|
+
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
3209
3488
|
);
|
|
3210
|
-
const indexPath =
|
|
3211
|
-
if (!
|
|
3489
|
+
const indexPath = join14(dir, "index.json");
|
|
3490
|
+
if (!existsSync14(indexPath)) {
|
|
3212
3491
|
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
3213
3492
|
return 0;
|
|
3214
3493
|
}
|
|
3215
3494
|
let index;
|
|
3216
3495
|
try {
|
|
3217
|
-
index = JSON.parse(
|
|
3496
|
+
index = JSON.parse(readFileSync15(indexPath, "utf-8"));
|
|
3218
3497
|
} catch (err) {
|
|
3219
3498
|
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
3220
3499
|
return 0;
|
|
3221
3500
|
}
|
|
3222
3501
|
let loaded = 0;
|
|
3223
3502
|
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
3224
|
-
const manifestPath =
|
|
3503
|
+
const manifestPath = join14(dir, relativePath);
|
|
3225
3504
|
try {
|
|
3226
|
-
const manifest = JSON.parse(
|
|
3505
|
+
const manifest = JSON.parse(readFileSync15(manifestPath, "utf-8"));
|
|
3227
3506
|
const pinReason = invalidNpmPinReason(manifest);
|
|
3228
3507
|
if (pinReason) {
|
|
3229
3508
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
|
|
@@ -5134,6 +5413,22 @@ function listIntegrationsHandler(state2, integrationManager2) {
|
|
|
5134
5413
|
|
|
5135
5414
|
// src/tools/commitments.ts
|
|
5136
5415
|
import { ContentBindingError as ContentBindingError2 } from "@hap/core";
|
|
5416
|
+
|
|
5417
|
+
// src/lib/who-label.ts
|
|
5418
|
+
var UNKNOWN_DECIDER = "the person";
|
|
5419
|
+
async function resolveWhoLabel(archive, domainKey, userId) {
|
|
5420
|
+
if (!archive) return UNKNOWN_DECIDER;
|
|
5421
|
+
try {
|
|
5422
|
+
const dir = await getIdentityDirectory(archive);
|
|
5423
|
+
const did = dir.domainDids.get(domainKey) ?? dir.domainDids.get(userId);
|
|
5424
|
+
const name = did ? dir.names.get(did) : void 0;
|
|
5425
|
+
return name ?? UNKNOWN_DECIDER;
|
|
5426
|
+
} catch {
|
|
5427
|
+
return UNKNOWN_DECIDER;
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5431
|
+
// src/tools/commitments.ts
|
|
5137
5432
|
function buildSkippedProposalNote(proposalId) {
|
|
5138
5433
|
return `Proposal ${proposalId} was approved, but was submitted from another device/installation of this operator (or one whose local record is gone) \u2014 this gateway will not execute it. Re-run the same request here to execute it from this gateway.`;
|
|
5139
5434
|
}
|
|
@@ -5432,11 +5727,34 @@ Result: ${JSON.stringify(match.executionResult, null, 2)}` : " The action ran; t
|
|
|
5432
5727
|
}
|
|
5433
5728
|
if (match.status === "rejected") {
|
|
5434
5729
|
const by = match.approverRejectedBy ?? match.rejectedBy;
|
|
5435
|
-
const
|
|
5730
|
+
const outcome = by?.outcome ?? "rejected";
|
|
5731
|
+
const legacyReason = match.approverRejectedBy?.reason;
|
|
5732
|
+
const who = by ? await resolveWhoLabel(
|
|
5733
|
+
state2.receiptArchive,
|
|
5734
|
+
"domain" in by ? by.domain : by.userId,
|
|
5735
|
+
by.userId
|
|
5736
|
+
) : "a person";
|
|
5737
|
+
if (outcome === "change_needed") {
|
|
5738
|
+
return {
|
|
5739
|
+
content: [{
|
|
5740
|
+
type: "text",
|
|
5741
|
+
text: `Proposal ${match.id} was SENT BACK FOR A CHANGE by ${who}: "${by?.note ?? ""}". The action did not run. You may adjust it and propose again \u2014 a new proposal the person approves again. Do not resend the same values.`
|
|
5742
|
+
}]
|
|
5743
|
+
};
|
|
5744
|
+
}
|
|
5745
|
+
if (outcome === "replaced") {
|
|
5746
|
+
return {
|
|
5747
|
+
content: [{
|
|
5748
|
+
type: "text",
|
|
5749
|
+
text: `Proposal ${match.id} was REPLACED: the person signed their own version of this mandate on the mandate screen. Nothing was created from this proposal. Use the person's mandate \u2014 check list-authorizations.`
|
|
5750
|
+
}]
|
|
5751
|
+
};
|
|
5752
|
+
}
|
|
5753
|
+
const reasonSuffix = legacyReason ? `: "${legacyReason}".` : ".";
|
|
5436
5754
|
return {
|
|
5437
5755
|
content: [{
|
|
5438
5756
|
type: "text",
|
|
5439
|
-
text: `Proposal ${match.id} was REJECTED
|
|
5757
|
+
text: `Proposal ${match.id} was REJECTED by ${who}${reasonSuffix} The action did not run and must not be retried. Ask the person before proposing anything similar.`
|
|
5440
5758
|
}]
|
|
5441
5759
|
};
|
|
5442
5760
|
}
|
|
@@ -5585,6 +5903,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5585
5903
|
{ name: "suveren-gateway", version: "0.1.0" },
|
|
5586
5904
|
{ instructions }
|
|
5587
5905
|
);
|
|
5906
|
+
const withOutcomeOutbox = (handler) => wrapWithOutcomeOutbox(state2.outcomeOutbox, handler);
|
|
5588
5907
|
server.registerTool(
|
|
5589
5908
|
"list-authorizations",
|
|
5590
5909
|
{
|
|
@@ -5593,7 +5912,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5593
5912
|
domain: z2.string().optional().describe('Profile domain to show full details for (e.g., "charge", "deploy"). Omit for compact overview.')
|
|
5594
5913
|
}
|
|
5595
5914
|
},
|
|
5596
|
-
listAuthorizationsHandler(state2, integrationManager2)
|
|
5915
|
+
withOutcomeOutbox(listAuthorizationsHandler(state2, integrationManager2))
|
|
5597
5916
|
);
|
|
5598
5917
|
server.registerTool(
|
|
5599
5918
|
"check-pending-mandates",
|
|
@@ -5603,7 +5922,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5603
5922
|
domain: z2.string().describe(`The owner's domain (e.g., "compliance")`)
|
|
5604
5923
|
}
|
|
5605
5924
|
},
|
|
5606
|
-
checkPendingHandler(cache)
|
|
5925
|
+
withOutcomeOutbox(checkPendingHandler(cache))
|
|
5607
5926
|
);
|
|
5608
5927
|
server.registerTool(
|
|
5609
5928
|
"list-integrations",
|
|
@@ -5611,7 +5930,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5611
5930
|
description: "List all running integrations and their authorization status. Returns a compact overview \u2014 use list-authorizations(domain) for full details on a specific profile.",
|
|
5612
5931
|
inputSchema: {}
|
|
5613
5932
|
},
|
|
5614
|
-
listIntegrationsHandler(state2, integrationManager2)
|
|
5933
|
+
withOutcomeOutbox(listIntegrationsHandler(state2, integrationManager2))
|
|
5615
5934
|
);
|
|
5616
5935
|
server.registerTool(
|
|
5617
5936
|
"check-pending-commitments",
|
|
@@ -5621,7 +5940,7 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5621
5940
|
proposal_id: z2.string().optional().describe("Specific proposal ID to check. Omit to see all.")
|
|
5622
5941
|
}
|
|
5623
5942
|
},
|
|
5624
|
-
checkPendingCommitmentsHandler(state2, integrationManager2)
|
|
5943
|
+
withOutcomeOutbox(checkPendingCommitmentsHandler(state2, integrationManager2))
|
|
5625
5944
|
);
|
|
5626
5945
|
const proxiedTools = /* @__PURE__ */ new Map();
|
|
5627
5946
|
function registerProxiedTools() {
|
|
@@ -5655,7 +5974,10 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5655
5974
|
description,
|
|
5656
5975
|
...Object.keys(zodShape).length > 0 ? { inputSchema: zodShape } : {}
|
|
5657
5976
|
},
|
|
5658
|
-
|
|
5977
|
+
// Built-in tools (reports, setup) never carry the outcome line: a
|
|
5978
|
+
// report's output is evidence and must not mix in a notice (nor the
|
|
5979
|
+
// decider's identity); setup tools are not the working agent's.
|
|
5980
|
+
integrationManager2?.isBuiltin?.(tool.integrationId) ? handler : withOutcomeOutbox(handler)
|
|
5659
5981
|
);
|
|
5660
5982
|
proxiedTools.set(tool.namespacedName, { tool, registered });
|
|
5661
5983
|
} catch (err) {
|
|
@@ -5688,21 +6010,21 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
5688
6010
|
}
|
|
5689
6011
|
|
|
5690
6012
|
// src/lib/agent-contact.ts
|
|
5691
|
-
import { existsSync as
|
|
5692
|
-
import { dirname as
|
|
6013
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync12, readFileSync as readFileSync16, renameSync as renameSync5, writeFileSync as writeFileSync12 } from "fs";
|
|
6014
|
+
import { dirname as dirname12, join as join15 } from "path";
|
|
5693
6015
|
var FILE = "agent-contact.json";
|
|
5694
6016
|
var REWRITE_AFTER_MS = 6e4;
|
|
5695
6017
|
var AgentContactStore = class {
|
|
5696
6018
|
path;
|
|
5697
6019
|
current;
|
|
5698
6020
|
constructor(dataDir2) {
|
|
5699
|
-
this.path =
|
|
6021
|
+
this.path = join15(dataDir2, FILE);
|
|
5700
6022
|
this.current = this.load();
|
|
5701
6023
|
}
|
|
5702
6024
|
load() {
|
|
5703
|
-
if (!
|
|
6025
|
+
if (!existsSync15(this.path)) return null;
|
|
5704
6026
|
try {
|
|
5705
|
-
const v = JSON.parse(
|
|
6027
|
+
const v = JSON.parse(readFileSync16(this.path, "utf8"));
|
|
5706
6028
|
if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
|
|
5707
6029
|
return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
|
|
5708
6030
|
} catch {
|
|
@@ -5725,9 +6047,9 @@ var AgentContactStore = class {
|
|
|
5725
6047
|
};
|
|
5726
6048
|
if (!due) return;
|
|
5727
6049
|
try {
|
|
5728
|
-
|
|
6050
|
+
mkdirSync12(dirname12(this.path), { recursive: true });
|
|
5729
6051
|
const tmp = `${this.path}.tmp`;
|
|
5730
|
-
|
|
6052
|
+
writeFileSync12(tmp, JSON.stringify(this.current, null, 2));
|
|
5731
6053
|
renameSync5(tmp, this.path);
|
|
5732
6054
|
} catch (err) {
|
|
5733
6055
|
console.error("[Suveren MCP] Could not save agent contact:", err);
|
|
@@ -5762,16 +6084,16 @@ function verifyGateContentHashes(content, auth) {
|
|
|
5762
6084
|
}
|
|
5763
6085
|
|
|
5764
6086
|
// src/lib/integration-registry.ts
|
|
5765
|
-
import { mkdirSync as
|
|
5766
|
-
import { homedir as
|
|
5767
|
-
import { dirname as
|
|
5768
|
-
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
6087
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync17, writeFileSync as writeFileSync13, existsSync as existsSync16 } from "fs";
|
|
6088
|
+
import { homedir as homedir11 } from "os";
|
|
6089
|
+
import { dirname as dirname13, join as join16 } from "path";
|
|
6090
|
+
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join16(homedir11(), ".suveren");
|
|
5769
6091
|
var IntegrationRegistry = class {
|
|
5770
6092
|
integrations = /* @__PURE__ */ new Map();
|
|
5771
6093
|
filePath;
|
|
5772
6094
|
constructor(dataDir2) {
|
|
5773
6095
|
const dir = dataDir2 ?? DEFAULT_DATA_DIR;
|
|
5774
|
-
this.filePath =
|
|
6096
|
+
this.filePath = join16(dir, "integrations.json");
|
|
5775
6097
|
this.load();
|
|
5776
6098
|
}
|
|
5777
6099
|
// ─── CRUD ───────────────────────────────────────────────────────────────
|
|
@@ -5802,12 +6124,12 @@ var IntegrationRegistry = class {
|
|
|
5802
6124
|
}
|
|
5803
6125
|
// ─── Load / Save ────────────────────────────────────────────────────────
|
|
5804
6126
|
load() {
|
|
5805
|
-
if (!
|
|
5806
|
-
|
|
6127
|
+
if (!existsSync16(this.filePath)) {
|
|
6128
|
+
mkdirSync13(dirname13(this.filePath), { recursive: true });
|
|
5807
6129
|
return;
|
|
5808
6130
|
}
|
|
5809
6131
|
try {
|
|
5810
|
-
const raw =
|
|
6132
|
+
const raw = readFileSync17(this.filePath, "utf-8");
|
|
5811
6133
|
const data = JSON.parse(raw);
|
|
5812
6134
|
const migrated = data.integrations.map((i) => {
|
|
5813
6135
|
if ("toolGating" in i && !("profile" in i)) {
|
|
@@ -5836,15 +6158,15 @@ var IntegrationRegistry = class {
|
|
|
5836
6158
|
version: 1,
|
|
5837
6159
|
integrations: Array.from(this.integrations.values())
|
|
5838
6160
|
};
|
|
5839
|
-
|
|
5840
|
-
|
|
6161
|
+
mkdirSync13(dirname13(this.filePath), { recursive: true });
|
|
6162
|
+
writeFileSync13(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
|
|
5841
6163
|
}
|
|
5842
6164
|
};
|
|
5843
6165
|
|
|
5844
6166
|
// src/lib/integration-manager.ts
|
|
5845
|
-
import { homedir as
|
|
5846
|
-
import { join as
|
|
5847
|
-
import { existsSync as
|
|
6167
|
+
import { homedir as homedir12 } from "os";
|
|
6168
|
+
import { join as join17, delimiter } from "path";
|
|
6169
|
+
import { existsSync as existsSync17, writeFileSync as writeFileSync14, mkdirSync as mkdirSync14, readFileSync as readFileSync18, rmSync } from "fs";
|
|
5848
6170
|
import { execFile } from "child_process";
|
|
5849
6171
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5850
6172
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
@@ -5974,8 +6296,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
|
|
|
5974
6296
|
}
|
|
5975
6297
|
|
|
5976
6298
|
// src/lib/integration-manager.ts
|
|
5977
|
-
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ??
|
|
5978
|
-
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ??
|
|
6299
|
+
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join17(homedir12(), ".suveren");
|
|
6300
|
+
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join17(DEFAULT_DATA_DIR2, "integrations");
|
|
5979
6301
|
var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
|
|
5980
6302
|
var installLock = Promise.resolve();
|
|
5981
6303
|
function withInstallLock(task) {
|
|
@@ -5983,7 +6305,7 @@ function withInstallLock(task) {
|
|
|
5983
6305
|
installLock = run.then(() => void 0, () => void 0);
|
|
5984
6306
|
return run;
|
|
5985
6307
|
}
|
|
5986
|
-
var INTEGRATIONS_BIN =
|
|
6308
|
+
var INTEGRATIONS_BIN = join17(INTEGRATIONS_DIR, "node_modules", ".bin");
|
|
5987
6309
|
function getIntegrationsBinDir() {
|
|
5988
6310
|
return INTEGRATIONS_BIN;
|
|
5989
6311
|
}
|
|
@@ -5992,12 +6314,12 @@ function buildPath() {
|
|
|
5992
6314
|
return [INTEGRATIONS_BIN, base].join(delimiter);
|
|
5993
6315
|
}
|
|
5994
6316
|
function ensureIntegrationsDir() {
|
|
5995
|
-
if (!
|
|
5996
|
-
|
|
6317
|
+
if (!existsSync17(INTEGRATIONS_DIR)) {
|
|
6318
|
+
mkdirSync14(INTEGRATIONS_DIR, { recursive: true });
|
|
5997
6319
|
}
|
|
5998
|
-
const pkgPath =
|
|
5999
|
-
if (!
|
|
6000
|
-
|
|
6320
|
+
const pkgPath = join17(INTEGRATIONS_DIR, "package.json");
|
|
6321
|
+
if (!existsSync17(pkgPath)) {
|
|
6322
|
+
writeFileSync14(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
|
|
6001
6323
|
}
|
|
6002
6324
|
}
|
|
6003
6325
|
function readAgeOf(config) {
|
|
@@ -6086,19 +6408,19 @@ var IntegrationManager = class {
|
|
|
6086
6408
|
* exist, and treat anything else as not-installed (and reinstallable).
|
|
6087
6409
|
*/
|
|
6088
6410
|
isUsableInstall(npmPackage) {
|
|
6089
|
-
const pkgDir =
|
|
6090
|
-
const pkgJson =
|
|
6091
|
-
if (!
|
|
6411
|
+
const pkgDir = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
6412
|
+
const pkgJson = join17(pkgDir, "package.json");
|
|
6413
|
+
if (!existsSync17(pkgDir) || !existsSync17(pkgJson)) return false;
|
|
6092
6414
|
try {
|
|
6093
|
-
const pkg = JSON.parse(
|
|
6415
|
+
const pkg = JSON.parse(readFileSync18(pkgJson, "utf8"));
|
|
6094
6416
|
const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
|
|
6095
6417
|
const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
|
|
6096
6418
|
const binEntries = Object.entries(binMap);
|
|
6097
6419
|
for (const [binName, target] of binEntries) {
|
|
6098
|
-
if (!
|
|
6420
|
+
if (!existsSync17(join17(pkgDir, target))) return false;
|
|
6099
6421
|
if (!this.binShimExists(binName)) return false;
|
|
6100
6422
|
}
|
|
6101
|
-
if (binEntries.length === 0 && pkg.main && !
|
|
6423
|
+
if (binEntries.length === 0 && pkg.main && !existsSync17(join17(pkgDir, pkg.main))) return false;
|
|
6102
6424
|
return true;
|
|
6103
6425
|
} catch {
|
|
6104
6426
|
return false;
|
|
@@ -6113,9 +6435,9 @@ var IntegrationManager = class {
|
|
|
6113
6435
|
*/
|
|
6114
6436
|
readInstalledVersion(npmPackage) {
|
|
6115
6437
|
if (!this.isUsableInstall(npmPackage)) return null;
|
|
6116
|
-
const pkgJson =
|
|
6438
|
+
const pkgJson = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
|
|
6117
6439
|
try {
|
|
6118
|
-
const pkg = JSON.parse(
|
|
6440
|
+
const pkg = JSON.parse(readFileSync18(pkgJson, "utf8"));
|
|
6119
6441
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
6120
6442
|
} catch {
|
|
6121
6443
|
return null;
|
|
@@ -6129,7 +6451,7 @@ var IntegrationManager = class {
|
|
|
6129
6451
|
binShimExists(binName) {
|
|
6130
6452
|
if (!binName) return false;
|
|
6131
6453
|
const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
|
|
6132
|
-
return candidates.some((c) =>
|
|
6454
|
+
return candidates.some((c) => existsSync17(join17(INTEGRATIONS_BIN, c)));
|
|
6133
6455
|
}
|
|
6134
6456
|
/**
|
|
6135
6457
|
* The exact version THIS gateway's CURRENTLY LOADED manifest pins
|
|
@@ -6219,8 +6541,8 @@ var IntegrationManager = class {
|
|
|
6219
6541
|
* nothing at all.
|
|
6220
6542
|
*/
|
|
6221
6543
|
async installNow(npmPackage, exactVersion, cleanFirst = true) {
|
|
6222
|
-
const pkgDir =
|
|
6223
|
-
if (cleanFirst &&
|
|
6544
|
+
const pkgDir = join17(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
6545
|
+
if (cleanFirst && existsSync17(pkgDir)) {
|
|
6224
6546
|
console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
|
|
6225
6547
|
try {
|
|
6226
6548
|
rmSync(pkgDir, { recursive: true, force: true });
|
|
@@ -7160,28 +7482,28 @@ function agentAccess(token, opensSession) {
|
|
|
7160
7482
|
}
|
|
7161
7483
|
|
|
7162
7484
|
// src/lib/profile-loader.ts
|
|
7163
|
-
import { readFileSync as
|
|
7164
|
-
import { join as
|
|
7485
|
+
import { readFileSync as readFileSync19, existsSync as existsSync18 } from "fs";
|
|
7486
|
+
import { join as join18, resolve as resolve2 } from "path";
|
|
7165
7487
|
import { registerProfile } from "@hap/core";
|
|
7166
7488
|
function loadProfiles(profilesDir) {
|
|
7167
|
-
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ??
|
|
7168
|
-
const indexPath =
|
|
7169
|
-
if (!
|
|
7489
|
+
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join18(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
|
|
7490
|
+
const indexPath = join18(dir, "index.json");
|
|
7491
|
+
if (!existsSync18(indexPath)) {
|
|
7170
7492
|
warnNoProfiles(`No index.json at ${indexPath}`);
|
|
7171
7493
|
return 0;
|
|
7172
7494
|
}
|
|
7173
7495
|
let index;
|
|
7174
7496
|
try {
|
|
7175
|
-
index = JSON.parse(
|
|
7497
|
+
index = JSON.parse(readFileSync19(indexPath, "utf-8"));
|
|
7176
7498
|
} catch (err) {
|
|
7177
7499
|
console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
|
|
7178
7500
|
return 0;
|
|
7179
7501
|
}
|
|
7180
7502
|
let loaded = 0;
|
|
7181
7503
|
for (const [profileId, relativePath] of Object.entries(index.profiles)) {
|
|
7182
|
-
const profilePath =
|
|
7504
|
+
const profilePath = join18(dir, relativePath);
|
|
7183
7505
|
try {
|
|
7184
|
-
const profile = JSON.parse(
|
|
7506
|
+
const profile = JSON.parse(readFileSync19(profilePath, "utf-8"));
|
|
7185
7507
|
registerProfile(profileId, profile);
|
|
7186
7508
|
const shortName2 = profileId.split("/").pop()?.replace(/@.*$/, "");
|
|
7187
7509
|
if (shortName2 && shortName2 !== profileId) {
|
|
@@ -7522,20 +7844,20 @@ async function controlPlaneMandate(dryRun, request) {
|
|
|
7522
7844
|
}
|
|
7523
7845
|
|
|
7524
7846
|
// src/lib/guides.ts
|
|
7525
|
-
import { existsSync as
|
|
7526
|
-
import { dirname as
|
|
7847
|
+
import { existsSync as existsSync19, readdirSync, readFileSync as readFileSync20 } from "fs";
|
|
7848
|
+
import { dirname as dirname14, join as join19 } from "path";
|
|
7527
7849
|
var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
|
|
7528
7850
|
function builtinGuidesDir() {
|
|
7529
7851
|
if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
|
|
7530
7852
|
let dir = import.meta.dirname ?? __dirname;
|
|
7531
7853
|
for (let i = 0; i < 8; i++) {
|
|
7532
|
-
const candidate =
|
|
7533
|
-
if (
|
|
7534
|
-
const parent =
|
|
7854
|
+
const candidate = join19(dir, "content", "guides");
|
|
7855
|
+
if (existsSync19(candidate)) return candidate;
|
|
7856
|
+
const parent = dirname14(dir);
|
|
7535
7857
|
if (parent === dir) break;
|
|
7536
7858
|
dir = parent;
|
|
7537
7859
|
}
|
|
7538
|
-
return
|
|
7860
|
+
return join19(import.meta.dirname ?? __dirname, "content", "guides");
|
|
7539
7861
|
}
|
|
7540
7862
|
function parse(body) {
|
|
7541
7863
|
const lines = body.split(/\r?\n/);
|
|
@@ -7546,14 +7868,14 @@ function parse(body) {
|
|
|
7546
7868
|
return { title, summary };
|
|
7547
7869
|
}
|
|
7548
7870
|
function readDir(dir, source) {
|
|
7549
|
-
if (!dir || !
|
|
7871
|
+
if (!dir || !existsSync19(dir)) return [];
|
|
7550
7872
|
const out = [];
|
|
7551
7873
|
for (const file of readdirSync(dir)) {
|
|
7552
7874
|
const m = FILE_RE.exec(file);
|
|
7553
7875
|
if (!m) continue;
|
|
7554
7876
|
let body;
|
|
7555
7877
|
try {
|
|
7556
|
-
body =
|
|
7878
|
+
body = readFileSync20(join19(dir, file), "utf8");
|
|
7557
7879
|
} catch {
|
|
7558
7880
|
continue;
|
|
7559
7881
|
}
|
|
@@ -7815,7 +8137,6 @@ function resolveTargetProfile(input) {
|
|
|
7815
8137
|
var CREATE_MANDATE_INPUT_SCHEMA = {
|
|
7816
8138
|
type: "object",
|
|
7817
8139
|
properties: {
|
|
7818
|
-
team: { type: "string", description: 'Team name or id, or "personal". Omit to use the workspace of your Delegation mandate.' },
|
|
7819
8140
|
// Short name only (e.g. "sales") — a full profile id is refused here, at input
|
|
7820
8141
|
// validation, before the tool ever runs (DL1 Interface contract).
|
|
7821
8142
|
profile: { type: "string", pattern: "^[a-z][a-z0-9-]*$", description: 'Profile short name, e.g. "sales" (newest version).' },
|
|
@@ -7905,7 +8226,6 @@ function setupBuiltin(deps) {
|
|
|
7905
8226
|
hideUnlessAuthorized: true,
|
|
7906
8227
|
// Shown like the mandate screen: limits and scope by the profile's own names.
|
|
7907
8228
|
approvalView: {
|
|
7908
|
-
team: { label: "Team" },
|
|
7909
8229
|
profile: { label: "Profile", kind: "profile" },
|
|
7910
8230
|
limits: { label: "Limits", kind: "profile-limits", profileArg: "profile" },
|
|
7911
8231
|
scope: { label: "Scope", kind: "profile-scope", profileArg: "profile" },
|
|
@@ -7914,7 +8234,7 @@ function setupBuiltin(deps) {
|
|
|
7914
8234
|
duration_hours: { label: "Valid for (hours)" },
|
|
7915
8235
|
title: { label: "Title" }
|
|
7916
8236
|
},
|
|
7917
|
-
preview: { tool: "describe_mandate_proposal", args: {
|
|
8237
|
+
preview: { tool: "describe_mandate_proposal", args: { profile: "profile", limits: "limits", scope: "scope", intent: "intent", mode: "mode", duration_hours: "duration_hours", title: "title" } }
|
|
7918
8238
|
},
|
|
7919
8239
|
// Internal preview tools (DL1): never listed, never callable by an
|
|
7920
8240
|
// agent (BuiltinTool.internal — enforced in index.ts and
|
|
@@ -7987,7 +8307,7 @@ ${body}`);
|
|
|
7987
8307
|
},
|
|
7988
8308
|
{
|
|
7989
8309
|
name: "create_mandate",
|
|
7990
|
-
description: "Propose a new mandate for the person running this gateway \u2014 the authority another tool needs (e.g. sales quotes up to a value). A person approves or rejects the proposal; only after approval is the mandate created, exactly as if they had signed it on the mandate screen. Checked against the profile before the proposal: unknown limits, a mode the profile does not allow, or a team where the person may not give this mandate are refused at once. Outside simulation mode, the profile and mode you may propose are themselves limited by your Delegation mandate's scope \u2014 Delegation itself can never be proposed. Create only.",
|
|
8310
|
+
description: "Propose a new mandate for the person running this gateway \u2014 the authority another tool needs (e.g. sales quotes up to a value). Always goes to the person's current workspace (their team, or Personal if they are not in one) \u2014 there is no way to name a different one. A person approves or rejects the proposal; only after approval is the mandate created, exactly as if they had signed it on the mandate screen. Checked against the profile before the proposal: unknown limits, a mode the profile does not allow, or a team where the person may not give this mandate are refused at once. Outside simulation mode, the profile and mode you may propose are themselves limited by your Delegation mandate's scope \u2014 Delegation itself can never be proposed. Create only.",
|
|
7991
8311
|
inputSchema: CREATE_MANDATE_INPUT_SCHEMA,
|
|
7992
8312
|
validate: async (args) => {
|
|
7993
8313
|
const fixed = delegationItselfRefusal(args);
|
|
@@ -8033,8 +8353,8 @@ ${body}`);
|
|
|
8033
8353
|
}
|
|
8034
8354
|
const { profile, shortName: targetShortName } = resolved;
|
|
8035
8355
|
const displayName = profile.name ?? targetShortName;
|
|
8036
|
-
const
|
|
8037
|
-
const workspace =
|
|
8356
|
+
const check = await controlPlaneMandate(true, mandateRequest(args));
|
|
8357
|
+
const workspace = check.ok && check.groupName ? check.groupName : "Personal";
|
|
8038
8358
|
const mode = typeof args.mode === "string" ? args.mode : "";
|
|
8039
8359
|
const structured = {
|
|
8040
8360
|
proposal: `New mandate \xB7 ${displayName}`,
|
|
@@ -8073,7 +8393,6 @@ ${body}`);
|
|
|
8073
8393
|
}
|
|
8074
8394
|
function mandateRequest(args) {
|
|
8075
8395
|
return {
|
|
8076
|
-
team: typeof args.team === "string" && args.team.trim() ? args.team.trim() : void 0,
|
|
8077
8396
|
profile: args.profile,
|
|
8078
8397
|
limits: args.limits,
|
|
8079
8398
|
scope: args.scope,
|
|
@@ -8104,8 +8423,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
|
|
|
8104
8423
|
import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
|
|
8105
8424
|
|
|
8106
8425
|
// src/lib/committed-executor.ts
|
|
8107
|
-
import { existsSync as
|
|
8108
|
-
import { dirname as
|
|
8426
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync15, readFileSync as readFileSync21, unlinkSync as unlinkSync7, writeFileSync as writeFileSync15 } from "fs";
|
|
8427
|
+
import { dirname as dirname15, join as join20 } from "path";
|
|
8109
8428
|
var CommittedExecutor = class {
|
|
8110
8429
|
constructor(run) {
|
|
8111
8430
|
this.run = run;
|
|
@@ -8133,7 +8452,7 @@ var ExecutorLock = class {
|
|
|
8133
8452
|
path;
|
|
8134
8453
|
held = false;
|
|
8135
8454
|
constructor(dataDir2) {
|
|
8136
|
-
this.path =
|
|
8455
|
+
this.path = join20(dataDir2, "executor.lock");
|
|
8137
8456
|
}
|
|
8138
8457
|
/** Try to take the lock. Returns the holder's pid when someone else has it. */
|
|
8139
8458
|
acquire() {
|
|
@@ -8141,9 +8460,9 @@ var ExecutorLock = class {
|
|
|
8141
8460
|
if (current && current.pid !== process.pid && isAlive(current.pid)) {
|
|
8142
8461
|
return { held: false, holderPid: current.pid };
|
|
8143
8462
|
}
|
|
8144
|
-
|
|
8463
|
+
mkdirSync15(dirname15(this.path), { recursive: true, mode: 448 });
|
|
8145
8464
|
const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
|
|
8146
|
-
|
|
8465
|
+
writeFileSync15(this.path, JSON.stringify(lock), { mode: 384 });
|
|
8147
8466
|
this.held = true;
|
|
8148
8467
|
return { held: true };
|
|
8149
8468
|
}
|
|
@@ -8159,15 +8478,15 @@ var ExecutorLock = class {
|
|
|
8159
8478
|
const current = this.read();
|
|
8160
8479
|
if (current?.pid === process.pid) {
|
|
8161
8480
|
try {
|
|
8162
|
-
|
|
8481
|
+
unlinkSync7(this.path);
|
|
8163
8482
|
} catch {
|
|
8164
8483
|
}
|
|
8165
8484
|
}
|
|
8166
8485
|
}
|
|
8167
8486
|
read() {
|
|
8168
|
-
if (!
|
|
8487
|
+
if (!existsSync20(this.path)) return null;
|
|
8169
8488
|
try {
|
|
8170
|
-
const parsed = JSON.parse(
|
|
8489
|
+
const parsed = JSON.parse(readFileSync21(this.path, "utf8"));
|
|
8171
8490
|
return typeof parsed?.pid === "number" ? parsed : null;
|
|
8172
8491
|
} catch {
|
|
8173
8492
|
return null;
|
|
@@ -8184,20 +8503,20 @@ function isAlive(pid) {
|
|
|
8184
8503
|
}
|
|
8185
8504
|
|
|
8186
8505
|
// bin/http.ts
|
|
8187
|
-
import { homedir as
|
|
8188
|
-
import { join as
|
|
8506
|
+
import { homedir as homedir13 } from "os";
|
|
8507
|
+
import { join as join22 } from "path";
|
|
8189
8508
|
|
|
8190
8509
|
// src/lib/mcp-as-tracker.ts
|
|
8191
|
-
import { existsSync as
|
|
8192
|
-
import { dirname as
|
|
8510
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync22, writeFileSync as writeFileSync16 } from "fs";
|
|
8511
|
+
import { dirname as dirname16, join as join21 } from "path";
|
|
8193
8512
|
function trackerPath(dataDir2) {
|
|
8194
|
-
return
|
|
8513
|
+
return join21(dataDir2, "mcp-as-pairing.json");
|
|
8195
8514
|
}
|
|
8196
8515
|
function readMcpPairedAsUrl(dataDir2) {
|
|
8197
8516
|
const path = trackerPath(dataDir2);
|
|
8198
|
-
if (!
|
|
8517
|
+
if (!existsSync21(path)) return null;
|
|
8199
8518
|
try {
|
|
8200
|
-
const data = JSON.parse(
|
|
8519
|
+
const data = JSON.parse(readFileSync22(path, "utf-8"));
|
|
8201
8520
|
return typeof data.asUrl === "string" ? data.asUrl : null;
|
|
8202
8521
|
} catch {
|
|
8203
8522
|
return null;
|
|
@@ -8205,12 +8524,12 @@ function readMcpPairedAsUrl(dataDir2) {
|
|
|
8205
8524
|
}
|
|
8206
8525
|
function writeMcpPairedAsUrl(dataDir2, asUrl) {
|
|
8207
8526
|
const path = trackerPath(dataDir2);
|
|
8208
|
-
|
|
8209
|
-
|
|
8527
|
+
mkdirSync16(dirname16(path), { recursive: true, mode: 448 });
|
|
8528
|
+
writeFileSync16(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
|
|
8210
8529
|
}
|
|
8211
8530
|
|
|
8212
8531
|
// bin/http.ts
|
|
8213
|
-
var dataDir = process.env.SUVEREN_DATA_DIR ??
|
|
8532
|
+
var dataDir = process.env.SUVEREN_DATA_DIR ?? join22(homedir13(), ".suveren");
|
|
8214
8533
|
var spUrl = resolveAsUrl(dataDir);
|
|
8215
8534
|
validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
|
|
8216
8535
|
var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
|
|
@@ -8295,7 +8614,8 @@ app.post("/internal/configure", internalOnly, (req, res) => {
|
|
|
8295
8614
|
state.denialLog.setVaultKey(key);
|
|
8296
8615
|
state.receiptArchive.setVaultKey(key);
|
|
8297
8616
|
state.reportStore.setVaultKey(key);
|
|
8298
|
-
|
|
8617
|
+
state.outcomeOutbox.setVaultKey(key);
|
|
8618
|
+
console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log + receipt archive + report store + outcome outbox encryption active");
|
|
8299
8619
|
}
|
|
8300
8620
|
if (apiKey) {
|
|
8301
8621
|
state.spClient.setApiKey(apiKey);
|
|
@@ -8305,7 +8625,7 @@ app.post("/internal/configure", internalOnly, (req, res) => {
|
|
|
8305
8625
|
});
|
|
8306
8626
|
app.post("/internal/clear-session", internalOnly, (req, res) => {
|
|
8307
8627
|
const reason = req.body?.reason;
|
|
8308
|
-
const validReason = reason === "as-key-mismatch" || reason === "as-tls-mismatch" || reason === "as-url-changed" ? reason : void 0;
|
|
8628
|
+
const validReason = reason === "as-key-mismatch" || reason === "as-tls-mismatch" || reason === "as-url-changed" || reason === "managed-wrong-team" ? reason : void 0;
|
|
8309
8629
|
state.spClient.clearSession(validReason);
|
|
8310
8630
|
console.error(`[Suveren MCP] Session cleared by control-plane (${validReason ?? "expired"})`);
|
|
8311
8631
|
res.json({ ok: true });
|
|
@@ -8379,6 +8699,7 @@ app.post("/internal/run-committed", internalOnly, (_req, res) => {
|
|
|
8379
8699
|
res.json({ ok: true });
|
|
8380
8700
|
});
|
|
8381
8701
|
app.post("/internal/resync-gates", internalOnly, async (_req, res) => {
|
|
8702
|
+
await state.cache.refreshActiveWorkspace();
|
|
8382
8703
|
const gates = state.gateStore.getAll();
|
|
8383
8704
|
if (gates.length === 0) {
|
|
8384
8705
|
res.json({ ok: true, synced: 0 });
|
|
@@ -8936,7 +9257,7 @@ app.listen(port, bindHost, () => {
|
|
|
8936
9257
|
(proposal) => executeCommitted(proposal, state, integrationManager)
|
|
8937
9258
|
);
|
|
8938
9259
|
installCommittedExecutor(committedExecutor);
|
|
8939
|
-
const dataDir2 = process.env.SUVEREN_DATA_DIR ??
|
|
9260
|
+
const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join22(homedir13(), ".suveren");
|
|
8940
9261
|
const executorLock = new ExecutorLock(dataDir2);
|
|
8941
9262
|
const lock = executorLock.acquire();
|
|
8942
9263
|
if (!lock.held) {
|
|
@@ -8947,8 +9268,28 @@ app.listen(port, bindHost, () => {
|
|
|
8947
9268
|
for (const sig of ["SIGINT", "SIGTERM", "exit"]) {
|
|
8948
9269
|
process.once(sig, () => executorLock.release());
|
|
8949
9270
|
}
|
|
9271
|
+
async function pollRejectedOutcomes() {
|
|
9272
|
+
try {
|
|
9273
|
+
const rejected = await state.spClient.getRejectedProposals();
|
|
9274
|
+
for (const proposal of rejected) {
|
|
9275
|
+
const by = proposal.approverRejectedBy ?? proposal.rejectedBy;
|
|
9276
|
+
if (!by) continue;
|
|
9277
|
+
const submittedHere = state.proposalSubmissions.get(proposal.id) !== void 0;
|
|
9278
|
+
if (!outcomeEntryFor(proposal, submittedHere, by.userId, Date.now())) continue;
|
|
9279
|
+
const who = await resolveWhoLabel(
|
|
9280
|
+
state.receiptArchive,
|
|
9281
|
+
"domain" in by ? by.domain : by.userId,
|
|
9282
|
+
by.userId
|
|
9283
|
+
);
|
|
9284
|
+
const entry = outcomeEntryFor(proposal, submittedHere, who, Date.now());
|
|
9285
|
+
if (entry) state.outcomeOutbox.recordIfNew(entry);
|
|
9286
|
+
}
|
|
9287
|
+
} catch {
|
|
9288
|
+
}
|
|
9289
|
+
}
|
|
8950
9290
|
let draining = false;
|
|
8951
9291
|
async function executeCommittedProposals() {
|
|
9292
|
+
void pollRejectedOutcomes();
|
|
8952
9293
|
if (!executorLock.isHeld()) return;
|
|
8953
9294
|
if (draining) return;
|
|
8954
9295
|
draining = true;
|