@suveren/gateway 0.34.0 → 0.35.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/erp.json +75 -8
- package/content/integrations/gmail.json +8 -1
- package/content/integrations/mail.json +9 -2
- package/content/integrations/mollie.json +8 -0
- package/dist/control-plane/index.mjs +79 -0
- package/dist/mcp-server/http.mjs +841 -98
- package/dist/ui/assets/index-M1sLTJjt.js +123 -0
- package/dist/ui/assets/{index-BiGnI2Tk.css → index-c8cmIEPx.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/profiles/email/0.9.profile.json +252 -0
- package/profiles/index.json +2 -1
- package/dist/ui/assets/index-DKBwi4Yw.js +0 -123
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -3411,7 +3411,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3411
3411
|
import { z as z2 } from "zod";
|
|
3412
3412
|
|
|
3413
3413
|
// src/lib/mandate-brief.ts
|
|
3414
|
-
import { getProfile as
|
|
3414
|
+
import { getProfile as getProfile6 } from "@hap/core";
|
|
3415
3415
|
|
|
3416
3416
|
// src/lib/consumption.ts
|
|
3417
3417
|
import { boundActionTypes } from "@hap/core";
|
|
@@ -3647,67 +3647,17 @@ function appendVerificationFooter(tool, args, receiptId, subject) {
|
|
|
3647
3647
|
|
|
3648
3648
|
// src/lib/content-binding.ts
|
|
3649
3649
|
import {
|
|
3650
|
-
computeContentHash,
|
|
3650
|
+
computeContentHash as computeContentHash2,
|
|
3651
3651
|
selectBoundFields,
|
|
3652
3652
|
isFieldBinding,
|
|
3653
3653
|
bindingAppliesTo,
|
|
3654
|
-
getProfile
|
|
3654
|
+
getProfile as getProfile3
|
|
3655
3655
|
} from "@hap/core";
|
|
3656
|
-
function getContentBinding(profileId) {
|
|
3657
|
-
return getProfile(profileId)?.content_binding;
|
|
3658
|
-
}
|
|
3659
|
-
function computeContentBinding(profileId, tool, toolArgs, actionType) {
|
|
3660
|
-
const binding = getContentBinding(profileId);
|
|
3661
|
-
if (!binding) return void 0;
|
|
3662
|
-
if (isFieldBinding(binding)) {
|
|
3663
|
-
if (!bindingAppliesTo(binding, actionType)) {
|
|
3664
|
-
if (!actionType && binding.appliesTo) {
|
|
3665
|
-
console.error(
|
|
3666
|
-
`[Suveren MCP] Warning: ${tool?.namespacedName ?? profileId} has no action_type in staticExecution, so the ${profileId} content binding (${binding.appliesTo.join(", ")}) does not apply and the receipt will bind no content. Fix the integration manifest.`
|
|
3667
|
-
);
|
|
3668
|
-
}
|
|
3669
|
-
return void 0;
|
|
3670
|
-
}
|
|
3671
|
-
const bound = selectBoundFields(binding, toolArgs);
|
|
3672
|
-
return {
|
|
3673
|
-
contentHash: computeContentHash(binding, bound),
|
|
3674
|
-
contentBinding: { version: binding.version, kind: binding.kind, fields: binding.fields },
|
|
3675
|
-
boundContent: bound
|
|
3676
|
-
};
|
|
3677
|
-
}
|
|
3678
|
-
let contentHash;
|
|
3679
|
-
let boundContent;
|
|
3680
|
-
if (binding.kind === "jcs") {
|
|
3681
|
-
contentHash = computeContentHash(binding, toolArgs);
|
|
3682
|
-
boundContent = toolArgs;
|
|
3683
|
-
} else {
|
|
3684
|
-
const declared = tool?.gating?.contentField;
|
|
3685
|
-
const field = declared ?? (tool ? detectContentField(tool) : null);
|
|
3686
|
-
if (!field) return void 0;
|
|
3687
|
-
const raw = typeof toolArgs[field] === "string" ? toolArgs[field] : "";
|
|
3688
|
-
if (raw === "") return void 0;
|
|
3689
|
-
contentHash = computeContentHash(binding, raw);
|
|
3690
|
-
boundContent = raw;
|
|
3691
|
-
}
|
|
3692
|
-
return {
|
|
3693
|
-
contentHash,
|
|
3694
|
-
contentBinding: { version: binding.version, kind: binding.kind },
|
|
3695
|
-
boundContent
|
|
3696
|
-
};
|
|
3697
|
-
}
|
|
3698
|
-
function attachTicketId(tool, args, ticketId) {
|
|
3699
|
-
const schema = tool.inputSchema;
|
|
3700
|
-
if (!schema?.properties || !("ticket_id" in schema.properties)) return args;
|
|
3701
|
-
return { ...args, ticket_id: ticketId };
|
|
3702
|
-
}
|
|
3703
|
-
|
|
3704
|
-
// src/lib/preview.ts
|
|
3705
|
-
import { computeContentHash as computeContentHash2 } from "@hap/core";
|
|
3706
3656
|
|
|
3707
3657
|
// src/lib/manifest-loader.ts
|
|
3708
3658
|
import { readFileSync as readFileSync16, existsSync as existsSync15 } from "fs";
|
|
3709
3659
|
import { join as join15, resolve } from "path";
|
|
3710
|
-
import { getProfile
|
|
3660
|
+
import { getProfile } from "@hap/core";
|
|
3711
3661
|
|
|
3712
3662
|
// src/lib/tool-gating-types.ts
|
|
3713
3663
|
var MAX_PREVIEW_READS = 10;
|
|
@@ -3765,6 +3715,7 @@ function boundsSatisfyReadGate(bounds, gate) {
|
|
|
3765
3715
|
}
|
|
3766
3716
|
function readToolIsGoverned(g) {
|
|
3767
3717
|
if (!g) return false;
|
|
3718
|
+
if (g.internal === true) return true;
|
|
3768
3719
|
return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
|
|
3769
3720
|
}
|
|
3770
3721
|
function allowedResources(contexts, boundField) {
|
|
@@ -3888,6 +3839,7 @@ function uncoveredReadSwitchReason(overrides, boundsSchema) {
|
|
|
3888
3839
|
if (entry !== null && typeof entry !== "object") continue;
|
|
3889
3840
|
const e = entry ?? {};
|
|
3890
3841
|
if (entry !== null && e.category !== "read") continue;
|
|
3842
|
+
if (e.internal === true) continue;
|
|
3891
3843
|
if (e.readGovernance === "none") {
|
|
3892
3844
|
if (!e.readGovernanceReason) {
|
|
3893
3845
|
return `override "${toolName}" declares readGovernance:"none" with no readGovernanceReason \u2014 an exemption from this profile's switch(es) [${anySwitch.join(", ")}] needs a written reason`;
|
|
@@ -3994,6 +3946,9 @@ function invalidReadConfigReason(toolName, idx, entry) {
|
|
|
3994
3946
|
}
|
|
3995
3947
|
}
|
|
3996
3948
|
}
|
|
3949
|
+
if (p.kind !== void 0 && p.kind !== "document") {
|
|
3950
|
+
return `${label}.kind must be "document" (the only recognized value)`;
|
|
3951
|
+
}
|
|
3997
3952
|
return null;
|
|
3998
3953
|
}
|
|
3999
3954
|
function invalidPreviewReason(toolName, entry) {
|
|
@@ -4047,6 +4002,9 @@ function invalidDisplayHintsReason(toolName, entry) {
|
|
|
4047
4002
|
}
|
|
4048
4003
|
}
|
|
4049
4004
|
}
|
|
4005
|
+
if (d.aiText !== void 0 && (typeof d.aiText !== "string" || d.aiText.length === 0)) {
|
|
4006
|
+
return `override "${toolName}".display.aiText must be a non-empty string naming an argument`;
|
|
4007
|
+
}
|
|
4050
4008
|
return null;
|
|
4051
4009
|
}
|
|
4052
4010
|
var SUMMARY_FORMATS = ["money", "percent", "list", "short", "date"];
|
|
@@ -4083,6 +4041,21 @@ function invalidSummaryReason(toolName, entry) {
|
|
|
4083
4041
|
}
|
|
4084
4042
|
return null;
|
|
4085
4043
|
}
|
|
4044
|
+
function invalidInternalReason(toolName, entry) {
|
|
4045
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
4046
|
+
const internal = entry.internal;
|
|
4047
|
+
if (internal === void 0) return null;
|
|
4048
|
+
if (typeof internal !== "boolean") return `override "${toolName}".internal must be a boolean`;
|
|
4049
|
+
return null;
|
|
4050
|
+
}
|
|
4051
|
+
function invalidManifestInternalReason(manifest) {
|
|
4052
|
+
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4053
|
+
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
4054
|
+
const reason = invalidInternalReason(toolName, entry);
|
|
4055
|
+
if (reason) return reason;
|
|
4056
|
+
}
|
|
4057
|
+
return null;
|
|
4058
|
+
}
|
|
4086
4059
|
function invalidManifestPreviewReason(manifest) {
|
|
4087
4060
|
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4088
4061
|
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
@@ -4316,6 +4289,92 @@ function invalidManifestContextReason(manifest) {
|
|
|
4316
4289
|
}
|
|
4317
4290
|
return null;
|
|
4318
4291
|
}
|
|
4292
|
+
function invalidDocumentSourceReason(kind, entry, overrides) {
|
|
4293
|
+
const label = `documents.${kind}`;
|
|
4294
|
+
const readReason = invalidContextReadReason(label, entry);
|
|
4295
|
+
if (readReason) return readReason;
|
|
4296
|
+
const tool = entry.tool;
|
|
4297
|
+
const toolEntry = overrides[tool];
|
|
4298
|
+
if (toolEntry?.internal !== true) {
|
|
4299
|
+
return `${label}.tool "${tool}" must be marked "internal: true" on this manifest's toolGating.overrides \u2014 a document read must never be directly callable by the AI`;
|
|
4300
|
+
}
|
|
4301
|
+
const docLabel = entry.label;
|
|
4302
|
+
if (docLabel !== void 0 && (typeof docLabel !== "string" || docLabel.length === 0)) {
|
|
4303
|
+
return `${label}.label must be a non-empty string`;
|
|
4304
|
+
}
|
|
4305
|
+
return null;
|
|
4306
|
+
}
|
|
4307
|
+
function invalidAttachmentTransportReason(entry) {
|
|
4308
|
+
const label = "attachments";
|
|
4309
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return `${label} must be an object`;
|
|
4310
|
+
const a = entry;
|
|
4311
|
+
if (a.transport !== "field" && a.transport !== "mime-raw") {
|
|
4312
|
+
return `${label}.transport must be "field" or "mime-raw"`;
|
|
4313
|
+
}
|
|
4314
|
+
if (!isNonEmptyString(a.arg)) return `${label}.arg must be a non-empty string`;
|
|
4315
|
+
return null;
|
|
4316
|
+
}
|
|
4317
|
+
function invalidInsertValueSourceReason(kind, entry) {
|
|
4318
|
+
const label = `values.${kind}`;
|
|
4319
|
+
const readReason = invalidContextReadReason(label, entry);
|
|
4320
|
+
if (readReason) return readReason;
|
|
4321
|
+
const v = entry;
|
|
4322
|
+
if (!isNonEmptyString(v.path)) return `${label}.path must be a non-empty string`;
|
|
4323
|
+
if (!Array.isArray(v.allowedHosts) || v.allowedHosts.length === 0) {
|
|
4324
|
+
return `${label}.allowedHosts must be a non-empty array of hostnames`;
|
|
4325
|
+
}
|
|
4326
|
+
for (const [i, h] of v.allowedHosts.entries()) {
|
|
4327
|
+
if (typeof h !== "string" || h.length === 0) return `${label}.allowedHosts[${i}] must be a non-empty string`;
|
|
4328
|
+
}
|
|
4329
|
+
return null;
|
|
4330
|
+
}
|
|
4331
|
+
function invalidManifestDocumentsReason(manifest) {
|
|
4332
|
+
if (!manifest.documents) return null;
|
|
4333
|
+
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4334
|
+
for (const [kind, entry] of Object.entries(manifest.documents)) {
|
|
4335
|
+
const reason = invalidDocumentSourceReason(kind, entry, overrides);
|
|
4336
|
+
if (reason) return reason;
|
|
4337
|
+
}
|
|
4338
|
+
return null;
|
|
4339
|
+
}
|
|
4340
|
+
function invalidManifestAttachmentsReason(manifest) {
|
|
4341
|
+
if (manifest.attachments !== void 0) {
|
|
4342
|
+
const reason = invalidAttachmentTransportReason(manifest.attachments);
|
|
4343
|
+
if (reason) return reason;
|
|
4344
|
+
}
|
|
4345
|
+
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4346
|
+
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
4347
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
4348
|
+
const e = entry;
|
|
4349
|
+
if (e.attachmentsArg !== void 0) {
|
|
4350
|
+
if (typeof e.attachmentsArg !== "string" || e.attachmentsArg.length === 0) {
|
|
4351
|
+
return `override "${toolName}".attachmentsArg must be a non-empty string`;
|
|
4352
|
+
}
|
|
4353
|
+
if (!manifest.attachments) {
|
|
4354
|
+
return `override "${toolName}" declares "attachmentsArg" but this manifest declares no top-level "attachments" transport`;
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
if (e.insertFields !== void 0) {
|
|
4358
|
+
if (!Array.isArray(e.insertFields) || e.insertFields.length === 0) {
|
|
4359
|
+
return `override "${toolName}".insertFields must be a non-empty array of argument names`;
|
|
4360
|
+
}
|
|
4361
|
+
for (const [i, f] of e.insertFields.entries()) {
|
|
4362
|
+
if (typeof f !== "string" || f.length === 0) {
|
|
4363
|
+
return `override "${toolName}".insertFields[${i}] must be a non-empty string`;
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
return null;
|
|
4369
|
+
}
|
|
4370
|
+
function invalidManifestValuesReason(manifest) {
|
|
4371
|
+
if (!manifest.values) return null;
|
|
4372
|
+
for (const [kind, entry] of Object.entries(manifest.values)) {
|
|
4373
|
+
const reason = invalidInsertValueSourceReason(kind, entry);
|
|
4374
|
+
if (reason) return reason;
|
|
4375
|
+
}
|
|
4376
|
+
return null;
|
|
4377
|
+
}
|
|
4319
4378
|
function overrideProfile(entry) {
|
|
4320
4379
|
if (entry === null || typeof entry !== "object") return void 0;
|
|
4321
4380
|
const p = entry.profile;
|
|
@@ -4334,8 +4393,8 @@ function invalidManifestOverrideProfilesReason(manifest) {
|
|
|
4334
4393
|
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
4335
4394
|
const profile = overrideProfile(entry);
|
|
4336
4395
|
if (!profile) continue;
|
|
4337
|
-
if (!
|
|
4338
|
-
if (!
|
|
4396
|
+
if (!getProfile(manifest.profile) && !getProfile(profile)) continue;
|
|
4397
|
+
if (!getProfile(profile)) {
|
|
4339
4398
|
return `override "${toolName}".profile "${profile}" does not resolve to a known profile (full id or short name) \u2014 area-per-tool overrides must name a real profile, same as the manifest's own "profile"`;
|
|
4340
4399
|
}
|
|
4341
4400
|
}
|
|
@@ -4344,7 +4403,7 @@ function invalidManifestOverrideProfilesReason(manifest) {
|
|
|
4344
4403
|
function invalidManifestReadGateReason(manifest) {
|
|
4345
4404
|
const allOverrides = manifest.toolGating?.overrides ?? {};
|
|
4346
4405
|
for (const profileId of distinctProfilesOf(manifest)) {
|
|
4347
|
-
const profileDef =
|
|
4406
|
+
const profileDef = getProfile(profileId);
|
|
4348
4407
|
if (!profileDef) continue;
|
|
4349
4408
|
const ownOverrides = {};
|
|
4350
4409
|
for (const [toolName, entry] of Object.entries(allOverrides)) {
|
|
@@ -4388,6 +4447,11 @@ function loadManifests(integrationsDir) {
|
|
|
4388
4447
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${previewReason}`);
|
|
4389
4448
|
continue;
|
|
4390
4449
|
}
|
|
4450
|
+
const internalReason = invalidManifestInternalReason(manifest);
|
|
4451
|
+
if (internalReason) {
|
|
4452
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${internalReason}`);
|
|
4453
|
+
continue;
|
|
4454
|
+
}
|
|
4391
4455
|
const contextReason = invalidManifestContextReason(manifest);
|
|
4392
4456
|
if (contextReason) {
|
|
4393
4457
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${contextReason}`);
|
|
@@ -4403,6 +4467,21 @@ function loadManifests(integrationsDir) {
|
|
|
4403
4467
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${readGateReason}`);
|
|
4404
4468
|
continue;
|
|
4405
4469
|
}
|
|
4470
|
+
const documentsReason = invalidManifestDocumentsReason(manifest);
|
|
4471
|
+
if (documentsReason) {
|
|
4472
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${documentsReason}`);
|
|
4473
|
+
continue;
|
|
4474
|
+
}
|
|
4475
|
+
const attachmentsReason = invalidManifestAttachmentsReason(manifest);
|
|
4476
|
+
if (attachmentsReason) {
|
|
4477
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${attachmentsReason}`);
|
|
4478
|
+
continue;
|
|
4479
|
+
}
|
|
4480
|
+
const valuesReason = invalidManifestValuesReason(manifest);
|
|
4481
|
+
if (valuesReason) {
|
|
4482
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${valuesReason}`);
|
|
4483
|
+
continue;
|
|
4484
|
+
}
|
|
4406
4485
|
manifests.set(id, manifest);
|
|
4407
4486
|
loaded++;
|
|
4408
4487
|
} catch (err) {
|
|
@@ -4419,7 +4498,138 @@ function getAllManifests() {
|
|
|
4419
4498
|
return Array.from(manifests.values());
|
|
4420
4499
|
}
|
|
4421
4500
|
|
|
4501
|
+
// src/lib/attachment-resolution.ts
|
|
4502
|
+
import { getProfile as getProfile2 } from "@hap/core";
|
|
4503
|
+
|
|
4504
|
+
// src/lib/attachment-reference.ts
|
|
4505
|
+
var REF_KEYS = ["connector", "kind", "id"];
|
|
4506
|
+
var INSERT_KEYS = ["placeholder", "connector", "kind", "id"];
|
|
4507
|
+
var ATTACHMENT_REFERENCE_SCHEMA = {
|
|
4508
|
+
type: "object",
|
|
4509
|
+
description: "A reference to a document already known to a connector \u2014 never file bytes. The gateway fetches, shows, and seals the file; it is never carried here.",
|
|
4510
|
+
properties: {
|
|
4511
|
+
connector: { type: "string", description: 'The integration id that can provide this document (e.g. "erp").' },
|
|
4512
|
+
kind: { type: "string", description: 'The document kind that connector declares (e.g. "invoice-pdf").' },
|
|
4513
|
+
id: { type: "string", description: "The id of the specific record (e.g. the invoice id)." }
|
|
4514
|
+
},
|
|
4515
|
+
required: ["connector", "kind", "id"],
|
|
4516
|
+
additionalProperties: false
|
|
4517
|
+
};
|
|
4518
|
+
var INSERT_REFERENCE_SCHEMA = {
|
|
4519
|
+
type: "object",
|
|
4520
|
+
description: "A value read from another system to insert into this text \u2014 never typed here. The gateway reads it, checks its source, and replaces the placeholder before sending.",
|
|
4521
|
+
properties: {
|
|
4522
|
+
placeholder: { type: "string", description: "The exact {{placeholder}} token in the text to replace." },
|
|
4523
|
+
connector: { type: "string", description: 'The integration id that can provide this value (e.g. "mollie").' },
|
|
4524
|
+
kind: { type: "string", description: 'The value kind that connector declares (e.g. "payment-link").' },
|
|
4525
|
+
id: { type: "string", description: "The id of the specific record (e.g. the payment link id)." }
|
|
4526
|
+
},
|
|
4527
|
+
required: ["placeholder", "connector", "kind", "id"],
|
|
4528
|
+
additionalProperties: false
|
|
4529
|
+
};
|
|
4530
|
+
function withAttachmentAndInsertSchema(schema, gating) {
|
|
4531
|
+
if (!gating?.attachmentsArg && !gating?.insertFields?.length) return schema;
|
|
4532
|
+
const props = { ...schema.properties ?? {} };
|
|
4533
|
+
if (gating.attachmentsArg) {
|
|
4534
|
+
props[gating.attachmentsArg] = {
|
|
4535
|
+
type: "array",
|
|
4536
|
+
description: "Attachments by reference only \u2014 see items.",
|
|
4537
|
+
items: ATTACHMENT_REFERENCE_SCHEMA
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
if (gating.insertFields?.length) {
|
|
4541
|
+
props.insert = {
|
|
4542
|
+
type: "array",
|
|
4543
|
+
description: "Values to insert into this call\u2019s text by reference only \u2014 see items.",
|
|
4544
|
+
items: INSERT_REFERENCE_SCHEMA
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4547
|
+
return { ...schema, properties: props };
|
|
4548
|
+
}
|
|
4549
|
+
function validateOne(value, index, keys) {
|
|
4550
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4551
|
+
return {
|
|
4552
|
+
ok: false,
|
|
4553
|
+
error: { code: "NOT_AN_OBJECT", index, message: `item ${index} must be an object with exactly ${keys.join(", ")}` }
|
|
4554
|
+
};
|
|
4555
|
+
}
|
|
4556
|
+
const obj = value;
|
|
4557
|
+
const extra = Object.keys(obj).filter((k) => !keys.includes(k));
|
|
4558
|
+
if (extra.length > 0) {
|
|
4559
|
+
return {
|
|
4560
|
+
ok: false,
|
|
4561
|
+
error: {
|
|
4562
|
+
code: "EXTRA_FIELD",
|
|
4563
|
+
index,
|
|
4564
|
+
field: extra[0],
|
|
4565
|
+
message: `item ${index} carries "${extra[0]}", which this reference does not accept. Only ${keys.join(", ")} are allowed \u2014 a reference never carries file bytes or a typed value.`
|
|
4566
|
+
}
|
|
4567
|
+
};
|
|
4568
|
+
}
|
|
4569
|
+
for (const k of keys) {
|
|
4570
|
+
if (typeof obj[k] !== "string" || obj[k].length === 0) {
|
|
4571
|
+
return {
|
|
4572
|
+
ok: false,
|
|
4573
|
+
error: { code: "MISSING_FIELD", index, field: k, message: `item ${index}.${k} must be a non-empty string` }
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
return { ok: true, value: obj };
|
|
4578
|
+
}
|
|
4579
|
+
function validateDocumentReferences(value) {
|
|
4580
|
+
if (value === void 0) return { ok: true, refs: [] };
|
|
4581
|
+
if (!Array.isArray(value)) {
|
|
4582
|
+
return { ok: false, error: { code: "NOT_AN_ARRAY", message: "attachments must be an array of references" } };
|
|
4583
|
+
}
|
|
4584
|
+
const refs = [];
|
|
4585
|
+
for (let i = 0; i < value.length; i++) {
|
|
4586
|
+
const r = validateOne(value[i], i, REF_KEYS);
|
|
4587
|
+
if (!r.ok) return r;
|
|
4588
|
+
refs.push(r.value);
|
|
4589
|
+
}
|
|
4590
|
+
return { ok: true, refs };
|
|
4591
|
+
}
|
|
4592
|
+
function validateInsertReferences(value) {
|
|
4593
|
+
if (value === void 0) return { ok: true, refs: [] };
|
|
4594
|
+
if (!Array.isArray(value)) {
|
|
4595
|
+
return { ok: false, error: { code: "NOT_AN_ARRAY", message: "insert must be an array of references" } };
|
|
4596
|
+
}
|
|
4597
|
+
const refs = [];
|
|
4598
|
+
for (let i = 0; i < value.length; i++) {
|
|
4599
|
+
const r = validateOne(value[i], i, INSERT_KEYS);
|
|
4600
|
+
if (!r.ok) return r;
|
|
4601
|
+
refs.push(r.value);
|
|
4602
|
+
}
|
|
4603
|
+
return { ok: true, refs };
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
// src/lib/attachment-policy.ts
|
|
4607
|
+
var ATTACHMENT_SOURCES_FIELD = "allowed_attachment_sources";
|
|
4608
|
+
function attachmentSourcesFieldDeclared(profile) {
|
|
4609
|
+
return !!profile?.scopeSchema?.fields?.[ATTACHMENT_SOURCES_FIELD];
|
|
4610
|
+
}
|
|
4611
|
+
function isAttachmentSourceAllowed(profileId, profile, allowedSourcesValue, ref) {
|
|
4612
|
+
if (!attachmentSourcesFieldDeclared(profile)) {
|
|
4613
|
+
return {
|
|
4614
|
+
allowed: false,
|
|
4615
|
+
reason: `Profile "${profileId}" does not yet declare "${ATTACHMENT_SOURCES_FIELD}" \u2014 no mandate can authorize an attachment until it does. Refusing by default (suveren-invoicing-plan.md \xA76).`
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
const allowedSet = new Set(
|
|
4619
|
+
(allowedSourcesValue ?? "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
4620
|
+
);
|
|
4621
|
+
const key = `${ref.connector}:${ref.kind}`;
|
|
4622
|
+
if (!allowedSet.has(key)) {
|
|
4623
|
+
return {
|
|
4624
|
+
allowed: false,
|
|
4625
|
+
reason: `"${key}" is not in this mandate's "${ATTACHMENT_SOURCES_FIELD}" (${allowedSourcesValue || "(none)"}).`
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
return { allowed: true, reason: "" };
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4422
4631
|
// src/lib/preview.ts
|
|
4632
|
+
import { computeContentHash } from "@hap/core";
|
|
4423
4633
|
var PREVIEW_READ_TIMEOUT_MS = 5e3;
|
|
4424
4634
|
var PREVIEW_MESSAGE_CAP = 500;
|
|
4425
4635
|
function capText(text, max = PREVIEW_MESSAGE_CAP) {
|
|
@@ -4496,9 +4706,9 @@ async function readPreview(integrationManager2, integrationId, previewTool, args
|
|
|
4496
4706
|
}
|
|
4497
4707
|
function hashPreviewBody(body) {
|
|
4498
4708
|
if (body.structured !== void 0) {
|
|
4499
|
-
return
|
|
4709
|
+
return computeContentHash({ kind: "jcs", version: "1" }, body.structured);
|
|
4500
4710
|
}
|
|
4501
|
-
return
|
|
4711
|
+
return computeContentHash({ kind: "text", version: "1" }, body.text ?? "");
|
|
4502
4712
|
}
|
|
4503
4713
|
function isPlainRecord(v) {
|
|
4504
4714
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -4511,6 +4721,7 @@ function planPreviewReads(declaration, actionArgs) {
|
|
|
4511
4721
|
attach: config.attach,
|
|
4512
4722
|
fields: config.fields,
|
|
4513
4723
|
version: config.version,
|
|
4724
|
+
kind: config.kind,
|
|
4514
4725
|
argsMapping: config.args
|
|
4515
4726
|
};
|
|
4516
4727
|
if (config.forEach) {
|
|
@@ -4537,7 +4748,8 @@ async function runPreviewReadPlan(integrationManager2, integrationId, plan) {
|
|
|
4537
4748
|
tool: item.tool,
|
|
4538
4749
|
...item.attach ? { attach: item.attach } : {},
|
|
4539
4750
|
...sourceArgNames.length === 1 ? { sourceArg: sourceArgNames[0] } : {},
|
|
4540
|
-
...item.fields ? { fields: item.fields } : {}
|
|
4751
|
+
...item.fields ? { fields: item.fields } : {},
|
|
4752
|
+
...item.kind ? { kind: item.kind } : {}
|
|
4541
4753
|
};
|
|
4542
4754
|
if (item.elements !== void 0) {
|
|
4543
4755
|
const elements = [];
|
|
@@ -4636,7 +4848,7 @@ async function combinedPreviewReadsHash(integrationManager2, integrationId, read
|
|
|
4636
4848
|
hashes.push(hashPreviewBody(read.body));
|
|
4637
4849
|
}
|
|
4638
4850
|
if (hashes.length === 0) return void 0;
|
|
4639
|
-
return
|
|
4851
|
+
return computeContentHash({ kind: "jcs", version: "1" }, { reads: hashes });
|
|
4640
4852
|
}
|
|
4641
4853
|
async function buildInternalPreview(integrationManager2, integrationId, toolName, actionArgs) {
|
|
4642
4854
|
const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
|
|
@@ -4686,6 +4898,20 @@ async function buildInternalPreview(integrationManager2, integrationId, toolName
|
|
|
4686
4898
|
}
|
|
4687
4899
|
});
|
|
4688
4900
|
}
|
|
4901
|
+
function structuredOrJsonText(body) {
|
|
4902
|
+
if (body.structured && typeof body.structured === "object") return body.structured;
|
|
4903
|
+
const t = body.text?.trim();
|
|
4904
|
+
if (!t || !t.startsWith("{")) return void 0;
|
|
4905
|
+
try {
|
|
4906
|
+
const parsed = JSON.parse(t);
|
|
4907
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
4908
|
+
} catch {
|
|
4909
|
+
return void 0;
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
// src/lib/document-source.ts
|
|
4914
|
+
import { createHash as createHash3 } from "crypto";
|
|
4689
4915
|
|
|
4690
4916
|
// src/lib/field-path.ts
|
|
4691
4917
|
function matchesWhere(element, where) {
|
|
@@ -5103,6 +5329,447 @@ async function buildRequestContext(integrationManager2, executionJournal, action
|
|
|
5103
5329
|
return { request, history };
|
|
5104
5330
|
}
|
|
5105
5331
|
|
|
5332
|
+
// src/lib/document-source.ts
|
|
5333
|
+
function sha256Hex(buf) {
|
|
5334
|
+
return createHash3("sha256").update(buf).digest("hex");
|
|
5335
|
+
}
|
|
5336
|
+
async function resolveDocumentReference(integrationManager2, ref) {
|
|
5337
|
+
const manifest = getManifest(ref.connector);
|
|
5338
|
+
if (!manifest) {
|
|
5339
|
+
return { ok: false, error: { code: "UNKNOWN_CONNECTOR", message: `"${ref.connector}" is not a known connector.` } };
|
|
5340
|
+
}
|
|
5341
|
+
const docConfig = manifest.documents?.[ref.kind];
|
|
5342
|
+
if (!docConfig) {
|
|
5343
|
+
return {
|
|
5344
|
+
ok: false,
|
|
5345
|
+
error: {
|
|
5346
|
+
code: "UNDECLARED_KIND",
|
|
5347
|
+
message: `Connector "${ref.connector}" declares no document kind "${ref.kind}".`
|
|
5348
|
+
}
|
|
5349
|
+
};
|
|
5350
|
+
}
|
|
5351
|
+
const toolEntry = manifest.toolGating?.overrides?.[docConfig.tool];
|
|
5352
|
+
if (!toolEntry?.internal) {
|
|
5353
|
+
return {
|
|
5354
|
+
ok: false,
|
|
5355
|
+
error: {
|
|
5356
|
+
code: "NOT_INTERNAL",
|
|
5357
|
+
message: `Connector "${ref.connector}"'s document read "${docConfig.tool}" is not marked internal \u2014 refusing to call it for a document reference.`
|
|
5358
|
+
}
|
|
5359
|
+
};
|
|
5360
|
+
}
|
|
5361
|
+
const args = substitute(docConfig.args, { id: ref.id });
|
|
5362
|
+
const read = await readPreview(integrationManager2, ref.connector, docConfig.tool, args);
|
|
5363
|
+
if (read.status === "not_found") {
|
|
5364
|
+
return { ok: false, error: { code: "NOT_FOUND", message: read.message ?? `No document "${ref.kind}"/"${ref.id}" on "${ref.connector}".` } };
|
|
5365
|
+
}
|
|
5366
|
+
if (read.status !== "ok") {
|
|
5367
|
+
return { ok: false, error: { code: "UNAVAILABLE", message: read.message ?? `Connector "${ref.connector}" could not be reached.` } };
|
|
5368
|
+
}
|
|
5369
|
+
const body = structuredOrJsonText(read.body);
|
|
5370
|
+
if (!body || typeof body !== "object") {
|
|
5371
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: `Document read for "${ref.connector}:${ref.kind}" returned no structured result.` } };
|
|
5372
|
+
}
|
|
5373
|
+
const { filename, contentType, contentBase64, sha256: claimedSha256 } = body;
|
|
5374
|
+
if (typeof filename !== "string" || !filename) {
|
|
5375
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: 'Document result is missing "filename".' } };
|
|
5376
|
+
}
|
|
5377
|
+
if (typeof contentType !== "string" || !contentType) {
|
|
5378
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: 'Document result is missing "contentType".' } };
|
|
5379
|
+
}
|
|
5380
|
+
if (typeof contentBase64 !== "string" || !contentBase64) {
|
|
5381
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: 'Document result is missing "contentBase64".' } };
|
|
5382
|
+
}
|
|
5383
|
+
let bytes;
|
|
5384
|
+
try {
|
|
5385
|
+
bytes = Buffer.from(contentBase64, "base64");
|
|
5386
|
+
} catch {
|
|
5387
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: '"contentBase64" is not valid base64.' } };
|
|
5388
|
+
}
|
|
5389
|
+
const computedSha256 = sha256Hex(bytes);
|
|
5390
|
+
if (typeof claimedSha256 === "string" && claimedSha256.length > 0 && claimedSha256 !== computedSha256) {
|
|
5391
|
+
return {
|
|
5392
|
+
ok: false,
|
|
5393
|
+
error: {
|
|
5394
|
+
code: "HASH_MISMATCH",
|
|
5395
|
+
message: `Connector "${ref.connector}" claimed sha256 ${claimedSha256} for "${ref.kind}:${ref.id}" but the bytes hash to ${computedSha256}.`
|
|
5396
|
+
}
|
|
5397
|
+
};
|
|
5398
|
+
}
|
|
5399
|
+
return { ok: true, doc: { filename, contentType, contentBase64, sha256: computedSha256, bytes: bytes.byteLength } };
|
|
5400
|
+
}
|
|
5401
|
+
async function resolveAttachmentPreview(integrationManager2, ref, sealedSha256) {
|
|
5402
|
+
const result = await resolveDocumentReference(integrationManager2, ref);
|
|
5403
|
+
if (!result.ok) {
|
|
5404
|
+
switch (result.error.code) {
|
|
5405
|
+
case "NOT_FOUND":
|
|
5406
|
+
return { status: "not_found", message: result.error.message };
|
|
5407
|
+
case "UNKNOWN_CONNECTOR":
|
|
5408
|
+
case "UNDECLARED_KIND":
|
|
5409
|
+
case "NOT_INTERNAL":
|
|
5410
|
+
return { status: "error", message: result.error.message };
|
|
5411
|
+
default:
|
|
5412
|
+
return { status: "unavailable", message: result.error.message };
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
if (result.doc.sha256 !== sealedSha256) {
|
|
5416
|
+
return {
|
|
5417
|
+
status: "changed",
|
|
5418
|
+
message: `"${result.doc.filename}" (${ref.connector}:${ref.kind}/${ref.id}) changed since it was sealed \u2014 its fingerprint no longer matches.`
|
|
5419
|
+
};
|
|
5420
|
+
}
|
|
5421
|
+
return { status: "ok", ...result.doc };
|
|
5422
|
+
}
|
|
5423
|
+
|
|
5424
|
+
// src/lib/insert-values.ts
|
|
5425
|
+
function hostAllowed(host, allowed) {
|
|
5426
|
+
const h = host.toLowerCase();
|
|
5427
|
+
const a = allowed.toLowerCase();
|
|
5428
|
+
return h === a || h.endsWith(`.${a}`);
|
|
5429
|
+
}
|
|
5430
|
+
async function resolveInsertValue(integrationManager2, ref) {
|
|
5431
|
+
const manifest = getManifest(ref.connector);
|
|
5432
|
+
if (!manifest) {
|
|
5433
|
+
return { ok: false, error: { code: "UNKNOWN_CONNECTOR", message: `"${ref.connector}" is not a known connector.` } };
|
|
5434
|
+
}
|
|
5435
|
+
const valueConfig = manifest.values?.[ref.kind];
|
|
5436
|
+
if (!valueConfig) {
|
|
5437
|
+
return {
|
|
5438
|
+
ok: false,
|
|
5439
|
+
error: { code: "UNDECLARED_KIND", message: `Connector "${ref.connector}" declares no insertable value kind "${ref.kind}".` }
|
|
5440
|
+
};
|
|
5441
|
+
}
|
|
5442
|
+
const args = substitute(valueConfig.args, { id: ref.id });
|
|
5443
|
+
const read = await readPreview(integrationManager2, ref.connector, valueConfig.tool, args);
|
|
5444
|
+
if (read.status === "not_found") {
|
|
5445
|
+
return { ok: false, error: { code: "NOT_FOUND", message: read.message ?? `No value "${ref.kind}"/"${ref.id}" on "${ref.connector}".` } };
|
|
5446
|
+
}
|
|
5447
|
+
if (read.status !== "ok") {
|
|
5448
|
+
return { ok: false, error: { code: "UNAVAILABLE", message: read.message ?? `Connector "${ref.connector}" could not be reached.` } };
|
|
5449
|
+
}
|
|
5450
|
+
const body = structuredOrJsonText(read.body);
|
|
5451
|
+
if (!body || typeof body !== "object") {
|
|
5452
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: `Value read for "${ref.connector}:${ref.kind}" returned no structured result.` } };
|
|
5453
|
+
}
|
|
5454
|
+
const raw = getByDottedPath(body, valueConfig.path);
|
|
5455
|
+
if (typeof raw !== "string" || !raw) {
|
|
5456
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: `Value read for "${ref.connector}:${ref.kind}" has nothing at "${valueConfig.path}".` } };
|
|
5457
|
+
}
|
|
5458
|
+
let url;
|
|
5459
|
+
try {
|
|
5460
|
+
url = new URL(raw);
|
|
5461
|
+
} catch {
|
|
5462
|
+
return { ok: false, error: { code: "NOT_HTTPS", message: `Value "${raw}" is not a valid URL.` } };
|
|
5463
|
+
}
|
|
5464
|
+
if (url.protocol !== "https:") {
|
|
5465
|
+
return { ok: false, error: { code: "NOT_HTTPS", message: `Value "${raw}" is not an https URL.` } };
|
|
5466
|
+
}
|
|
5467
|
+
if (!valueConfig.allowedHosts.some((h) => hostAllowed(url.hostname, h))) {
|
|
5468
|
+
return {
|
|
5469
|
+
ok: false,
|
|
5470
|
+
error: {
|
|
5471
|
+
code: "DISALLOWED_HOST",
|
|
5472
|
+
message: `Value "${raw}" is on host "${url.hostname}", which "${ref.connector}:${ref.kind}" does not allow (allowed: ${valueConfig.allowedHosts.join(", ")}).`
|
|
5473
|
+
}
|
|
5474
|
+
};
|
|
5475
|
+
}
|
|
5476
|
+
return { ok: true, insert: { placeholder: ref.placeholder, value: raw, connector: ref.connector, kind: ref.kind } };
|
|
5477
|
+
}
|
|
5478
|
+
function placeholdersIn(text) {
|
|
5479
|
+
const found = [];
|
|
5480
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5481
|
+
for (const m of text.matchAll(/\{\{[^{}]+\}\}/g)) {
|
|
5482
|
+
if (!seen.has(m[0])) {
|
|
5483
|
+
seen.add(m[0]);
|
|
5484
|
+
found.push(m[0]);
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
return found;
|
|
5488
|
+
}
|
|
5489
|
+
function applyInserts(fields, inserts) {
|
|
5490
|
+
const allPlaceholders = /* @__PURE__ */ new Set();
|
|
5491
|
+
for (const text of Object.values(fields)) {
|
|
5492
|
+
for (const p of placeholdersIn(text)) allPlaceholders.add(p);
|
|
5493
|
+
}
|
|
5494
|
+
const insertByPlaceholder = new Map(inserts.map((i) => [i.placeholder, i]));
|
|
5495
|
+
for (const p of allPlaceholders) {
|
|
5496
|
+
if (!insertByPlaceholder.has(p)) {
|
|
5497
|
+
return {
|
|
5498
|
+
ok: false,
|
|
5499
|
+
error: {
|
|
5500
|
+
code: "UNMATCHED_PLACEHOLDER",
|
|
5501
|
+
placeholder: p,
|
|
5502
|
+
message: `The text contains "${p}" but no "insert" entry names that placeholder.`
|
|
5503
|
+
}
|
|
5504
|
+
};
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
for (const insert of inserts) {
|
|
5508
|
+
if (!allPlaceholders.has(insert.placeholder)) {
|
|
5509
|
+
return {
|
|
5510
|
+
ok: false,
|
|
5511
|
+
error: {
|
|
5512
|
+
code: "PLACEHOLDER_NOT_FOUND",
|
|
5513
|
+
placeholder: insert.placeholder,
|
|
5514
|
+
message: `"insert" names placeholder "${insert.placeholder}", which does not appear in the text.`
|
|
5515
|
+
}
|
|
5516
|
+
};
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
const out = {};
|
|
5520
|
+
for (const [field, text] of Object.entries(fields)) {
|
|
5521
|
+
let replaced = text;
|
|
5522
|
+
for (const insert of inserts) {
|
|
5523
|
+
replaced = replaced.split(insert.placeholder).join(insert.value);
|
|
5524
|
+
}
|
|
5525
|
+
out[field] = replaced;
|
|
5526
|
+
}
|
|
5527
|
+
return { ok: true, fields: out };
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5530
|
+
// src/lib/mime-message.ts
|
|
5531
|
+
var CRLF = "\r\n";
|
|
5532
|
+
function isAscii(s) {
|
|
5533
|
+
return /^[\x20-\x7e]*$/.test(s);
|
|
5534
|
+
}
|
|
5535
|
+
function rfc2047Encode(value) {
|
|
5536
|
+
if (isAscii(value)) return value;
|
|
5537
|
+
const b64 = Buffer.from(value, "utf8").toString("base64");
|
|
5538
|
+
return `=?UTF-8?B?${b64}?=`;
|
|
5539
|
+
}
|
|
5540
|
+
function wrapBase64(b64) {
|
|
5541
|
+
const lines = [];
|
|
5542
|
+
for (let i = 0; i < b64.length; i += 76) lines.push(b64.slice(i, i + 76));
|
|
5543
|
+
return lines.join(CRLF);
|
|
5544
|
+
}
|
|
5545
|
+
function makeBoundary() {
|
|
5546
|
+
const random = Buffer.from(
|
|
5547
|
+
Array.from({ length: 16 }, () => Math.floor(Math.random() * 256))
|
|
5548
|
+
).toString("hex");
|
|
5549
|
+
return `----=_SuverenPart_${random}`;
|
|
5550
|
+
}
|
|
5551
|
+
function addressHeader(name, addresses) {
|
|
5552
|
+
if (!addresses?.length) return "";
|
|
5553
|
+
return `${name}: ${addresses.join(", ")}${CRLF}`;
|
|
5554
|
+
}
|
|
5555
|
+
function buildMimeMessage(input) {
|
|
5556
|
+
const boundary = makeBoundary();
|
|
5557
|
+
const headers = `MIME-Version: 1.0${CRLF}` + addressHeader("To", input.to) + addressHeader("Cc", input.cc) + `Subject: ${rfc2047Encode(input.subject)}${CRLF}Content-Type: multipart/mixed; boundary="${boundary}"${CRLF}`;
|
|
5558
|
+
const textPart = `--${boundary}${CRLF}Content-Type: text/plain; charset="UTF-8"${CRLF}Content-Transfer-Encoding: base64${CRLF}${CRLF}` + wrapBase64(Buffer.from(input.bodyText, "utf8").toString("base64")) + CRLF;
|
|
5559
|
+
const attachmentParts = input.attachments.map((att) => {
|
|
5560
|
+
const name = rfc2047Encode(att.filename);
|
|
5561
|
+
return `--${boundary}${CRLF}Content-Type: ${att.contentType}; name="${name}"${CRLF}Content-Disposition: attachment; filename="${name}"${CRLF}Content-Transfer-Encoding: base64${CRLF}${CRLF}` + wrapBase64(att.contentBase64) + CRLF;
|
|
5562
|
+
});
|
|
5563
|
+
return headers + CRLF + [textPart, ...attachmentParts].join("") + `--${boundary}--${CRLF}`;
|
|
5564
|
+
}
|
|
5565
|
+
function toBase64Url(mime) {
|
|
5566
|
+
return Buffer.from(mime, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5567
|
+
}
|
|
5568
|
+
|
|
5569
|
+
// src/lib/attachment-resolution.ts
|
|
5570
|
+
async function resolveAttachmentsAndInserts(integrationManager2, tool, profileId, args, allowedSourcesValue) {
|
|
5571
|
+
const attachmentsArg = tool.gating?.attachmentsArg;
|
|
5572
|
+
const insertFields = tool.gating?.insertFields;
|
|
5573
|
+
if (!attachmentsArg && !insertFields?.length) return { ok: true, args };
|
|
5574
|
+
const refsResult = attachmentsArg ? validateDocumentReferences(args[attachmentsArg]) : { ok: true, refs: [] };
|
|
5575
|
+
if (!refsResult.ok) {
|
|
5576
|
+
return { ok: false, message: `Refused: ${tool.namespacedName}'s attachments ${refsResult.error.message}` };
|
|
5577
|
+
}
|
|
5578
|
+
const insertResult = validateInsertReferences(args.insert);
|
|
5579
|
+
if (!insertResult.ok) {
|
|
5580
|
+
return { ok: false, message: `Refused: ${tool.namespacedName}'s insert ${insertResult.error.message}` };
|
|
5581
|
+
}
|
|
5582
|
+
const sealedAttachments = [];
|
|
5583
|
+
if (refsResult.refs.length > 0) {
|
|
5584
|
+
const profile = getProfile2(profileId);
|
|
5585
|
+
for (const ref of refsResult.refs) {
|
|
5586
|
+
const policy = isAttachmentSourceAllowed(profileId, profile, allowedSourcesValue, ref);
|
|
5587
|
+
if (!policy.allowed) {
|
|
5588
|
+
return { ok: false, message: `Refused: attachment "${ref.connector}:${ref.kind}" is not allowed. ${policy.reason}` };
|
|
5589
|
+
}
|
|
5590
|
+
const resolved = await resolveDocumentReference(integrationManager2, ref);
|
|
5591
|
+
if (!resolved.ok) {
|
|
5592
|
+
return { ok: false, message: `Refused: could not resolve attachment "${ref.connector}:${ref.kind}/${ref.id}" \u2014 ${resolved.error.message}` };
|
|
5593
|
+
}
|
|
5594
|
+
sealedAttachments.push({
|
|
5595
|
+
connector: ref.connector,
|
|
5596
|
+
kind: ref.kind,
|
|
5597
|
+
id: ref.id,
|
|
5598
|
+
filename: resolved.doc.filename,
|
|
5599
|
+
contentType: resolved.doc.contentType,
|
|
5600
|
+
sha256: resolved.doc.sha256,
|
|
5601
|
+
bytes: resolved.doc.bytes
|
|
5602
|
+
});
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
const sealedInserts = [];
|
|
5606
|
+
const resolvedInserts = [];
|
|
5607
|
+
if (insertResult.refs.length > 0) {
|
|
5608
|
+
for (const ref of insertResult.refs) {
|
|
5609
|
+
const resolved = await resolveInsertValue(integrationManager2, ref);
|
|
5610
|
+
if (!resolved.ok) {
|
|
5611
|
+
return { ok: false, message: `Refused: could not resolve insert "${ref.connector}:${ref.kind}/${ref.id}" for "${ref.placeholder}" \u2014 ${resolved.error.message}` };
|
|
5612
|
+
}
|
|
5613
|
+
resolvedInserts.push(resolved.insert);
|
|
5614
|
+
sealedInserts.push({ placeholder: ref.placeholder, connector: ref.connector, kind: ref.kind, id: ref.id, value: resolved.insert.value });
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
let outgoing = { ...args };
|
|
5618
|
+
if (insertFields?.length) {
|
|
5619
|
+
const fields = {};
|
|
5620
|
+
for (const field of insertFields) {
|
|
5621
|
+
fields[field] = typeof args[field] === "string" ? args[field] : "";
|
|
5622
|
+
}
|
|
5623
|
+
const applied = applyInserts(fields, resolvedInserts);
|
|
5624
|
+
if (!applied.ok) {
|
|
5625
|
+
return { ok: false, message: `Refused: ${tool.namespacedName}'s insert \u2014 ${applied.error.message}` };
|
|
5626
|
+
}
|
|
5627
|
+
outgoing = { ...outgoing, ...applied.fields };
|
|
5628
|
+
} else if (insertResult.refs.length > 0) {
|
|
5629
|
+
return { ok: false, message: `Refused: ${tool.namespacedName} does not declare any insertable text fields.` };
|
|
5630
|
+
}
|
|
5631
|
+
if (attachmentsArg) delete outgoing[attachmentsArg];
|
|
5632
|
+
delete outgoing.insert;
|
|
5633
|
+
return {
|
|
5634
|
+
ok: true,
|
|
5635
|
+
args: outgoing,
|
|
5636
|
+
...sealedAttachments.length > 0 || sealedInserts.length > 0 ? { sealed: { attachments: sealedAttachments, inserts: sealedInserts } } : {}
|
|
5637
|
+
};
|
|
5638
|
+
}
|
|
5639
|
+
function toStringArray(value) {
|
|
5640
|
+
if (Array.isArray(value)) return value.map((v) => String(v));
|
|
5641
|
+
if (typeof value === "string" && value) return [value];
|
|
5642
|
+
return [];
|
|
5643
|
+
}
|
|
5644
|
+
async function finalizeAttachmentsForExecution(integrationManager2, integrationId, args) {
|
|
5645
|
+
const sealed = args[ATTACHMENTS_SEALED_META_KEY];
|
|
5646
|
+
if (!sealed || sealed.attachments.length === 0 && sealed.inserts.length === 0) {
|
|
5647
|
+
return { ok: true, args };
|
|
5648
|
+
}
|
|
5649
|
+
const resolvedDocs = [];
|
|
5650
|
+
for (const a of sealed.attachments) {
|
|
5651
|
+
const resolved = await resolveDocumentReference(integrationManager2, { connector: a.connector, kind: a.kind, id: a.id });
|
|
5652
|
+
if (!resolved.ok) {
|
|
5653
|
+
return {
|
|
5654
|
+
ok: false,
|
|
5655
|
+
message: `Refused: could not re-resolve attachment "${a.filename}" (${a.connector}:${a.kind}/${a.id}) at execution \u2014 ${resolved.error.message}`
|
|
5656
|
+
};
|
|
5657
|
+
}
|
|
5658
|
+
if (resolved.doc.sha256 !== a.sha256) {
|
|
5659
|
+
return {
|
|
5660
|
+
ok: false,
|
|
5661
|
+
message: `Refused: attachment "${a.filename}" (${a.connector}:${a.kind}/${a.id}) changed since this was approved \u2014 its fingerprint no longer matches.`
|
|
5662
|
+
};
|
|
5663
|
+
}
|
|
5664
|
+
resolvedDocs.push(resolved.doc);
|
|
5665
|
+
}
|
|
5666
|
+
for (const i of sealed.inserts) {
|
|
5667
|
+
const resolved = await resolveInsertValue(integrationManager2, { connector: i.connector, kind: i.kind, id: i.id, placeholder: i.placeholder });
|
|
5668
|
+
if (!resolved.ok) {
|
|
5669
|
+
return {
|
|
5670
|
+
ok: false,
|
|
5671
|
+
message: `Refused: could not re-resolve the inserted value for "${i.placeholder}" (${i.connector}:${i.kind}/${i.id}) at execution \u2014 ${resolved.error.message}`
|
|
5672
|
+
};
|
|
5673
|
+
}
|
|
5674
|
+
if (resolved.insert.value !== i.value) {
|
|
5675
|
+
return {
|
|
5676
|
+
ok: false,
|
|
5677
|
+
message: `Refused: the value inserted for "${i.placeholder}" (${i.connector}:${i.kind}/${i.id}) changed since this was approved.`
|
|
5678
|
+
};
|
|
5679
|
+
}
|
|
5680
|
+
}
|
|
5681
|
+
let outgoing = { ...args };
|
|
5682
|
+
if (resolvedDocs.length > 0) {
|
|
5683
|
+
const manifest = getManifest(integrationId);
|
|
5684
|
+
const transport = manifest?.attachments;
|
|
5685
|
+
if (!transport) {
|
|
5686
|
+
return { ok: false, message: `Refused: "${integrationId}" declares no attachments transport.` };
|
|
5687
|
+
}
|
|
5688
|
+
if (transport.transport === "field") {
|
|
5689
|
+
outgoing[transport.arg] = resolvedDocs.map((d) => ({
|
|
5690
|
+
filename: d.filename,
|
|
5691
|
+
contentType: d.contentType,
|
|
5692
|
+
contentBase64: d.contentBase64
|
|
5693
|
+
}));
|
|
5694
|
+
} else {
|
|
5695
|
+
const to = toStringArray(outgoing.to);
|
|
5696
|
+
const cc = toStringArray(outgoing.cc);
|
|
5697
|
+
const subject = typeof outgoing.subject === "string" ? outgoing.subject : "";
|
|
5698
|
+
const bodyText = typeof outgoing.body === "string" ? outgoing.body : "";
|
|
5699
|
+
const mime = buildMimeMessage({
|
|
5700
|
+
to,
|
|
5701
|
+
cc,
|
|
5702
|
+
subject,
|
|
5703
|
+
bodyText,
|
|
5704
|
+
attachments: resolvedDocs.map((d) => ({ filename: d.filename, contentType: d.contentType, contentBase64: d.contentBase64 }))
|
|
5705
|
+
});
|
|
5706
|
+
outgoing[transport.arg] = toBase64Url(mime);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
return { ok: true, args: outgoing };
|
|
5710
|
+
}
|
|
5711
|
+
var ATTACHMENTS_SEALED_META_KEY = "_attachmentsSealed";
|
|
5712
|
+
function stripAttachmentsSealedMeta(args) {
|
|
5713
|
+
if (!(ATTACHMENTS_SEALED_META_KEY in args)) return args;
|
|
5714
|
+
const { [ATTACHMENTS_SEALED_META_KEY]: _omit, ...rest } = args;
|
|
5715
|
+
return rest;
|
|
5716
|
+
}
|
|
5717
|
+
|
|
5718
|
+
// src/lib/content-binding.ts
|
|
5719
|
+
function getContentBinding(profileId) {
|
|
5720
|
+
return getProfile3(profileId)?.content_binding;
|
|
5721
|
+
}
|
|
5722
|
+
function computeContentBinding(profileId, tool, toolArgs, actionType) {
|
|
5723
|
+
const binding = getContentBinding(profileId);
|
|
5724
|
+
if (!binding) return void 0;
|
|
5725
|
+
if (isFieldBinding(binding)) {
|
|
5726
|
+
if (!bindingAppliesTo(binding, actionType)) {
|
|
5727
|
+
if (!actionType && binding.appliesTo) {
|
|
5728
|
+
console.error(
|
|
5729
|
+
`[Suveren MCP] Warning: ${tool?.namespacedName ?? profileId} has no action_type in staticExecution, so the ${profileId} content binding (${binding.appliesTo.join(", ")}) does not apply and the receipt will bind no content. Fix the integration manifest.`
|
|
5730
|
+
);
|
|
5731
|
+
}
|
|
5732
|
+
return void 0;
|
|
5733
|
+
}
|
|
5734
|
+
let effectiveArgs = toolArgs;
|
|
5735
|
+
if (binding.fields?.includes("attachments")) {
|
|
5736
|
+
const sealed = toolArgs[ATTACHMENTS_SEALED_META_KEY];
|
|
5737
|
+
const fingerprints = (sealed?.attachments ?? []).map((a) => ({ filename: a.filename, sha256: a.sha256 }));
|
|
5738
|
+
effectiveArgs = { ...toolArgs, attachments: fingerprints };
|
|
5739
|
+
}
|
|
5740
|
+
const bound = selectBoundFields(binding, effectiveArgs);
|
|
5741
|
+
return {
|
|
5742
|
+
contentHash: computeContentHash2(binding, bound),
|
|
5743
|
+
contentBinding: { version: binding.version, kind: binding.kind, fields: binding.fields },
|
|
5744
|
+
boundContent: bound
|
|
5745
|
+
};
|
|
5746
|
+
}
|
|
5747
|
+
let contentHash;
|
|
5748
|
+
let boundContent;
|
|
5749
|
+
if (binding.kind === "jcs") {
|
|
5750
|
+
contentHash = computeContentHash2(binding, toolArgs);
|
|
5751
|
+
boundContent = toolArgs;
|
|
5752
|
+
} else {
|
|
5753
|
+
const declared = tool?.gating?.contentField;
|
|
5754
|
+
const field = declared ?? (tool ? detectContentField(tool) : null);
|
|
5755
|
+
if (!field) return void 0;
|
|
5756
|
+
const raw = typeof toolArgs[field] === "string" ? toolArgs[field] : "";
|
|
5757
|
+
if (raw === "") return void 0;
|
|
5758
|
+
contentHash = computeContentHash2(binding, raw);
|
|
5759
|
+
boundContent = raw;
|
|
5760
|
+
}
|
|
5761
|
+
return {
|
|
5762
|
+
contentHash,
|
|
5763
|
+
contentBinding: { version: binding.version, kind: binding.kind },
|
|
5764
|
+
boundContent
|
|
5765
|
+
};
|
|
5766
|
+
}
|
|
5767
|
+
function attachTicketId(tool, args, ticketId) {
|
|
5768
|
+
const schema = tool.inputSchema;
|
|
5769
|
+
if (!schema?.properties || !("ticket_id" in schema.properties)) return args;
|
|
5770
|
+
return { ...args, ticket_id: ticketId };
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5106
5773
|
// src/lib/ticket-verify.ts
|
|
5107
5774
|
import { verifyTicketSignature } from "@hap/core";
|
|
5108
5775
|
var TicketBindingMismatchError = class extends Error {
|
|
@@ -5200,7 +5867,7 @@ async function resolveWhoLabel(archive, domainKey, userId) {
|
|
|
5200
5867
|
}
|
|
5201
5868
|
|
|
5202
5869
|
// src/lib/arg-encoding.ts
|
|
5203
|
-
function
|
|
5870
|
+
function rfc2047Encode2(value) {
|
|
5204
5871
|
if (!/[^\x00-\x7F]/.test(value)) return value;
|
|
5205
5872
|
const CHARSET = "=?UTF-8?Q?";
|
|
5206
5873
|
const SUFFIX = "?=";
|
|
@@ -5234,7 +5901,7 @@ function rfc2047Encode(value) {
|
|
|
5234
5901
|
return words.join(" ");
|
|
5235
5902
|
}
|
|
5236
5903
|
var ENCODERS = {
|
|
5237
|
-
rfc2047:
|
|
5904
|
+
rfc2047: rfc2047Encode2
|
|
5238
5905
|
};
|
|
5239
5906
|
function encodeOutgoingArgs(tool, args) {
|
|
5240
5907
|
const declared = tool?.gating?.argEncoding;
|
|
@@ -5307,7 +5974,7 @@ function normalizeIncomingArgs(tool, args) {
|
|
|
5307
5974
|
}
|
|
5308
5975
|
|
|
5309
5976
|
// src/lib/numeric-execution.ts
|
|
5310
|
-
import { getProfile as
|
|
5977
|
+
import { getProfile as getProfile4 } from "@hap/core";
|
|
5311
5978
|
function parseNumericString(value) {
|
|
5312
5979
|
if (!/^-?\d+(\.\d+)?$/.test(value)) return void 0;
|
|
5313
5980
|
const parsed = Number(value);
|
|
@@ -5323,7 +5990,7 @@ function declaredNumericFields(profile) {
|
|
|
5323
5990
|
return out;
|
|
5324
5991
|
}
|
|
5325
5992
|
function coerceDeclaredNumericFields(profileId, execution) {
|
|
5326
|
-
const fields = declaredNumericFields(
|
|
5993
|
+
const fields = declaredNumericFields(getProfile4(profileId));
|
|
5327
5994
|
for (const field of fields) {
|
|
5328
5995
|
const value = execution[field];
|
|
5329
5996
|
if (typeof value !== "string") continue;
|
|
@@ -5333,7 +6000,7 @@ function coerceDeclaredNumericFields(profileId, execution) {
|
|
|
5333
6000
|
}
|
|
5334
6001
|
|
|
5335
6002
|
// src/lib/tool-proxy.ts
|
|
5336
|
-
import { getProfile as
|
|
6003
|
+
import { getProfile as getProfile5, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
|
|
5337
6004
|
import { readFile } from "fs/promises";
|
|
5338
6005
|
import { extname } from "path";
|
|
5339
6006
|
import { randomUUID } from "crypto";
|
|
@@ -5405,7 +6072,7 @@ function profileMatches(profileId, shortName2) {
|
|
|
5405
6072
|
return profileId === shortName2 || profileId.includes("/" + shortName2 + "@") || profileId.endsWith("/" + shortName2);
|
|
5406
6073
|
}
|
|
5407
6074
|
function wireExecutionContext(profileId, execution) {
|
|
5408
|
-
const profile =
|
|
6075
|
+
const profile = getProfile5(profileId);
|
|
5409
6076
|
const allowed = /* @__PURE__ */ new Set(["action_type"]);
|
|
5410
6077
|
const fields = profile?.boundsSchema?.fields;
|
|
5411
6078
|
if (fields) {
|
|
@@ -5423,7 +6090,7 @@ function wireExecutionContext(profileId, execution) {
|
|
|
5423
6090
|
return out;
|
|
5424
6091
|
}
|
|
5425
6092
|
function countBoundsFor(profileId, actionType) {
|
|
5426
|
-
const fields =
|
|
6093
|
+
const fields = getProfile5(profileId)?.boundsSchema?.fields;
|
|
5427
6094
|
if (!fields) return null;
|
|
5428
6095
|
return Object.entries(fields).filter(([, def]) => def.boundType?.kind === "cumulative_count").filter(([name, def]) => {
|
|
5429
6096
|
const applies = boundActionTypes2(name, def);
|
|
@@ -5498,7 +6165,7 @@ function recordBlocked(state2, tool, rec) {
|
|
|
5498
6165
|
function recordGatekeeperRefusal(state2, tool, refusals) {
|
|
5499
6166
|
if (refusals.length === 0) return;
|
|
5500
6167
|
const pick = refusals.find((r) => r.bound !== void 0 && r.actual !== void 0) ?? refusals[0];
|
|
5501
|
-
const scoped = pick.field ? Boolean(
|
|
6168
|
+
const scoped = pick.field ? Boolean(getProfile5(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
|
|
5502
6169
|
const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
|
|
5503
6170
|
recordBlocked(state2, tool, {
|
|
5504
6171
|
kind: scoped ? "scope" : "bound",
|
|
@@ -5677,7 +6344,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5677
6344
|
);
|
|
5678
6345
|
}
|
|
5679
6346
|
}
|
|
5680
|
-
const readProfile = readAdapter ?
|
|
6347
|
+
const readProfile = readAdapter ? getProfile5(matchingAuths[0].profileId) : void 0;
|
|
5681
6348
|
const boundsSchema = readProfile?.boundsSchema;
|
|
5682
6349
|
const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
|
|
5683
6350
|
const hasAgeDimension = Boolean(readAdapter?.ageField);
|
|
@@ -5808,6 +6475,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5808
6475
|
});
|
|
5809
6476
|
}
|
|
5810
6477
|
return lockedGuard(async (args) => {
|
|
6478
|
+
let attachmentSourcesForExecution;
|
|
6479
|
+
if (tool.gating?.attachmentsArg || tool.gating?.insertFields?.length) {
|
|
6480
|
+
const refsResult = tool.gating.attachmentsArg ? validateDocumentReferences(args[tool.gating.attachmentsArg]) : { ok: true, refs: [] };
|
|
6481
|
+
if (!refsResult.ok) {
|
|
6482
|
+
return { content: [{ type: "text", text: `Refused: ${tool.namespacedName}'s attachments ${refsResult.error.message}` }], isError: true };
|
|
6483
|
+
}
|
|
6484
|
+
const insertResult = validateInsertReferences(args.insert);
|
|
6485
|
+
if (!insertResult.ok) {
|
|
6486
|
+
return { content: [{ type: "text", text: `Refused: ${tool.namespacedName}'s insert ${insertResult.error.message}` }], isError: true };
|
|
6487
|
+
}
|
|
6488
|
+
if (refsResult.refs.length > 0) {
|
|
6489
|
+
attachmentSourcesForExecution = [...new Set(refsResult.refs.map((r) => `${r.connector}:${r.kind}`))].join(",");
|
|
6490
|
+
}
|
|
6491
|
+
}
|
|
5811
6492
|
const execution = { ...staticExecution };
|
|
5812
6493
|
for (const [argName, mapping] of Object.entries(executionMapping)) {
|
|
5813
6494
|
const value = args[argName];
|
|
@@ -5824,6 +6505,9 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5824
6505
|
}
|
|
5825
6506
|
}
|
|
5826
6507
|
}
|
|
6508
|
+
if (attachmentSourcesForExecution) {
|
|
6509
|
+
execution[ATTACHMENT_SOURCES_FIELD] = attachmentSourcesForExecution;
|
|
6510
|
+
}
|
|
5827
6511
|
coerceDeclaredNumericFields(profile, execution);
|
|
5828
6512
|
const declaredActionType = typeof execution.action_type === "string" && execution.action_type.length > 0 ? execution.action_type : void 0;
|
|
5829
6513
|
if (!declaredActionType) {
|
|
@@ -5843,7 +6527,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5843
6527
|
};
|
|
5844
6528
|
}
|
|
5845
6529
|
{
|
|
5846
|
-
const profileDef =
|
|
6530
|
+
const profileDef = getProfile5(profile);
|
|
5847
6531
|
const registry = profileDef?.boundsSchema?.actionTypes;
|
|
5848
6532
|
if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
|
|
5849
6533
|
recordBlocked(state2, tool, {
|
|
@@ -5947,7 +6631,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5947
6631
|
isError: true
|
|
5948
6632
|
};
|
|
5949
6633
|
}
|
|
5950
|
-
const contextKeys =
|
|
6634
|
+
const contextKeys = getProfile5(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
|
|
5951
6635
|
return attemptWithCandidates(passers);
|
|
5952
6636
|
async function attemptWithCandidates(candidates) {
|
|
5953
6637
|
const selection = selectAuthorization(
|
|
@@ -5975,9 +6659,18 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5975
6659
|
isError: true
|
|
5976
6660
|
};
|
|
5977
6661
|
}
|
|
6662
|
+
let callArgs = args;
|
|
6663
|
+
if (tool.gating?.attachmentsArg || tool.gating?.insertFields?.length) {
|
|
6664
|
+
const allowedSourcesValue = typeof auth.context?.[ATTACHMENT_SOURCES_FIELD] === "string" ? auth.context[ATTACHMENT_SOURCES_FIELD] : void 0;
|
|
6665
|
+
const resolved = await resolveAttachmentsAndInserts(integrationManager2, tool, auth.profileId, args, allowedSourcesValue);
|
|
6666
|
+
if (!resolved.ok) {
|
|
6667
|
+
return { content: [{ type: "text", text: resolved.message }], isError: true };
|
|
6668
|
+
}
|
|
6669
|
+
callArgs = resolved.sealed ? { ...resolved.args, [ATTACHMENTS_SEALED_META_KEY]: resolved.sealed } : resolved.args;
|
|
6670
|
+
}
|
|
5978
6671
|
if ((auth.deferredCommitmentDomains ?? []).length > 0) {
|
|
5979
6672
|
try {
|
|
5980
|
-
const enrichedArgs = await attachImagePreview(
|
|
6673
|
+
const enrichedArgs = await attachImagePreview(callArgs);
|
|
5981
6674
|
const wireExecCtx = wireExecutionContext(auth.profileId, execution);
|
|
5982
6675
|
let recipientUserIds = auth.deferredCommitmentDomains.filter((d) => d !== "owner");
|
|
5983
6676
|
try {
|
|
@@ -6031,10 +6724,18 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
6031
6724
|
};
|
|
6032
6725
|
}
|
|
6033
6726
|
}
|
|
6727
|
+
let finalArgs = callArgs;
|
|
6728
|
+
if (tool.gating?.attachmentsArg || tool.gating?.insertFields?.length) {
|
|
6729
|
+
const finalized = await finalizeAttachmentsForExecution(integrationManager2, tool.integrationId, callArgs);
|
|
6730
|
+
if (!finalized.ok) {
|
|
6731
|
+
return { content: [{ type: "text", text: finalized.message }], isError: true };
|
|
6732
|
+
}
|
|
6733
|
+
finalArgs = finalized.args;
|
|
6734
|
+
}
|
|
6034
6735
|
let receiptId;
|
|
6035
6736
|
try {
|
|
6036
6737
|
const actionType = declaredActionType;
|
|
6037
|
-
const binding = computeContentBinding(auth.profileId, tool,
|
|
6738
|
+
const binding = computeContentBinding(auth.profileId, tool, finalArgs, actionType);
|
|
6038
6739
|
const idempotencyKey = randomUUID();
|
|
6039
6740
|
const wireExecCtx = wireExecutionContext(auth.profileId, execution);
|
|
6040
6741
|
const { receipt } = await state2.spClient.postReceipt({
|
|
@@ -6077,10 +6778,12 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
6077
6778
|
// would hold a hash of content it cannot reproduce.
|
|
6078
6779
|
boundContent: binding?.boundContent,
|
|
6079
6780
|
// UI spec §14 — what this call sends, exactly the object the
|
|
6080
|
-
// journal's argsHash is computed from below
|
|
6081
|
-
// so the ticket view can show it and
|
|
6082
|
-
//
|
|
6083
|
-
|
|
6781
|
+
// journal's argsHash is computed from below
|
|
6782
|
+
// (`hashToolArgs(finalArgs)`), so the ticket view can show it and
|
|
6783
|
+
// check it (incl. any attachments' sealed fingerprints — UI spec
|
|
6784
|
+
// §15.2). Same write, so it is as fail-closed and as encrypted
|
|
6785
|
+
// as the rest of the entry.
|
|
6786
|
+
sentArgs: finalArgs
|
|
6084
6787
|
});
|
|
6085
6788
|
} catch (err) {
|
|
6086
6789
|
if (err instanceof AsKeyMismatchError || err instanceof TicketBindingMismatchError) {
|
|
@@ -6172,7 +6875,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
6172
6875
|
const uniqueApprovers = [...new Set(pendingApprovers)];
|
|
6173
6876
|
const recipientUserIds = ownerId ? [.../* @__PURE__ */ new Set([ownerId, ...uniqueApprovers])] : uniqueApprovers;
|
|
6174
6877
|
try {
|
|
6175
|
-
const enrichedArgs = await attachImagePreview(
|
|
6878
|
+
const enrichedArgs = await attachImagePreview(callArgs);
|
|
6176
6879
|
const wireExecCtx = wireExecutionContext(auth.profileId, execution);
|
|
6177
6880
|
const { proposal, unreadableFor } = await state2.spClient.submitProposal({
|
|
6178
6881
|
authorizationId: authzId,
|
|
@@ -6256,15 +6959,16 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + a
|
|
|
6256
6959
|
execution: { ...execution },
|
|
6257
6960
|
timestamp: Math.floor(Date.now() / 1e3)
|
|
6258
6961
|
});
|
|
6259
|
-
let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool,
|
|
6962
|
+
let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, finalArgs, receiptId, auth.subjects?.[0]) : finalArgs;
|
|
6260
6963
|
if (receiptId) outgoingArgs = attachTicketId(tool, outgoingArgs, receiptId);
|
|
6261
6964
|
outgoingArgs = encodeOutgoingArgs(tool, outgoingArgs);
|
|
6262
6965
|
outgoingArgs = stripSourceLink(outgoingArgs);
|
|
6966
|
+
outgoingArgs = stripAttachmentsSealedMeta(outgoingArgs);
|
|
6263
6967
|
if (receiptId) {
|
|
6264
6968
|
const begun = state2.executionJournal.begin({
|
|
6265
6969
|
ticketId: receiptId,
|
|
6266
6970
|
tool: tool.namespacedName,
|
|
6267
|
-
argsHash: hashToolArgs(
|
|
6971
|
+
argsHash: hashToolArgs(finalArgs)
|
|
6268
6972
|
});
|
|
6269
6973
|
if (!begun.ok) {
|
|
6270
6974
|
return {
|
|
@@ -6433,7 +7137,7 @@ function buildMandateBrief(opts) {
|
|
|
6433
7137
|
const parts = [];
|
|
6434
7138
|
if (boundsDesc) parts.push(boundsDesc);
|
|
6435
7139
|
if (executionLog) {
|
|
6436
|
-
const profile =
|
|
7140
|
+
const profile = getProfile6(auth.profileId) ?? getProfile6(shortName2);
|
|
6437
7141
|
const consumption = getConsumptionState(auth, executionLog, profile);
|
|
6438
7142
|
const compact = formatConsumptionCompact(consumption);
|
|
6439
7143
|
if (compact) parts.push(compact);
|
|
@@ -6471,7 +7175,7 @@ function buildMandateBrief(opts) {
|
|
|
6471
7175
|
}
|
|
6472
7176
|
|
|
6473
7177
|
// src/tools/authorizations.ts
|
|
6474
|
-
import { getProfile as
|
|
7178
|
+
import { getProfile as getProfile7 } from "@hap/core";
|
|
6475
7179
|
function shortProfileName2(profileId) {
|
|
6476
7180
|
const withoutVersion = profileId.replace(/@.*$/, "");
|
|
6477
7181
|
const parts = withoutVersion.split("/");
|
|
@@ -6590,7 +7294,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
6590
7294
|
} catch {
|
|
6591
7295
|
}
|
|
6592
7296
|
const shortName2 = shortProfileName2(auth.profileId);
|
|
6593
|
-
const profile =
|
|
7297
|
+
const profile = getProfile7(auth.profileId) ?? getProfile7(shortName2);
|
|
6594
7298
|
if (profile) {
|
|
6595
7299
|
const consumption = getConsumptionState(auth, state2.executionLog, profile);
|
|
6596
7300
|
const consumptionText = formatConsumptionFull(consumption);
|
|
@@ -6650,7 +7354,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
6650
7354
|
lines.push(" Mode: review (each action requires your approval)");
|
|
6651
7355
|
}
|
|
6652
7356
|
const shortName2 = shortProfileName2(auth.profileId);
|
|
6653
|
-
const profile =
|
|
7357
|
+
const profile = getProfile7(auth.profileId) ?? getProfile7(shortName2);
|
|
6654
7358
|
if (profile) {
|
|
6655
7359
|
const consumption = getConsumptionState(auth, state2.executionLog, profile);
|
|
6656
7360
|
const compact = formatConsumptionCompact(consumption);
|
|
@@ -6793,12 +7497,12 @@ function listIntegrationsHandler(state2, integrationManager2) {
|
|
|
6793
7497
|
import { ContentBindingError as ContentBindingError2 } from "@hap/core";
|
|
6794
7498
|
|
|
6795
7499
|
// src/lib/proposal-content.ts
|
|
6796
|
-
import { createHash as
|
|
7500
|
+
import { createHash as createHash4 } from "crypto";
|
|
6797
7501
|
import { canonicalize } from "@hap/core";
|
|
6798
7502
|
var ProposalContentError = class extends Error {
|
|
6799
7503
|
};
|
|
6800
7504
|
function computeProposalContentHash(toolArgs) {
|
|
6801
|
-
return `sha256:${
|
|
7505
|
+
return `sha256:${createHash4("sha256").update(canonicalize(toolArgs)).digest("hex")}`;
|
|
6802
7506
|
}
|
|
6803
7507
|
async function resolveProposalToolArgs(proposal) {
|
|
6804
7508
|
if (proposal.toolArgs) return proposal.toolArgs;
|
|
@@ -7108,8 +7812,23 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
7108
7812
|
};
|
|
7109
7813
|
}
|
|
7110
7814
|
}
|
|
7815
|
+
let finalToolArgs = toolArgs;
|
|
7816
|
+
{
|
|
7817
|
+
const finalized = await finalizeAttachmentsForExecution(integrationManager2, integrationId, toolArgs);
|
|
7818
|
+
if (!finalized.ok) {
|
|
7819
|
+
state2.executionJournal.complete(receiptId, "failed", "changed", finalized.message);
|
|
7820
|
+
recordExecutionOutcome(state2, proposal.id, receiptId, false, finalized.message);
|
|
7821
|
+
logExecutionOutcome(proposal.id, false, "attachment or inserted value changed since approval");
|
|
7822
|
+
void notifyControlPlane("execution-failed");
|
|
7823
|
+
return {
|
|
7824
|
+
text: `Proposal ${proposal.id} was approved, but ${finalized.message} Nothing was sent.`,
|
|
7825
|
+
isError: true
|
|
7826
|
+
};
|
|
7827
|
+
}
|
|
7828
|
+
finalToolArgs = finalized.args;
|
|
7829
|
+
}
|
|
7111
7830
|
try {
|
|
7112
|
-
let outgoingArgs =
|
|
7831
|
+
let outgoingArgs = finalToolArgs;
|
|
7113
7832
|
if (discovered && receiptId) {
|
|
7114
7833
|
if (shouldAttachFooter()) {
|
|
7115
7834
|
outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
|
|
@@ -7118,6 +7837,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
7118
7837
|
outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
|
|
7119
7838
|
}
|
|
7120
7839
|
outgoingArgs = stripSourceLink(outgoingArgs);
|
|
7840
|
+
outgoingArgs = stripAttachmentsSealedMeta(outgoingArgs);
|
|
7121
7841
|
let result;
|
|
7122
7842
|
try {
|
|
7123
7843
|
result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
|
|
@@ -8254,6 +8974,7 @@ var IntegrationManager = class {
|
|
|
8254
8974
|
if (gating && gating.category !== "read" && gating.category !== "disabled") {
|
|
8255
8975
|
inputSchema = withSourceLink(inputSchema);
|
|
8256
8976
|
}
|
|
8977
|
+
inputSchema = withAttachmentAndInsertSchema(inputSchema, gating ?? void 0);
|
|
8257
8978
|
return {
|
|
8258
8979
|
originalName: tool.name,
|
|
8259
8980
|
namespacedName: `${config.id}__${tool.name}`,
|
|
@@ -8261,6 +8982,13 @@ var IntegrationManager = class {
|
|
|
8261
8982
|
description: tool.description ?? "",
|
|
8262
8983
|
inputSchema,
|
|
8263
8984
|
outputSchema: tool.outputSchema,
|
|
8985
|
+
// A connector's own `internal: true` override (tool-gating-types.ts
|
|
8986
|
+
// `ProfileToolGatingEntry.internal`) — same meaning and enforcement
|
|
8987
|
+
// as a built-in's `BuiltinTool.internal` above: never registered
|
|
8988
|
+
// with the MCP server (index.ts), never callable directly
|
|
8989
|
+
// (tool-proxy.ts); only the gateway's own code (e.g. a document
|
|
8990
|
+
// preview read — preview.ts) calls it, via `callTool` directly.
|
|
8991
|
+
...gating?.internal ? { internal: true } : {},
|
|
8264
8992
|
gating
|
|
8265
8993
|
};
|
|
8266
8994
|
});
|
|
@@ -8361,7 +9089,8 @@ var IntegrationManager = class {
|
|
|
8361
9089
|
...t.gating?.approvalView ? { approvalView: t.gating.approvalView } : {},
|
|
8362
9090
|
// UI spec §14.1 — which sent values the signed ticket also carries
|
|
8363
9091
|
// (arg → executionContext field), for the "in the ticket" tag.
|
|
8364
|
-
...mapping && Object.keys(mapping).length > 0 ? { executionMapping: mapping } : {}
|
|
9092
|
+
...mapping && Object.keys(mapping).length > 0 ? { executionMapping: mapping } : {},
|
|
9093
|
+
...t.gating?.aiTextArg ? { aiTextArg: t.gating.aiTextArg } : {}
|
|
8365
9094
|
};
|
|
8366
9095
|
}
|
|
8367
9096
|
return out;
|
|
@@ -8671,7 +9400,8 @@ var IntegrationManager = class {
|
|
|
8671
9400
|
blockedArgs: ext.blockedArgs,
|
|
8672
9401
|
hideUnlessAuthorized: ext.hideUnlessAuthorized,
|
|
8673
9402
|
simulationOnly: ext.simulationOnly,
|
|
8674
|
-
approvalView: ext.approvalView
|
|
9403
|
+
approvalView: ext.approvalView,
|
|
9404
|
+
internal: ext.internal
|
|
8675
9405
|
};
|
|
8676
9406
|
}
|
|
8677
9407
|
return {
|
|
@@ -8701,7 +9431,11 @@ var IntegrationManager = class {
|
|
|
8701
9431
|
// this, boundField/requiredValue on a non-"read" override was silently
|
|
8702
9432
|
// discarded here — the config in the manifest existed but did nothing.
|
|
8703
9433
|
boundField: ext.boundField,
|
|
8704
|
-
requiredValue: ext.requiredValue
|
|
9434
|
+
requiredValue: ext.requiredValue,
|
|
9435
|
+
internal: ext.internal,
|
|
9436
|
+
attachmentsArg: ext.attachmentsArg,
|
|
9437
|
+
insertFields: ext.insertFields,
|
|
9438
|
+
aiTextArg: ext.display?.aiText
|
|
8705
9439
|
};
|
|
8706
9440
|
}
|
|
8707
9441
|
return {
|
|
@@ -8832,7 +9566,7 @@ async function buildTicketDetails(archive, ticketIds) {
|
|
|
8832
9566
|
}
|
|
8833
9567
|
|
|
8834
9568
|
// src/lib/report/window.ts
|
|
8835
|
-
import { getProfile as
|
|
9569
|
+
import { getProfile as getProfile8 } from "@hap/core";
|
|
8836
9570
|
var REPORT_AGE_FIELD = "read_age_days";
|
|
8837
9571
|
var REPORTING_PROFILE = "reporting";
|
|
8838
9572
|
var END_SKEW_SECONDS = 120;
|
|
@@ -8846,7 +9580,7 @@ function windowLabel(start, days, loadedAt) {
|
|
|
8846
9580
|
if (loadedAt !== null && loadedAt === start) return `${since} (when the test data was loaded)`;
|
|
8847
9581
|
return `${since} (the last ${days} ${days === 1 ? "day" : "days"})`;
|
|
8848
9582
|
}
|
|
8849
|
-
var defaultLookup = (id) =>
|
|
9583
|
+
var defaultLookup = (id) => getProfile8(id);
|
|
8850
9584
|
function reportingMandateRefusal(m, lookup = defaultLookup) {
|
|
8851
9585
|
const schema = lookup(m.profileId)?.boundsSchema;
|
|
8852
9586
|
const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
|
|
@@ -9597,7 +10331,7 @@ function unifiedLineDiff(before, after) {
|
|
|
9597
10331
|
}
|
|
9598
10332
|
|
|
9599
10333
|
// src/lib/builtins/setup.ts
|
|
9600
|
-
import { getProfile as
|
|
10334
|
+
import { getProfile as getProfile9 } from "@hap/core";
|
|
9601
10335
|
var DELEGATION_PROFILE = "delegation";
|
|
9602
10336
|
var DELEGATION_INTENT_HINT = [
|
|
9603
10337
|
"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.",
|
|
@@ -9649,7 +10383,7 @@ function delegationItselfRefusal(args) {
|
|
|
9649
10383
|
}
|
|
9650
10384
|
function delegationMandateRefusal(auth) {
|
|
9651
10385
|
if (isSimulationMode()) return void 0;
|
|
9652
|
-
const profile =
|
|
10386
|
+
const profile = getProfile9(auth.profileId);
|
|
9653
10387
|
if (profile?.scopeSchema?.fields?.allowed_profiles) return void 0;
|
|
9654
10388
|
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.`;
|
|
9655
10389
|
}
|
|
@@ -9707,7 +10441,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
|
|
|
9707
10441
|
);
|
|
9708
10442
|
const first = governing[0];
|
|
9709
10443
|
if (first) {
|
|
9710
|
-
const delegationProfile =
|
|
10444
|
+
const delegationProfile = getProfile9(first.profileId);
|
|
9711
10445
|
const entry = getConsumptionState(first, deps.state.executionLog, delegationProfile).find((e) => e.field === "mandate_daily_max");
|
|
9712
10446
|
if (entry && entry.limit !== null) {
|
|
9713
10447
|
checks.push(`Proposals today: ${entry.current} of ${entry.limit}`);
|
|
@@ -9717,7 +10451,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
|
|
|
9717
10451
|
}
|
|
9718
10452
|
function resolveTargetProfile(input) {
|
|
9719
10453
|
if (typeof input !== "string" || !input.trim()) return void 0;
|
|
9720
|
-
const profile =
|
|
10454
|
+
const profile = getProfile9(input.trim());
|
|
9721
10455
|
if (!profile) return void 0;
|
|
9722
10456
|
return { profile, shortName: shortName(profile.id) };
|
|
9723
10457
|
}
|
|
@@ -9870,7 +10604,7 @@ Read each topic with get_guide(topic) before doing that step.`);
|
|
|
9870
10604
|
return { ...builtinText(`Unknown topic "${topic}". Topics: ${guides.map((g) => g.topic).join(", ") || "none installed"}.`), isError: true };
|
|
9871
10605
|
}
|
|
9872
10606
|
const body = fillGenerated(guide.body.trim(), {
|
|
9873
|
-
limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile:
|
|
10607
|
+
limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile9(s.profile) }))),
|
|
9874
10608
|
package: () => packageSection(
|
|
9875
10609
|
deps.integrationManager.getAllTools().find((t) => !deps.integrationManager.isBuiltin(t.integrationId) && t.originalName === "load_simulation")
|
|
9876
10610
|
)
|
|
@@ -10404,6 +11138,15 @@ app.post("/internal/preview", internalOnly, async (req, res) => {
|
|
|
10404
11138
|
});
|
|
10405
11139
|
res.json(result);
|
|
10406
11140
|
});
|
|
11141
|
+
app.post("/internal/document-preview", internalOnly, async (req, res) => {
|
|
11142
|
+
const { connector, kind, id, sha256 } = req.body;
|
|
11143
|
+
if (!connector || !kind || !id || !sha256) {
|
|
11144
|
+
res.status(400).json({ error: "Missing connector, kind, id, or sha256" });
|
|
11145
|
+
return;
|
|
11146
|
+
}
|
|
11147
|
+
const result = await resolveAttachmentPreview(integrationManager, { connector, kind, id }, sha256);
|
|
11148
|
+
res.json(result);
|
|
11149
|
+
});
|
|
10407
11150
|
app.post("/internal/request-context", internalOnly, async (req, res) => {
|
|
10408
11151
|
const { tool, toolArgs } = req.body;
|
|
10409
11152
|
if (!tool || typeof toolArgs !== "object" || toolArgs === null) {
|