@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.
@@ -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 getProfile5 } from "@hap/core";
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 manifestIsSimulated(manifest) {
3534
- return Boolean(manifest?.simulation);
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 as getProfile2 } from "@hap/core";
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 (!getProfile2(manifest.profile) && !getProfile2(profile)) continue;
4338
- if (!getProfile2(profile)) {
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 = getProfile2(profileId);
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 computeContentHash2({ kind: "jcs", version: "1" }, body.structured);
4766
+ return computeContentHash({ kind: "jcs", version: "1" }, body.structured);
4500
4767
  }
4501
- return computeContentHash2({ kind: "text", version: "1" }, body.text ?? "");
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 computeContentHash2({ kind: "jcs", version: "1" }, { reads: hashes });
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 rfc2047Encode(value) {
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: rfc2047Encode
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 getProfile3 } from "@hap/core";
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(getProfile3(profileId));
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 getProfile4, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
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 = getProfile4(profileId);
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 = getProfile4(profileId)?.boundsSchema?.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(getProfile4(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
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 ? getProfile4(matchingAuths[0].profileId) : void 0;
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 = getProfile4(profile);
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 = getProfile4(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
5951
- return attemptWithCandidates(passers);
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(args);
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, args, actionType);
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 (`hashToolArgs(args)`),
6081
- // so the ticket view can show it and check it. Same write, so it
6082
- // is as fail-closed and as encrypted as the rest of the entry.
6083
- sentArgs: args
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(args);
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, args, receiptId, auth.subjects?.[0]) : args;
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(args)
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 = getProfile5(auth.profileId) ?? getProfile5(shortName2);
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 getProfile6 } from "@hap/core";
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 = getProfile6(auth.profileId) ?? getProfile6(shortName2);
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 = getProfile6(auth.profileId) ?? getProfile6(shortName2);
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 createHash3 } from "crypto";
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:${createHash3("sha256").update(canonicalize(toolArgs)).digest("hex")}`;
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 = toolArgs;
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() && !manifestIsSimulated(getManifest(config.id))) {
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 connector whose manifest
8394
- * carries the `simulation` marker, or a built-in that declares `simulation: true`.
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 manifestIsSimulated(getManifest(id));
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 has no manifest `simulation` marker, which is
8562
- // exactly the condition startIntegrationLocked refuses under — so
8563
- // the UI always reflects why a real connector never started,
8564
- // without needing a separate tracking path for this one reason.
8565
- // This is `paused`, NOT `error`: it is not a problem, it's the
8566
- // mode working as designed (see IntegrationStatus.paused). Genuine
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() && !manifestIsSimulated(getManifest(config.id)) ? "simulation" : void 0
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 getProfile7 } from "@hap/core";
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) => getProfile7(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 getProfile8 } from "@hap/core";
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 s = byId.get(t.integrationId) ?? { id: t.integrationId, profile: t.gating.profile, actionTypes: [], writeTools: [], readTools: [] };
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(t.integrationId, s);
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 = getProfile8(auth.profileId);
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 = getProfile8(first.profileId);
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 = getProfile8(input.trim());
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) => manifestIsSimulated(m) && !running.has(m.id)).map((m) => m.name);
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: getProfile8(s.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) {