@suveren/gateway 0.29.0 → 0.30.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/content/integrations/gmail.json +13 -2
- package/content/integrations/mail.json +12 -2
- package/content/integrations/mollie.json +10 -0
- package/dist/control-plane/index.mjs +125 -37
- package/dist/mcp-server/http.mjs +772 -420
- package/dist/ui/assets/index-C2BnlNib.js +123 -0
- package/dist/ui/assets/{index-DUIlMVlP.css → index-DPgewsLG.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/node_modules/qs/.github/THREAT_MODEL.md +4 -2
- package/node_modules/qs/CHANGELOG.md +38 -14
- package/node_modules/qs/README.md +31 -3
- package/node_modules/qs/dist/qs.js +20 -20
- package/node_modules/qs/lib/parse.js +94 -4
- package/node_modules/qs/lib/stringify.js +4 -4
- package/node_modules/qs/lib/utils.js +130 -6
- package/node_modules/qs/package.json +4 -4
- package/node_modules/qs/test/parse.js +174 -0
- package/node_modules/qs/test/stringify.js +263 -33
- package/node_modules/qs/test/utils.js +184 -0
- package/node_modules/sanitize-html/index.js +47 -10
- package/node_modules/sanitize-html/package.json +2 -3
- package/package.json +1 -1
- package/dist/ui/assets/index-C3vTF2Ye.js +0 -123
- package/node_modules/deepmerge/.editorconfig +0 -7
- package/node_modules/deepmerge/.eslintcache +0 -1
- package/node_modules/deepmerge/changelog.md +0 -167
- package/node_modules/deepmerge/dist/cjs.js +0 -133
- package/node_modules/deepmerge/dist/umd.js +0 -139
- package/node_modules/deepmerge/index.d.ts +0 -20
- package/node_modules/deepmerge/index.js +0 -106
- package/node_modules/deepmerge/license.txt +0 -21
- package/node_modules/deepmerge/package.json +0 -42
- package/node_modules/deepmerge/readme.md +0 -264
- package/node_modules/deepmerge/rollup.config.js +0 -22
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -1071,6 +1071,25 @@ var SPClient = class {
|
|
|
1071
1071
|
if (!res.ok) return null;
|
|
1072
1072
|
return res.json();
|
|
1073
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* One proposal by id, whatever its status — `GET /api/proposals/:id`
|
|
1076
|
+
* (the AS admits the creator, an approver, or the authorization's owner;
|
|
1077
|
+
* this gateway only ever calls it for an id it itself submitted, via
|
|
1078
|
+
* proposalSubmissions, so the creator case always applies). Used by the
|
|
1079
|
+
* `/internal/my-pending-proposals` stand-in (bin/http.ts) — see that
|
|
1080
|
+
* route's doc comment for why it exists at all: the AS has no endpoint
|
|
1081
|
+
* to list "proposals I created" scoped by creator, only `?domain=`,
|
|
1082
|
+
* `?status=committed/rejected`, and `?approver=me`, none of which ever
|
|
1083
|
+
* match an above-cap proposal that has no pendingDomains unless the
|
|
1084
|
+
* caller also happens to be a pendingApprover.
|
|
1085
|
+
*/
|
|
1086
|
+
async getProposal(id) {
|
|
1087
|
+
const res = await this.fetch(`/api/proposals/${encodeURIComponent(id)}`);
|
|
1088
|
+
if (res.status === 404) return null;
|
|
1089
|
+
if (!res.ok) throw new Error(`SP proposal request failed: ${res.status}`);
|
|
1090
|
+
const data = await res.json();
|
|
1091
|
+
return data.proposal;
|
|
1092
|
+
}
|
|
1074
1093
|
/**
|
|
1075
1094
|
* Get proposals that have been fully committed and are ready for execution.
|
|
1076
1095
|
*/
|
|
@@ -1124,7 +1143,7 @@ var AsKeyMismatchError = class extends Error {
|
|
|
1124
1143
|
}
|
|
1125
1144
|
};
|
|
1126
1145
|
function isCommitmentDowngrade(auth) {
|
|
1127
|
-
const requiresReview = auth.signedCommitmentMode === "review"
|
|
1146
|
+
const requiresReview = auth.signedCommitmentMode === "review";
|
|
1128
1147
|
return requiresReview && (auth.deferredCommitmentDomains ?? []).length === 0;
|
|
1129
1148
|
}
|
|
1130
1149
|
var AttestationCache = class {
|
|
@@ -1171,23 +1190,37 @@ var AttestationCache = class {
|
|
|
1171
1190
|
*/
|
|
1172
1191
|
activeWorkspaceGroupId = null;
|
|
1173
1192
|
/**
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
* the
|
|
1178
|
-
*
|
|
1179
|
-
|
|
1180
|
-
|
|
1193
|
+
* TR7 (work-plan "Added 2026-10-14"): the account's own Personal group id
|
|
1194
|
+
* — learned alongside {@link activeWorkspaceGroupId} so {@link isTeamGroup}
|
|
1195
|
+
* can tell a TEAM-owned authorization apart from a Personal one without a
|
|
1196
|
+
* live AS call on the hot read path. `null` = not yet known, same
|
|
1197
|
+
* "skip rather than guess" posture as `activeWorkspaceGroupId`.
|
|
1198
|
+
*/
|
|
1199
|
+
personalGroupId = null;
|
|
1200
|
+
/**
|
|
1201
|
+
* Re-resolve {@link activeWorkspaceGroupId} and {@link personalGroupId}
|
|
1202
|
+
* from the Authority Server. Called by the resync-gates flow (http.ts's
|
|
1203
|
+
* `/internal/resync-gates`), never on the hot tool-call path — it makes up
|
|
1204
|
+
* to two AS calls (`getActiveTeamGroupId` + `getPersonalGroupId`, the
|
|
1205
|
+
* second already needed for both values). Leaves the previous value(s) in
|
|
1206
|
+
* place on any failure (network error, unexpected shape): losing a
|
|
1207
|
+
* known-good value on a transient blip would turn this defence-in-depth
|
|
1208
|
+
* check into a false refusal of every cached mandate, which is worse than
|
|
1209
|
+
* leaving it momentarily stale.
|
|
1181
1210
|
*/
|
|
1182
1211
|
async refreshActiveWorkspace() {
|
|
1212
|
+
let activeTeam = null;
|
|
1213
|
+
try {
|
|
1214
|
+
activeTeam = await this.spClient.getActiveTeamGroupId();
|
|
1215
|
+
if (activeTeam) this.activeWorkspaceGroupId = activeTeam;
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1183
1218
|
try {
|
|
1184
|
-
const activeTeam = await this.spClient.getActiveTeamGroupId();
|
|
1185
|
-
if (activeTeam) {
|
|
1186
|
-
this.activeWorkspaceGroupId = activeTeam;
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
1219
|
const personalId = await this.spClient.getPersonalGroupId();
|
|
1190
|
-
if (personalId)
|
|
1220
|
+
if (personalId) {
|
|
1221
|
+
this.personalGroupId = personalId;
|
|
1222
|
+
if (!activeTeam) this.activeWorkspaceGroupId = personalId;
|
|
1223
|
+
}
|
|
1191
1224
|
} catch {
|
|
1192
1225
|
}
|
|
1193
1226
|
}
|
|
@@ -1200,6 +1233,19 @@ var AttestationCache = class {
|
|
|
1200
1233
|
isForeignWorkspace(groupId) {
|
|
1201
1234
|
return groupId !== void 0 && this.activeWorkspaceGroupId !== null && groupId !== this.activeWorkspaceGroupId;
|
|
1202
1235
|
}
|
|
1236
|
+
/**
|
|
1237
|
+
* TR7 — is `groupId` a TEAM group (not the account's own Personal
|
|
1238
|
+
* workspace)? `undefined` ("don't know") when either `groupId` itself or
|
|
1239
|
+
* the account's Personal group id hasn't been learned yet — callers MUST
|
|
1240
|
+
* treat that the same as Personal (read enforcement has no Authority
|
|
1241
|
+
* Server backstop to fail closed to, unlike {@link isForeignWorkspace}'s
|
|
1242
|
+
* ticket-time safety net, so an unknown status must never be read as
|
|
1243
|
+
* "team" — that would apply a ceiling nobody has actually confirmed).
|
|
1244
|
+
*/
|
|
1245
|
+
isTeamGroup(groupId) {
|
|
1246
|
+
if (groupId === void 0 || this.personalGroupId === null) return void 0;
|
|
1247
|
+
return groupId !== this.personalGroupId;
|
|
1248
|
+
}
|
|
1203
1249
|
/**
|
|
1204
1250
|
* Get the SP public key, fetching from SP if not cached or expired, then —
|
|
1205
1251
|
* when a pin exists for this exact AS URL — verifying the live key still
|
|
@@ -1779,6 +1825,7 @@ var DenialLog = class {
|
|
|
1779
1825
|
} catch (err) {
|
|
1780
1826
|
console.error("[DenialLog] failed to record denial:", err);
|
|
1781
1827
|
}
|
|
1828
|
+
void notifyControlPlane("denial-recorded");
|
|
1782
1829
|
}
|
|
1783
1830
|
/** Recent denials, newest first. `sinceMs` and `limit` are optional filters. */
|
|
1784
1831
|
getRecent(sinceMs, limit = MAX_RECORDS) {
|
|
@@ -2190,21 +2237,168 @@ function nowSec() {
|
|
|
2190
2237
|
return Math.floor(Date.now() / 1e3);
|
|
2191
2238
|
}
|
|
2192
2239
|
|
|
2193
|
-
// src/lib/
|
|
2194
|
-
import { mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync9,
|
|
2240
|
+
// src/lib/execution-result-store.ts
|
|
2241
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync9, unlinkSync as unlinkSync5 } from "fs";
|
|
2195
2242
|
import { homedir as homedir6 } from "os";
|
|
2196
2243
|
import { dirname as dirname7, join as join9 } from "path";
|
|
2244
|
+
import { createCipheriv as createCipheriv5, createDecipheriv as createDecipheriv5, randomBytes as randomBytes5 } from "crypto";
|
|
2197
2245
|
var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
|
|
2198
|
-
var FILE_NAME2 = "proposal-submissions.json";
|
|
2199
2246
|
var MAX_AGE_SECONDS3 = 90 * 24 * 60 * 60;
|
|
2247
|
+
var MAX_BYTES = 16 * 1024;
|
|
2248
|
+
function capResult(text, structuredContent) {
|
|
2249
|
+
const structJson = structuredContent ? JSON.stringify(structuredContent) : void 0;
|
|
2250
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
2251
|
+
const structBytes = structJson ? Buffer.byteLength(structJson, "utf8") : 0;
|
|
2252
|
+
if (textBytes + structBytes <= MAX_BYTES) {
|
|
2253
|
+
return { text, ...structuredContent ? { structuredContent } : {}, truncated: false };
|
|
2254
|
+
}
|
|
2255
|
+
if (textBytes <= MAX_BYTES) {
|
|
2256
|
+
return { text, truncated: true };
|
|
2257
|
+
}
|
|
2258
|
+
const cappedText = Buffer.from(text, "utf8").subarray(0, MAX_BYTES).toString("utf8");
|
|
2259
|
+
return { text: cappedText, truncated: true };
|
|
2260
|
+
}
|
|
2261
|
+
var ExecutionResultStore = class {
|
|
2262
|
+
entries = [];
|
|
2263
|
+
baseDir;
|
|
2264
|
+
vaultKey = null;
|
|
2265
|
+
constructor(filePath) {
|
|
2266
|
+
this.baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR6;
|
|
2267
|
+
this.loadPlaintext();
|
|
2268
|
+
}
|
|
2269
|
+
/** Mirrors OutcomeOutbox.setVaultKey — migrates plaintext -> encrypted once, in place. */
|
|
2270
|
+
setVaultKey(key) {
|
|
2271
|
+
this.vaultKey = key;
|
|
2272
|
+
if (existsSync9(this.encryptedFilePath)) {
|
|
2273
|
+
this.loadEncrypted();
|
|
2274
|
+
} else if (this.entries.length > 0) {
|
|
2275
|
+
this.persistEncrypted();
|
|
2276
|
+
if (existsSync9(this.plaintextFilePath)) {
|
|
2277
|
+
try {
|
|
2278
|
+
unlinkSync5(this.plaintextFilePath);
|
|
2279
|
+
} catch {
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* True when encrypted evidence exists on disk that this process cannot
|
|
2286
|
+
* read yet (no vault key). Callers should not report "nothing retained" in
|
|
2287
|
+
* this state — that would claim a hollow absence over unreadable content.
|
|
2288
|
+
*/
|
|
2289
|
+
isLocked() {
|
|
2290
|
+
return this.vaultKey === null && existsSync9(this.encryptedFilePath);
|
|
2291
|
+
}
|
|
2292
|
+
/** Record the outcome of one execution. Replaces any prior entry for the same proposal. */
|
|
2293
|
+
record(input, now = nowSec2()) {
|
|
2294
|
+
const capped = capResult(input.text, input.structuredContent);
|
|
2295
|
+
this.entries = this.entries.filter((e) => e.proposalId !== input.proposalId);
|
|
2296
|
+
this.entries.push({
|
|
2297
|
+
proposalId: input.proposalId,
|
|
2298
|
+
ticketId: input.ticketId,
|
|
2299
|
+
ok: input.ok,
|
|
2300
|
+
text: capped.text,
|
|
2301
|
+
...capped.structuredContent ? { structuredContent: capped.structuredContent } : {},
|
|
2302
|
+
...capped.truncated ? { truncated: true } : {},
|
|
2303
|
+
recordedAt: now
|
|
2304
|
+
});
|
|
2305
|
+
this.prune(now);
|
|
2306
|
+
this.persist();
|
|
2307
|
+
}
|
|
2308
|
+
/** The retained outcome for a proposal, or undefined if never recorded (or pruned). */
|
|
2309
|
+
get(proposalId) {
|
|
2310
|
+
return this.entries.find((e) => e.proposalId === proposalId);
|
|
2311
|
+
}
|
|
2312
|
+
/** Diagnostic / tests. */
|
|
2313
|
+
get size() {
|
|
2314
|
+
return this.entries.length;
|
|
2315
|
+
}
|
|
2316
|
+
// ─── Retention ────────────────────────────────────────────────────────────
|
|
2317
|
+
prune(now) {
|
|
2318
|
+
const cutoff = now - MAX_AGE_SECONDS3;
|
|
2319
|
+
this.entries = this.entries.filter((e) => e.recordedAt >= cutoff);
|
|
2320
|
+
}
|
|
2321
|
+
// ─── Encryption (mirrors OutcomeOutbox / DenialLog) ────────────────────────
|
|
2322
|
+
encrypt(plaintext) {
|
|
2323
|
+
if (!this.vaultKey) throw new Error("No vault key");
|
|
2324
|
+
const iv = randomBytes5(12);
|
|
2325
|
+
const cipher = createCipheriv5("aes-256-gcm", this.vaultKey, iv);
|
|
2326
|
+
const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2327
|
+
return { iv: iv.toString("hex"), ciphertext: enc.toString("hex"), tag: cipher.getAuthTag().toString("hex") };
|
|
2328
|
+
}
|
|
2329
|
+
decrypt(blob) {
|
|
2330
|
+
if (!this.vaultKey) throw new Error("No vault key");
|
|
2331
|
+
const decipher = createDecipheriv5("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
|
|
2332
|
+
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
2333
|
+
return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
|
|
2334
|
+
}
|
|
2335
|
+
get plaintextFilePath() {
|
|
2336
|
+
return join9(this.baseDir, "execution-results.json");
|
|
2337
|
+
}
|
|
2338
|
+
get encryptedFilePath() {
|
|
2339
|
+
return join9(this.baseDir, "execution-results.enc.json");
|
|
2340
|
+
}
|
|
2341
|
+
loadPlaintext() {
|
|
2342
|
+
if (!existsSync9(this.plaintextFilePath)) {
|
|
2343
|
+
mkdirSync7(this.baseDir, { recursive: true });
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
try {
|
|
2347
|
+
const data = JSON.parse(readFileSync10(this.plaintextFilePath, "utf-8"));
|
|
2348
|
+
this.entries = data.entries ?? [];
|
|
2349
|
+
this.prune(nowSec2());
|
|
2350
|
+
} catch {
|
|
2351
|
+
console.error(`[ExecutionResultStore] Could not parse ${this.plaintextFilePath}, starting fresh`);
|
|
2352
|
+
this.entries = [];
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
loadEncrypted() {
|
|
2356
|
+
if (!existsSync9(this.encryptedFilePath)) return;
|
|
2357
|
+
try {
|
|
2358
|
+
const data = JSON.parse(readFileSync10(this.encryptedFilePath, "utf-8"));
|
|
2359
|
+
const storeFile = JSON.parse(this.decrypt(data.blob));
|
|
2360
|
+
this.entries = storeFile.entries ?? [];
|
|
2361
|
+
this.prune(nowSec2());
|
|
2362
|
+
} catch (err) {
|
|
2363
|
+
console.error(`[ExecutionResultStore] Could not decrypt ${this.encryptedFilePath}:`, err);
|
|
2364
|
+
this.entries = [];
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
persist() {
|
|
2368
|
+
if (this.vaultKey) this.persistEncrypted();
|
|
2369
|
+
else this.persistPlaintext();
|
|
2370
|
+
}
|
|
2371
|
+
persistPlaintext() {
|
|
2372
|
+
const data = { version: 1, entries: this.entries };
|
|
2373
|
+
mkdirSync7(this.baseDir, { recursive: true, mode: 448 });
|
|
2374
|
+
writeFileSync7(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2375
|
+
}
|
|
2376
|
+
persistEncrypted() {
|
|
2377
|
+
const storeFile = { version: 1, entries: this.entries };
|
|
2378
|
+
const data = { version: 1, blob: this.encrypt(JSON.stringify(storeFile)) };
|
|
2379
|
+
mkdirSync7(this.baseDir, { recursive: true, mode: 448 });
|
|
2380
|
+
writeFileSync7(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2200
2383
|
function nowSec2() {
|
|
2201
2384
|
return Math.floor(Date.now() / 1e3);
|
|
2202
2385
|
}
|
|
2386
|
+
|
|
2387
|
+
// src/lib/proposal-submission-store.ts
|
|
2388
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8, existsSync as existsSync10, renameSync as renameSync2 } from "fs";
|
|
2389
|
+
import { homedir as homedir7 } from "os";
|
|
2390
|
+
import { dirname as dirname8, join as join10 } from "path";
|
|
2391
|
+
var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
|
|
2392
|
+
var FILE_NAME2 = "proposal-submissions.json";
|
|
2393
|
+
var MAX_AGE_SECONDS4 = 90 * 24 * 60 * 60;
|
|
2394
|
+
function nowSec3() {
|
|
2395
|
+
return Math.floor(Date.now() / 1e3);
|
|
2396
|
+
}
|
|
2203
2397
|
var ProposalSubmissionStore = class {
|
|
2204
2398
|
filePath;
|
|
2205
2399
|
constructor(filePath) {
|
|
2206
|
-
const baseDir = filePath && filePath.endsWith(".json") ?
|
|
2207
|
-
this.filePath =
|
|
2400
|
+
const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR7;
|
|
2401
|
+
this.filePath = join10(baseDir, FILE_NAME2);
|
|
2208
2402
|
}
|
|
2209
2403
|
/** Record what was submitted, right after a successful submitProposal(). */
|
|
2210
2404
|
record(entry) {
|
|
@@ -2217,7 +2411,7 @@ var ProposalSubmissionStore = class {
|
|
|
2217
2411
|
executionContextHash: hashToolArgs(entry.executionContext),
|
|
2218
2412
|
authorizationId: entry.authorizationId,
|
|
2219
2413
|
profileId: entry.profileId,
|
|
2220
|
-
submittedAt:
|
|
2414
|
+
submittedAt: nowSec3(),
|
|
2221
2415
|
...entry.previewHash ? { previewHash: entry.previewHash } : {}
|
|
2222
2416
|
});
|
|
2223
2417
|
this.persist(file);
|
|
@@ -2227,11 +2421,23 @@ var ProposalSubmissionStore = class {
|
|
|
2227
2421
|
get(proposalId) {
|
|
2228
2422
|
return this.load().entries.find((e) => e.proposalId === proposalId);
|
|
2229
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Every submission still on record (newest-pruned by `MAX_AGE_SECONDS`
|
|
2426
|
+
* — see `load`). "Awaiting others" stand-in (2026-10-15, real-gateway
|
|
2427
|
+
* e2e): until the Authority Server has a creator-scoped proposal query,
|
|
2428
|
+
* this (every id THIS gateway has ever submitted) is the only local
|
|
2429
|
+
* source for "proposals I created" — bin/http.ts's
|
|
2430
|
+
* `/internal/my-pending-proposals` resolves each id's CURRENT state from
|
|
2431
|
+
* the AS and filters to `status: 'pending'`.
|
|
2432
|
+
*/
|
|
2433
|
+
list() {
|
|
2434
|
+
return this.load().entries;
|
|
2435
|
+
}
|
|
2230
2436
|
load() {
|
|
2231
|
-
if (!
|
|
2437
|
+
if (!existsSync10(this.filePath)) return { version: 1, entries: [] };
|
|
2232
2438
|
try {
|
|
2233
|
-
const parsed = JSON.parse(
|
|
2234
|
-
const cutoff =
|
|
2439
|
+
const parsed = JSON.parse(readFileSync11(this.filePath, "utf-8"));
|
|
2440
|
+
const cutoff = nowSec3() - MAX_AGE_SECONDS4;
|
|
2235
2441
|
parsed.entries = (parsed.entries ?? []).filter((e) => e.submittedAt >= cutoff);
|
|
2236
2442
|
return parsed;
|
|
2237
2443
|
} catch {
|
|
@@ -2239,26 +2445,26 @@ var ProposalSubmissionStore = class {
|
|
|
2239
2445
|
}
|
|
2240
2446
|
}
|
|
2241
2447
|
persist(file) {
|
|
2242
|
-
|
|
2448
|
+
mkdirSync8(dirname8(this.filePath), { recursive: true, mode: 448 });
|
|
2243
2449
|
const tmpPath = `${this.filePath}.tmp`;
|
|
2244
|
-
|
|
2450
|
+
writeFileSync8(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2245
2451
|
renameSync2(tmpPath, this.filePath);
|
|
2246
2452
|
}
|
|
2247
2453
|
};
|
|
2248
2454
|
|
|
2249
2455
|
// src/lib/preview-log.ts
|
|
2250
|
-
import { mkdirSync as
|
|
2251
|
-
import { homedir as
|
|
2252
|
-
import { dirname as
|
|
2253
|
-
var
|
|
2456
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11, renameSync as renameSync3 } from "fs";
|
|
2457
|
+
import { homedir as homedir8 } from "os";
|
|
2458
|
+
import { dirname as dirname9, join as join11 } from "path";
|
|
2459
|
+
var DEFAULT_DIR8 = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
|
|
2254
2460
|
var FILE_NAME3 = "preview-log.json";
|
|
2255
2461
|
var MAX_RECORDS2 = 500;
|
|
2256
2462
|
var MAX_AGE_MS2 = 30 * 24 * 60 * 60 * 1e3;
|
|
2257
2463
|
var PreviewLog = class {
|
|
2258
2464
|
filePath;
|
|
2259
2465
|
constructor(filePath) {
|
|
2260
|
-
const baseDir = filePath && filePath.endsWith(".json") ?
|
|
2261
|
-
this.filePath =
|
|
2466
|
+
const baseDir = filePath && filePath.endsWith(".json") ? dirname9(filePath) : filePath ?? DEFAULT_DIR8;
|
|
2467
|
+
this.filePath = join11(baseDir, FILE_NAME3);
|
|
2262
2468
|
}
|
|
2263
2469
|
/** Record one preview read. Never throws — recording must not break the read. */
|
|
2264
2470
|
record(entry) {
|
|
@@ -2280,28 +2486,28 @@ var PreviewLog = class {
|
|
|
2280
2486
|
return [...this.load().entries];
|
|
2281
2487
|
}
|
|
2282
2488
|
load() {
|
|
2283
|
-
if (!
|
|
2489
|
+
if (!existsSync11(this.filePath)) return { version: 1, entries: [] };
|
|
2284
2490
|
try {
|
|
2285
|
-
const parsed = JSON.parse(
|
|
2491
|
+
const parsed = JSON.parse(readFileSync12(this.filePath, "utf-8"));
|
|
2286
2492
|
return { version: 1, entries: parsed.entries ?? [] };
|
|
2287
2493
|
} catch {
|
|
2288
2494
|
return { version: 1, entries: [] };
|
|
2289
2495
|
}
|
|
2290
2496
|
}
|
|
2291
2497
|
persist(file) {
|
|
2292
|
-
|
|
2498
|
+
mkdirSync9(dirname9(this.filePath), { recursive: true, mode: 448 });
|
|
2293
2499
|
const tmp = `${this.filePath}.tmp`;
|
|
2294
|
-
|
|
2500
|
+
writeFileSync9(tmp, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2295
2501
|
renameSync3(tmp, this.filePath);
|
|
2296
2502
|
}
|
|
2297
2503
|
};
|
|
2298
2504
|
|
|
2299
2505
|
// src/lib/outcome-outbox.ts
|
|
2300
|
-
import { mkdirSync as
|
|
2301
|
-
import { homedir as
|
|
2302
|
-
import { dirname as
|
|
2303
|
-
import { createCipheriv as
|
|
2304
|
-
var
|
|
2506
|
+
import { mkdirSync as mkdirSync10, readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync12, unlinkSync as unlinkSync6 } from "fs";
|
|
2507
|
+
import { homedir as homedir9 } from "os";
|
|
2508
|
+
import { dirname as dirname10, join as join12 } from "path";
|
|
2509
|
+
import { createCipheriv as createCipheriv6, createDecipheriv as createDecipheriv6, randomBytes as randomBytes6 } from "crypto";
|
|
2510
|
+
var DEFAULT_DIR9 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
|
|
2305
2511
|
var MAX_AGE_MS3 = 7 * 24 * 60 * 60 * 1e3;
|
|
2306
2512
|
function outcomeEntryFor(proposal, submittedHere, who, nowMs) {
|
|
2307
2513
|
if (!submittedHere) return null;
|
|
@@ -2316,19 +2522,19 @@ var OutcomeOutbox = class {
|
|
|
2316
2522
|
baseDir;
|
|
2317
2523
|
vaultKey = null;
|
|
2318
2524
|
constructor(filePath) {
|
|
2319
|
-
this.baseDir = filePath && filePath.endsWith(".json") ?
|
|
2525
|
+
this.baseDir = filePath && filePath.endsWith(".json") ? dirname10(filePath) : filePath ?? DEFAULT_DIR9;
|
|
2320
2526
|
this.loadPlaintext();
|
|
2321
2527
|
}
|
|
2322
2528
|
/** Mirrors DenialLog.setVaultKey — migrates plaintext -> encrypted once, in place. */
|
|
2323
2529
|
setVaultKey(key) {
|
|
2324
2530
|
this.vaultKey = key;
|
|
2325
|
-
if (
|
|
2531
|
+
if (existsSync12(this.encryptedFilePath)) {
|
|
2326
2532
|
this.loadEncrypted();
|
|
2327
2533
|
} else if (this.entries.length > 0) {
|
|
2328
2534
|
this.persistEncrypted();
|
|
2329
|
-
if (
|
|
2535
|
+
if (existsSync12(this.plaintextFilePath)) {
|
|
2330
2536
|
try {
|
|
2331
|
-
|
|
2537
|
+
unlinkSync6(this.plaintextFilePath);
|
|
2332
2538
|
} catch {
|
|
2333
2539
|
}
|
|
2334
2540
|
}
|
|
@@ -2377,30 +2583,30 @@ var OutcomeOutbox = class {
|
|
|
2377
2583
|
// ─── Encryption (mirrors DenialLog / ExecutionLog) ─────────────────────────
|
|
2378
2584
|
encrypt(plaintext) {
|
|
2379
2585
|
if (!this.vaultKey) throw new Error("No vault key");
|
|
2380
|
-
const iv =
|
|
2381
|
-
const cipher =
|
|
2586
|
+
const iv = randomBytes6(12);
|
|
2587
|
+
const cipher = createCipheriv6("aes-256-gcm", this.vaultKey, iv);
|
|
2382
2588
|
const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2383
2589
|
return { iv: iv.toString("hex"), ciphertext: enc.toString("hex"), tag: cipher.getAuthTag().toString("hex") };
|
|
2384
2590
|
}
|
|
2385
2591
|
decrypt(blob) {
|
|
2386
2592
|
if (!this.vaultKey) throw new Error("No vault key");
|
|
2387
|
-
const decipher =
|
|
2593
|
+
const decipher = createDecipheriv6("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
|
|
2388
2594
|
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
2389
2595
|
return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
|
|
2390
2596
|
}
|
|
2391
2597
|
get plaintextFilePath() {
|
|
2392
|
-
return
|
|
2598
|
+
return join12(this.baseDir, "outcome-outbox.json");
|
|
2393
2599
|
}
|
|
2394
2600
|
get encryptedFilePath() {
|
|
2395
|
-
return
|
|
2601
|
+
return join12(this.baseDir, "outcome-outbox.enc.json");
|
|
2396
2602
|
}
|
|
2397
2603
|
loadPlaintext() {
|
|
2398
|
-
if (!
|
|
2399
|
-
|
|
2604
|
+
if (!existsSync12(this.plaintextFilePath)) {
|
|
2605
|
+
mkdirSync10(this.baseDir, { recursive: true });
|
|
2400
2606
|
return;
|
|
2401
2607
|
}
|
|
2402
2608
|
try {
|
|
2403
|
-
const data = JSON.parse(
|
|
2609
|
+
const data = JSON.parse(readFileSync13(this.plaintextFilePath, "utf-8"));
|
|
2404
2610
|
this.entries = data.entries ?? [];
|
|
2405
2611
|
this.prune(Date.now());
|
|
2406
2612
|
} catch {
|
|
@@ -2409,9 +2615,9 @@ var OutcomeOutbox = class {
|
|
|
2409
2615
|
}
|
|
2410
2616
|
}
|
|
2411
2617
|
loadEncrypted() {
|
|
2412
|
-
if (!
|
|
2618
|
+
if (!existsSync12(this.encryptedFilePath)) return;
|
|
2413
2619
|
try {
|
|
2414
|
-
const data = JSON.parse(
|
|
2620
|
+
const data = JSON.parse(readFileSync13(this.encryptedFilePath, "utf-8"));
|
|
2415
2621
|
const storeFile = JSON.parse(this.decrypt(data.blob));
|
|
2416
2622
|
this.entries = storeFile.entries ?? [];
|
|
2417
2623
|
this.prune(Date.now());
|
|
@@ -2426,14 +2632,14 @@ var OutcomeOutbox = class {
|
|
|
2426
2632
|
}
|
|
2427
2633
|
persistPlaintext() {
|
|
2428
2634
|
const data = { version: 1, entries: this.entries };
|
|
2429
|
-
|
|
2430
|
-
|
|
2635
|
+
mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
|
|
2636
|
+
writeFileSync10(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2431
2637
|
}
|
|
2432
2638
|
persistEncrypted() {
|
|
2433
2639
|
const storeFile = { version: 1, entries: this.entries };
|
|
2434
2640
|
const data = { version: 1, blob: this.encrypt(JSON.stringify(storeFile)) };
|
|
2435
|
-
|
|
2436
|
-
|
|
2641
|
+
mkdirSync10(this.baseDir, { recursive: true, mode: 448 });
|
|
2642
|
+
writeFileSync10(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2437
2643
|
}
|
|
2438
2644
|
};
|
|
2439
2645
|
function formatOutcomeLine(e) {
|
|
@@ -2545,10 +2751,10 @@ var MCPGatekeeper = class {
|
|
|
2545
2751
|
};
|
|
2546
2752
|
|
|
2547
2753
|
// src/lib/report/report-store.ts
|
|
2548
|
-
import { mkdirSync as
|
|
2549
|
-
import { homedir as
|
|
2550
|
-
import { dirname as
|
|
2551
|
-
import { createCipheriv as
|
|
2754
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync13, unlinkSync as unlinkSync7 } from "fs";
|
|
2755
|
+
import { homedir as homedir10 } from "os";
|
|
2756
|
+
import { dirname as dirname11, join as join13 } from "path";
|
|
2757
|
+
import { createCipheriv as createCipheriv7, createDecipheriv as createDecipheriv7, randomBytes as randomBytes7 } from "crypto";
|
|
2552
2758
|
|
|
2553
2759
|
// src/lib/report/record-resolvers.ts
|
|
2554
2760
|
function findById(rows, id) {
|
|
@@ -2856,29 +3062,29 @@ async function buildCoverage(caseElements, getExport, archive, referenced, windo
|
|
|
2856
3062
|
}
|
|
2857
3063
|
|
|
2858
3064
|
// src/lib/report/report-store.ts
|
|
2859
|
-
var
|
|
3065
|
+
var DEFAULT_DIR10 = process.env.SUVEREN_DATA_DIR ?? join13(homedir10(), ".suveren");
|
|
2860
3066
|
var ReportStore = class {
|
|
2861
3067
|
report = null;
|
|
2862
3068
|
baseDir;
|
|
2863
3069
|
vaultKey = null;
|
|
2864
3070
|
constructor(filePath) {
|
|
2865
3071
|
if (filePath && filePath.endsWith(".json")) {
|
|
2866
|
-
this.baseDir =
|
|
3072
|
+
this.baseDir = dirname11(filePath);
|
|
2867
3073
|
} else {
|
|
2868
|
-
this.baseDir = filePath ??
|
|
3074
|
+
this.baseDir = filePath ?? DEFAULT_DIR10;
|
|
2869
3075
|
}
|
|
2870
3076
|
this.loadPlaintext();
|
|
2871
3077
|
}
|
|
2872
3078
|
/** Set the vault key for encryption. Triggers migration from plaintext if needed. */
|
|
2873
3079
|
setVaultKey(key) {
|
|
2874
3080
|
this.vaultKey = key;
|
|
2875
|
-
if (
|
|
3081
|
+
if (existsSync13(this.encryptedFilePath)) {
|
|
2876
3082
|
this.loadEncrypted();
|
|
2877
3083
|
} else if (this.report !== null) {
|
|
2878
3084
|
this.persistEncrypted();
|
|
2879
|
-
if (
|
|
3085
|
+
if (existsSync13(this.plaintextFilePath)) {
|
|
2880
3086
|
try {
|
|
2881
|
-
|
|
3087
|
+
unlinkSync7(this.plaintextFilePath);
|
|
2882
3088
|
} catch {
|
|
2883
3089
|
}
|
|
2884
3090
|
}
|
|
@@ -2891,7 +3097,7 @@ var ReportStore = class {
|
|
|
2891
3097
|
* "the report exists but is locked".
|
|
2892
3098
|
*/
|
|
2893
3099
|
isLocked() {
|
|
2894
|
-
return this.vaultKey === null &&
|
|
3100
|
+
return this.vaultKey === null && existsSync13(this.encryptedFilePath);
|
|
2895
3101
|
}
|
|
2896
3102
|
/** The stored, already-verified report — or null if none has been saved
|
|
2897
3103
|
* (and the store is not locked; check `isLocked()` first). */
|
|
@@ -2925,49 +3131,49 @@ var ReportStore = class {
|
|
|
2925
3131
|
// ─── Encryption helpers ─────────────────────────────────────────────────
|
|
2926
3132
|
encrypt(plaintext) {
|
|
2927
3133
|
if (!this.vaultKey) throw new Error("No vault key");
|
|
2928
|
-
const iv =
|
|
2929
|
-
const cipher =
|
|
3134
|
+
const iv = randomBytes7(12);
|
|
3135
|
+
const cipher = createCipheriv7("aes-256-gcm", this.vaultKey, iv);
|
|
2930
3136
|
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
2931
3137
|
const tag = cipher.getAuthTag();
|
|
2932
3138
|
return { iv: iv.toString("hex"), ciphertext: encrypted.toString("hex"), tag: tag.toString("hex") };
|
|
2933
3139
|
}
|
|
2934
3140
|
decrypt(blob) {
|
|
2935
3141
|
if (!this.vaultKey) throw new Error("No vault key");
|
|
2936
|
-
const decipher =
|
|
3142
|
+
const decipher = createDecipheriv7("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
|
|
2937
3143
|
decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
|
|
2938
3144
|
const decrypted = Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]);
|
|
2939
3145
|
return decrypted.toString("utf8");
|
|
2940
3146
|
}
|
|
2941
3147
|
// ─── File paths ─────────────────────────────────────────────────────────
|
|
2942
3148
|
get plaintextFilePath() {
|
|
2943
|
-
return
|
|
3149
|
+
return join13(this.baseDir, "report.json");
|
|
2944
3150
|
}
|
|
2945
3151
|
get encryptedFilePath() {
|
|
2946
|
-
return
|
|
3152
|
+
return join13(this.baseDir, "report.enc.json");
|
|
2947
3153
|
}
|
|
2948
3154
|
// ─── Load / Persist ─────────────────────────────────────────────────────
|
|
2949
3155
|
loadPlaintext() {
|
|
2950
|
-
if (!
|
|
2951
|
-
|
|
3156
|
+
if (!existsSync13(this.plaintextFilePath)) {
|
|
3157
|
+
mkdirSync11(this.baseDir, { recursive: true });
|
|
2952
3158
|
return;
|
|
2953
3159
|
}
|
|
2954
3160
|
try {
|
|
2955
|
-
const raw =
|
|
3161
|
+
const raw = readFileSync14(this.plaintextFilePath, "utf-8");
|
|
2956
3162
|
const parsed = JSON.parse(raw);
|
|
2957
3163
|
this.report = parsed.report ?? null;
|
|
2958
3164
|
} catch {
|
|
2959
3165
|
console.error(`[ReportStore] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
|
|
2960
3166
|
try {
|
|
2961
|
-
|
|
3167
|
+
writeFileSync11(`${this.plaintextFilePath}.corrupt-${Date.now()}`, readFileSync14(this.plaintextFilePath));
|
|
2962
3168
|
} catch {
|
|
2963
3169
|
}
|
|
2964
3170
|
this.report = null;
|
|
2965
3171
|
}
|
|
2966
3172
|
}
|
|
2967
3173
|
loadEncrypted() {
|
|
2968
|
-
if (!
|
|
3174
|
+
if (!existsSync13(this.encryptedFilePath)) return;
|
|
2969
3175
|
try {
|
|
2970
|
-
const raw =
|
|
3176
|
+
const raw = readFileSync14(this.encryptedFilePath, "utf-8");
|
|
2971
3177
|
const data = JSON.parse(raw);
|
|
2972
3178
|
const decrypted = this.decrypt(data.blob);
|
|
2973
3179
|
const parsed = JSON.parse(decrypted);
|
|
@@ -2975,7 +3181,7 @@ var ReportStore = class {
|
|
|
2975
3181
|
} catch (err) {
|
|
2976
3182
|
console.error(`[ReportStore] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
|
|
2977
3183
|
try {
|
|
2978
|
-
|
|
3184
|
+
writeFileSync11(`${this.encryptedFilePath}.corrupt-${Date.now()}`, readFileSync14(this.encryptedFilePath));
|
|
2979
3185
|
} catch {
|
|
2980
3186
|
}
|
|
2981
3187
|
this.report = null;
|
|
@@ -2990,16 +3196,16 @@ var ReportStore = class {
|
|
|
2990
3196
|
}
|
|
2991
3197
|
persistPlaintext() {
|
|
2992
3198
|
const data = { version: 1, report: this.report };
|
|
2993
|
-
|
|
2994
|
-
|
|
3199
|
+
mkdirSync11(this.baseDir, { recursive: true, mode: 448 });
|
|
3200
|
+
writeFileSync11(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
2995
3201
|
}
|
|
2996
3202
|
persistEncrypted() {
|
|
2997
3203
|
const data = {
|
|
2998
3204
|
version: 1,
|
|
2999
3205
|
blob: this.encrypt(JSON.stringify({ version: 1, report: this.report }))
|
|
3000
3206
|
};
|
|
3001
|
-
|
|
3002
|
-
|
|
3207
|
+
mkdirSync11(this.baseDir, { recursive: true, mode: 448 });
|
|
3208
|
+
writeFileSync11(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
|
|
3003
3209
|
}
|
|
3004
3210
|
};
|
|
3005
3211
|
|
|
@@ -3017,6 +3223,8 @@ var SharedState = class {
|
|
|
3017
3223
|
reportStore;
|
|
3018
3224
|
/** Which tickets this Gatekeeper has executed — the "one execution per ticket" half of exactly-once. */
|
|
3019
3225
|
executionJournal;
|
|
3226
|
+
/** The content half of what executionJournal records — see execution-result-store.ts. */
|
|
3227
|
+
executionResults;
|
|
3020
3228
|
/** What THIS gateway submitted for each review-mode proposal it created —
|
|
3021
3229
|
* see ticket-verify.ts / commitments.ts. */
|
|
3022
3230
|
proposalSubmissions;
|
|
@@ -3057,6 +3265,7 @@ var SharedState = class {
|
|
|
3057
3265
|
this.receiptArchive = new ReceiptArchive(gateStorePath);
|
|
3058
3266
|
this.reportStore = new ReportStore(gateStorePath);
|
|
3059
3267
|
this.executionJournal = new ExecutionJournal(gateStorePath);
|
|
3268
|
+
this.executionResults = new ExecutionResultStore(gateStorePath);
|
|
3060
3269
|
this.proposalSubmissions = new ProposalSubmissionStore(gateStorePath);
|
|
3061
3270
|
this.previewLog = new PreviewLog(gateStorePath);
|
|
3062
3271
|
this.outcomeOutbox = new OutcomeOutbox(gateStorePath);
|
|
@@ -3180,7 +3389,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3180
3389
|
import { z as z2 } from "zod";
|
|
3181
3390
|
|
|
3182
3391
|
// src/lib/mandate-brief.ts
|
|
3183
|
-
import { getProfile as
|
|
3392
|
+
import { getProfile as getProfile5 } from "@hap/core";
|
|
3184
3393
|
|
|
3185
3394
|
// src/lib/consumption.ts
|
|
3186
3395
|
import { boundActionTypes } from "@hap/core";
|
|
@@ -3248,29 +3457,29 @@ function formatConsumptionFull(entries) {
|
|
|
3248
3457
|
}
|
|
3249
3458
|
|
|
3250
3459
|
// src/lib/context-loader.ts
|
|
3251
|
-
import { readFileSync as
|
|
3252
|
-
import { homedir as
|
|
3253
|
-
import { join as
|
|
3254
|
-
var
|
|
3460
|
+
import { readFileSync as readFileSync15, existsSync as existsSync14, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12, renameSync as renameSync4 } from "fs";
|
|
3461
|
+
import { homedir as homedir11 } from "os";
|
|
3462
|
+
import { join as join14, dirname as dirname12 } from "path";
|
|
3463
|
+
var DEFAULT_DIR11 = process.env.SUVEREN_DATA_DIR ?? join14(homedir11(), ".suveren");
|
|
3255
3464
|
var CONTEXT_MAX_BYTES = 16 * 1024;
|
|
3256
3465
|
function contextFilePath(dataDir2) {
|
|
3257
|
-
return
|
|
3466
|
+
return join14(dataDir2 ?? DEFAULT_DIR11, "context.md");
|
|
3258
3467
|
}
|
|
3259
3468
|
function writeContextFile(content, dataDir2) {
|
|
3260
3469
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
3261
3470
|
if (bytes > CONTEXT_MAX_BYTES) throw new Error(`the brief is ${bytes} bytes; the limit is ${CONTEXT_MAX_BYTES}.`);
|
|
3262
3471
|
const filePath = contextFilePath(dataDir2);
|
|
3263
|
-
|
|
3472
|
+
mkdirSync12(dirname12(filePath), { recursive: true });
|
|
3264
3473
|
const tmpPath = `${filePath}.tmp`;
|
|
3265
|
-
|
|
3474
|
+
writeFileSync12(tmpPath, content, "utf-8");
|
|
3266
3475
|
renameSync4(tmpPath, filePath);
|
|
3267
3476
|
}
|
|
3268
3477
|
var BRIEF_MAX_CHARS = 16 * 1024;
|
|
3269
3478
|
function readContextFile(dataDir2) {
|
|
3270
3479
|
const filePath = contextFilePath(dataDir2);
|
|
3271
|
-
if (!
|
|
3480
|
+
if (!existsSync14(filePath)) return null;
|
|
3272
3481
|
try {
|
|
3273
|
-
const content =
|
|
3482
|
+
const content = readFileSync15(filePath, "utf-8").trim();
|
|
3274
3483
|
return content || null;
|
|
3275
3484
|
} catch {
|
|
3276
3485
|
return null;
|
|
@@ -3463,18 +3672,239 @@ function attachTicketId(tool, args, ticketId) {
|
|
|
3463
3672
|
return { ...args, ticket_id: ticketId };
|
|
3464
3673
|
}
|
|
3465
3674
|
|
|
3466
|
-
// src/lib/preview.ts
|
|
3467
|
-
import { computeContentHash as computeContentHash2 } from "@hap/core";
|
|
3468
|
-
|
|
3469
|
-
// src/lib/manifest-loader.ts
|
|
3470
|
-
import { readFileSync as
|
|
3471
|
-
import { join as
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
var
|
|
3476
|
-
var
|
|
3477
|
-
|
|
3675
|
+
// src/lib/preview.ts
|
|
3676
|
+
import { computeContentHash as computeContentHash2 } from "@hap/core";
|
|
3677
|
+
|
|
3678
|
+
// src/lib/manifest-loader.ts
|
|
3679
|
+
import { readFileSync as readFileSync16, existsSync as existsSync15 } from "fs";
|
|
3680
|
+
import { join as join15, resolve } from "path";
|
|
3681
|
+
import { getProfile as getProfile2 } from "@hap/core";
|
|
3682
|
+
|
|
3683
|
+
// src/lib/tool-gating-types.ts
|
|
3684
|
+
var MAX_PREVIEW_READS = 10;
|
|
3685
|
+
var MAX_FOREACH_ELEMENTS = 50;
|
|
3686
|
+
var MAX_RECORD_KEY_STEPS = 2;
|
|
3687
|
+
|
|
3688
|
+
// src/lib/scope-specificity.ts
|
|
3689
|
+
function tokenSet(value) {
|
|
3690
|
+
if (value === void 0 || value === null) return /* @__PURE__ */ new Set();
|
|
3691
|
+
return new Set(
|
|
3692
|
+
String(value).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
3693
|
+
);
|
|
3694
|
+
}
|
|
3695
|
+
function subsetOrEqual(a, b) {
|
|
3696
|
+
if (b.size === 0) return true;
|
|
3697
|
+
if (a.size === 0) return false;
|
|
3698
|
+
for (const x of a) if (!b.has(x)) return false;
|
|
3699
|
+
return true;
|
|
3700
|
+
}
|
|
3701
|
+
function atLeastAsSpecific(keys, a, b) {
|
|
3702
|
+
for (const k of keys) {
|
|
3703
|
+
if (!subsetOrEqual(tokenSet(a[k]), tokenSet(b[k]))) return false;
|
|
3704
|
+
}
|
|
3705
|
+
return true;
|
|
3706
|
+
}
|
|
3707
|
+
function strictlyMoreSpecific(keys, a, b) {
|
|
3708
|
+
return atLeastAsSpecific(keys, a, b) && !atLeastAsSpecific(keys, b, a);
|
|
3709
|
+
}
|
|
3710
|
+
function selectAuthorization(contextKeys, passers) {
|
|
3711
|
+
if (passers.length === 1) {
|
|
3712
|
+
return { chosen: passers[0], superseded: [], reason: "sole" };
|
|
3713
|
+
}
|
|
3714
|
+
const dominators = passers.filter(
|
|
3715
|
+
(a) => passers.every((b) => b === a || strictlyMoreSpecific(contextKeys, a.context, b.context))
|
|
3716
|
+
);
|
|
3717
|
+
if (dominators.length === 1) {
|
|
3718
|
+
const chosen2 = dominators[0];
|
|
3719
|
+
return { chosen: chosen2, superseded: passers.filter((p) => p !== chosen2), reason: "most-specific" };
|
|
3720
|
+
}
|
|
3721
|
+
const approvers = passers.filter((p) => p.requiresApproval);
|
|
3722
|
+
const pool = approvers.length > 0 ? approvers : passers;
|
|
3723
|
+
const chosen = pool.slice().sort((a, b) => a.id.localeCompare(b.id))[0];
|
|
3724
|
+
return {
|
|
3725
|
+
chosen,
|
|
3726
|
+
superseded: passers.filter((p) => p !== chosen),
|
|
3727
|
+
reason: approvers.length > 0 ? "fail-safe-approval" : "fail-safe-tiebreak"
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
// src/lib/read-gate.ts
|
|
3732
|
+
function boundsSatisfyReadGate(bounds, gate) {
|
|
3733
|
+
if (!gate.boundField) return true;
|
|
3734
|
+
const actual = bounds?.[gate.boundField];
|
|
3735
|
+
return actual !== void 0 && String(actual) === gate.requiredValue;
|
|
3736
|
+
}
|
|
3737
|
+
function readToolIsGoverned(g) {
|
|
3738
|
+
if (!g) return false;
|
|
3739
|
+
return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
|
|
3740
|
+
}
|
|
3741
|
+
function allowedResources(contexts, boundField) {
|
|
3742
|
+
const out = /* @__PURE__ */ new Set();
|
|
3743
|
+
for (const ctx of contexts) {
|
|
3744
|
+
for (const v of tokenSet(ctx?.[boundField])) out.add(v);
|
|
3745
|
+
}
|
|
3746
|
+
return out;
|
|
3747
|
+
}
|
|
3748
|
+
function deniedResources(requested, allowed) {
|
|
3749
|
+
return requested.filter((r) => !allowed.has(r));
|
|
3750
|
+
}
|
|
3751
|
+
function filterItemsByResource(items, idPath, allowed) {
|
|
3752
|
+
return items.filter((it) => {
|
|
3753
|
+
const id = getByDottedPath(it, idPath);
|
|
3754
|
+
return typeof id === "string" && allowed.has(id);
|
|
3755
|
+
});
|
|
3756
|
+
}
|
|
3757
|
+
function hasBlockedValue(item, valuesPath, blocked) {
|
|
3758
|
+
const v = getByDottedPath(item, valuesPath);
|
|
3759
|
+
const arr = Array.isArray(v) ? v : v != null ? [v] : [];
|
|
3760
|
+
return arr.some((x) => typeof x === "string" && blocked.has(x));
|
|
3761
|
+
}
|
|
3762
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
3763
|
+
function parseMessageTimestamp(value) {
|
|
3764
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3765
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
3766
|
+
const n = Number(value);
|
|
3767
|
+
if (Number.isFinite(n)) return n;
|
|
3768
|
+
const iso = Date.parse(value);
|
|
3769
|
+
if (Number.isFinite(iso)) return iso;
|
|
3770
|
+
}
|
|
3771
|
+
return null;
|
|
3772
|
+
}
|
|
3773
|
+
function ageInDays(timestampMs, nowMs) {
|
|
3774
|
+
return Math.floor((nowMs - timestampMs) / MS_PER_DAY);
|
|
3775
|
+
}
|
|
3776
|
+
function maxReadAgeDays(boundsList, field) {
|
|
3777
|
+
let max = null;
|
|
3778
|
+
for (const b of boundsList) {
|
|
3779
|
+
const raw = b?.[field];
|
|
3780
|
+
const n = typeof raw === "number" ? raw : raw !== void 0 ? Number(raw) : NaN;
|
|
3781
|
+
if (Number.isFinite(n)) max = max === null ? n : Math.max(max, n);
|
|
3782
|
+
}
|
|
3783
|
+
return max;
|
|
3784
|
+
}
|
|
3785
|
+
function isOlderThanMaxAge(timestampMs, maxAgeDays, nowMs) {
|
|
3786
|
+
if (timestampMs === null) return true;
|
|
3787
|
+
return ageInDays(timestampMs, nowMs) > maxAgeDays;
|
|
3788
|
+
}
|
|
3789
|
+
var TRAILING_OPERATOR = /(?:^|\s)(?:OR|AND|NOT)$|[-+|&:]$/i;
|
|
3790
|
+
function parensBalanced(s) {
|
|
3791
|
+
let depth = 0;
|
|
3792
|
+
let inQuote = false;
|
|
3793
|
+
for (const c of s) {
|
|
3794
|
+
if (c === '"') {
|
|
3795
|
+
inQuote = !inQuote;
|
|
3796
|
+
continue;
|
|
3797
|
+
}
|
|
3798
|
+
if (inQuote) continue;
|
|
3799
|
+
if (c === "(") depth++;
|
|
3800
|
+
else if (c === ")" && --depth < 0) return false;
|
|
3801
|
+
}
|
|
3802
|
+
return !inQuote && depth === 0;
|
|
3803
|
+
}
|
|
3804
|
+
function composeReadQuery(agentQuery, clauses) {
|
|
3805
|
+
const injected = clauses.filter((c) => typeof c === "string" && c.trim() !== "").map((c) => c.trim());
|
|
3806
|
+
const base = (agentQuery ?? "").trim();
|
|
3807
|
+
if (base === "") return { ok: true, query: injected.join(" ") };
|
|
3808
|
+
if (!parensBalanced(base)) {
|
|
3809
|
+
return { ok: false, reason: "the requested search query has unbalanced parentheses" };
|
|
3810
|
+
}
|
|
3811
|
+
if (TRAILING_OPERATOR.test(base)) {
|
|
3812
|
+
return { ok: false, reason: "the requested search query ends in a dangling operator" };
|
|
3813
|
+
}
|
|
3814
|
+
if (injected.length === 0) return { ok: true, query: base };
|
|
3815
|
+
return { ok: true, query: `(${base}) ${injected.join(" ")}` };
|
|
3816
|
+
}
|
|
3817
|
+
function detectAgeConflict(agentQuery, conflictPattern, maxAgeDays) {
|
|
3818
|
+
if (!agentQuery || !conflictPattern || maxAgeDays === null) return null;
|
|
3819
|
+
let re;
|
|
3820
|
+
try {
|
|
3821
|
+
re = new RegExp(conflictPattern, "gi");
|
|
3822
|
+
} catch {
|
|
3823
|
+
return null;
|
|
3824
|
+
}
|
|
3825
|
+
let worst = null;
|
|
3826
|
+
for (const match of agentQuery.matchAll(re)) {
|
|
3827
|
+
const days = Number(match[1]);
|
|
3828
|
+
if (!Number.isFinite(days)) continue;
|
|
3829
|
+
if (days >= maxAgeDays && (worst === null || days > worst)) worst = days;
|
|
3830
|
+
}
|
|
3831
|
+
return worst;
|
|
3832
|
+
}
|
|
3833
|
+
function resolveAgeBoundField(boundsSchema, ageExecField) {
|
|
3834
|
+
const fields = boundsSchema?.fields;
|
|
3835
|
+
if (!fields) return null;
|
|
3836
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
3837
|
+
if (def?.boundType?.kind === "per_transaction" && def.boundType.of === ageExecField) return name;
|
|
3838
|
+
}
|
|
3839
|
+
return null;
|
|
3840
|
+
}
|
|
3841
|
+
function classifyEnumSwitches(boundsSchema) {
|
|
3842
|
+
const readSide = [];
|
|
3843
|
+
const writeSide = [];
|
|
3844
|
+
for (const [name, def] of Object.entries(boundsSchema?.fields ?? {})) {
|
|
3845
|
+
if (def?.boundType?.kind !== "enum") continue;
|
|
3846
|
+
const values = new Set(def.boundType.values ?? []);
|
|
3847
|
+
if (!values.has("none")) continue;
|
|
3848
|
+
if (values.size === 2 && values.has("unlimited")) readSide.push(name);
|
|
3849
|
+
else writeSide.push(name);
|
|
3850
|
+
}
|
|
3851
|
+
return { readSide, writeSide };
|
|
3852
|
+
}
|
|
3853
|
+
function uncoveredReadSwitchReason(overrides, boundsSchema) {
|
|
3854
|
+
const { readSide, writeSide } = classifyEnumSwitches(boundsSchema);
|
|
3855
|
+
const anySwitch = [...readSide, ...writeSide];
|
|
3856
|
+
if (anySwitch.length === 0) return null;
|
|
3857
|
+
const valuesOf = (field) => boundsSchema?.fields?.[field]?.boundType?.values ?? [];
|
|
3858
|
+
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
3859
|
+
if (entry !== null && typeof entry !== "object") continue;
|
|
3860
|
+
const e = entry ?? {};
|
|
3861
|
+
if (entry !== null && e.category !== "read") continue;
|
|
3862
|
+
if (e.readGovernance === "none") {
|
|
3863
|
+
if (!e.readGovernanceReason) {
|
|
3864
|
+
return `override "${toolName}" declares readGovernance:"none" with no readGovernanceReason \u2014 an exemption from this profile's switch(es) [${anySwitch.join(", ")}] needs a written reason`;
|
|
3865
|
+
}
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
if (!e.boundField || !anySwitch.includes(e.boundField)) {
|
|
3869
|
+
return `override "${toolName}" is category:"read" under a profile that declares enum switch(es) [${anySwitch.join(", ")}] (read-side: [${readSide.join(", ")}], write-side: [${writeSide.join(", ")}]), but does not reference one via boundField (and carries no readGovernance exemption) \u2014 it would silently ignore e.g. "${anySwitch[0]}: none"`;
|
|
3870
|
+
}
|
|
3871
|
+
const positiveValues = valuesOf(e.boundField).filter((v) => v !== "none");
|
|
3872
|
+
if (!e.requiredValue || !positiveValues.includes(e.requiredValue)) {
|
|
3873
|
+
return `override "${toolName}".requiredValue for boundField "${e.boundField}" must be ${positiveValues.map((v) => `"${v}"`).join(" or ")}, not ${JSON.stringify(e.requiredValue)} \u2014 requiring "none" (or an unlisted value) would invert the gate`;
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3878
|
+
function getByDottedPath(obj, path) {
|
|
3879
|
+
return path.split(".").reduce(
|
|
3880
|
+
(o, k) => o == null ? void 0 : o[k],
|
|
3881
|
+
obj
|
|
3882
|
+
);
|
|
3883
|
+
}
|
|
3884
|
+
function firstParsableDate(obj, paths) {
|
|
3885
|
+
for (const path of Array.isArray(paths) ? paths : [paths]) {
|
|
3886
|
+
const parsed = parseMessageTimestamp(getByDottedPath(obj, path));
|
|
3887
|
+
if (parsed !== null) return parsed;
|
|
3888
|
+
}
|
|
3889
|
+
return null;
|
|
3890
|
+
}
|
|
3891
|
+
function renderAgeConstraint(template, maxAgeDays, nowMs) {
|
|
3892
|
+
const floorIso = new Date(nowMs - maxAgeDays * MS_PER_DAY).toISOString();
|
|
3893
|
+
return template.replace(/\{days\}/g, String(maxAgeDays)).replace(/\{date\}/g, floorIso.slice(0, 10));
|
|
3894
|
+
}
|
|
3895
|
+
function ageFloorValue(maxAgeDays, nowMs, format = "iso") {
|
|
3896
|
+
const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
|
|
3897
|
+
if (format === "epoch_ms") return floorMs;
|
|
3898
|
+
if (format === "epoch_s") return String(Math.floor(floorMs / 1e3));
|
|
3899
|
+
return new Date(floorMs).toISOString();
|
|
3900
|
+
}
|
|
3901
|
+
function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
|
|
3902
|
+
const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
|
|
3903
|
+
const asked = parseMessageTimestamp(requested);
|
|
3904
|
+
if (asked !== null && asked >= floorMs) return requested;
|
|
3905
|
+
return ageFloorValue(maxAgeDays, nowMs, format);
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3478
3908
|
// src/lib/manifest-loader.ts
|
|
3479
3909
|
var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
3480
3910
|
function isExactSemver(value) {
|
|
@@ -3823,28 +4253,34 @@ function invalidManifestContextReason(manifest) {
|
|
|
3823
4253
|
}
|
|
3824
4254
|
return null;
|
|
3825
4255
|
}
|
|
4256
|
+
function invalidManifestReadGateReason(manifest) {
|
|
4257
|
+
const profileDef = getProfile2(manifest.profile);
|
|
4258
|
+
if (!profileDef) return null;
|
|
4259
|
+
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4260
|
+
return uncoveredReadSwitchReason(overrides, profileDef.boundsSchema);
|
|
4261
|
+
}
|
|
3826
4262
|
var manifests = /* @__PURE__ */ new Map();
|
|
3827
4263
|
function loadManifests(integrationsDir) {
|
|
3828
4264
|
const dir = resolve(
|
|
3829
|
-
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ??
|
|
4265
|
+
integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join15(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
|
|
3830
4266
|
);
|
|
3831
|
-
const indexPath =
|
|
3832
|
-
if (!
|
|
4267
|
+
const indexPath = join15(dir, "index.json");
|
|
4268
|
+
if (!existsSync15(indexPath)) {
|
|
3833
4269
|
console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
|
|
3834
4270
|
return 0;
|
|
3835
4271
|
}
|
|
3836
4272
|
let index;
|
|
3837
4273
|
try {
|
|
3838
|
-
index = JSON.parse(
|
|
4274
|
+
index = JSON.parse(readFileSync16(indexPath, "utf-8"));
|
|
3839
4275
|
} catch (err) {
|
|
3840
4276
|
console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
|
|
3841
4277
|
return 0;
|
|
3842
4278
|
}
|
|
3843
4279
|
let loaded = 0;
|
|
3844
4280
|
for (const [id, relativePath] of Object.entries(index.integrations)) {
|
|
3845
|
-
const manifestPath =
|
|
4281
|
+
const manifestPath = join15(dir, relativePath);
|
|
3846
4282
|
try {
|
|
3847
|
-
const manifest = JSON.parse(
|
|
4283
|
+
const manifest = JSON.parse(readFileSync16(manifestPath, "utf-8"));
|
|
3848
4284
|
const pinReason = invalidNpmPinReason(manifest);
|
|
3849
4285
|
if (pinReason) {
|
|
3850
4286
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${pinReason}`);
|
|
@@ -3860,6 +4296,11 @@ function loadManifests(integrationsDir) {
|
|
|
3860
4296
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${contextReason}`);
|
|
3861
4297
|
continue;
|
|
3862
4298
|
}
|
|
4299
|
+
const readGateReason = invalidManifestReadGateReason(manifest);
|
|
4300
|
+
if (readGateReason) {
|
|
4301
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${readGateReason}`);
|
|
4302
|
+
continue;
|
|
4303
|
+
}
|
|
3863
4304
|
manifests.set(id, manifest);
|
|
3864
4305
|
loaded++;
|
|
3865
4306
|
} catch (err) {
|
|
@@ -4144,189 +4585,6 @@ async function buildInternalPreview(integrationManager2, integrationId, toolName
|
|
|
4144
4585
|
});
|
|
4145
4586
|
}
|
|
4146
4587
|
|
|
4147
|
-
// src/lib/scope-specificity.ts
|
|
4148
|
-
function tokenSet(value) {
|
|
4149
|
-
if (value === void 0 || value === null) return /* @__PURE__ */ new Set();
|
|
4150
|
-
return new Set(
|
|
4151
|
-
String(value).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
4152
|
-
);
|
|
4153
|
-
}
|
|
4154
|
-
function subsetOrEqual(a, b) {
|
|
4155
|
-
if (b.size === 0) return true;
|
|
4156
|
-
if (a.size === 0) return false;
|
|
4157
|
-
for (const x of a) if (!b.has(x)) return false;
|
|
4158
|
-
return true;
|
|
4159
|
-
}
|
|
4160
|
-
function atLeastAsSpecific(keys, a, b) {
|
|
4161
|
-
for (const k of keys) {
|
|
4162
|
-
if (!subsetOrEqual(tokenSet(a[k]), tokenSet(b[k]))) return false;
|
|
4163
|
-
}
|
|
4164
|
-
return true;
|
|
4165
|
-
}
|
|
4166
|
-
function strictlyMoreSpecific(keys, a, b) {
|
|
4167
|
-
return atLeastAsSpecific(keys, a, b) && !atLeastAsSpecific(keys, b, a);
|
|
4168
|
-
}
|
|
4169
|
-
function selectAuthorization(contextKeys, passers) {
|
|
4170
|
-
if (passers.length === 1) {
|
|
4171
|
-
return { chosen: passers[0], superseded: [], reason: "sole" };
|
|
4172
|
-
}
|
|
4173
|
-
const dominators = passers.filter(
|
|
4174
|
-
(a) => passers.every((b) => b === a || strictlyMoreSpecific(contextKeys, a.context, b.context))
|
|
4175
|
-
);
|
|
4176
|
-
if (dominators.length === 1) {
|
|
4177
|
-
const chosen2 = dominators[0];
|
|
4178
|
-
return { chosen: chosen2, superseded: passers.filter((p) => p !== chosen2), reason: "most-specific" };
|
|
4179
|
-
}
|
|
4180
|
-
const approvers = passers.filter((p) => p.requiresApproval);
|
|
4181
|
-
const pool = approvers.length > 0 ? approvers : passers;
|
|
4182
|
-
const chosen = pool.slice().sort((a, b) => a.id.localeCompare(b.id))[0];
|
|
4183
|
-
return {
|
|
4184
|
-
chosen,
|
|
4185
|
-
superseded: passers.filter((p) => p !== chosen),
|
|
4186
|
-
reason: approvers.length > 0 ? "fail-safe-approval" : "fail-safe-tiebreak"
|
|
4187
|
-
};
|
|
4188
|
-
}
|
|
4189
|
-
|
|
4190
|
-
// src/lib/read-gate.ts
|
|
4191
|
-
function boundsSatisfyReadGate(bounds, gate) {
|
|
4192
|
-
if (!gate.boundField) return true;
|
|
4193
|
-
const actual = bounds?.[gate.boundField];
|
|
4194
|
-
return actual !== void 0 && String(actual) === gate.requiredValue;
|
|
4195
|
-
}
|
|
4196
|
-
function readToolIsGoverned(g) {
|
|
4197
|
-
if (!g) return false;
|
|
4198
|
-
return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
|
|
4199
|
-
}
|
|
4200
|
-
function allowedResources(contexts, boundField) {
|
|
4201
|
-
const out = /* @__PURE__ */ new Set();
|
|
4202
|
-
for (const ctx of contexts) {
|
|
4203
|
-
for (const v of tokenSet(ctx?.[boundField])) out.add(v);
|
|
4204
|
-
}
|
|
4205
|
-
return out;
|
|
4206
|
-
}
|
|
4207
|
-
function deniedResources(requested, allowed) {
|
|
4208
|
-
return requested.filter((r) => !allowed.has(r));
|
|
4209
|
-
}
|
|
4210
|
-
function filterItemsByResource(items, idPath, allowed) {
|
|
4211
|
-
return items.filter((it) => {
|
|
4212
|
-
const id = getByDottedPath(it, idPath);
|
|
4213
|
-
return typeof id === "string" && allowed.has(id);
|
|
4214
|
-
});
|
|
4215
|
-
}
|
|
4216
|
-
function hasBlockedValue(item, valuesPath, blocked) {
|
|
4217
|
-
const v = getByDottedPath(item, valuesPath);
|
|
4218
|
-
const arr = Array.isArray(v) ? v : v != null ? [v] : [];
|
|
4219
|
-
return arr.some((x) => typeof x === "string" && blocked.has(x));
|
|
4220
|
-
}
|
|
4221
|
-
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
4222
|
-
function parseMessageTimestamp(value) {
|
|
4223
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
4224
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
4225
|
-
const n = Number(value);
|
|
4226
|
-
if (Number.isFinite(n)) return n;
|
|
4227
|
-
const iso = Date.parse(value);
|
|
4228
|
-
if (Number.isFinite(iso)) return iso;
|
|
4229
|
-
}
|
|
4230
|
-
return null;
|
|
4231
|
-
}
|
|
4232
|
-
function ageInDays(timestampMs, nowMs) {
|
|
4233
|
-
return Math.floor((nowMs - timestampMs) / MS_PER_DAY);
|
|
4234
|
-
}
|
|
4235
|
-
function maxReadAgeDays(boundsList, field) {
|
|
4236
|
-
let max = null;
|
|
4237
|
-
for (const b of boundsList) {
|
|
4238
|
-
const raw = b?.[field];
|
|
4239
|
-
const n = typeof raw === "number" ? raw : raw !== void 0 ? Number(raw) : NaN;
|
|
4240
|
-
if (Number.isFinite(n)) max = max === null ? n : Math.max(max, n);
|
|
4241
|
-
}
|
|
4242
|
-
return max;
|
|
4243
|
-
}
|
|
4244
|
-
function isOlderThanMaxAge(timestampMs, maxAgeDays, nowMs) {
|
|
4245
|
-
if (timestampMs === null) return true;
|
|
4246
|
-
return ageInDays(timestampMs, nowMs) > maxAgeDays;
|
|
4247
|
-
}
|
|
4248
|
-
var TRAILING_OPERATOR = /(?:^|\s)(?:OR|AND|NOT)$|[-+|&:]$/i;
|
|
4249
|
-
function parensBalanced(s) {
|
|
4250
|
-
let depth = 0;
|
|
4251
|
-
let inQuote = false;
|
|
4252
|
-
for (const c of s) {
|
|
4253
|
-
if (c === '"') {
|
|
4254
|
-
inQuote = !inQuote;
|
|
4255
|
-
continue;
|
|
4256
|
-
}
|
|
4257
|
-
if (inQuote) continue;
|
|
4258
|
-
if (c === "(") depth++;
|
|
4259
|
-
else if (c === ")" && --depth < 0) return false;
|
|
4260
|
-
}
|
|
4261
|
-
return !inQuote && depth === 0;
|
|
4262
|
-
}
|
|
4263
|
-
function composeReadQuery(agentQuery, clauses) {
|
|
4264
|
-
const injected = clauses.filter((c) => typeof c === "string" && c.trim() !== "").map((c) => c.trim());
|
|
4265
|
-
const base = (agentQuery ?? "").trim();
|
|
4266
|
-
if (base === "") return { ok: true, query: injected.join(" ") };
|
|
4267
|
-
if (!parensBalanced(base)) {
|
|
4268
|
-
return { ok: false, reason: "the requested search query has unbalanced parentheses" };
|
|
4269
|
-
}
|
|
4270
|
-
if (TRAILING_OPERATOR.test(base)) {
|
|
4271
|
-
return { ok: false, reason: "the requested search query ends in a dangling operator" };
|
|
4272
|
-
}
|
|
4273
|
-
if (injected.length === 0) return { ok: true, query: base };
|
|
4274
|
-
return { ok: true, query: `(${base}) ${injected.join(" ")}` };
|
|
4275
|
-
}
|
|
4276
|
-
function detectAgeConflict(agentQuery, conflictPattern, maxAgeDays) {
|
|
4277
|
-
if (!agentQuery || !conflictPattern || maxAgeDays === null) return null;
|
|
4278
|
-
let re;
|
|
4279
|
-
try {
|
|
4280
|
-
re = new RegExp(conflictPattern, "gi");
|
|
4281
|
-
} catch {
|
|
4282
|
-
return null;
|
|
4283
|
-
}
|
|
4284
|
-
let worst = null;
|
|
4285
|
-
for (const match of agentQuery.matchAll(re)) {
|
|
4286
|
-
const days = Number(match[1]);
|
|
4287
|
-
if (!Number.isFinite(days)) continue;
|
|
4288
|
-
if (days >= maxAgeDays && (worst === null || days > worst)) worst = days;
|
|
4289
|
-
}
|
|
4290
|
-
return worst;
|
|
4291
|
-
}
|
|
4292
|
-
function resolveAgeBoundField(boundsSchema, ageExecField) {
|
|
4293
|
-
const fields = boundsSchema?.fields;
|
|
4294
|
-
if (!fields) return null;
|
|
4295
|
-
for (const [name, def] of Object.entries(fields)) {
|
|
4296
|
-
if (def?.boundType?.kind === "per_transaction" && def.boundType.of === ageExecField) return name;
|
|
4297
|
-
}
|
|
4298
|
-
return null;
|
|
4299
|
-
}
|
|
4300
|
-
function getByDottedPath(obj, path) {
|
|
4301
|
-
return path.split(".").reduce(
|
|
4302
|
-
(o, k) => o == null ? void 0 : o[k],
|
|
4303
|
-
obj
|
|
4304
|
-
);
|
|
4305
|
-
}
|
|
4306
|
-
function firstParsableDate(obj, paths) {
|
|
4307
|
-
for (const path of Array.isArray(paths) ? paths : [paths]) {
|
|
4308
|
-
const parsed = parseMessageTimestamp(getByDottedPath(obj, path));
|
|
4309
|
-
if (parsed !== null) return parsed;
|
|
4310
|
-
}
|
|
4311
|
-
return null;
|
|
4312
|
-
}
|
|
4313
|
-
function renderAgeConstraint(template, maxAgeDays, nowMs) {
|
|
4314
|
-
const floorIso = new Date(nowMs - maxAgeDays * MS_PER_DAY).toISOString();
|
|
4315
|
-
return template.replace(/\{days\}/g, String(maxAgeDays)).replace(/\{date\}/g, floorIso.slice(0, 10));
|
|
4316
|
-
}
|
|
4317
|
-
function ageFloorValue(maxAgeDays, nowMs, format = "iso") {
|
|
4318
|
-
const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
|
|
4319
|
-
if (format === "epoch_ms") return floorMs;
|
|
4320
|
-
if (format === "epoch_s") return String(Math.floor(floorMs / 1e3));
|
|
4321
|
-
return new Date(floorMs).toISOString();
|
|
4322
|
-
}
|
|
4323
|
-
function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
|
|
4324
|
-
const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
|
|
4325
|
-
const asked = parseMessageTimestamp(requested);
|
|
4326
|
-
if (asked !== null && asked >= floorMs) return requested;
|
|
4327
|
-
return ageFloorValue(maxAgeDays, nowMs, format);
|
|
4328
|
-
}
|
|
4329
|
-
|
|
4330
4588
|
// src/lib/field-path.ts
|
|
4331
4589
|
function matchesWhere(element, where) {
|
|
4332
4590
|
if (element === null || typeof element !== "object" || Array.isArray(element)) return false;
|
|
@@ -4811,8 +5069,8 @@ async function verifyTicket(cache, receipt, expected) {
|
|
|
4811
5069
|
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
|
|
4812
5070
|
throw new TicketBindingMismatchError("Ticket carries no valid timestamp \u2014 refusing to execute.");
|
|
4813
5071
|
}
|
|
4814
|
-
const
|
|
4815
|
-
const ageSec =
|
|
5072
|
+
const nowSec4 = Math.floor(Date.now() / 1e3);
|
|
5073
|
+
const ageSec = nowSec4 - timestamp;
|
|
4816
5074
|
if (ageSec > TICKET_MAX_AGE_SECONDS) {
|
|
4817
5075
|
throw new TicketBindingMismatchError(
|
|
4818
5076
|
`Ticket is ${ageSec}s old, older than the ${TICKET_MAX_AGE_SECONDS}s freshness window \u2014 refusing to execute.`
|
|
@@ -4947,7 +5205,7 @@ function normalizeIncomingArgs(tool, args) {
|
|
|
4947
5205
|
}
|
|
4948
5206
|
|
|
4949
5207
|
// src/lib/numeric-execution.ts
|
|
4950
|
-
import { getProfile as
|
|
5208
|
+
import { getProfile as getProfile3 } from "@hap/core";
|
|
4951
5209
|
function parseNumericString(value) {
|
|
4952
5210
|
if (!/^-?\d+(\.\d+)?$/.test(value)) return void 0;
|
|
4953
5211
|
const parsed = Number(value);
|
|
@@ -4963,7 +5221,7 @@ function declaredNumericFields(profile) {
|
|
|
4963
5221
|
return out;
|
|
4964
5222
|
}
|
|
4965
5223
|
function coerceDeclaredNumericFields(profileId, execution) {
|
|
4966
|
-
const fields = declaredNumericFields(
|
|
5224
|
+
const fields = declaredNumericFields(getProfile3(profileId));
|
|
4967
5225
|
for (const field of fields) {
|
|
4968
5226
|
const value = execution[field];
|
|
4969
5227
|
if (typeof value !== "string") continue;
|
|
@@ -4973,7 +5231,7 @@ function coerceDeclaredNumericFields(profileId, execution) {
|
|
|
4973
5231
|
}
|
|
4974
5232
|
|
|
4975
5233
|
// src/lib/tool-proxy.ts
|
|
4976
|
-
import { getProfile as
|
|
5234
|
+
import { getProfile as getProfile4, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
|
|
4977
5235
|
import { readFile } from "fs/promises";
|
|
4978
5236
|
import { extname } from "path";
|
|
4979
5237
|
import { randomUUID } from "crypto";
|
|
@@ -5045,7 +5303,7 @@ function profileMatches(profileId, shortName2) {
|
|
|
5045
5303
|
return profileId === shortName2 || profileId.includes("/" + shortName2 + "@") || profileId.endsWith("/" + shortName2);
|
|
5046
5304
|
}
|
|
5047
5305
|
function wireExecutionContext(profileId, execution) {
|
|
5048
|
-
const profile =
|
|
5306
|
+
const profile = getProfile4(profileId);
|
|
5049
5307
|
const allowed = /* @__PURE__ */ new Set(["action_type"]);
|
|
5050
5308
|
const fields = profile?.boundsSchema?.fields;
|
|
5051
5309
|
if (fields) {
|
|
@@ -5063,7 +5321,7 @@ function wireExecutionContext(profileId, execution) {
|
|
|
5063
5321
|
return out;
|
|
5064
5322
|
}
|
|
5065
5323
|
function countBoundsFor(profileId, actionType) {
|
|
5066
|
-
const fields =
|
|
5324
|
+
const fields = getProfile4(profileId)?.boundsSchema?.fields;
|
|
5067
5325
|
if (!fields) return null;
|
|
5068
5326
|
return Object.entries(fields).filter(([, def]) => def.boundType?.kind === "cumulative_count").filter(([name, def]) => {
|
|
5069
5327
|
const applies = boundActionTypes2(name, def);
|
|
@@ -5138,7 +5396,7 @@ function recordBlocked(state2, tool, rec) {
|
|
|
5138
5396
|
function recordGatekeeperRefusal(state2, tool, refusals) {
|
|
5139
5397
|
if (refusals.length === 0) return;
|
|
5140
5398
|
const pick = refusals.find((r) => r.bound !== void 0 && r.actual !== void 0) ?? refusals[0];
|
|
5141
|
-
const scoped = pick.field ? Boolean(
|
|
5399
|
+
const scoped = pick.field ? Boolean(getProfile4(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
|
|
5142
5400
|
const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
|
|
5143
5401
|
recordBlocked(state2, tool, {
|
|
5144
5402
|
kind: scoped ? "scope" : "bound",
|
|
@@ -5238,7 +5496,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5238
5496
|
};
|
|
5239
5497
|
};
|
|
5240
5498
|
}
|
|
5241
|
-
const { profile, executionMapping, staticExecution, category } = tool.gating;
|
|
5499
|
+
const { profile, executionMapping, staticExecution, category, boundField: toolBoundField, requiredValue: toolRequiredValue } = tool.gating;
|
|
5242
5500
|
const eligibleMandates = () => {
|
|
5243
5501
|
const refusals = [];
|
|
5244
5502
|
const matching = state2.getEnrichedAuthorizations().filter((a) => a.complete && profileMatches(a.profileId, profile)).filter((a) => {
|
|
@@ -5317,17 +5575,19 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5317
5575
|
);
|
|
5318
5576
|
}
|
|
5319
5577
|
}
|
|
5320
|
-
const readProfile = readAdapter ?
|
|
5578
|
+
const readProfile = readAdapter ? getProfile4(matchingAuths[0].profileId) : void 0;
|
|
5321
5579
|
const boundsSchema = readProfile?.boundsSchema;
|
|
5322
5580
|
const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
|
|
5323
5581
|
const hasAgeDimension = Boolean(readAdapter?.ageField);
|
|
5324
5582
|
let readMaxAge = null;
|
|
5325
5583
|
if (hasAgeDimension) {
|
|
5326
5584
|
const localAge = integrationManager2.getReadAgeDays(tool.integrationId);
|
|
5327
|
-
|
|
5585
|
+
const signedAge = ageBoundField ? maxReadAgeDays(
|
|
5328
5586
|
matchingAuths.map((a) => a.bounds ?? a.frame),
|
|
5329
5587
|
ageBoundField
|
|
5330
|
-
) : null
|
|
5588
|
+
) : null;
|
|
5589
|
+
const isTeam = typeof state2.cache?.isTeamGroup === "function" ? state2.cache.isTeamGroup(matchingAuths[0]?.groupId) : void 0;
|
|
5590
|
+
readMaxAge = isTeam === true && signedAge !== null ? localAge !== null ? Math.min(localAge, signedAge) : signedAge : localAge ?? signedAge;
|
|
5331
5591
|
if (readMaxAge === null) {
|
|
5332
5592
|
return denyRead(
|
|
5333
5593
|
state2,
|
|
@@ -5481,7 +5741,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5481
5741
|
};
|
|
5482
5742
|
}
|
|
5483
5743
|
{
|
|
5484
|
-
const profileDef =
|
|
5744
|
+
const profileDef = getProfile4(profile);
|
|
5485
5745
|
const registry = profileDef?.boundsSchema?.actionTypes;
|
|
5486
5746
|
if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
|
|
5487
5747
|
recordBlocked(state2, tool, {
|
|
@@ -5519,6 +5779,25 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5519
5779
|
const passers = [];
|
|
5520
5780
|
const structuredRefusals = [];
|
|
5521
5781
|
for (const candidate of matchingAuths) {
|
|
5782
|
+
if (toolBoundField) {
|
|
5783
|
+
const gateOk = boundsSatisfyReadGate(
|
|
5784
|
+
candidate.bounds ?? candidate.frame,
|
|
5785
|
+
{ boundField: toolBoundField, requiredValue: toolRequiredValue }
|
|
5786
|
+
);
|
|
5787
|
+
if (!gateOk) {
|
|
5788
|
+
errors.push(
|
|
5789
|
+
`${candidate.path}: requires "${toolBoundField}: ${toolRequiredValue}", but this authorization does not grant it`
|
|
5790
|
+
);
|
|
5791
|
+
structuredRefusals.push({
|
|
5792
|
+
authorizationId: candidate.authorizationId,
|
|
5793
|
+
profileId: candidate.profileId,
|
|
5794
|
+
code: "BOUND_EXCEEDED",
|
|
5795
|
+
field: toolBoundField,
|
|
5796
|
+
bound: toolRequiredValue
|
|
5797
|
+
});
|
|
5798
|
+
continue;
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5522
5801
|
const { result } = await state2.gatekeeper.verifyExecution(candidate.authorizationId, execution, {
|
|
5523
5802
|
bounds: candidate.bounds,
|
|
5524
5803
|
context: candidate.context
|
|
@@ -5566,7 +5845,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5566
5845
|
isError: true
|
|
5567
5846
|
};
|
|
5568
5847
|
}
|
|
5569
|
-
const contextKeys =
|
|
5848
|
+
const contextKeys = getProfile4(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
|
|
5570
5849
|
return attemptWithCandidates(passers);
|
|
5571
5850
|
async function attemptWithCandidates(candidates) {
|
|
5572
5851
|
const selection = selectAuthorization(
|
|
@@ -5758,29 +6037,33 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
5758
6037
|
return attemptWithCandidates(remaining);
|
|
5759
6038
|
}
|
|
5760
6039
|
if (err instanceof SPReceiptError && err.statusCode === 409) {
|
|
5761
|
-
const
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
}
|
|
5772
|
-
} catch {
|
|
5773
|
-
}
|
|
5774
|
-
} else {
|
|
5775
|
-
try {
|
|
5776
|
-
const summary = await state2.spClient.getAuthorizationSummary(authzId);
|
|
5777
|
-
if (summary?.created_by) {
|
|
5778
|
-
pendingApprovers = [summary.created_by, ...pendingApprovers];
|
|
6040
|
+
const rawBody = err.body;
|
|
6041
|
+
const spBody = rawBody?.errors?.[0] ?? rawBody;
|
|
6042
|
+
let pendingApprovers = spBody?.approvers ?? [];
|
|
6043
|
+
let summary;
|
|
6044
|
+
const authSummary = async () => {
|
|
6045
|
+
if (summary === void 0) {
|
|
6046
|
+
try {
|
|
6047
|
+
summary = await state2.spClient.getAuthorizationSummary(authzId);
|
|
6048
|
+
} catch {
|
|
6049
|
+
summary = null;
|
|
5779
6050
|
}
|
|
5780
|
-
} catch {
|
|
5781
6051
|
}
|
|
6052
|
+
return summary;
|
|
6053
|
+
};
|
|
6054
|
+
if (pendingApprovers.length === 0) {
|
|
6055
|
+
const s = await authSummary();
|
|
6056
|
+
if (s?.approvers_frozen) pendingApprovers = s.approvers_frozen;
|
|
6057
|
+
}
|
|
6058
|
+
const isTeam = typeof state2.cache?.isTeamGroup === "function" ? state2.cache.isTeamGroup(auth.groupId) : void 0;
|
|
6059
|
+
const ownerId = (await authSummary())?.created_by;
|
|
6060
|
+
if (isTeam === true) {
|
|
6061
|
+
if (ownerId) pendingApprovers = pendingApprovers.filter((uid) => uid !== ownerId);
|
|
6062
|
+
} else if (ownerId) {
|
|
6063
|
+
pendingApprovers = [ownerId, ...pendingApprovers];
|
|
5782
6064
|
}
|
|
5783
6065
|
const uniqueApprovers = [...new Set(pendingApprovers)];
|
|
6066
|
+
const recipientUserIds = ownerId ? [.../* @__PURE__ */ new Set([ownerId, ...uniqueApprovers])] : uniqueApprovers;
|
|
5784
6067
|
try {
|
|
5785
6068
|
const enrichedArgs = await attachImagePreview(args);
|
|
5786
6069
|
const wireExecCtx = wireExecutionContext(auth.profileId, execution);
|
|
@@ -5793,7 +6076,8 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
5793
6076
|
toolArgs: enrichedArgs,
|
|
5794
6077
|
executionContext: wireExecCtx,
|
|
5795
6078
|
pendingApprovers: uniqueApprovers,
|
|
5796
|
-
recipientUserIds
|
|
6079
|
+
recipientUserIds,
|
|
6080
|
+
...ownerId ? { createdBy: ownerId } : {}
|
|
5797
6081
|
});
|
|
5798
6082
|
const previewHash = await computeSubmissionPreviewHash(
|
|
5799
6083
|
integrationManager2,
|
|
@@ -6026,7 +6310,7 @@ function buildMandateBrief(opts) {
|
|
|
6026
6310
|
const parts = [];
|
|
6027
6311
|
if (boundsDesc) parts.push(boundsDesc);
|
|
6028
6312
|
if (executionLog) {
|
|
6029
|
-
const profile =
|
|
6313
|
+
const profile = getProfile5(auth.profileId) ?? getProfile5(shortName2);
|
|
6030
6314
|
const consumption = getConsumptionState(auth, executionLog, profile);
|
|
6031
6315
|
const compact = formatConsumptionCompact(consumption);
|
|
6032
6316
|
if (compact) parts.push(compact);
|
|
@@ -6064,7 +6348,7 @@ function buildMandateBrief(opts) {
|
|
|
6064
6348
|
}
|
|
6065
6349
|
|
|
6066
6350
|
// src/tools/authorizations.ts
|
|
6067
|
-
import { getProfile as
|
|
6351
|
+
import { getProfile as getProfile6 } from "@hap/core";
|
|
6068
6352
|
function shortProfileName2(profileId) {
|
|
6069
6353
|
const withoutVersion = profileId.replace(/@.*$/, "");
|
|
6070
6354
|
const parts = withoutVersion.split("/");
|
|
@@ -6182,7 +6466,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
6182
6466
|
} catch {
|
|
6183
6467
|
}
|
|
6184
6468
|
const shortName2 = shortProfileName2(auth.profileId);
|
|
6185
|
-
const profile =
|
|
6469
|
+
const profile = getProfile6(auth.profileId) ?? getProfile6(shortName2);
|
|
6186
6470
|
if (profile) {
|
|
6187
6471
|
const consumption = getConsumptionState(auth, state2.executionLog, profile);
|
|
6188
6472
|
const consumptionText = formatConsumptionFull(consumption);
|
|
@@ -6242,7 +6526,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
6242
6526
|
lines.push(" Mode: review (each action requires your approval)");
|
|
6243
6527
|
}
|
|
6244
6528
|
const shortName2 = shortProfileName2(auth.profileId);
|
|
6245
|
-
const profile =
|
|
6529
|
+
const profile = getProfile6(auth.profileId) ?? getProfile6(shortName2);
|
|
6246
6530
|
if (profile) {
|
|
6247
6531
|
const consumption = getConsumptionState(auth, state2.executionLog, profile);
|
|
6248
6532
|
const compact = formatConsumptionCompact(consumption);
|
|
@@ -6435,6 +6719,18 @@ async function decryptRejectionNote(by) {
|
|
|
6435
6719
|
}
|
|
6436
6720
|
|
|
6437
6721
|
// src/tools/commitments.ts
|
|
6722
|
+
function recordExecutionOutcome(state2, proposalId, ticketId, ok, text, structuredContent) {
|
|
6723
|
+
try {
|
|
6724
|
+
state2.executionResults?.record({ proposalId, ticketId, ok, text, structuredContent });
|
|
6725
|
+
} catch (err) {
|
|
6726
|
+
console.error(`[Suveren MCP] Could not record the execution result for proposal ${proposalId}:`, err);
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
function logExecutionOutcome(proposalId, ok, shortMessage) {
|
|
6730
|
+
console.error(
|
|
6731
|
+
`[Suveren MCP] Proposal ${proposalId}: execution outcome=${ok ? "ok" : `error (${shortMessage ?? "unknown"})`}`
|
|
6732
|
+
);
|
|
6733
|
+
}
|
|
6438
6734
|
function buildSkippedProposalNote(proposalId) {
|
|
6439
6735
|
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.`;
|
|
6440
6736
|
}
|
|
@@ -6701,12 +6997,19 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
6701
6997
|
try {
|
|
6702
6998
|
result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
|
|
6703
6999
|
} catch (err) {
|
|
7000
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6704
7001
|
state2.executionJournal.complete(receiptId, "failed");
|
|
7002
|
+
recordExecutionOutcome(state2, proposal.id, receiptId, false, `The tool call threw: ${msg}`);
|
|
7003
|
+
logExecutionOutcome(proposal.id, false, err instanceof Error ? err.name : "thrown error");
|
|
7004
|
+
void notifyControlPlane("execution-failed");
|
|
6705
7005
|
throw err;
|
|
6706
7006
|
}
|
|
6707
7007
|
const resultText = result.content?.[0]?.text ?? JSON.stringify(result);
|
|
6708
7008
|
if (result.isError) {
|
|
6709
7009
|
state2.executionJournal.complete(receiptId, "failed", "refused", resultText);
|
|
7010
|
+
recordExecutionOutcome(state2, proposal.id, receiptId, false, resultText);
|
|
7011
|
+
logExecutionOutcome(proposal.id, false, "the connector returned an error");
|
|
7012
|
+
void notifyControlPlane("execution-failed");
|
|
6710
7013
|
return {
|
|
6711
7014
|
text: `Proposal ${proposal.id} was approved, but ${integrationId} refused to run it \u2014 nothing was done.
|
|
6712
7015
|
Reason: ${resultText}`,
|
|
@@ -6714,6 +7017,8 @@ Reason: ${resultText}`,
|
|
|
6714
7017
|
};
|
|
6715
7018
|
}
|
|
6716
7019
|
state2.executionJournal.complete(receiptId, "done");
|
|
7020
|
+
recordExecutionOutcome(state2, proposal.id, receiptId, true, resultText, result.structuredContent);
|
|
7021
|
+
logExecutionOutcome(proposal.id, true);
|
|
6717
7022
|
state2.executionLog.record({
|
|
6718
7023
|
profileId: proposal.profileId,
|
|
6719
7024
|
path: proposal.path,
|
|
@@ -6724,6 +7029,12 @@ Reason: ${resultText}`,
|
|
|
6724
7029
|
Result: ${resultText}` };
|
|
6725
7030
|
} catch (err) {
|
|
6726
7031
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7032
|
+
if (state2.executionJournal.get(receiptId)?.state === "intent") {
|
|
7033
|
+
state2.executionJournal.complete(receiptId, "failed");
|
|
7034
|
+
recordExecutionOutcome(state2, proposal.id, receiptId, false, `Receipt issued but tool execution failed: ${msg}`);
|
|
7035
|
+
logExecutionOutcome(proposal.id, false, err instanceof Error ? err.name : "error");
|
|
7036
|
+
void notifyControlPlane("execution-failed");
|
|
7037
|
+
}
|
|
6727
7038
|
return {
|
|
6728
7039
|
text: `Proposal ${proposal.id} receipt issued but tool execution failed: ${msg}`,
|
|
6729
7040
|
isError: true
|
|
@@ -6732,11 +7043,22 @@ Result: ${resultText}` };
|
|
|
6732
7043
|
}
|
|
6733
7044
|
function localOutcomeOfExecuted(proposal, state2) {
|
|
6734
7045
|
const entry = state2.executionJournal?.findByProposal?.(proposal.id);
|
|
6735
|
-
if (!entry
|
|
7046
|
+
if (!entry) return null;
|
|
6736
7047
|
const integration = proposal.tool.split("__")[0];
|
|
7048
|
+
if (entry.state === "done") {
|
|
7049
|
+
const stored = state2.executionResults?.get(proposal.id);
|
|
7050
|
+
if (!stored || !stored.ok) return null;
|
|
7051
|
+
return {
|
|
7052
|
+
text: `Proposal ${proposal.id} was approved and executed.
|
|
7053
|
+
Result: ${stored.text}`,
|
|
7054
|
+
isError: false
|
|
7055
|
+
};
|
|
7056
|
+
}
|
|
6737
7057
|
if (entry.state === "failed") {
|
|
6738
|
-
const
|
|
6739
|
-
|
|
7058
|
+
const stored = state2.executionResults?.get(proposal.id);
|
|
7059
|
+
const detail = (stored && !stored.ok ? stored.text : void 0) ?? entry.detail;
|
|
7060
|
+
const reasonSuffix = detail ? `
|
|
7061
|
+
Reason: ${detail}` : "";
|
|
6740
7062
|
return {
|
|
6741
7063
|
text: entry.outcome === "as_refused" ? `Proposal ${proposal.id} was approved, but the Authority Server refused its ticket \u2014 nothing ran. Do not retry the same values; ask the person (e.g. for a higher limit).${reasonSuffix}` : entry.outcome === "refused" ? `Proposal ${proposal.id} was approved, but ${integration} refused to run it \u2014 nothing was done. Do not call the tool again with the same values; a new attempt needs a new request and approval.${reasonSuffix}` : entry.outcome === "changed" ? `Proposal ${proposal.id} was approved, but the record changed in ${integration} since it was requested \u2014 nothing was done. A new attempt needs a new request and approval.${reasonSuffix}` : `Proposal ${proposal.id} was approved, but running it failed \u2014 whether anything happened in ${integration} is unknown. Check ${integration} before trying again; a new attempt needs a new request and approval.${reasonSuffix}`,
|
|
6742
7064
|
isError: true
|
|
@@ -7083,21 +7405,21 @@ function createMcpServer(state2, integrationManager2) {
|
|
|
7083
7405
|
}
|
|
7084
7406
|
|
|
7085
7407
|
// src/lib/agent-contact.ts
|
|
7086
|
-
import { existsSync as
|
|
7087
|
-
import { dirname as
|
|
7408
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync13, readFileSync as readFileSync17, renameSync as renameSync5, writeFileSync as writeFileSync13 } from "fs";
|
|
7409
|
+
import { dirname as dirname13, join as join16 } from "path";
|
|
7088
7410
|
var FILE = "agent-contact.json";
|
|
7089
7411
|
var REWRITE_AFTER_MS = 6e4;
|
|
7090
7412
|
var AgentContactStore = class {
|
|
7091
7413
|
path;
|
|
7092
7414
|
current;
|
|
7093
7415
|
constructor(dataDir2) {
|
|
7094
|
-
this.path =
|
|
7416
|
+
this.path = join16(dataDir2, FILE);
|
|
7095
7417
|
this.current = this.load();
|
|
7096
7418
|
}
|
|
7097
7419
|
load() {
|
|
7098
|
-
if (!
|
|
7420
|
+
if (!existsSync16(this.path)) return null;
|
|
7099
7421
|
try {
|
|
7100
|
-
const v = JSON.parse(
|
|
7422
|
+
const v = JSON.parse(readFileSync17(this.path, "utf8"));
|
|
7101
7423
|
if (typeof v.clientName !== "string" || typeof v.firstSeenAt !== "string" || typeof v.lastSeenAt !== "string") return null;
|
|
7102
7424
|
return { clientName: v.clientName, clientVersion: v.clientVersion, firstSeenAt: v.firstSeenAt, lastSeenAt: v.lastSeenAt };
|
|
7103
7425
|
} catch {
|
|
@@ -7120,9 +7442,9 @@ var AgentContactStore = class {
|
|
|
7120
7442
|
};
|
|
7121
7443
|
if (!due) return;
|
|
7122
7444
|
try {
|
|
7123
|
-
|
|
7445
|
+
mkdirSync13(dirname13(this.path), { recursive: true });
|
|
7124
7446
|
const tmp = `${this.path}.tmp`;
|
|
7125
|
-
|
|
7447
|
+
writeFileSync13(tmp, JSON.stringify(this.current, null, 2));
|
|
7126
7448
|
renameSync5(tmp, this.path);
|
|
7127
7449
|
} catch (err) {
|
|
7128
7450
|
console.error("[Suveren MCP] Could not save agent contact:", err);
|
|
@@ -7157,16 +7479,16 @@ function verifyGateContentHashes(content, auth) {
|
|
|
7157
7479
|
}
|
|
7158
7480
|
|
|
7159
7481
|
// src/lib/integration-registry.ts
|
|
7160
|
-
import { mkdirSync as
|
|
7161
|
-
import { homedir as
|
|
7162
|
-
import { dirname as
|
|
7163
|
-
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ??
|
|
7482
|
+
import { mkdirSync as mkdirSync14, readFileSync as readFileSync18, writeFileSync as writeFileSync14, existsSync as existsSync17 } from "fs";
|
|
7483
|
+
import { homedir as homedir12 } from "os";
|
|
7484
|
+
import { dirname as dirname14, join as join17 } from "path";
|
|
7485
|
+
var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join17(homedir12(), ".suveren");
|
|
7164
7486
|
var IntegrationRegistry = class {
|
|
7165
7487
|
integrations = /* @__PURE__ */ new Map();
|
|
7166
7488
|
filePath;
|
|
7167
7489
|
constructor(dataDir2) {
|
|
7168
7490
|
const dir = dataDir2 ?? DEFAULT_DATA_DIR;
|
|
7169
|
-
this.filePath =
|
|
7491
|
+
this.filePath = join17(dir, "integrations.json");
|
|
7170
7492
|
this.load();
|
|
7171
7493
|
}
|
|
7172
7494
|
// ─── CRUD ───────────────────────────────────────────────────────────────
|
|
@@ -7197,12 +7519,12 @@ var IntegrationRegistry = class {
|
|
|
7197
7519
|
}
|
|
7198
7520
|
// ─── Load / Save ────────────────────────────────────────────────────────
|
|
7199
7521
|
load() {
|
|
7200
|
-
if (!
|
|
7201
|
-
|
|
7522
|
+
if (!existsSync17(this.filePath)) {
|
|
7523
|
+
mkdirSync14(dirname14(this.filePath), { recursive: true });
|
|
7202
7524
|
return;
|
|
7203
7525
|
}
|
|
7204
7526
|
try {
|
|
7205
|
-
const raw =
|
|
7527
|
+
const raw = readFileSync18(this.filePath, "utf-8");
|
|
7206
7528
|
const data = JSON.parse(raw);
|
|
7207
7529
|
const migrated = data.integrations.map((i) => {
|
|
7208
7530
|
if ("toolGating" in i && !("profile" in i)) {
|
|
@@ -7231,15 +7553,15 @@ var IntegrationRegistry = class {
|
|
|
7231
7553
|
version: 1,
|
|
7232
7554
|
integrations: Array.from(this.integrations.values())
|
|
7233
7555
|
};
|
|
7234
|
-
|
|
7235
|
-
|
|
7556
|
+
mkdirSync14(dirname14(this.filePath), { recursive: true });
|
|
7557
|
+
writeFileSync14(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
|
|
7236
7558
|
}
|
|
7237
7559
|
};
|
|
7238
7560
|
|
|
7239
7561
|
// src/lib/integration-manager.ts
|
|
7240
|
-
import { homedir as
|
|
7241
|
-
import { join as
|
|
7242
|
-
import { existsSync as
|
|
7562
|
+
import { homedir as homedir13 } from "os";
|
|
7563
|
+
import { join as join18, delimiter } from "path";
|
|
7564
|
+
import { existsSync as existsSync18, writeFileSync as writeFileSync15, mkdirSync as mkdirSync15, readFileSync as readFileSync19, rmSync } from "fs";
|
|
7243
7565
|
import { execFile } from "child_process";
|
|
7244
7566
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7245
7567
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
@@ -7369,8 +7691,8 @@ function inheritedConnectorEnv(parent, passEnv = []) {
|
|
|
7369
7691
|
}
|
|
7370
7692
|
|
|
7371
7693
|
// src/lib/integration-manager.ts
|
|
7372
|
-
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ??
|
|
7373
|
-
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ??
|
|
7694
|
+
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join18(homedir13(), ".suveren");
|
|
7695
|
+
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join18(DEFAULT_DATA_DIR2, "integrations");
|
|
7374
7696
|
var OFFLINE_MODE = process.env.SUVEREN_OFFLINE === "1";
|
|
7375
7697
|
var installLock = Promise.resolve();
|
|
7376
7698
|
function withInstallLock(task) {
|
|
@@ -7378,7 +7700,7 @@ function withInstallLock(task) {
|
|
|
7378
7700
|
installLock = run.then(() => void 0, () => void 0);
|
|
7379
7701
|
return run;
|
|
7380
7702
|
}
|
|
7381
|
-
var INTEGRATIONS_BIN =
|
|
7703
|
+
var INTEGRATIONS_BIN = join18(INTEGRATIONS_DIR, "node_modules", ".bin");
|
|
7382
7704
|
function getIntegrationsBinDir() {
|
|
7383
7705
|
return INTEGRATIONS_BIN;
|
|
7384
7706
|
}
|
|
@@ -7387,12 +7709,12 @@ function buildPath() {
|
|
|
7387
7709
|
return [INTEGRATIONS_BIN, base].join(delimiter);
|
|
7388
7710
|
}
|
|
7389
7711
|
function ensureIntegrationsDir() {
|
|
7390
|
-
if (!
|
|
7391
|
-
|
|
7712
|
+
if (!existsSync18(INTEGRATIONS_DIR)) {
|
|
7713
|
+
mkdirSync15(INTEGRATIONS_DIR, { recursive: true });
|
|
7392
7714
|
}
|
|
7393
|
-
const pkgPath =
|
|
7394
|
-
if (!
|
|
7395
|
-
|
|
7715
|
+
const pkgPath = join18(INTEGRATIONS_DIR, "package.json");
|
|
7716
|
+
if (!existsSync18(pkgPath)) {
|
|
7717
|
+
writeFileSync15(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
|
|
7396
7718
|
}
|
|
7397
7719
|
}
|
|
7398
7720
|
function readAgeOf(config) {
|
|
@@ -7481,19 +7803,19 @@ var IntegrationManager = class {
|
|
|
7481
7803
|
* exist, and treat anything else as not-installed (and reinstallable).
|
|
7482
7804
|
*/
|
|
7483
7805
|
isUsableInstall(npmPackage) {
|
|
7484
|
-
const pkgDir =
|
|
7485
|
-
const pkgJson =
|
|
7486
|
-
if (!
|
|
7806
|
+
const pkgDir = join18(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
7807
|
+
const pkgJson = join18(pkgDir, "package.json");
|
|
7808
|
+
if (!existsSync18(pkgDir) || !existsSync18(pkgJson)) return false;
|
|
7487
7809
|
try {
|
|
7488
|
-
const pkg = JSON.parse(
|
|
7810
|
+
const pkg = JSON.parse(readFileSync19(pkgJson, "utf8"));
|
|
7489
7811
|
const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
|
|
7490
7812
|
const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
|
|
7491
7813
|
const binEntries = Object.entries(binMap);
|
|
7492
7814
|
for (const [binName, target] of binEntries) {
|
|
7493
|
-
if (!
|
|
7815
|
+
if (!existsSync18(join18(pkgDir, target))) return false;
|
|
7494
7816
|
if (!this.binShimExists(binName)) return false;
|
|
7495
7817
|
}
|
|
7496
|
-
if (binEntries.length === 0 && pkg.main && !
|
|
7818
|
+
if (binEntries.length === 0 && pkg.main && !existsSync18(join18(pkgDir, pkg.main))) return false;
|
|
7497
7819
|
return true;
|
|
7498
7820
|
} catch {
|
|
7499
7821
|
return false;
|
|
@@ -7508,9 +7830,9 @@ var IntegrationManager = class {
|
|
|
7508
7830
|
*/
|
|
7509
7831
|
readInstalledVersion(npmPackage) {
|
|
7510
7832
|
if (!this.isUsableInstall(npmPackage)) return null;
|
|
7511
|
-
const pkgJson =
|
|
7833
|
+
const pkgJson = join18(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"), "package.json");
|
|
7512
7834
|
try {
|
|
7513
|
-
const pkg = JSON.parse(
|
|
7835
|
+
const pkg = JSON.parse(readFileSync19(pkgJson, "utf8"));
|
|
7514
7836
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
7515
7837
|
} catch {
|
|
7516
7838
|
return null;
|
|
@@ -7524,7 +7846,7 @@ var IntegrationManager = class {
|
|
|
7524
7846
|
binShimExists(binName) {
|
|
7525
7847
|
if (!binName) return false;
|
|
7526
7848
|
const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
|
|
7527
|
-
return candidates.some((c) =>
|
|
7849
|
+
return candidates.some((c) => existsSync18(join18(INTEGRATIONS_BIN, c)));
|
|
7528
7850
|
}
|
|
7529
7851
|
/**
|
|
7530
7852
|
* The exact version THIS gateway's CURRENTLY LOADED manifest pins
|
|
@@ -7614,8 +7936,8 @@ var IntegrationManager = class {
|
|
|
7614
7936
|
* nothing at all.
|
|
7615
7937
|
*/
|
|
7616
7938
|
async installNow(npmPackage, exactVersion, cleanFirst = true) {
|
|
7617
|
-
const pkgDir =
|
|
7618
|
-
if (cleanFirst &&
|
|
7939
|
+
const pkgDir = join18(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
|
|
7940
|
+
if (cleanFirst && existsSync18(pkgDir)) {
|
|
7619
7941
|
console.error(`[IntegrationManager] Removing ${exactVersion ? "outdated" : "unusable"} install of ${npmPackage}`);
|
|
7620
7942
|
try {
|
|
7621
7943
|
rmSync(pkgDir, { recursive: true, force: true });
|
|
@@ -8204,7 +8526,18 @@ var IntegrationManager = class {
|
|
|
8204
8526
|
simulationOnly: ext.simulationOnly,
|
|
8205
8527
|
approvalView: ext.approvalView,
|
|
8206
8528
|
contextKeys: ext.contextKeys,
|
|
8207
|
-
sourceFromArg: ext.sourceFromArg
|
|
8529
|
+
sourceFromArg: ext.sourceFromArg,
|
|
8530
|
+
// A WRITE override may ALSO declare a capability-gate (enum boundType,
|
|
8531
|
+
// e.g. records.json's archive_access/delete_access) — the same
|
|
8532
|
+
// mechanism the read branch uses, now generalized: hap-core's
|
|
8533
|
+
// Gatekeeper deliberately never compares an `enum` bound against
|
|
8534
|
+
// execution (gatekeeper.ts's 'enum' case: "the gateway tool-proxy
|
|
8535
|
+
// gates tool calls ... before they reach the Gatekeeper"), so if this
|
|
8536
|
+
// branch dropped it, nothing anywhere would ever enforce it. Before
|
|
8537
|
+
// this, boundField/requiredValue on a non-"read" override was silently
|
|
8538
|
+
// discarded here — the config in the manifest existed but did nothing.
|
|
8539
|
+
boundField: ext.boundField,
|
|
8540
|
+
requiredValue: ext.requiredValue
|
|
8208
8541
|
};
|
|
8209
8542
|
}
|
|
8210
8543
|
return {
|
|
@@ -8335,7 +8668,7 @@ async function buildTicketDetails(archive, ticketIds) {
|
|
|
8335
8668
|
}
|
|
8336
8669
|
|
|
8337
8670
|
// src/lib/report/window.ts
|
|
8338
|
-
import { getProfile as
|
|
8671
|
+
import { getProfile as getProfile7 } from "@hap/core";
|
|
8339
8672
|
var REPORT_AGE_FIELD = "read_age_days";
|
|
8340
8673
|
var REPORTING_PROFILE = "reporting";
|
|
8341
8674
|
var END_SKEW_SECONDS = 120;
|
|
@@ -8349,7 +8682,7 @@ function windowLabel(start, days, loadedAt) {
|
|
|
8349
8682
|
if (loadedAt !== null && loadedAt === start) return `${since} (when the test data was loaded)`;
|
|
8350
8683
|
return `${since} (the last ${days} ${days === 1 ? "day" : "days"})`;
|
|
8351
8684
|
}
|
|
8352
|
-
var defaultLookup = (id) =>
|
|
8685
|
+
var defaultLookup = (id) => getProfile7(id);
|
|
8353
8686
|
function reportingMandateRefusal(m, lookup = defaultLookup) {
|
|
8354
8687
|
const schema = lookup(m.profileId)?.boundsSchema;
|
|
8355
8688
|
const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
|
|
@@ -8559,28 +8892,28 @@ function agentAccess(token, opensSession) {
|
|
|
8559
8892
|
}
|
|
8560
8893
|
|
|
8561
8894
|
// src/lib/profile-loader.ts
|
|
8562
|
-
import { readFileSync as
|
|
8563
|
-
import { join as
|
|
8895
|
+
import { readFileSync as readFileSync20, existsSync as existsSync19 } from "fs";
|
|
8896
|
+
import { join as join19, resolve as resolve2 } from "path";
|
|
8564
8897
|
import { registerProfile } from "@hap/core";
|
|
8565
8898
|
function loadProfiles(profilesDir) {
|
|
8566
|
-
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ??
|
|
8567
|
-
const indexPath =
|
|
8568
|
-
if (!
|
|
8899
|
+
const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join19(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
|
|
8900
|
+
const indexPath = join19(dir, "index.json");
|
|
8901
|
+
if (!existsSync19(indexPath)) {
|
|
8569
8902
|
warnNoProfiles(`No index.json at ${indexPath}`);
|
|
8570
8903
|
return 0;
|
|
8571
8904
|
}
|
|
8572
8905
|
let index;
|
|
8573
8906
|
try {
|
|
8574
|
-
index = JSON.parse(
|
|
8907
|
+
index = JSON.parse(readFileSync20(indexPath, "utf-8"));
|
|
8575
8908
|
} catch (err) {
|
|
8576
8909
|
console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
|
|
8577
8910
|
return 0;
|
|
8578
8911
|
}
|
|
8579
8912
|
let loaded = 0;
|
|
8580
8913
|
for (const [profileId, relativePath] of Object.entries(index.profiles)) {
|
|
8581
|
-
const profilePath =
|
|
8914
|
+
const profilePath = join19(dir, relativePath);
|
|
8582
8915
|
try {
|
|
8583
|
-
const profile = JSON.parse(
|
|
8916
|
+
const profile = JSON.parse(readFileSync20(profilePath, "utf-8"));
|
|
8584
8917
|
registerProfile(profileId, profile);
|
|
8585
8918
|
const shortName2 = profileId.split("/").pop()?.replace(/@.*$/, "");
|
|
8586
8919
|
if (shortName2 && shortName2 !== profileId) {
|
|
@@ -8921,20 +9254,20 @@ async function controlPlaneMandate(dryRun, request) {
|
|
|
8921
9254
|
}
|
|
8922
9255
|
|
|
8923
9256
|
// src/lib/guides.ts
|
|
8924
|
-
import { existsSync as
|
|
8925
|
-
import { dirname as
|
|
9257
|
+
import { existsSync as existsSync20, readdirSync, readFileSync as readFileSync21 } from "fs";
|
|
9258
|
+
import { dirname as dirname15, join as join20 } from "path";
|
|
8926
9259
|
var FILE_RE = /^(?:(\d+)-)?([a-z0-9][a-z0-9-]*)\.md$/;
|
|
8927
9260
|
function builtinGuidesDir() {
|
|
8928
9261
|
if (process.env.SUVEREN_BUILTIN_GUIDES_DIR) return process.env.SUVEREN_BUILTIN_GUIDES_DIR;
|
|
8929
9262
|
let dir = import.meta.dirname ?? __dirname;
|
|
8930
9263
|
for (let i = 0; i < 8; i++) {
|
|
8931
|
-
const candidate =
|
|
8932
|
-
if (
|
|
8933
|
-
const parent =
|
|
9264
|
+
const candidate = join20(dir, "content", "guides");
|
|
9265
|
+
if (existsSync20(candidate)) return candidate;
|
|
9266
|
+
const parent = dirname15(dir);
|
|
8934
9267
|
if (parent === dir) break;
|
|
8935
9268
|
dir = parent;
|
|
8936
9269
|
}
|
|
8937
|
-
return
|
|
9270
|
+
return join20(import.meta.dirname ?? __dirname, "content", "guides");
|
|
8938
9271
|
}
|
|
8939
9272
|
function parse(body) {
|
|
8940
9273
|
const lines = body.split(/\r?\n/);
|
|
@@ -8945,14 +9278,14 @@ function parse(body) {
|
|
|
8945
9278
|
return { title, summary };
|
|
8946
9279
|
}
|
|
8947
9280
|
function readDir(dir, source) {
|
|
8948
|
-
if (!dir || !
|
|
9281
|
+
if (!dir || !existsSync20(dir)) return [];
|
|
8949
9282
|
const out = [];
|
|
8950
9283
|
for (const file of readdirSync(dir)) {
|
|
8951
9284
|
const m = FILE_RE.exec(file);
|
|
8952
9285
|
if (!m) continue;
|
|
8953
9286
|
let body;
|
|
8954
9287
|
try {
|
|
8955
|
-
body =
|
|
9288
|
+
body = readFileSync21(join20(dir, file), "utf8");
|
|
8956
9289
|
} catch {
|
|
8957
9290
|
continue;
|
|
8958
9291
|
}
|
|
@@ -9087,7 +9420,7 @@ function unifiedLineDiff(before, after) {
|
|
|
9087
9420
|
}
|
|
9088
9421
|
|
|
9089
9422
|
// src/lib/builtins/setup.ts
|
|
9090
|
-
import { getProfile as
|
|
9423
|
+
import { getProfile as getProfile8 } from "@hap/core";
|
|
9091
9424
|
var DELEGATION_PROFILE = "delegation";
|
|
9092
9425
|
var DELEGATION_INTENT_HINT = [
|
|
9093
9426
|
"Why \u2014 With test data only, I want to find out which routine parts of my work an AI could take over, so people gain time \u2014 before anything touches real systems.",
|
|
@@ -9139,7 +9472,7 @@ function delegationItselfRefusal(args) {
|
|
|
9139
9472
|
}
|
|
9140
9473
|
function delegationMandateRefusal(auth) {
|
|
9141
9474
|
if (isSimulationMode()) return void 0;
|
|
9142
|
-
const profile =
|
|
9475
|
+
const profile = getProfile8(auth.profileId);
|
|
9143
9476
|
if (profile?.scopeSchema?.fields?.allowed_profiles) return void 0;
|
|
9144
9477
|
return `your Delegation mandate (${shortName(auth.profileId)} ${versionOf(auth.profileId)}) does not declare which profiles or modes you may propose \u2014 only a Delegation mandate on delegation 0.4 or later works outside simulation mode. Ask the person to give a current Delegation mandate.`;
|
|
9145
9478
|
}
|
|
@@ -9197,7 +9530,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
|
|
|
9197
9530
|
);
|
|
9198
9531
|
const first = governing[0];
|
|
9199
9532
|
if (first) {
|
|
9200
|
-
const delegationProfile =
|
|
9533
|
+
const delegationProfile = getProfile8(first.profileId);
|
|
9201
9534
|
const entry = getConsumptionState(first, deps.state.executionLog, delegationProfile).find((e) => e.field === "mandate_daily_max");
|
|
9202
9535
|
if (entry && entry.limit !== null) {
|
|
9203
9536
|
checks.push(`Proposals today: ${entry.current} of ${entry.limit}`);
|
|
@@ -9207,7 +9540,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
|
|
|
9207
9540
|
}
|
|
9208
9541
|
function resolveTargetProfile(input) {
|
|
9209
9542
|
if (typeof input !== "string" || !input.trim()) return void 0;
|
|
9210
|
-
const profile =
|
|
9543
|
+
const profile = getProfile8(input.trim());
|
|
9211
9544
|
if (!profile) return void 0;
|
|
9212
9545
|
return { profile, shortName: shortName(profile.id) };
|
|
9213
9546
|
}
|
|
@@ -9358,7 +9691,7 @@ Read each topic with get_guide(topic) before doing that step.`);
|
|
|
9358
9691
|
return { ...builtinText(`Unknown topic "${topic}". Topics: ${guides.map((g) => g.topic).join(", ") || "none installed"}.`), isError: true };
|
|
9359
9692
|
}
|
|
9360
9693
|
const body = fillGenerated(guide.body.trim(), {
|
|
9361
|
-
limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile:
|
|
9694
|
+
limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile8(s.profile) }))),
|
|
9362
9695
|
package: () => packageSection(
|
|
9363
9696
|
deps.integrationManager.getAllTools().find((t) => !deps.integrationManager.isBuiltin(t.integrationId) && t.originalName === "load_simulation")
|
|
9364
9697
|
)
|
|
@@ -9500,8 +9833,8 @@ function registerBuiltins(deps, factories = BUILTIN_FACTORIES) {
|
|
|
9500
9833
|
import { decodeMandateBlob as decodeMandateBlob3 } from "@hap/core";
|
|
9501
9834
|
|
|
9502
9835
|
// src/lib/committed-executor.ts
|
|
9503
|
-
import { existsSync as
|
|
9504
|
-
import { dirname as
|
|
9836
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync16, readFileSync as readFileSync22, unlinkSync as unlinkSync8, writeFileSync as writeFileSync16 } from "fs";
|
|
9837
|
+
import { dirname as dirname16, join as join21 } from "path";
|
|
9505
9838
|
function describeAutoExecutionLog(proposalId, tool, result) {
|
|
9506
9839
|
if (result.skipped) return null;
|
|
9507
9840
|
return result.isError ? `[Suveren MCP] Auto-exec proposal ${proposalId}: ${result.text}` : `[Suveren MCP] Auto-executed proposal ${proposalId}: ${tool}`;
|
|
@@ -9533,7 +9866,7 @@ var ExecutorLock = class {
|
|
|
9533
9866
|
path;
|
|
9534
9867
|
held = false;
|
|
9535
9868
|
constructor(dataDir2) {
|
|
9536
|
-
this.path =
|
|
9869
|
+
this.path = join21(dataDir2, "executor.lock");
|
|
9537
9870
|
}
|
|
9538
9871
|
/** Try to take the lock. Returns the holder's pid when someone else has it. */
|
|
9539
9872
|
acquire() {
|
|
@@ -9541,9 +9874,9 @@ var ExecutorLock = class {
|
|
|
9541
9874
|
if (current && current.pid !== process.pid && isAlive(current.pid)) {
|
|
9542
9875
|
return { held: false, holderPid: current.pid };
|
|
9543
9876
|
}
|
|
9544
|
-
|
|
9877
|
+
mkdirSync16(dirname16(this.path), { recursive: true, mode: 448 });
|
|
9545
9878
|
const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
|
|
9546
|
-
|
|
9879
|
+
writeFileSync16(this.path, JSON.stringify(lock), { mode: 384 });
|
|
9547
9880
|
this.held = true;
|
|
9548
9881
|
return { held: true };
|
|
9549
9882
|
}
|
|
@@ -9559,15 +9892,15 @@ var ExecutorLock = class {
|
|
|
9559
9892
|
const current = this.read();
|
|
9560
9893
|
if (current?.pid === process.pid) {
|
|
9561
9894
|
try {
|
|
9562
|
-
|
|
9895
|
+
unlinkSync8(this.path);
|
|
9563
9896
|
} catch {
|
|
9564
9897
|
}
|
|
9565
9898
|
}
|
|
9566
9899
|
}
|
|
9567
9900
|
read() {
|
|
9568
|
-
if (!
|
|
9901
|
+
if (!existsSync21(this.path)) return null;
|
|
9569
9902
|
try {
|
|
9570
|
-
const parsed = JSON.parse(
|
|
9903
|
+
const parsed = JSON.parse(readFileSync22(this.path, "utf8"));
|
|
9571
9904
|
return typeof parsed?.pid === "number" ? parsed : null;
|
|
9572
9905
|
} catch {
|
|
9573
9906
|
return null;
|
|
@@ -9584,20 +9917,20 @@ function isAlive(pid) {
|
|
|
9584
9917
|
}
|
|
9585
9918
|
|
|
9586
9919
|
// bin/http.ts
|
|
9587
|
-
import { homedir as
|
|
9588
|
-
import { join as
|
|
9920
|
+
import { homedir as homedir14 } from "os";
|
|
9921
|
+
import { join as join23 } from "path";
|
|
9589
9922
|
|
|
9590
9923
|
// src/lib/mcp-as-tracker.ts
|
|
9591
|
-
import { existsSync as
|
|
9592
|
-
import { dirname as
|
|
9924
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync17 } from "fs";
|
|
9925
|
+
import { dirname as dirname17, join as join22 } from "path";
|
|
9593
9926
|
function trackerPath(dataDir2) {
|
|
9594
|
-
return
|
|
9927
|
+
return join22(dataDir2, "mcp-as-pairing.json");
|
|
9595
9928
|
}
|
|
9596
9929
|
function readMcpPairedAsUrl(dataDir2) {
|
|
9597
9930
|
const path = trackerPath(dataDir2);
|
|
9598
|
-
if (!
|
|
9931
|
+
if (!existsSync22(path)) return null;
|
|
9599
9932
|
try {
|
|
9600
|
-
const data = JSON.parse(
|
|
9933
|
+
const data = JSON.parse(readFileSync23(path, "utf-8"));
|
|
9601
9934
|
return typeof data.asUrl === "string" ? data.asUrl : null;
|
|
9602
9935
|
} catch {
|
|
9603
9936
|
return null;
|
|
@@ -9605,12 +9938,12 @@ function readMcpPairedAsUrl(dataDir2) {
|
|
|
9605
9938
|
}
|
|
9606
9939
|
function writeMcpPairedAsUrl(dataDir2, asUrl) {
|
|
9607
9940
|
const path = trackerPath(dataDir2);
|
|
9608
|
-
|
|
9609
|
-
|
|
9941
|
+
mkdirSync17(dirname17(path), { recursive: true, mode: 448 });
|
|
9942
|
+
writeFileSync17(path, JSON.stringify({ asUrl }, null, 2), { encoding: "utf-8", mode: 384 });
|
|
9610
9943
|
}
|
|
9611
9944
|
|
|
9612
9945
|
// bin/http.ts
|
|
9613
|
-
var dataDir = process.env.SUVEREN_DATA_DIR ??
|
|
9946
|
+
var dataDir = process.env.SUVEREN_DATA_DIR ?? join23(homedir14(), ".suveren");
|
|
9614
9947
|
var spUrl = resolveAsUrl(dataDir);
|
|
9615
9948
|
validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
|
|
9616
9949
|
var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
|
|
@@ -9696,7 +10029,8 @@ app.post("/internal/configure", internalOnly, (req, res) => {
|
|
|
9696
10029
|
state.receiptArchive.setVaultKey(key);
|
|
9697
10030
|
state.reportStore.setVaultKey(key);
|
|
9698
10031
|
state.outcomeOutbox.setVaultKey(key);
|
|
9699
|
-
|
|
10032
|
+
state.executionResults.setVaultKey(key);
|
|
10033
|
+
console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log + receipt archive + report store + outcome outbox + execution results encryption active");
|
|
9700
10034
|
}
|
|
9701
10035
|
if (apiKey) {
|
|
9702
10036
|
state.spClient.setApiKey(apiKey);
|
|
@@ -10079,6 +10413,24 @@ app.get("/internal/skipped-commitments", internalOnly, async (_req, res) => {
|
|
|
10079
10413
|
res.status(502).json({ error: err instanceof Error ? err.message : "Could not reach the Authority Server" });
|
|
10080
10414
|
}
|
|
10081
10415
|
});
|
|
10416
|
+
app.get("/internal/my-pending-proposals", internalOnly, async (_req, res) => {
|
|
10417
|
+
try {
|
|
10418
|
+
const submitted = state.proposalSubmissions.list();
|
|
10419
|
+
const resolved = await Promise.all(
|
|
10420
|
+
submitted.map(async (s) => {
|
|
10421
|
+
try {
|
|
10422
|
+
return await state.spClient.getProposal(s.proposalId);
|
|
10423
|
+
} catch {
|
|
10424
|
+
return null;
|
|
10425
|
+
}
|
|
10426
|
+
})
|
|
10427
|
+
);
|
|
10428
|
+
const pending = resolved.filter((p) => p !== null && p.status === "pending");
|
|
10429
|
+
res.json({ proposals: pending });
|
|
10430
|
+
} catch (err) {
|
|
10431
|
+
res.status(502).json({ error: err instanceof Error ? err.message : "Could not reach the Authority Server" });
|
|
10432
|
+
}
|
|
10433
|
+
});
|
|
10082
10434
|
app.get("/internal/evidence", internalOnly, (_req, res) => {
|
|
10083
10435
|
if (state.receiptArchive.isLocked()) {
|
|
10084
10436
|
res.status(503).json({ error: "Vault locked \u2014 the evidence archive is encrypted until sign-in." });
|
|
@@ -10364,7 +10716,7 @@ app.listen(port, bindHost, () => {
|
|
|
10364
10716
|
(proposal) => executeCommitted(proposal, state, integrationManager)
|
|
10365
10717
|
);
|
|
10366
10718
|
installCommittedExecutor(committedExecutor);
|
|
10367
|
-
const dataDir2 = process.env.SUVEREN_DATA_DIR ??
|
|
10719
|
+
const dataDir2 = process.env.SUVEREN_DATA_DIR ?? join23(homedir14(), ".suveren");
|
|
10368
10720
|
const executorLock = new ExecutorLock(dataDir2);
|
|
10369
10721
|
const lock = executorLock.acquire();
|
|
10370
10722
|
if (!lock.held) {
|