@suveren/gateway 0.34.0 → 0.35.1
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/crm.json +18 -2
- package/content/integrations/erp.json +83 -8
- package/content/integrations/gmail.json +8 -1
- package/content/integrations/mail.json +13 -3
- package/content/integrations/mollie.json +8 -0
- package/dist/control-plane/index.mjs +79 -0
- package/dist/mcp-server/http.mjs +944 -117
- package/dist/ui/assets/index-BHxzXFpf.js +123 -0
- package/dist/ui/assets/{index-BiGnI2Tk.css → index-XiOgY6b5.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";
|
|
@@ -3530,8 +3530,11 @@ function isSimulationMode() {
|
|
|
3530
3530
|
if (policySimulation !== void 0) return policySimulation;
|
|
3531
3531
|
return process.env.SUVEREN_SIMULATION === "1";
|
|
3532
3532
|
}
|
|
3533
|
-
function
|
|
3534
|
-
|
|
3533
|
+
function connectorCurrentlySimulated(manifest, fields) {
|
|
3534
|
+
const decl = manifest?.simulation;
|
|
3535
|
+
if (!decl) return false;
|
|
3536
|
+
const value = (fields?.[decl.field] ?? "").trim().toLowerCase() || decl.default.trim().toLowerCase();
|
|
3537
|
+
return value === "simulation";
|
|
3535
3538
|
}
|
|
3536
3539
|
var SIMULATION_BLOCK_REASON = "blocked: simulation mode \u2014 real systems are off";
|
|
3537
3540
|
|
|
@@ -3647,67 +3650,17 @@ function appendVerificationFooter(tool, args, receiptId, subject) {
|
|
|
3647
3650
|
|
|
3648
3651
|
// src/lib/content-binding.ts
|
|
3649
3652
|
import {
|
|
3650
|
-
computeContentHash,
|
|
3653
|
+
computeContentHash as computeContentHash2,
|
|
3651
3654
|
selectBoundFields,
|
|
3652
3655
|
isFieldBinding,
|
|
3653
3656
|
bindingAppliesTo,
|
|
3654
|
-
getProfile
|
|
3657
|
+
getProfile as getProfile3
|
|
3655
3658
|
} 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
3659
|
|
|
3707
3660
|
// src/lib/manifest-loader.ts
|
|
3708
3661
|
import { readFileSync as readFileSync16, existsSync as existsSync15 } from "fs";
|
|
3709
3662
|
import { join as join15, resolve } from "path";
|
|
3710
|
-
import { getProfile
|
|
3663
|
+
import { getProfile } from "@hap/core";
|
|
3711
3664
|
|
|
3712
3665
|
// src/lib/tool-gating-types.ts
|
|
3713
3666
|
var MAX_PREVIEW_READS = 10;
|
|
@@ -3765,6 +3718,7 @@ function boundsSatisfyReadGate(bounds, gate) {
|
|
|
3765
3718
|
}
|
|
3766
3719
|
function readToolIsGoverned(g) {
|
|
3767
3720
|
if (!g) return false;
|
|
3721
|
+
if (g.internal === true) return true;
|
|
3768
3722
|
return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
|
|
3769
3723
|
}
|
|
3770
3724
|
function allowedResources(contexts, boundField) {
|
|
@@ -3888,6 +3842,7 @@ function uncoveredReadSwitchReason(overrides, boundsSchema) {
|
|
|
3888
3842
|
if (entry !== null && typeof entry !== "object") continue;
|
|
3889
3843
|
const e = entry ?? {};
|
|
3890
3844
|
if (entry !== null && e.category !== "read") continue;
|
|
3845
|
+
if (e.internal === true) continue;
|
|
3891
3846
|
if (e.readGovernance === "none") {
|
|
3892
3847
|
if (!e.readGovernanceReason) {
|
|
3893
3848
|
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 +3949,9 @@ function invalidReadConfigReason(toolName, idx, entry) {
|
|
|
3994
3949
|
}
|
|
3995
3950
|
}
|
|
3996
3951
|
}
|
|
3952
|
+
if (p.kind !== void 0 && p.kind !== "document") {
|
|
3953
|
+
return `${label}.kind must be "document" (the only recognized value)`;
|
|
3954
|
+
}
|
|
3997
3955
|
return null;
|
|
3998
3956
|
}
|
|
3999
3957
|
function invalidPreviewReason(toolName, entry) {
|
|
@@ -4047,6 +4005,9 @@ function invalidDisplayHintsReason(toolName, entry) {
|
|
|
4047
4005
|
}
|
|
4048
4006
|
}
|
|
4049
4007
|
}
|
|
4008
|
+
if (d.aiText !== void 0 && (typeof d.aiText !== "string" || d.aiText.length === 0)) {
|
|
4009
|
+
return `override "${toolName}".display.aiText must be a non-empty string naming an argument`;
|
|
4010
|
+
}
|
|
4050
4011
|
return null;
|
|
4051
4012
|
}
|
|
4052
4013
|
var SUMMARY_FORMATS = ["money", "percent", "list", "short", "date"];
|
|
@@ -4083,6 +4044,21 @@ function invalidSummaryReason(toolName, entry) {
|
|
|
4083
4044
|
}
|
|
4084
4045
|
return null;
|
|
4085
4046
|
}
|
|
4047
|
+
function invalidInternalReason(toolName, entry) {
|
|
4048
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
4049
|
+
const internal = entry.internal;
|
|
4050
|
+
if (internal === void 0) return null;
|
|
4051
|
+
if (typeof internal !== "boolean") return `override "${toolName}".internal must be a boolean`;
|
|
4052
|
+
return null;
|
|
4053
|
+
}
|
|
4054
|
+
function invalidManifestInternalReason(manifest) {
|
|
4055
|
+
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4056
|
+
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
4057
|
+
const reason = invalidInternalReason(toolName, entry);
|
|
4058
|
+
if (reason) return reason;
|
|
4059
|
+
}
|
|
4060
|
+
return null;
|
|
4061
|
+
}
|
|
4086
4062
|
function invalidManifestPreviewReason(manifest) {
|
|
4087
4063
|
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4088
4064
|
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
@@ -4316,6 +4292,92 @@ function invalidManifestContextReason(manifest) {
|
|
|
4316
4292
|
}
|
|
4317
4293
|
return null;
|
|
4318
4294
|
}
|
|
4295
|
+
function invalidDocumentSourceReason(kind, entry, overrides) {
|
|
4296
|
+
const label = `documents.${kind}`;
|
|
4297
|
+
const readReason = invalidContextReadReason(label, entry);
|
|
4298
|
+
if (readReason) return readReason;
|
|
4299
|
+
const tool = entry.tool;
|
|
4300
|
+
const toolEntry = overrides[tool];
|
|
4301
|
+
if (toolEntry?.internal !== true) {
|
|
4302
|
+
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`;
|
|
4303
|
+
}
|
|
4304
|
+
const docLabel = entry.label;
|
|
4305
|
+
if (docLabel !== void 0 && (typeof docLabel !== "string" || docLabel.length === 0)) {
|
|
4306
|
+
return `${label}.label must be a non-empty string`;
|
|
4307
|
+
}
|
|
4308
|
+
return null;
|
|
4309
|
+
}
|
|
4310
|
+
function invalidAttachmentTransportReason(entry) {
|
|
4311
|
+
const label = "attachments";
|
|
4312
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return `${label} must be an object`;
|
|
4313
|
+
const a = entry;
|
|
4314
|
+
if (a.transport !== "field" && a.transport !== "mime-raw") {
|
|
4315
|
+
return `${label}.transport must be "field" or "mime-raw"`;
|
|
4316
|
+
}
|
|
4317
|
+
if (!isNonEmptyString(a.arg)) return `${label}.arg must be a non-empty string`;
|
|
4318
|
+
return null;
|
|
4319
|
+
}
|
|
4320
|
+
function invalidInsertValueSourceReason(kind, entry) {
|
|
4321
|
+
const label = `values.${kind}`;
|
|
4322
|
+
const readReason = invalidContextReadReason(label, entry);
|
|
4323
|
+
if (readReason) return readReason;
|
|
4324
|
+
const v = entry;
|
|
4325
|
+
if (!isNonEmptyString(v.path)) return `${label}.path must be a non-empty string`;
|
|
4326
|
+
if (!Array.isArray(v.allowedHosts) || v.allowedHosts.length === 0) {
|
|
4327
|
+
return `${label}.allowedHosts must be a non-empty array of hostnames`;
|
|
4328
|
+
}
|
|
4329
|
+
for (const [i, h] of v.allowedHosts.entries()) {
|
|
4330
|
+
if (typeof h !== "string" || h.length === 0) return `${label}.allowedHosts[${i}] must be a non-empty string`;
|
|
4331
|
+
}
|
|
4332
|
+
return null;
|
|
4333
|
+
}
|
|
4334
|
+
function invalidManifestDocumentsReason(manifest) {
|
|
4335
|
+
if (!manifest.documents) return null;
|
|
4336
|
+
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4337
|
+
for (const [kind, entry] of Object.entries(manifest.documents)) {
|
|
4338
|
+
const reason = invalidDocumentSourceReason(kind, entry, overrides);
|
|
4339
|
+
if (reason) return reason;
|
|
4340
|
+
}
|
|
4341
|
+
return null;
|
|
4342
|
+
}
|
|
4343
|
+
function invalidManifestAttachmentsReason(manifest) {
|
|
4344
|
+
if (manifest.attachments !== void 0) {
|
|
4345
|
+
const reason = invalidAttachmentTransportReason(manifest.attachments);
|
|
4346
|
+
if (reason) return reason;
|
|
4347
|
+
}
|
|
4348
|
+
const overrides = manifest.toolGating?.overrides ?? {};
|
|
4349
|
+
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
4350
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
4351
|
+
const e = entry;
|
|
4352
|
+
if (e.attachmentsArg !== void 0) {
|
|
4353
|
+
if (typeof e.attachmentsArg !== "string" || e.attachmentsArg.length === 0) {
|
|
4354
|
+
return `override "${toolName}".attachmentsArg must be a non-empty string`;
|
|
4355
|
+
}
|
|
4356
|
+
if (!manifest.attachments) {
|
|
4357
|
+
return `override "${toolName}" declares "attachmentsArg" but this manifest declares no top-level "attachments" transport`;
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
if (e.insertFields !== void 0) {
|
|
4361
|
+
if (!Array.isArray(e.insertFields) || e.insertFields.length === 0) {
|
|
4362
|
+
return `override "${toolName}".insertFields must be a non-empty array of argument names`;
|
|
4363
|
+
}
|
|
4364
|
+
for (const [i, f] of e.insertFields.entries()) {
|
|
4365
|
+
if (typeof f !== "string" || f.length === 0) {
|
|
4366
|
+
return `override "${toolName}".insertFields[${i}] must be a non-empty string`;
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
return null;
|
|
4372
|
+
}
|
|
4373
|
+
function invalidManifestValuesReason(manifest) {
|
|
4374
|
+
if (!manifest.values) return null;
|
|
4375
|
+
for (const [kind, entry] of Object.entries(manifest.values)) {
|
|
4376
|
+
const reason = invalidInsertValueSourceReason(kind, entry);
|
|
4377
|
+
if (reason) return reason;
|
|
4378
|
+
}
|
|
4379
|
+
return null;
|
|
4380
|
+
}
|
|
4319
4381
|
function overrideProfile(entry) {
|
|
4320
4382
|
if (entry === null || typeof entry !== "object") return void 0;
|
|
4321
4383
|
const p = entry.profile;
|
|
@@ -4334,8 +4396,8 @@ function invalidManifestOverrideProfilesReason(manifest) {
|
|
|
4334
4396
|
for (const [toolName, entry] of Object.entries(overrides)) {
|
|
4335
4397
|
const profile = overrideProfile(entry);
|
|
4336
4398
|
if (!profile) continue;
|
|
4337
|
-
if (!
|
|
4338
|
-
if (!
|
|
4399
|
+
if (!getProfile(manifest.profile) && !getProfile(profile)) continue;
|
|
4400
|
+
if (!getProfile(profile)) {
|
|
4339
4401
|
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
4402
|
}
|
|
4341
4403
|
}
|
|
@@ -4344,7 +4406,7 @@ function invalidManifestOverrideProfilesReason(manifest) {
|
|
|
4344
4406
|
function invalidManifestReadGateReason(manifest) {
|
|
4345
4407
|
const allOverrides = manifest.toolGating?.overrides ?? {};
|
|
4346
4408
|
for (const profileId of distinctProfilesOf(manifest)) {
|
|
4347
|
-
const profileDef =
|
|
4409
|
+
const profileDef = getProfile(profileId);
|
|
4348
4410
|
if (!profileDef) continue;
|
|
4349
4411
|
const ownOverrides = {};
|
|
4350
4412
|
for (const [toolName, entry] of Object.entries(allOverrides)) {
|
|
@@ -4356,6 +4418,39 @@ function invalidManifestReadGateReason(manifest) {
|
|
|
4356
4418
|
}
|
|
4357
4419
|
return null;
|
|
4358
4420
|
}
|
|
4421
|
+
function invalidModeLabelsReason(manifest) {
|
|
4422
|
+
if (!manifest.modeLabels) return null;
|
|
4423
|
+
if (manifest.modeLabels === null || typeof manifest.modeLabels !== "object" || Array.isArray(manifest.modeLabels)) {
|
|
4424
|
+
return "modeLabels must be an object mapping mode values to labels";
|
|
4425
|
+
}
|
|
4426
|
+
if (!manifest.simulation) {
|
|
4427
|
+
return 'modeLabels is set but this manifest declares no "simulation" marker \u2014 mode labels need a mode field to key off';
|
|
4428
|
+
}
|
|
4429
|
+
const fieldKeys = new Set(manifest.credentials?.fields?.map((f) => f.key) ?? []);
|
|
4430
|
+
let needsAdapterField = false;
|
|
4431
|
+
for (const [mode, label] of Object.entries(manifest.modeLabels)) {
|
|
4432
|
+
if (typeof label === "string") {
|
|
4433
|
+
if (label.length === 0) return `modeLabels.${mode} must be a non-empty string`;
|
|
4434
|
+
continue;
|
|
4435
|
+
}
|
|
4436
|
+
if (label === null || typeof label !== "object" || Array.isArray(label)) {
|
|
4437
|
+
return `modeLabels.${mode} must be a non-empty string or an object keyed by adapter value`;
|
|
4438
|
+
}
|
|
4439
|
+
needsAdapterField = true;
|
|
4440
|
+
for (const [adapter, adapterLabel] of Object.entries(label)) {
|
|
4441
|
+
if (typeof adapterLabel !== "string" || adapterLabel.length === 0) {
|
|
4442
|
+
return `modeLabels.${mode}.${adapter} must be a non-empty string`;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
if (needsAdapterField && !manifest.adapterField) {
|
|
4447
|
+
return 'modeLabels has an adapter-keyed entry but "adapterField" is missing \u2014 name the credential field holding the adapter';
|
|
4448
|
+
}
|
|
4449
|
+
if (manifest.adapterField && !fieldKeys.has(manifest.adapterField)) {
|
|
4450
|
+
return `adapterField "${manifest.adapterField}" does not name a declared credential field`;
|
|
4451
|
+
}
|
|
4452
|
+
return null;
|
|
4453
|
+
}
|
|
4359
4454
|
var manifests = /* @__PURE__ */ new Map();
|
|
4360
4455
|
function loadManifests(integrationsDir) {
|
|
4361
4456
|
const dir = resolve(
|
|
@@ -4388,6 +4483,11 @@ function loadManifests(integrationsDir) {
|
|
|
4388
4483
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${previewReason}`);
|
|
4389
4484
|
continue;
|
|
4390
4485
|
}
|
|
4486
|
+
const internalReason = invalidManifestInternalReason(manifest);
|
|
4487
|
+
if (internalReason) {
|
|
4488
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${internalReason}`);
|
|
4489
|
+
continue;
|
|
4490
|
+
}
|
|
4391
4491
|
const contextReason = invalidManifestContextReason(manifest);
|
|
4392
4492
|
if (contextReason) {
|
|
4393
4493
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${contextReason}`);
|
|
@@ -4403,6 +4503,26 @@ function loadManifests(integrationsDir) {
|
|
|
4403
4503
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${readGateReason}`);
|
|
4404
4504
|
continue;
|
|
4405
4505
|
}
|
|
4506
|
+
const documentsReason = invalidManifestDocumentsReason(manifest);
|
|
4507
|
+
if (documentsReason) {
|
|
4508
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${documentsReason}`);
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
const attachmentsReason = invalidManifestAttachmentsReason(manifest);
|
|
4512
|
+
if (attachmentsReason) {
|
|
4513
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${attachmentsReason}`);
|
|
4514
|
+
continue;
|
|
4515
|
+
}
|
|
4516
|
+
const valuesReason = invalidManifestValuesReason(manifest);
|
|
4517
|
+
if (valuesReason) {
|
|
4518
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${valuesReason}`);
|
|
4519
|
+
continue;
|
|
4520
|
+
}
|
|
4521
|
+
const modeLabelsReason = invalidModeLabelsReason(manifest) ?? (manifest.modeNotes ? invalidModeLabelsReason({ ...manifest, modeLabels: manifest.modeNotes })?.replace(/modeLabels/g, "modeNotes") ?? null : null);
|
|
4522
|
+
if (modeLabelsReason) {
|
|
4523
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${modeLabelsReason}`);
|
|
4524
|
+
continue;
|
|
4525
|
+
}
|
|
4406
4526
|
manifests.set(id, manifest);
|
|
4407
4527
|
loaded++;
|
|
4408
4528
|
} catch (err) {
|
|
@@ -4419,7 +4539,154 @@ function getAllManifests() {
|
|
|
4419
4539
|
return Array.from(manifests.values());
|
|
4420
4540
|
}
|
|
4421
4541
|
|
|
4542
|
+
// src/lib/attachment-resolution.ts
|
|
4543
|
+
import { getProfile as getProfile2 } from "@hap/core";
|
|
4544
|
+
|
|
4545
|
+
// src/lib/attachment-reference.ts
|
|
4546
|
+
var REF_KEYS = ["connector", "kind", "id"];
|
|
4547
|
+
var INSERT_KEYS = ["placeholder", "connector", "kind", "id"];
|
|
4548
|
+
var ATTACHMENT_REFERENCE_SCHEMA = {
|
|
4549
|
+
type: "object",
|
|
4550
|
+
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.",
|
|
4551
|
+
properties: {
|
|
4552
|
+
connector: { type: "string", description: 'The integration id that can provide this document (e.g. "erp").' },
|
|
4553
|
+
kind: { type: "string", description: 'The document kind that connector declares (e.g. "invoice-pdf").' },
|
|
4554
|
+
id: { type: "string", description: "The id of the specific record (e.g. the invoice id)." }
|
|
4555
|
+
},
|
|
4556
|
+
required: ["connector", "kind", "id"],
|
|
4557
|
+
additionalProperties: false
|
|
4558
|
+
};
|
|
4559
|
+
var INSERT_REFERENCE_SCHEMA = {
|
|
4560
|
+
type: "object",
|
|
4561
|
+
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.",
|
|
4562
|
+
properties: {
|
|
4563
|
+
placeholder: { type: "string", description: "The exact {{placeholder}} token in the text to replace." },
|
|
4564
|
+
connector: { type: "string", description: 'The integration id that can provide this value (e.g. "mollie").' },
|
|
4565
|
+
kind: { type: "string", description: 'The value kind that connector declares (e.g. "payment-link").' },
|
|
4566
|
+
id: { type: "string", description: "The id of the specific record (e.g. the payment link id)." }
|
|
4567
|
+
},
|
|
4568
|
+
required: ["placeholder", "connector", "kind", "id"],
|
|
4569
|
+
additionalProperties: false
|
|
4570
|
+
};
|
|
4571
|
+
function withAttachmentAndInsertSchema(schema, gating) {
|
|
4572
|
+
if (!gating?.attachmentsArg && !gating?.insertFields?.length) return schema;
|
|
4573
|
+
const props = { ...schema.properties ?? {} };
|
|
4574
|
+
if (gating.attachmentsArg) {
|
|
4575
|
+
props[gating.attachmentsArg] = {
|
|
4576
|
+
type: "array",
|
|
4577
|
+
description: "Attachments by reference only \u2014 see items.",
|
|
4578
|
+
items: ATTACHMENT_REFERENCE_SCHEMA
|
|
4579
|
+
};
|
|
4580
|
+
}
|
|
4581
|
+
if (gating.insertFields?.length) {
|
|
4582
|
+
props.insert = {
|
|
4583
|
+
type: "array",
|
|
4584
|
+
description: "Values to insert into this call\u2019s text by reference only \u2014 see items.",
|
|
4585
|
+
items: INSERT_REFERENCE_SCHEMA
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4588
|
+
return { ...schema, properties: props };
|
|
4589
|
+
}
|
|
4590
|
+
function validateOne(value, index, keys) {
|
|
4591
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
4592
|
+
return {
|
|
4593
|
+
ok: false,
|
|
4594
|
+
error: { code: "NOT_AN_OBJECT", index, message: `item ${index} must be an object with exactly ${keys.join(", ")}` }
|
|
4595
|
+
};
|
|
4596
|
+
}
|
|
4597
|
+
const obj = value;
|
|
4598
|
+
const extra = Object.keys(obj).filter((k) => !keys.includes(k));
|
|
4599
|
+
if (extra.length > 0) {
|
|
4600
|
+
return {
|
|
4601
|
+
ok: false,
|
|
4602
|
+
error: {
|
|
4603
|
+
code: "EXTRA_FIELD",
|
|
4604
|
+
index,
|
|
4605
|
+
field: extra[0],
|
|
4606
|
+
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.`
|
|
4607
|
+
}
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
for (const k of keys) {
|
|
4611
|
+
if (typeof obj[k] !== "string" || obj[k].length === 0) {
|
|
4612
|
+
return {
|
|
4613
|
+
ok: false,
|
|
4614
|
+
error: { code: "MISSING_FIELD", index, field: k, message: `item ${index}.${k} must be a non-empty string` }
|
|
4615
|
+
};
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
return { ok: true, value: obj };
|
|
4619
|
+
}
|
|
4620
|
+
function validateDocumentReferences(value) {
|
|
4621
|
+
if (value === void 0) return { ok: true, refs: [] };
|
|
4622
|
+
if (!Array.isArray(value)) {
|
|
4623
|
+
return { ok: false, error: { code: "NOT_AN_ARRAY", message: "attachments must be an array of references" } };
|
|
4624
|
+
}
|
|
4625
|
+
const refs = [];
|
|
4626
|
+
for (let i = 0; i < value.length; i++) {
|
|
4627
|
+
const r = validateOne(value[i], i, REF_KEYS);
|
|
4628
|
+
if (!r.ok) return r;
|
|
4629
|
+
refs.push(r.value);
|
|
4630
|
+
}
|
|
4631
|
+
return { ok: true, refs };
|
|
4632
|
+
}
|
|
4633
|
+
function validateInsertReferences(value) {
|
|
4634
|
+
if (value === void 0) return { ok: true, refs: [] };
|
|
4635
|
+
if (!Array.isArray(value)) {
|
|
4636
|
+
return { ok: false, error: { code: "NOT_AN_ARRAY", message: "insert must be an array of references" } };
|
|
4637
|
+
}
|
|
4638
|
+
const refs = [];
|
|
4639
|
+
for (let i = 0; i < value.length; i++) {
|
|
4640
|
+
const r = validateOne(value[i], i, INSERT_KEYS);
|
|
4641
|
+
if (!r.ok) return r;
|
|
4642
|
+
refs.push(r.value);
|
|
4643
|
+
}
|
|
4644
|
+
return { ok: true, refs };
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
// src/lib/attachment-policy.ts
|
|
4648
|
+
var ATTACHMENT_SOURCES_FIELD = "allowed_attachment_sources";
|
|
4649
|
+
function attachmentSourcesFieldDeclared(profile) {
|
|
4650
|
+
return !!profile?.scopeSchema?.fields?.[ATTACHMENT_SOURCES_FIELD];
|
|
4651
|
+
}
|
|
4652
|
+
function isAttachmentSourceAllowed(profileId, profile, allowedSourcesValue, ref) {
|
|
4653
|
+
if (!attachmentSourcesFieldDeclared(profile)) {
|
|
4654
|
+
return {
|
|
4655
|
+
allowed: false,
|
|
4656
|
+
reason: `Profile "${profileId}" does not yet declare "${ATTACHMENT_SOURCES_FIELD}" \u2014 no mandate can authorize an attachment \u2014 a mandate on a newer email profile that lists the source is needed.`
|
|
4657
|
+
};
|
|
4658
|
+
}
|
|
4659
|
+
const allowedSet = new Set(
|
|
4660
|
+
(allowedSourcesValue ?? "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
4661
|
+
);
|
|
4662
|
+
const key = `${ref.connector}:${ref.kind}`;
|
|
4663
|
+
if (!allowedSet.has(key)) {
|
|
4664
|
+
return {
|
|
4665
|
+
allowed: false,
|
|
4666
|
+
reason: `"${key}" is not in this mandate's "${ATTACHMENT_SOURCES_FIELD}" (${allowedSourcesValue || "(none)"}).`
|
|
4667
|
+
};
|
|
4668
|
+
}
|
|
4669
|
+
return { allowed: true, reason: "" };
|
|
4670
|
+
}
|
|
4671
|
+
function candidatesAllowingAttachments(candidates, refs, profileOf) {
|
|
4672
|
+
if (refs.length === 0) return { allowed: candidates };
|
|
4673
|
+
let refusal;
|
|
4674
|
+
const allowed = candidates.filter((c) => {
|
|
4675
|
+
const value = typeof c.context?.[ATTACHMENT_SOURCES_FIELD] === "string" ? c.context[ATTACHMENT_SOURCES_FIELD] : void 0;
|
|
4676
|
+
for (const ref of refs) {
|
|
4677
|
+
const check = isAttachmentSourceAllowed(c.profileId, profileOf(c.profileId), value, ref);
|
|
4678
|
+
if (!check.allowed) {
|
|
4679
|
+
refusal ??= check.reason;
|
|
4680
|
+
return false;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
return true;
|
|
4684
|
+
});
|
|
4685
|
+
return { allowed, refusal: allowed.length ? void 0 : refusal };
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4422
4688
|
// src/lib/preview.ts
|
|
4689
|
+
import { computeContentHash } from "@hap/core";
|
|
4423
4690
|
var PREVIEW_READ_TIMEOUT_MS = 5e3;
|
|
4424
4691
|
var PREVIEW_MESSAGE_CAP = 500;
|
|
4425
4692
|
function capText(text, max = PREVIEW_MESSAGE_CAP) {
|
|
@@ -4496,9 +4763,9 @@ async function readPreview(integrationManager2, integrationId, previewTool, args
|
|
|
4496
4763
|
}
|
|
4497
4764
|
function hashPreviewBody(body) {
|
|
4498
4765
|
if (body.structured !== void 0) {
|
|
4499
|
-
return
|
|
4766
|
+
return computeContentHash({ kind: "jcs", version: "1" }, body.structured);
|
|
4500
4767
|
}
|
|
4501
|
-
return
|
|
4768
|
+
return computeContentHash({ kind: "text", version: "1" }, body.text ?? "");
|
|
4502
4769
|
}
|
|
4503
4770
|
function isPlainRecord(v) {
|
|
4504
4771
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -4511,6 +4778,7 @@ function planPreviewReads(declaration, actionArgs) {
|
|
|
4511
4778
|
attach: config.attach,
|
|
4512
4779
|
fields: config.fields,
|
|
4513
4780
|
version: config.version,
|
|
4781
|
+
kind: config.kind,
|
|
4514
4782
|
argsMapping: config.args
|
|
4515
4783
|
};
|
|
4516
4784
|
if (config.forEach) {
|
|
@@ -4537,7 +4805,8 @@ async function runPreviewReadPlan(integrationManager2, integrationId, plan) {
|
|
|
4537
4805
|
tool: item.tool,
|
|
4538
4806
|
...item.attach ? { attach: item.attach } : {},
|
|
4539
4807
|
...sourceArgNames.length === 1 ? { sourceArg: sourceArgNames[0] } : {},
|
|
4540
|
-
...item.fields ? { fields: item.fields } : {}
|
|
4808
|
+
...item.fields ? { fields: item.fields } : {},
|
|
4809
|
+
...item.kind ? { kind: item.kind } : {}
|
|
4541
4810
|
};
|
|
4542
4811
|
if (item.elements !== void 0) {
|
|
4543
4812
|
const elements = [];
|
|
@@ -4636,7 +4905,7 @@ async function combinedPreviewReadsHash(integrationManager2, integrationId, read
|
|
|
4636
4905
|
hashes.push(hashPreviewBody(read.body));
|
|
4637
4906
|
}
|
|
4638
4907
|
if (hashes.length === 0) return void 0;
|
|
4639
|
-
return
|
|
4908
|
+
return computeContentHash({ kind: "jcs", version: "1" }, { reads: hashes });
|
|
4640
4909
|
}
|
|
4641
4910
|
async function buildInternalPreview(integrationManager2, integrationId, toolName, actionArgs) {
|
|
4642
4911
|
const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
|
|
@@ -4686,6 +4955,20 @@ async function buildInternalPreview(integrationManager2, integrationId, toolName
|
|
|
4686
4955
|
}
|
|
4687
4956
|
});
|
|
4688
4957
|
}
|
|
4958
|
+
function structuredOrJsonText(body) {
|
|
4959
|
+
if (body.structured && typeof body.structured === "object") return body.structured;
|
|
4960
|
+
const t = body.text?.trim();
|
|
4961
|
+
if (!t || !t.startsWith("{")) return void 0;
|
|
4962
|
+
try {
|
|
4963
|
+
const parsed = JSON.parse(t);
|
|
4964
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
4965
|
+
} catch {
|
|
4966
|
+
return void 0;
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
// src/lib/document-source.ts
|
|
4971
|
+
import { createHash as createHash3 } from "crypto";
|
|
4689
4972
|
|
|
4690
4973
|
// src/lib/field-path.ts
|
|
4691
4974
|
function matchesWhere(element, where) {
|
|
@@ -5103,6 +5386,447 @@ async function buildRequestContext(integrationManager2, executionJournal, action
|
|
|
5103
5386
|
return { request, history };
|
|
5104
5387
|
}
|
|
5105
5388
|
|
|
5389
|
+
// src/lib/document-source.ts
|
|
5390
|
+
function sha256Hex(buf) {
|
|
5391
|
+
return createHash3("sha256").update(buf).digest("hex");
|
|
5392
|
+
}
|
|
5393
|
+
async function resolveDocumentReference(integrationManager2, ref) {
|
|
5394
|
+
const manifest = getManifest(ref.connector);
|
|
5395
|
+
if (!manifest) {
|
|
5396
|
+
return { ok: false, error: { code: "UNKNOWN_CONNECTOR", message: `"${ref.connector}" is not a known connector.` } };
|
|
5397
|
+
}
|
|
5398
|
+
const docConfig = manifest.documents?.[ref.kind];
|
|
5399
|
+
if (!docConfig) {
|
|
5400
|
+
return {
|
|
5401
|
+
ok: false,
|
|
5402
|
+
error: {
|
|
5403
|
+
code: "UNDECLARED_KIND",
|
|
5404
|
+
message: `Connector "${ref.connector}" declares no document kind "${ref.kind}".`
|
|
5405
|
+
}
|
|
5406
|
+
};
|
|
5407
|
+
}
|
|
5408
|
+
const toolEntry = manifest.toolGating?.overrides?.[docConfig.tool];
|
|
5409
|
+
if (!toolEntry?.internal) {
|
|
5410
|
+
return {
|
|
5411
|
+
ok: false,
|
|
5412
|
+
error: {
|
|
5413
|
+
code: "NOT_INTERNAL",
|
|
5414
|
+
message: `Connector "${ref.connector}"'s document read "${docConfig.tool}" is not marked internal \u2014 refusing to call it for a document reference.`
|
|
5415
|
+
}
|
|
5416
|
+
};
|
|
5417
|
+
}
|
|
5418
|
+
const args = substitute(docConfig.args, { id: ref.id });
|
|
5419
|
+
const read = await readPreview(integrationManager2, ref.connector, docConfig.tool, args);
|
|
5420
|
+
if (read.status === "not_found") {
|
|
5421
|
+
return { ok: false, error: { code: "NOT_FOUND", message: read.message ?? `No document "${ref.kind}"/"${ref.id}" on "${ref.connector}".` } };
|
|
5422
|
+
}
|
|
5423
|
+
if (read.status !== "ok") {
|
|
5424
|
+
return { ok: false, error: { code: "UNAVAILABLE", message: read.message ?? `Connector "${ref.connector}" could not be reached.` } };
|
|
5425
|
+
}
|
|
5426
|
+
const body = structuredOrJsonText(read.body);
|
|
5427
|
+
if (!body || typeof body !== "object") {
|
|
5428
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: `Document read for "${ref.connector}:${ref.kind}" returned no structured result.` } };
|
|
5429
|
+
}
|
|
5430
|
+
const { filename, contentType, contentBase64, sha256: claimedSha256 } = body;
|
|
5431
|
+
if (typeof filename !== "string" || !filename) {
|
|
5432
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: 'Document result is missing "filename".' } };
|
|
5433
|
+
}
|
|
5434
|
+
if (typeof contentType !== "string" || !contentType) {
|
|
5435
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: 'Document result is missing "contentType".' } };
|
|
5436
|
+
}
|
|
5437
|
+
if (typeof contentBase64 !== "string" || !contentBase64) {
|
|
5438
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: 'Document result is missing "contentBase64".' } };
|
|
5439
|
+
}
|
|
5440
|
+
let bytes;
|
|
5441
|
+
try {
|
|
5442
|
+
bytes = Buffer.from(contentBase64, "base64");
|
|
5443
|
+
} catch {
|
|
5444
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: '"contentBase64" is not valid base64.' } };
|
|
5445
|
+
}
|
|
5446
|
+
const computedSha256 = sha256Hex(bytes);
|
|
5447
|
+
if (typeof claimedSha256 === "string" && claimedSha256.length > 0 && claimedSha256 !== computedSha256) {
|
|
5448
|
+
return {
|
|
5449
|
+
ok: false,
|
|
5450
|
+
error: {
|
|
5451
|
+
code: "HASH_MISMATCH",
|
|
5452
|
+
message: `Connector "${ref.connector}" claimed sha256 ${claimedSha256} for "${ref.kind}:${ref.id}" but the bytes hash to ${computedSha256}.`
|
|
5453
|
+
}
|
|
5454
|
+
};
|
|
5455
|
+
}
|
|
5456
|
+
return { ok: true, doc: { filename, contentType, contentBase64, sha256: computedSha256, bytes: bytes.byteLength } };
|
|
5457
|
+
}
|
|
5458
|
+
async function resolveAttachmentPreview(integrationManager2, ref, sealedSha256) {
|
|
5459
|
+
const result = await resolveDocumentReference(integrationManager2, ref);
|
|
5460
|
+
if (!result.ok) {
|
|
5461
|
+
switch (result.error.code) {
|
|
5462
|
+
case "NOT_FOUND":
|
|
5463
|
+
return { status: "not_found", message: result.error.message };
|
|
5464
|
+
case "UNKNOWN_CONNECTOR":
|
|
5465
|
+
case "UNDECLARED_KIND":
|
|
5466
|
+
case "NOT_INTERNAL":
|
|
5467
|
+
return { status: "error", message: result.error.message };
|
|
5468
|
+
default:
|
|
5469
|
+
return { status: "unavailable", message: result.error.message };
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
if (result.doc.sha256 !== sealedSha256) {
|
|
5473
|
+
return {
|
|
5474
|
+
status: "changed",
|
|
5475
|
+
message: `"${result.doc.filename}" (${ref.connector}:${ref.kind}/${ref.id}) changed since it was sealed \u2014 its fingerprint no longer matches.`
|
|
5476
|
+
};
|
|
5477
|
+
}
|
|
5478
|
+
return { status: "ok", ...result.doc };
|
|
5479
|
+
}
|
|
5480
|
+
|
|
5481
|
+
// src/lib/insert-values.ts
|
|
5482
|
+
function hostAllowed(host, allowed) {
|
|
5483
|
+
const h = host.toLowerCase();
|
|
5484
|
+
const a = allowed.toLowerCase();
|
|
5485
|
+
return h === a || h.endsWith(`.${a}`);
|
|
5486
|
+
}
|
|
5487
|
+
async function resolveInsertValue(integrationManager2, ref) {
|
|
5488
|
+
const manifest = getManifest(ref.connector);
|
|
5489
|
+
if (!manifest) {
|
|
5490
|
+
return { ok: false, error: { code: "UNKNOWN_CONNECTOR", message: `"${ref.connector}" is not a known connector.` } };
|
|
5491
|
+
}
|
|
5492
|
+
const valueConfig = manifest.values?.[ref.kind];
|
|
5493
|
+
if (!valueConfig) {
|
|
5494
|
+
return {
|
|
5495
|
+
ok: false,
|
|
5496
|
+
error: { code: "UNDECLARED_KIND", message: `Connector "${ref.connector}" declares no insertable value kind "${ref.kind}".` }
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5499
|
+
const args = substitute(valueConfig.args, { id: ref.id });
|
|
5500
|
+
const read = await readPreview(integrationManager2, ref.connector, valueConfig.tool, args);
|
|
5501
|
+
if (read.status === "not_found") {
|
|
5502
|
+
return { ok: false, error: { code: "NOT_FOUND", message: read.message ?? `No value "${ref.kind}"/"${ref.id}" on "${ref.connector}".` } };
|
|
5503
|
+
}
|
|
5504
|
+
if (read.status !== "ok") {
|
|
5505
|
+
return { ok: false, error: { code: "UNAVAILABLE", message: read.message ?? `Connector "${ref.connector}" could not be reached.` } };
|
|
5506
|
+
}
|
|
5507
|
+
const body = structuredOrJsonText(read.body);
|
|
5508
|
+
if (!body || typeof body !== "object") {
|
|
5509
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: `Value read for "${ref.connector}:${ref.kind}" returned no structured result.` } };
|
|
5510
|
+
}
|
|
5511
|
+
const raw = getByDottedPath(body, valueConfig.path);
|
|
5512
|
+
if (typeof raw !== "string" || !raw) {
|
|
5513
|
+
return { ok: false, error: { code: "MALFORMED_RESULT", message: `Value read for "${ref.connector}:${ref.kind}" has nothing at "${valueConfig.path}".` } };
|
|
5514
|
+
}
|
|
5515
|
+
let url;
|
|
5516
|
+
try {
|
|
5517
|
+
url = new URL(raw);
|
|
5518
|
+
} catch {
|
|
5519
|
+
return { ok: false, error: { code: "NOT_HTTPS", message: `Value "${raw}" is not a valid URL.` } };
|
|
5520
|
+
}
|
|
5521
|
+
if (url.protocol !== "https:") {
|
|
5522
|
+
return { ok: false, error: { code: "NOT_HTTPS", message: `Value "${raw}" is not an https URL.` } };
|
|
5523
|
+
}
|
|
5524
|
+
if (!valueConfig.allowedHosts.some((h) => hostAllowed(url.hostname, h))) {
|
|
5525
|
+
return {
|
|
5526
|
+
ok: false,
|
|
5527
|
+
error: {
|
|
5528
|
+
code: "DISALLOWED_HOST",
|
|
5529
|
+
message: `Value "${raw}" is on host "${url.hostname}", which "${ref.connector}:${ref.kind}" does not allow (allowed: ${valueConfig.allowedHosts.join(", ")}).`
|
|
5530
|
+
}
|
|
5531
|
+
};
|
|
5532
|
+
}
|
|
5533
|
+
return { ok: true, insert: { placeholder: ref.placeholder, value: raw, connector: ref.connector, kind: ref.kind } };
|
|
5534
|
+
}
|
|
5535
|
+
function placeholdersIn(text) {
|
|
5536
|
+
const found = [];
|
|
5537
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5538
|
+
for (const m of text.matchAll(/\{\{[^{}]+\}\}/g)) {
|
|
5539
|
+
if (!seen.has(m[0])) {
|
|
5540
|
+
seen.add(m[0]);
|
|
5541
|
+
found.push(m[0]);
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
return found;
|
|
5545
|
+
}
|
|
5546
|
+
function applyInserts(fields, inserts) {
|
|
5547
|
+
const allPlaceholders = /* @__PURE__ */ new Set();
|
|
5548
|
+
for (const text of Object.values(fields)) {
|
|
5549
|
+
for (const p of placeholdersIn(text)) allPlaceholders.add(p);
|
|
5550
|
+
}
|
|
5551
|
+
const insertByPlaceholder = new Map(inserts.map((i) => [i.placeholder, i]));
|
|
5552
|
+
for (const p of allPlaceholders) {
|
|
5553
|
+
if (!insertByPlaceholder.has(p)) {
|
|
5554
|
+
return {
|
|
5555
|
+
ok: false,
|
|
5556
|
+
error: {
|
|
5557
|
+
code: "UNMATCHED_PLACEHOLDER",
|
|
5558
|
+
placeholder: p,
|
|
5559
|
+
message: `The text contains "${p}" but no "insert" entry names that placeholder.`
|
|
5560
|
+
}
|
|
5561
|
+
};
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
for (const insert of inserts) {
|
|
5565
|
+
if (!allPlaceholders.has(insert.placeholder)) {
|
|
5566
|
+
return {
|
|
5567
|
+
ok: false,
|
|
5568
|
+
error: {
|
|
5569
|
+
code: "PLACEHOLDER_NOT_FOUND",
|
|
5570
|
+
placeholder: insert.placeholder,
|
|
5571
|
+
message: `"insert" names placeholder "${insert.placeholder}", which does not appear in the text.`
|
|
5572
|
+
}
|
|
5573
|
+
};
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
const out = {};
|
|
5577
|
+
for (const [field, text] of Object.entries(fields)) {
|
|
5578
|
+
let replaced = text;
|
|
5579
|
+
for (const insert of inserts) {
|
|
5580
|
+
replaced = replaced.split(insert.placeholder).join(insert.value);
|
|
5581
|
+
}
|
|
5582
|
+
out[field] = replaced;
|
|
5583
|
+
}
|
|
5584
|
+
return { ok: true, fields: out };
|
|
5585
|
+
}
|
|
5586
|
+
|
|
5587
|
+
// src/lib/mime-message.ts
|
|
5588
|
+
var CRLF = "\r\n";
|
|
5589
|
+
function isAscii(s) {
|
|
5590
|
+
return /^[\x20-\x7e]*$/.test(s);
|
|
5591
|
+
}
|
|
5592
|
+
function rfc2047Encode(value) {
|
|
5593
|
+
if (isAscii(value)) return value;
|
|
5594
|
+
const b64 = Buffer.from(value, "utf8").toString("base64");
|
|
5595
|
+
return `=?UTF-8?B?${b64}?=`;
|
|
5596
|
+
}
|
|
5597
|
+
function wrapBase64(b64) {
|
|
5598
|
+
const lines = [];
|
|
5599
|
+
for (let i = 0; i < b64.length; i += 76) lines.push(b64.slice(i, i + 76));
|
|
5600
|
+
return lines.join(CRLF);
|
|
5601
|
+
}
|
|
5602
|
+
function makeBoundary() {
|
|
5603
|
+
const random = Buffer.from(
|
|
5604
|
+
Array.from({ length: 16 }, () => Math.floor(Math.random() * 256))
|
|
5605
|
+
).toString("hex");
|
|
5606
|
+
return `----=_SuverenPart_${random}`;
|
|
5607
|
+
}
|
|
5608
|
+
function addressHeader(name, addresses) {
|
|
5609
|
+
if (!addresses?.length) return "";
|
|
5610
|
+
return `${name}: ${addresses.join(", ")}${CRLF}`;
|
|
5611
|
+
}
|
|
5612
|
+
function buildMimeMessage(input) {
|
|
5613
|
+
const boundary = makeBoundary();
|
|
5614
|
+
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}`;
|
|
5615
|
+
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;
|
|
5616
|
+
const attachmentParts = input.attachments.map((att) => {
|
|
5617
|
+
const name = rfc2047Encode(att.filename);
|
|
5618
|
+
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;
|
|
5619
|
+
});
|
|
5620
|
+
return headers + CRLF + [textPart, ...attachmentParts].join("") + `--${boundary}--${CRLF}`;
|
|
5621
|
+
}
|
|
5622
|
+
function toBase64Url(mime) {
|
|
5623
|
+
return Buffer.from(mime, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5626
|
+
// src/lib/attachment-resolution.ts
|
|
5627
|
+
async function resolveAttachmentsAndInserts(integrationManager2, tool, profileId, args, allowedSourcesValue) {
|
|
5628
|
+
const attachmentsArg = tool.gating?.attachmentsArg;
|
|
5629
|
+
const insertFields = tool.gating?.insertFields;
|
|
5630
|
+
if (!attachmentsArg && !insertFields?.length) return { ok: true, args };
|
|
5631
|
+
const refsResult = attachmentsArg ? validateDocumentReferences(args[attachmentsArg]) : { ok: true, refs: [] };
|
|
5632
|
+
if (!refsResult.ok) {
|
|
5633
|
+
return { ok: false, message: `Refused: ${tool.namespacedName}'s attachments ${refsResult.error.message}` };
|
|
5634
|
+
}
|
|
5635
|
+
const insertResult = validateInsertReferences(args.insert);
|
|
5636
|
+
if (!insertResult.ok) {
|
|
5637
|
+
return { ok: false, message: `Refused: ${tool.namespacedName}'s insert ${insertResult.error.message}` };
|
|
5638
|
+
}
|
|
5639
|
+
const sealedAttachments = [];
|
|
5640
|
+
if (refsResult.refs.length > 0) {
|
|
5641
|
+
const profile = getProfile2(profileId);
|
|
5642
|
+
for (const ref of refsResult.refs) {
|
|
5643
|
+
const policy = isAttachmentSourceAllowed(profileId, profile, allowedSourcesValue, ref);
|
|
5644
|
+
if (!policy.allowed) {
|
|
5645
|
+
return { ok: false, message: `Refused: attachment "${ref.connector}:${ref.kind}" is not allowed. ${policy.reason}` };
|
|
5646
|
+
}
|
|
5647
|
+
const resolved = await resolveDocumentReference(integrationManager2, ref);
|
|
5648
|
+
if (!resolved.ok) {
|
|
5649
|
+
return { ok: false, message: `Refused: could not resolve attachment "${ref.connector}:${ref.kind}/${ref.id}" \u2014 ${resolved.error.message}` };
|
|
5650
|
+
}
|
|
5651
|
+
sealedAttachments.push({
|
|
5652
|
+
connector: ref.connector,
|
|
5653
|
+
kind: ref.kind,
|
|
5654
|
+
id: ref.id,
|
|
5655
|
+
filename: resolved.doc.filename,
|
|
5656
|
+
contentType: resolved.doc.contentType,
|
|
5657
|
+
sha256: resolved.doc.sha256,
|
|
5658
|
+
bytes: resolved.doc.bytes
|
|
5659
|
+
});
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
const sealedInserts = [];
|
|
5663
|
+
const resolvedInserts = [];
|
|
5664
|
+
if (insertResult.refs.length > 0) {
|
|
5665
|
+
for (const ref of insertResult.refs) {
|
|
5666
|
+
const resolved = await resolveInsertValue(integrationManager2, ref);
|
|
5667
|
+
if (!resolved.ok) {
|
|
5668
|
+
return { ok: false, message: `Refused: could not resolve insert "${ref.connector}:${ref.kind}/${ref.id}" for "${ref.placeholder}" \u2014 ${resolved.error.message}` };
|
|
5669
|
+
}
|
|
5670
|
+
resolvedInserts.push(resolved.insert);
|
|
5671
|
+
sealedInserts.push({ placeholder: ref.placeholder, connector: ref.connector, kind: ref.kind, id: ref.id, value: resolved.insert.value });
|
|
5672
|
+
}
|
|
5673
|
+
}
|
|
5674
|
+
let outgoing = { ...args };
|
|
5675
|
+
if (insertFields?.length) {
|
|
5676
|
+
const fields = {};
|
|
5677
|
+
for (const field of insertFields) {
|
|
5678
|
+
fields[field] = typeof args[field] === "string" ? args[field] : "";
|
|
5679
|
+
}
|
|
5680
|
+
const applied = applyInserts(fields, resolvedInserts);
|
|
5681
|
+
if (!applied.ok) {
|
|
5682
|
+
return { ok: false, message: `Refused: ${tool.namespacedName}'s insert \u2014 ${applied.error.message}` };
|
|
5683
|
+
}
|
|
5684
|
+
outgoing = { ...outgoing, ...applied.fields };
|
|
5685
|
+
} else if (insertResult.refs.length > 0) {
|
|
5686
|
+
return { ok: false, message: `Refused: ${tool.namespacedName} does not declare any insertable text fields.` };
|
|
5687
|
+
}
|
|
5688
|
+
if (attachmentsArg) delete outgoing[attachmentsArg];
|
|
5689
|
+
delete outgoing.insert;
|
|
5690
|
+
return {
|
|
5691
|
+
ok: true,
|
|
5692
|
+
args: outgoing,
|
|
5693
|
+
...sealedAttachments.length > 0 || sealedInserts.length > 0 ? { sealed: { attachments: sealedAttachments, inserts: sealedInserts } } : {}
|
|
5694
|
+
};
|
|
5695
|
+
}
|
|
5696
|
+
function toStringArray(value) {
|
|
5697
|
+
if (Array.isArray(value)) return value.map((v) => String(v));
|
|
5698
|
+
if (typeof value === "string" && value) return [value];
|
|
5699
|
+
return [];
|
|
5700
|
+
}
|
|
5701
|
+
async function finalizeAttachmentsForExecution(integrationManager2, integrationId, args) {
|
|
5702
|
+
const sealed = args[ATTACHMENTS_SEALED_META_KEY];
|
|
5703
|
+
if (!sealed || sealed.attachments.length === 0 && sealed.inserts.length === 0) {
|
|
5704
|
+
return { ok: true, args };
|
|
5705
|
+
}
|
|
5706
|
+
const resolvedDocs = [];
|
|
5707
|
+
for (const a of sealed.attachments) {
|
|
5708
|
+
const resolved = await resolveDocumentReference(integrationManager2, { connector: a.connector, kind: a.kind, id: a.id });
|
|
5709
|
+
if (!resolved.ok) {
|
|
5710
|
+
return {
|
|
5711
|
+
ok: false,
|
|
5712
|
+
message: `Refused: could not re-resolve attachment "${a.filename}" (${a.connector}:${a.kind}/${a.id}) at execution \u2014 ${resolved.error.message}`
|
|
5713
|
+
};
|
|
5714
|
+
}
|
|
5715
|
+
if (resolved.doc.sha256 !== a.sha256) {
|
|
5716
|
+
return {
|
|
5717
|
+
ok: false,
|
|
5718
|
+
message: `Refused: attachment "${a.filename}" (${a.connector}:${a.kind}/${a.id}) changed since this was approved \u2014 its fingerprint no longer matches.`
|
|
5719
|
+
};
|
|
5720
|
+
}
|
|
5721
|
+
resolvedDocs.push(resolved.doc);
|
|
5722
|
+
}
|
|
5723
|
+
for (const i of sealed.inserts) {
|
|
5724
|
+
const resolved = await resolveInsertValue(integrationManager2, { connector: i.connector, kind: i.kind, id: i.id, placeholder: i.placeholder });
|
|
5725
|
+
if (!resolved.ok) {
|
|
5726
|
+
return {
|
|
5727
|
+
ok: false,
|
|
5728
|
+
message: `Refused: could not re-resolve the inserted value for "${i.placeholder}" (${i.connector}:${i.kind}/${i.id}) at execution \u2014 ${resolved.error.message}`
|
|
5729
|
+
};
|
|
5730
|
+
}
|
|
5731
|
+
if (resolved.insert.value !== i.value) {
|
|
5732
|
+
return {
|
|
5733
|
+
ok: false,
|
|
5734
|
+
message: `Refused: the value inserted for "${i.placeholder}" (${i.connector}:${i.kind}/${i.id}) changed since this was approved.`
|
|
5735
|
+
};
|
|
5736
|
+
}
|
|
5737
|
+
}
|
|
5738
|
+
let outgoing = { ...args };
|
|
5739
|
+
if (resolvedDocs.length > 0) {
|
|
5740
|
+
const manifest = getManifest(integrationId);
|
|
5741
|
+
const transport = manifest?.attachments;
|
|
5742
|
+
if (!transport) {
|
|
5743
|
+
return { ok: false, message: `Refused: "${integrationId}" declares no attachments transport.` };
|
|
5744
|
+
}
|
|
5745
|
+
if (transport.transport === "field") {
|
|
5746
|
+
outgoing[transport.arg] = resolvedDocs.map((d) => ({
|
|
5747
|
+
filename: d.filename,
|
|
5748
|
+
contentType: d.contentType,
|
|
5749
|
+
contentBase64: d.contentBase64
|
|
5750
|
+
}));
|
|
5751
|
+
} else {
|
|
5752
|
+
const to = toStringArray(outgoing.to);
|
|
5753
|
+
const cc = toStringArray(outgoing.cc);
|
|
5754
|
+
const subject = typeof outgoing.subject === "string" ? outgoing.subject : "";
|
|
5755
|
+
const bodyText = typeof outgoing.body === "string" ? outgoing.body : "";
|
|
5756
|
+
const mime = buildMimeMessage({
|
|
5757
|
+
to,
|
|
5758
|
+
cc,
|
|
5759
|
+
subject,
|
|
5760
|
+
bodyText,
|
|
5761
|
+
attachments: resolvedDocs.map((d) => ({ filename: d.filename, contentType: d.contentType, contentBase64: d.contentBase64 }))
|
|
5762
|
+
});
|
|
5763
|
+
outgoing[transport.arg] = toBase64Url(mime);
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
return { ok: true, args: outgoing };
|
|
5767
|
+
}
|
|
5768
|
+
var ATTACHMENTS_SEALED_META_KEY = "_attachmentsSealed";
|
|
5769
|
+
function stripAttachmentsSealedMeta(args) {
|
|
5770
|
+
if (!(ATTACHMENTS_SEALED_META_KEY in args)) return args;
|
|
5771
|
+
const { [ATTACHMENTS_SEALED_META_KEY]: _omit, ...rest } = args;
|
|
5772
|
+
return rest;
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
// src/lib/content-binding.ts
|
|
5776
|
+
function getContentBinding(profileId) {
|
|
5777
|
+
return getProfile3(profileId)?.content_binding;
|
|
5778
|
+
}
|
|
5779
|
+
function computeContentBinding(profileId, tool, toolArgs, actionType) {
|
|
5780
|
+
const binding = getContentBinding(profileId);
|
|
5781
|
+
if (!binding) return void 0;
|
|
5782
|
+
if (isFieldBinding(binding)) {
|
|
5783
|
+
if (!bindingAppliesTo(binding, actionType)) {
|
|
5784
|
+
if (!actionType && binding.appliesTo) {
|
|
5785
|
+
console.error(
|
|
5786
|
+
`[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.`
|
|
5787
|
+
);
|
|
5788
|
+
}
|
|
5789
|
+
return void 0;
|
|
5790
|
+
}
|
|
5791
|
+
let effectiveArgs = toolArgs;
|
|
5792
|
+
if (binding.fields?.includes("attachments")) {
|
|
5793
|
+
const sealed = toolArgs[ATTACHMENTS_SEALED_META_KEY];
|
|
5794
|
+
const fingerprints = (sealed?.attachments ?? []).map((a) => ({ filename: a.filename, sha256: a.sha256 }));
|
|
5795
|
+
effectiveArgs = { ...toolArgs, attachments: fingerprints };
|
|
5796
|
+
}
|
|
5797
|
+
const bound = selectBoundFields(binding, effectiveArgs);
|
|
5798
|
+
return {
|
|
5799
|
+
contentHash: computeContentHash2(binding, bound),
|
|
5800
|
+
contentBinding: { version: binding.version, kind: binding.kind, fields: binding.fields },
|
|
5801
|
+
boundContent: bound
|
|
5802
|
+
};
|
|
5803
|
+
}
|
|
5804
|
+
let contentHash;
|
|
5805
|
+
let boundContent;
|
|
5806
|
+
if (binding.kind === "jcs") {
|
|
5807
|
+
contentHash = computeContentHash2(binding, toolArgs);
|
|
5808
|
+
boundContent = toolArgs;
|
|
5809
|
+
} else {
|
|
5810
|
+
const declared = tool?.gating?.contentField;
|
|
5811
|
+
const field = declared ?? (tool ? detectContentField(tool) : null);
|
|
5812
|
+
if (!field) return void 0;
|
|
5813
|
+
const raw = typeof toolArgs[field] === "string" ? toolArgs[field] : "";
|
|
5814
|
+
if (raw === "") return void 0;
|
|
5815
|
+
contentHash = computeContentHash2(binding, raw);
|
|
5816
|
+
boundContent = raw;
|
|
5817
|
+
}
|
|
5818
|
+
return {
|
|
5819
|
+
contentHash,
|
|
5820
|
+
contentBinding: { version: binding.version, kind: binding.kind },
|
|
5821
|
+
boundContent
|
|
5822
|
+
};
|
|
5823
|
+
}
|
|
5824
|
+
function attachTicketId(tool, args, ticketId) {
|
|
5825
|
+
const schema = tool.inputSchema;
|
|
5826
|
+
if (!schema?.properties || !("ticket_id" in schema.properties)) return args;
|
|
5827
|
+
return { ...args, ticket_id: ticketId };
|
|
5828
|
+
}
|
|
5829
|
+
|
|
5106
5830
|
// src/lib/ticket-verify.ts
|
|
5107
5831
|
import { verifyTicketSignature } from "@hap/core";
|
|
5108
5832
|
var TicketBindingMismatchError = class extends Error {
|
|
@@ -5200,7 +5924,7 @@ async function resolveWhoLabel(archive, domainKey, userId) {
|
|
|
5200
5924
|
}
|
|
5201
5925
|
|
|
5202
5926
|
// src/lib/arg-encoding.ts
|
|
5203
|
-
function
|
|
5927
|
+
function rfc2047Encode2(value) {
|
|
5204
5928
|
if (!/[^\x00-\x7F]/.test(value)) return value;
|
|
5205
5929
|
const CHARSET = "=?UTF-8?Q?";
|
|
5206
5930
|
const SUFFIX = "?=";
|
|
@@ -5234,7 +5958,7 @@ function rfc2047Encode(value) {
|
|
|
5234
5958
|
return words.join(" ");
|
|
5235
5959
|
}
|
|
5236
5960
|
var ENCODERS = {
|
|
5237
|
-
rfc2047:
|
|
5961
|
+
rfc2047: rfc2047Encode2
|
|
5238
5962
|
};
|
|
5239
5963
|
function encodeOutgoingArgs(tool, args) {
|
|
5240
5964
|
const declared = tool?.gating?.argEncoding;
|
|
@@ -5307,7 +6031,7 @@ function normalizeIncomingArgs(tool, args) {
|
|
|
5307
6031
|
}
|
|
5308
6032
|
|
|
5309
6033
|
// src/lib/numeric-execution.ts
|
|
5310
|
-
import { getProfile as
|
|
6034
|
+
import { getProfile as getProfile4 } from "@hap/core";
|
|
5311
6035
|
function parseNumericString(value) {
|
|
5312
6036
|
if (!/^-?\d+(\.\d+)?$/.test(value)) return void 0;
|
|
5313
6037
|
const parsed = Number(value);
|
|
@@ -5323,7 +6047,7 @@ function declaredNumericFields(profile) {
|
|
|
5323
6047
|
return out;
|
|
5324
6048
|
}
|
|
5325
6049
|
function coerceDeclaredNumericFields(profileId, execution) {
|
|
5326
|
-
const fields = declaredNumericFields(
|
|
6050
|
+
const fields = declaredNumericFields(getProfile4(profileId));
|
|
5327
6051
|
for (const field of fields) {
|
|
5328
6052
|
const value = execution[field];
|
|
5329
6053
|
if (typeof value !== "string") continue;
|
|
@@ -5333,7 +6057,7 @@ function coerceDeclaredNumericFields(profileId, execution) {
|
|
|
5333
6057
|
}
|
|
5334
6058
|
|
|
5335
6059
|
// src/lib/tool-proxy.ts
|
|
5336
|
-
import { getProfile as
|
|
6060
|
+
import { getProfile as getProfile5, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
|
|
5337
6061
|
import { readFile } from "fs/promises";
|
|
5338
6062
|
import { extname } from "path";
|
|
5339
6063
|
import { randomUUID } from "crypto";
|
|
@@ -5405,7 +6129,7 @@ function profileMatches(profileId, shortName2) {
|
|
|
5405
6129
|
return profileId === shortName2 || profileId.includes("/" + shortName2 + "@") || profileId.endsWith("/" + shortName2);
|
|
5406
6130
|
}
|
|
5407
6131
|
function wireExecutionContext(profileId, execution) {
|
|
5408
|
-
const profile =
|
|
6132
|
+
const profile = getProfile5(profileId);
|
|
5409
6133
|
const allowed = /* @__PURE__ */ new Set(["action_type"]);
|
|
5410
6134
|
const fields = profile?.boundsSchema?.fields;
|
|
5411
6135
|
if (fields) {
|
|
@@ -5423,7 +6147,7 @@ function wireExecutionContext(profileId, execution) {
|
|
|
5423
6147
|
return out;
|
|
5424
6148
|
}
|
|
5425
6149
|
function countBoundsFor(profileId, actionType) {
|
|
5426
|
-
const fields =
|
|
6150
|
+
const fields = getProfile5(profileId)?.boundsSchema?.fields;
|
|
5427
6151
|
if (!fields) return null;
|
|
5428
6152
|
return Object.entries(fields).filter(([, def]) => def.boundType?.kind === "cumulative_count").filter(([name, def]) => {
|
|
5429
6153
|
const applies = boundActionTypes2(name, def);
|
|
@@ -5498,7 +6222,7 @@ function recordBlocked(state2, tool, rec) {
|
|
|
5498
6222
|
function recordGatekeeperRefusal(state2, tool, refusals) {
|
|
5499
6223
|
if (refusals.length === 0) return;
|
|
5500
6224
|
const pick = refusals.find((r) => r.bound !== void 0 && r.actual !== void 0) ?? refusals[0];
|
|
5501
|
-
const scoped = pick.field ? Boolean(
|
|
6225
|
+
const scoped = pick.field ? Boolean(getProfile5(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
|
|
5502
6226
|
const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
|
|
5503
6227
|
recordBlocked(state2, tool, {
|
|
5504
6228
|
kind: scoped ? "scope" : "bound",
|
|
@@ -5677,7 +6401,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5677
6401
|
);
|
|
5678
6402
|
}
|
|
5679
6403
|
}
|
|
5680
|
-
const readProfile = readAdapter ?
|
|
6404
|
+
const readProfile = readAdapter ? getProfile5(matchingAuths[0].profileId) : void 0;
|
|
5681
6405
|
const boundsSchema = readProfile?.boundsSchema;
|
|
5682
6406
|
const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
|
|
5683
6407
|
const hasAgeDimension = Boolean(readAdapter?.ageField);
|
|
@@ -5808,6 +6532,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5808
6532
|
});
|
|
5809
6533
|
}
|
|
5810
6534
|
return lockedGuard(async (args) => {
|
|
6535
|
+
let attachmentSourcesForExecution;
|
|
6536
|
+
if (tool.gating?.attachmentsArg || tool.gating?.insertFields?.length) {
|
|
6537
|
+
const refsResult = tool.gating.attachmentsArg ? validateDocumentReferences(args[tool.gating.attachmentsArg]) : { ok: true, refs: [] };
|
|
6538
|
+
if (!refsResult.ok) {
|
|
6539
|
+
return { content: [{ type: "text", text: `Refused: ${tool.namespacedName}'s attachments ${refsResult.error.message}` }], isError: true };
|
|
6540
|
+
}
|
|
6541
|
+
const insertResult = validateInsertReferences(args.insert);
|
|
6542
|
+
if (!insertResult.ok) {
|
|
6543
|
+
return { content: [{ type: "text", text: `Refused: ${tool.namespacedName}'s insert ${insertResult.error.message}` }], isError: true };
|
|
6544
|
+
}
|
|
6545
|
+
if (refsResult.refs.length > 0) {
|
|
6546
|
+
attachmentSourcesForExecution = [...new Set(refsResult.refs.map((r) => `${r.connector}:${r.kind}`))].join(",");
|
|
6547
|
+
}
|
|
6548
|
+
}
|
|
5811
6549
|
const execution = { ...staticExecution };
|
|
5812
6550
|
for (const [argName, mapping] of Object.entries(executionMapping)) {
|
|
5813
6551
|
const value = args[argName];
|
|
@@ -5824,6 +6562,9 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5824
6562
|
}
|
|
5825
6563
|
}
|
|
5826
6564
|
}
|
|
6565
|
+
if (attachmentSourcesForExecution) {
|
|
6566
|
+
execution[ATTACHMENT_SOURCES_FIELD] = attachmentSourcesForExecution;
|
|
6567
|
+
}
|
|
5827
6568
|
coerceDeclaredNumericFields(profile, execution);
|
|
5828
6569
|
const declaredActionType = typeof execution.action_type === "string" && execution.action_type.length > 0 ? execution.action_type : void 0;
|
|
5829
6570
|
if (!declaredActionType) {
|
|
@@ -5843,7 +6584,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5843
6584
|
};
|
|
5844
6585
|
}
|
|
5845
6586
|
{
|
|
5846
|
-
const profileDef =
|
|
6587
|
+
const profileDef = getProfile5(profile);
|
|
5847
6588
|
const registry = profileDef?.boundsSchema?.actionTypes;
|
|
5848
6589
|
if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
|
|
5849
6590
|
recordBlocked(state2, tool, {
|
|
@@ -5947,8 +6688,21 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5947
6688
|
isError: true
|
|
5948
6689
|
};
|
|
5949
6690
|
}
|
|
5950
|
-
const contextKeys =
|
|
5951
|
-
|
|
6691
|
+
const contextKeys = getProfile5(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
|
|
6692
|
+
let pool = passers;
|
|
6693
|
+
if (tool.gating?.attachmentsArg) {
|
|
6694
|
+
const raw = args[tool.gating.attachmentsArg];
|
|
6695
|
+
const refs = Array.isArray(raw) ? raw.filter((r) => !!r && typeof r === "object" && typeof r.connector === "string" && typeof r.kind === "string") : [];
|
|
6696
|
+
const narrowed = candidatesAllowingAttachments(passers, refs, (id) => getProfile5(id));
|
|
6697
|
+
if (narrowed.allowed.length === 0) {
|
|
6698
|
+
return {
|
|
6699
|
+
content: [{ type: "text", text: `Refused: attachment not allowed by any ${tool.gating.profile ?? ""} mandate. ${narrowed.refusal ?? ""}`.trim() }],
|
|
6700
|
+
isError: true
|
|
6701
|
+
};
|
|
6702
|
+
}
|
|
6703
|
+
pool = narrowed.allowed;
|
|
6704
|
+
}
|
|
6705
|
+
return attemptWithCandidates(pool);
|
|
5952
6706
|
async function attemptWithCandidates(candidates) {
|
|
5953
6707
|
const selection = selectAuthorization(
|
|
5954
6708
|
contextKeys,
|
|
@@ -5975,9 +6729,18 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
5975
6729
|
isError: true
|
|
5976
6730
|
};
|
|
5977
6731
|
}
|
|
6732
|
+
let callArgs = args;
|
|
6733
|
+
if (tool.gating?.attachmentsArg || tool.gating?.insertFields?.length) {
|
|
6734
|
+
const allowedSourcesValue = typeof auth.context?.[ATTACHMENT_SOURCES_FIELD] === "string" ? auth.context[ATTACHMENT_SOURCES_FIELD] : void 0;
|
|
6735
|
+
const resolved = await resolveAttachmentsAndInserts(integrationManager2, tool, auth.profileId, args, allowedSourcesValue);
|
|
6736
|
+
if (!resolved.ok) {
|
|
6737
|
+
return { content: [{ type: "text", text: resolved.message }], isError: true };
|
|
6738
|
+
}
|
|
6739
|
+
callArgs = resolved.sealed ? { ...resolved.args, [ATTACHMENTS_SEALED_META_KEY]: resolved.sealed } : resolved.args;
|
|
6740
|
+
}
|
|
5978
6741
|
if ((auth.deferredCommitmentDomains ?? []).length > 0) {
|
|
5979
6742
|
try {
|
|
5980
|
-
const enrichedArgs = await attachImagePreview(
|
|
6743
|
+
const enrichedArgs = await attachImagePreview(callArgs);
|
|
5981
6744
|
const wireExecCtx = wireExecutionContext(auth.profileId, execution);
|
|
5982
6745
|
let recipientUserIds = auth.deferredCommitmentDomains.filter((d) => d !== "owner");
|
|
5983
6746
|
try {
|
|
@@ -6031,10 +6794,18 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
6031
6794
|
};
|
|
6032
6795
|
}
|
|
6033
6796
|
}
|
|
6797
|
+
let finalArgs = callArgs;
|
|
6798
|
+
if (tool.gating?.attachmentsArg || tool.gating?.insertFields?.length) {
|
|
6799
|
+
const finalized = await finalizeAttachmentsForExecution(integrationManager2, tool.integrationId, callArgs);
|
|
6800
|
+
if (!finalized.ok) {
|
|
6801
|
+
return { content: [{ type: "text", text: finalized.message }], isError: true };
|
|
6802
|
+
}
|
|
6803
|
+
finalArgs = finalized.args;
|
|
6804
|
+
}
|
|
6034
6805
|
let receiptId;
|
|
6035
6806
|
try {
|
|
6036
6807
|
const actionType = declaredActionType;
|
|
6037
|
-
const binding = computeContentBinding(auth.profileId, tool,
|
|
6808
|
+
const binding = computeContentBinding(auth.profileId, tool, finalArgs, actionType);
|
|
6038
6809
|
const idempotencyKey = randomUUID();
|
|
6039
6810
|
const wireExecCtx = wireExecutionContext(auth.profileId, execution);
|
|
6040
6811
|
const { receipt } = await state2.spClient.postReceipt({
|
|
@@ -6077,10 +6848,12 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
6077
6848
|
// would hold a hash of content it cannot reproduce.
|
|
6078
6849
|
boundContent: binding?.boundContent,
|
|
6079
6850
|
// 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
|
-
|
|
6851
|
+
// journal's argsHash is computed from below
|
|
6852
|
+
// (`hashToolArgs(finalArgs)`), so the ticket view can show it and
|
|
6853
|
+
// check it (incl. any attachments' sealed fingerprints — UI spec
|
|
6854
|
+
// §15.2). Same write, so it is as fail-closed and as encrypted
|
|
6855
|
+
// as the rest of the entry.
|
|
6856
|
+
sentArgs: finalArgs
|
|
6084
6857
|
});
|
|
6085
6858
|
} catch (err) {
|
|
6086
6859
|
if (err instanceof AsKeyMismatchError || err instanceof TicketBindingMismatchError) {
|
|
@@ -6172,7 +6945,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
|
|
|
6172
6945
|
const uniqueApprovers = [...new Set(pendingApprovers)];
|
|
6173
6946
|
const recipientUserIds = ownerId ? [.../* @__PURE__ */ new Set([ownerId, ...uniqueApprovers])] : uniqueApprovers;
|
|
6174
6947
|
try {
|
|
6175
|
-
const enrichedArgs = await attachImagePreview(
|
|
6948
|
+
const enrichedArgs = await attachImagePreview(callArgs);
|
|
6176
6949
|
const wireExecCtx = wireExecutionContext(auth.profileId, execution);
|
|
6177
6950
|
const { proposal, unreadableFor } = await state2.spClient.submitProposal({
|
|
6178
6951
|
authorizationId: authzId,
|
|
@@ -6256,15 +7029,16 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + a
|
|
|
6256
7029
|
execution: { ...execution },
|
|
6257
7030
|
timestamp: Math.floor(Date.now() / 1e3)
|
|
6258
7031
|
});
|
|
6259
|
-
let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool,
|
|
7032
|
+
let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, finalArgs, receiptId, auth.subjects?.[0]) : finalArgs;
|
|
6260
7033
|
if (receiptId) outgoingArgs = attachTicketId(tool, outgoingArgs, receiptId);
|
|
6261
7034
|
outgoingArgs = encodeOutgoingArgs(tool, outgoingArgs);
|
|
6262
7035
|
outgoingArgs = stripSourceLink(outgoingArgs);
|
|
7036
|
+
outgoingArgs = stripAttachmentsSealedMeta(outgoingArgs);
|
|
6263
7037
|
if (receiptId) {
|
|
6264
7038
|
const begun = state2.executionJournal.begin({
|
|
6265
7039
|
ticketId: receiptId,
|
|
6266
7040
|
tool: tool.namespacedName,
|
|
6267
|
-
argsHash: hashToolArgs(
|
|
7041
|
+
argsHash: hashToolArgs(finalArgs)
|
|
6268
7042
|
});
|
|
6269
7043
|
if (!begun.ok) {
|
|
6270
7044
|
return {
|
|
@@ -6433,7 +7207,7 @@ function buildMandateBrief(opts) {
|
|
|
6433
7207
|
const parts = [];
|
|
6434
7208
|
if (boundsDesc) parts.push(boundsDesc);
|
|
6435
7209
|
if (executionLog) {
|
|
6436
|
-
const profile =
|
|
7210
|
+
const profile = getProfile6(auth.profileId) ?? getProfile6(shortName2);
|
|
6437
7211
|
const consumption = getConsumptionState(auth, executionLog, profile);
|
|
6438
7212
|
const compact = formatConsumptionCompact(consumption);
|
|
6439
7213
|
if (compact) parts.push(compact);
|
|
@@ -6471,7 +7245,7 @@ function buildMandateBrief(opts) {
|
|
|
6471
7245
|
}
|
|
6472
7246
|
|
|
6473
7247
|
// src/tools/authorizations.ts
|
|
6474
|
-
import { getProfile as
|
|
7248
|
+
import { getProfile as getProfile7 } from "@hap/core";
|
|
6475
7249
|
function shortProfileName2(profileId) {
|
|
6476
7250
|
const withoutVersion = profileId.replace(/@.*$/, "");
|
|
6477
7251
|
const parts = withoutVersion.split("/");
|
|
@@ -6590,7 +7364,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
6590
7364
|
} catch {
|
|
6591
7365
|
}
|
|
6592
7366
|
const shortName2 = shortProfileName2(auth.profileId);
|
|
6593
|
-
const profile =
|
|
7367
|
+
const profile = getProfile7(auth.profileId) ?? getProfile7(shortName2);
|
|
6594
7368
|
if (profile) {
|
|
6595
7369
|
const consumption = getConsumptionState(auth, state2.executionLog, profile);
|
|
6596
7370
|
const consumptionText = formatConsumptionFull(consumption);
|
|
@@ -6650,7 +7424,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
|
|
|
6650
7424
|
lines.push(" Mode: review (each action requires your approval)");
|
|
6651
7425
|
}
|
|
6652
7426
|
const shortName2 = shortProfileName2(auth.profileId);
|
|
6653
|
-
const profile =
|
|
7427
|
+
const profile = getProfile7(auth.profileId) ?? getProfile7(shortName2);
|
|
6654
7428
|
if (profile) {
|
|
6655
7429
|
const consumption = getConsumptionState(auth, state2.executionLog, profile);
|
|
6656
7430
|
const compact = formatConsumptionCompact(consumption);
|
|
@@ -6793,12 +7567,12 @@ function listIntegrationsHandler(state2, integrationManager2) {
|
|
|
6793
7567
|
import { ContentBindingError as ContentBindingError2 } from "@hap/core";
|
|
6794
7568
|
|
|
6795
7569
|
// src/lib/proposal-content.ts
|
|
6796
|
-
import { createHash as
|
|
7570
|
+
import { createHash as createHash4 } from "crypto";
|
|
6797
7571
|
import { canonicalize } from "@hap/core";
|
|
6798
7572
|
var ProposalContentError = class extends Error {
|
|
6799
7573
|
};
|
|
6800
7574
|
function computeProposalContentHash(toolArgs) {
|
|
6801
|
-
return `sha256:${
|
|
7575
|
+
return `sha256:${createHash4("sha256").update(canonicalize(toolArgs)).digest("hex")}`;
|
|
6802
7576
|
}
|
|
6803
7577
|
async function resolveProposalToolArgs(proposal) {
|
|
6804
7578
|
if (proposal.toolArgs) return proposal.toolArgs;
|
|
@@ -7108,8 +7882,23 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
7108
7882
|
};
|
|
7109
7883
|
}
|
|
7110
7884
|
}
|
|
7885
|
+
let finalToolArgs = toolArgs;
|
|
7886
|
+
{
|
|
7887
|
+
const finalized = await finalizeAttachmentsForExecution(integrationManager2, integrationId, toolArgs);
|
|
7888
|
+
if (!finalized.ok) {
|
|
7889
|
+
state2.executionJournal.complete(receiptId, "failed", "changed", finalized.message);
|
|
7890
|
+
recordExecutionOutcome(state2, proposal.id, receiptId, false, finalized.message);
|
|
7891
|
+
logExecutionOutcome(proposal.id, false, "attachment or inserted value changed since approval");
|
|
7892
|
+
void notifyControlPlane("execution-failed");
|
|
7893
|
+
return {
|
|
7894
|
+
text: `Proposal ${proposal.id} was approved, but ${finalized.message} Nothing was sent.`,
|
|
7895
|
+
isError: true
|
|
7896
|
+
};
|
|
7897
|
+
}
|
|
7898
|
+
finalToolArgs = finalized.args;
|
|
7899
|
+
}
|
|
7111
7900
|
try {
|
|
7112
|
-
let outgoingArgs =
|
|
7901
|
+
let outgoingArgs = finalToolArgs;
|
|
7113
7902
|
if (discovered && receiptId) {
|
|
7114
7903
|
if (shouldAttachFooter()) {
|
|
7115
7904
|
outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
|
|
@@ -7118,6 +7907,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
|
|
|
7118
7907
|
outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
|
|
7119
7908
|
}
|
|
7120
7909
|
outgoingArgs = stripSourceLink(outgoingArgs);
|
|
7910
|
+
outgoingArgs = stripAttachmentsSealedMeta(outgoingArgs);
|
|
7121
7911
|
let result;
|
|
7122
7912
|
try {
|
|
7123
7913
|
result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
|
|
@@ -8179,7 +8969,7 @@ var IntegrationManager = class {
|
|
|
8179
8969
|
if (opts.skipIfRunning) return existing.tools;
|
|
8180
8970
|
await this.stopIntegrationLocked(config.id);
|
|
8181
8971
|
}
|
|
8182
|
-
if (isSimulationMode() && !
|
|
8972
|
+
if (isSimulationMode() && !this.isSimulationSafe(config.id)) {
|
|
8183
8973
|
throw new Error(`${config.id}: ${SIMULATION_BLOCK_REASON}`);
|
|
8184
8974
|
}
|
|
8185
8975
|
if (config.npmPackage) {
|
|
@@ -8254,6 +9044,7 @@ var IntegrationManager = class {
|
|
|
8254
9044
|
if (gating && gating.category !== "read" && gating.category !== "disabled") {
|
|
8255
9045
|
inputSchema = withSourceLink(inputSchema);
|
|
8256
9046
|
}
|
|
9047
|
+
inputSchema = withAttachmentAndInsertSchema(inputSchema, gating ?? void 0);
|
|
8257
9048
|
return {
|
|
8258
9049
|
originalName: tool.name,
|
|
8259
9050
|
namespacedName: `${config.id}__${tool.name}`,
|
|
@@ -8261,6 +9052,13 @@ var IntegrationManager = class {
|
|
|
8261
9052
|
description: tool.description ?? "",
|
|
8262
9053
|
inputSchema,
|
|
8263
9054
|
outputSchema: tool.outputSchema,
|
|
9055
|
+
// A connector's own `internal: true` override (tool-gating-types.ts
|
|
9056
|
+
// `ProfileToolGatingEntry.internal`) — same meaning and enforcement
|
|
9057
|
+
// as a built-in's `BuiltinTool.internal` above: never registered
|
|
9058
|
+
// with the MCP server (index.ts), never callable directly
|
|
9059
|
+
// (tool-proxy.ts); only the gateway's own code (e.g. a document
|
|
9060
|
+
// preview read — preview.ts) calls it, via `callTool` directly.
|
|
9061
|
+
...gating?.internal ? { internal: true } : {},
|
|
8264
9062
|
gating
|
|
8265
9063
|
};
|
|
8266
9064
|
});
|
|
@@ -8361,7 +9159,8 @@ var IntegrationManager = class {
|
|
|
8361
9159
|
...t.gating?.approvalView ? { approvalView: t.gating.approvalView } : {},
|
|
8362
9160
|
// UI spec §14.1 — which sent values the signed ticket also carries
|
|
8363
9161
|
// (arg → executionContext field), for the "in the ticket" tag.
|
|
8364
|
-
...mapping && Object.keys(mapping).length > 0 ? { executionMapping: mapping } : {}
|
|
9162
|
+
...mapping && Object.keys(mapping).length > 0 ? { executionMapping: mapping } : {},
|
|
9163
|
+
...t.gating?.aiTextArg ? { aiTextArg: t.gating.aiTextArg } : {}
|
|
8365
9164
|
};
|
|
8366
9165
|
}
|
|
8367
9166
|
return out;
|
|
@@ -8390,13 +9189,24 @@ var IntegrationManager = class {
|
|
|
8390
9189
|
return this.builtins.get(id)?.def.alwaysListed === true;
|
|
8391
9190
|
}
|
|
8392
9191
|
/**
|
|
8393
|
-
* Whether `id` may run while simulation mode is on: a
|
|
8394
|
-
*
|
|
9192
|
+
* Whether `id` may run while simulation mode is on: a built-in that
|
|
9193
|
+
* declares `simulation: true`, or a connector whose CURRENT setting (the
|
|
9194
|
+
* credential/setting field its manifest's `simulation.field` names,
|
|
9195
|
+
* defaulting per `simulation.default` when unset) resolves to simulated —
|
|
9196
|
+
* NOT merely a connector whose manifest supports a simulated mode at all.
|
|
9197
|
+
* A connector set to a real mode (e.g. ERP's `mode=live`) is refused here
|
|
9198
|
+
* exactly like one with no simulation capability at all.
|
|
9199
|
+
*
|
|
9200
|
+
* Reads `serviceCredentials` directly, keyed by `id` — the same convention
|
|
9201
|
+
* `resolveEnvKeys`'s manifest-mapping fallback and `getServiceCredentials`
|
|
9202
|
+
* already rely on: a connector's OWN credential fields (mode, liveAdapter,
|
|
9203
|
+
* …) are pushed to this gateway under its own id (control-plane's
|
|
9204
|
+
* `pushServiceCredentials(id, fields)`), never a shared/other service id.
|
|
8395
9205
|
*/
|
|
8396
9206
|
isSimulationSafe(id) {
|
|
8397
9207
|
const builtin = this.builtins.get(id);
|
|
8398
9208
|
if (builtin) return builtin.def.simulation === true;
|
|
8399
|
-
return
|
|
9209
|
+
return connectorCurrentlySimulated(getManifest(id), this.serviceCredentials.get(id));
|
|
8400
9210
|
}
|
|
8401
9211
|
/**
|
|
8402
9212
|
* The built-in's own refusal for this call, checked before the gate (no proposal,
|
|
@@ -8558,15 +9368,17 @@ var IntegrationManager = class {
|
|
|
8558
9368
|
toolCount: 0,
|
|
8559
9369
|
readAgeDays: readAgeOf(config),
|
|
8560
9370
|
// Computed fresh, not cached: true whenever simulation mode is on
|
|
8561
|
-
// AND this connector
|
|
8562
|
-
//
|
|
8563
|
-
//
|
|
8564
|
-
//
|
|
8565
|
-
//
|
|
8566
|
-
//
|
|
9371
|
+
// AND this connector is not currently simulation-safe (see
|
|
9372
|
+
// `isSimulationSafe` — no capability at all, OR capability
|
|
9373
|
+
// declared but currently SET to a real mode), which is exactly
|
|
9374
|
+
// the condition startIntegrationLocked refuses under — so the UI
|
|
9375
|
+
// always reflects why a real connector never started, without
|
|
9376
|
+
// needing a separate tracking path for this one reason. This is
|
|
9377
|
+
// `paused`, NOT `error`: it is not a problem, it's the mode
|
|
9378
|
+
// working as designed (see IntegrationStatus.paused). Genuine
|
|
8567
9379
|
// start failures still surface via the one-shot `warning` string
|
|
8568
9380
|
// returned by /internal/add-integration — they never land here.
|
|
8569
|
-
paused: isSimulationMode() && !
|
|
9381
|
+
paused: isSimulationMode() && !this.isSimulationSafe(config.id) ? "simulation" : void 0
|
|
8570
9382
|
});
|
|
8571
9383
|
}
|
|
8572
9384
|
}
|
|
@@ -8671,7 +9483,8 @@ var IntegrationManager = class {
|
|
|
8671
9483
|
blockedArgs: ext.blockedArgs,
|
|
8672
9484
|
hideUnlessAuthorized: ext.hideUnlessAuthorized,
|
|
8673
9485
|
simulationOnly: ext.simulationOnly,
|
|
8674
|
-
approvalView: ext.approvalView
|
|
9486
|
+
approvalView: ext.approvalView,
|
|
9487
|
+
internal: ext.internal
|
|
8675
9488
|
};
|
|
8676
9489
|
}
|
|
8677
9490
|
return {
|
|
@@ -8701,7 +9514,11 @@ var IntegrationManager = class {
|
|
|
8701
9514
|
// this, boundField/requiredValue on a non-"read" override was silently
|
|
8702
9515
|
// discarded here — the config in the manifest existed but did nothing.
|
|
8703
9516
|
boundField: ext.boundField,
|
|
8704
|
-
requiredValue: ext.requiredValue
|
|
9517
|
+
requiredValue: ext.requiredValue,
|
|
9518
|
+
internal: ext.internal,
|
|
9519
|
+
attachmentsArg: ext.attachmentsArg,
|
|
9520
|
+
insertFields: ext.insertFields,
|
|
9521
|
+
aiTextArg: ext.display?.aiText
|
|
8705
9522
|
};
|
|
8706
9523
|
}
|
|
8707
9524
|
return {
|
|
@@ -8832,7 +9649,7 @@ async function buildTicketDetails(archive, ticketIds) {
|
|
|
8832
9649
|
}
|
|
8833
9650
|
|
|
8834
9651
|
// src/lib/report/window.ts
|
|
8835
|
-
import { getProfile as
|
|
9652
|
+
import { getProfile as getProfile8 } from "@hap/core";
|
|
8836
9653
|
var REPORT_AGE_FIELD = "read_age_days";
|
|
8837
9654
|
var REPORTING_PROFILE = "reporting";
|
|
8838
9655
|
var END_SKEW_SECONDS = 120;
|
|
@@ -8846,7 +9663,7 @@ function windowLabel(start, days, loadedAt) {
|
|
|
8846
9663
|
if (loadedAt !== null && loadedAt === start) return `${since} (when the test data was loaded)`;
|
|
8847
9664
|
return `${since} (the last ${days} ${days === 1 ? "day" : "days"})`;
|
|
8848
9665
|
}
|
|
8849
|
-
var defaultLookup = (id) =>
|
|
9666
|
+
var defaultLookup = (id) => getProfile8(id);
|
|
8850
9667
|
function reportingMandateRefusal(m, lookup = defaultLookup) {
|
|
8851
9668
|
const schema = lookup(m.profileId)?.boundsSchema;
|
|
8852
9669
|
const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
|
|
@@ -9597,7 +10414,7 @@ function unifiedLineDiff(before, after) {
|
|
|
9597
10414
|
}
|
|
9598
10415
|
|
|
9599
10416
|
// src/lib/builtins/setup.ts
|
|
9600
|
-
import { getProfile as
|
|
10417
|
+
import { getProfile as getProfile9 } from "@hap/core";
|
|
9601
10418
|
var DELEGATION_PROFILE = "delegation";
|
|
9602
10419
|
var DELEGATION_INTENT_HINT = [
|
|
9603
10420
|
"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.",
|
|
@@ -9620,8 +10437,9 @@ function briefRefusal(args) {
|
|
|
9620
10437
|
function connectedSystems(im) {
|
|
9621
10438
|
const byId = /* @__PURE__ */ new Map();
|
|
9622
10439
|
for (const t of im.getAllTools()) {
|
|
9623
|
-
if (im.isBuiltin(t.integrationId) || !t.gating?.profile || t.gating.category === "disabled") continue;
|
|
9624
|
-
const
|
|
10440
|
+
if (im.isBuiltin(t.integrationId) || t.internal || !t.gating?.profile || t.gating.category === "disabled") continue;
|
|
10441
|
+
const key = `${t.integrationId}|${t.gating.profile}`;
|
|
10442
|
+
const s = byId.get(key) ?? { id: t.integrationId, profile: t.gating.profile, actionTypes: [], writeTools: [], readTools: [] };
|
|
9625
10443
|
if (t.gating.category === "read") {
|
|
9626
10444
|
s.readTools.push(t.originalName);
|
|
9627
10445
|
} else {
|
|
@@ -9629,9 +10447,9 @@ function connectedSystems(im) {
|
|
|
9629
10447
|
const at = t.gating.staticExecution?.action_type;
|
|
9630
10448
|
if (typeof at === "string" && !s.actionTypes.includes(at)) s.actionTypes.push(at);
|
|
9631
10449
|
}
|
|
9632
|
-
byId.set(
|
|
10450
|
+
byId.set(key, s);
|
|
9633
10451
|
}
|
|
9634
|
-
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
10452
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id) || a.profile.localeCompare(b.profile));
|
|
9635
10453
|
}
|
|
9636
10454
|
function shortName(idOrName) {
|
|
9637
10455
|
const stripped = idOrName.replace(/@.*$/, "").split("/").pop() ?? idOrName;
|
|
@@ -9649,7 +10467,7 @@ function delegationItselfRefusal(args) {
|
|
|
9649
10467
|
}
|
|
9650
10468
|
function delegationMandateRefusal(auth) {
|
|
9651
10469
|
if (isSimulationMode()) return void 0;
|
|
9652
|
-
const profile =
|
|
10470
|
+
const profile = getProfile9(auth.profileId);
|
|
9653
10471
|
if (profile?.scopeSchema?.fields?.allowed_profiles) return void 0;
|
|
9654
10472
|
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
10473
|
}
|
|
@@ -9707,7 +10525,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
|
|
|
9707
10525
|
);
|
|
9708
10526
|
const first = governing[0];
|
|
9709
10527
|
if (first) {
|
|
9710
|
-
const delegationProfile =
|
|
10528
|
+
const delegationProfile = getProfile9(first.profileId);
|
|
9711
10529
|
const entry = getConsumptionState(first, deps.state.executionLog, delegationProfile).find((e) => e.field === "mandate_daily_max");
|
|
9712
10530
|
if (entry && entry.limit !== null) {
|
|
9713
10531
|
checks.push(`Proposals today: ${entry.current} of ${entry.limit}`);
|
|
@@ -9717,7 +10535,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
|
|
|
9717
10535
|
}
|
|
9718
10536
|
function resolveTargetProfile(input) {
|
|
9719
10537
|
if (typeof input !== "string" || !input.trim()) return void 0;
|
|
9720
|
-
const profile =
|
|
10538
|
+
const profile = getProfile9(input.trim());
|
|
9721
10539
|
if (!profile) return void 0;
|
|
9722
10540
|
return { profile, shortName: shortName(profile.id) };
|
|
9723
10541
|
}
|
|
@@ -9852,7 +10670,7 @@ function setupBuiltin(deps) {
|
|
|
9852
10670
|
const guides = loadGuides();
|
|
9853
10671
|
const systems = connectedSystems(deps.integrationManager);
|
|
9854
10672
|
const running = new Set(systems.map((s) => s.id));
|
|
9855
|
-
const inactive = getAllManifests().filter((m) =>
|
|
10673
|
+
const inactive = getAllManifests().filter((m) => deps.integrationManager.isSimulationSafe(m.id) && !running.has(m.id)).map((m) => m.name);
|
|
9856
10674
|
const header = guideHeader(systems, inactive);
|
|
9857
10675
|
const topic = typeof args.topic === "string" ? args.topic.trim().toLowerCase() : "";
|
|
9858
10676
|
if (!topic) {
|
|
@@ -9870,7 +10688,7 @@ Read each topic with get_guide(topic) before doing that step.`);
|
|
|
9870
10688
|
return { ...builtinText(`Unknown topic "${topic}". Topics: ${guides.map((g) => g.topic).join(", ") || "none installed"}.`), isError: true };
|
|
9871
10689
|
}
|
|
9872
10690
|
const body = fillGenerated(guide.body.trim(), {
|
|
9873
|
-
limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile:
|
|
10691
|
+
limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile9(s.profile) }))),
|
|
9874
10692
|
package: () => packageSection(
|
|
9875
10693
|
deps.integrationManager.getAllTools().find((t) => !deps.integrationManager.isBuiltin(t.integrationId) && t.originalName === "load_simulation")
|
|
9876
10694
|
)
|
|
@@ -10404,6 +11222,15 @@ app.post("/internal/preview", internalOnly, async (req, res) => {
|
|
|
10404
11222
|
});
|
|
10405
11223
|
res.json(result);
|
|
10406
11224
|
});
|
|
11225
|
+
app.post("/internal/document-preview", internalOnly, async (req, res) => {
|
|
11226
|
+
const { connector, kind, id, sha256 } = req.body;
|
|
11227
|
+
if (!connector || !kind || !id || !sha256) {
|
|
11228
|
+
res.status(400).json({ error: "Missing connector, kind, id, or sha256" });
|
|
11229
|
+
return;
|
|
11230
|
+
}
|
|
11231
|
+
const result = await resolveAttachmentPreview(integrationManager, { connector, kind, id }, sha256);
|
|
11232
|
+
res.json(result);
|
|
11233
|
+
});
|
|
10407
11234
|
app.post("/internal/request-context", internalOnly, async (req, res) => {
|
|
10408
11235
|
const { tool, toolArgs } = req.body;
|
|
10409
11236
|
if (!tool || typeof toolArgs !== "object" || toolArgs === null) {
|