@suveren/gateway 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3180,7 +3180,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3180
3180
  import { z as z2 } from "zod";
3181
3181
 
3182
3182
  // src/lib/mandate-brief.ts
3183
- import { getProfile as getProfile3 } from "@hap/core";
3183
+ import { getProfile as getProfile4 } from "@hap/core";
3184
3184
 
3185
3185
  // src/lib/consumption.ts
3186
3186
  import { boundActionTypes } from "@hap/core";
@@ -3473,6 +3473,7 @@ import { join as join14, resolve } from "path";
3473
3473
  // src/lib/tool-gating-types.ts
3474
3474
  var MAX_PREVIEW_READS = 10;
3475
3475
  var MAX_FOREACH_ELEMENTS = 50;
3476
+ var MAX_RECORD_KEY_STEPS = 2;
3476
3477
 
3477
3478
  // src/lib/manifest-loader.ts
3478
3479
  var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
@@ -3597,6 +3598,231 @@ function invalidManifestPreviewReason(manifest) {
3597
3598
  }
3598
3599
  return null;
3599
3600
  }
3601
+ function isNonEmptyString(v) {
3602
+ return typeof v === "string" && v.length > 0;
3603
+ }
3604
+ var VALID_FIELD_PATH_PARSE = ["address.name", "address.email", "epoch-ms"];
3605
+ function invalidFieldPathReason(label, value) {
3606
+ if (isNonEmptyString(value)) return null;
3607
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3608
+ return `${label} must be a non-empty string or a field-path object`;
3609
+ }
3610
+ const f = value;
3611
+ if (!isNonEmptyString(f.path)) return `${label}.path must be a non-empty string`;
3612
+ if (f.where !== void 0) {
3613
+ if (f.where === null || typeof f.where !== "object" || Array.isArray(f.where)) {
3614
+ return `${label}.where must be an object`;
3615
+ }
3616
+ for (const [k, v] of Object.entries(f.where)) {
3617
+ if (typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean") {
3618
+ return `${label}.where.${k} must be a string, number, or boolean`;
3619
+ }
3620
+ }
3621
+ }
3622
+ if (f.pick !== void 0 && !isNonEmptyString(f.pick)) {
3623
+ return `${label}.pick must be a non-empty string`;
3624
+ }
3625
+ if (f.parse !== void 0 && !VALID_FIELD_PATH_PARSE.includes(f.parse)) {
3626
+ return `${label}.parse must be one of ${VALID_FIELD_PATH_PARSE.join(", ")}`;
3627
+ }
3628
+ return null;
3629
+ }
3630
+ function invalidContextReadReason(label, entry) {
3631
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
3632
+ return `${label} must be an object`;
3633
+ }
3634
+ const r = entry;
3635
+ if (!isNonEmptyString(r.tool)) return `${label}.tool must be a non-empty string`;
3636
+ if (r.args === void 0 || r.args === null || typeof r.args !== "object" || Array.isArray(r.args)) {
3637
+ return `${label}.args must be an object`;
3638
+ }
3639
+ for (const [k, v] of Object.entries(r.args)) {
3640
+ if (typeof v !== "string" || v.length === 0) {
3641
+ return `${label}.args.${k} must be a non-empty string`;
3642
+ }
3643
+ }
3644
+ return null;
3645
+ }
3646
+ function invalidContextSourceReason(toolName, entry) {
3647
+ const label = `context.sources.${toolName}`;
3648
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
3649
+ return `${label} must be an object`;
3650
+ }
3651
+ const s = entry;
3652
+ const readReason = invalidContextReadReason(`${label}.read`, s.read);
3653
+ if (readReason) return readReason;
3654
+ for (const field of ["title", "author", "at", "body"]) {
3655
+ if (s[field] !== void 0) {
3656
+ const reason = invalidFieldPathReason(`${label}.${field}`, s[field]);
3657
+ if (reason) return reason;
3658
+ }
3659
+ }
3660
+ if (s.keys !== void 0) {
3661
+ if (s.keys === null || typeof s.keys !== "object" || Array.isArray(s.keys)) {
3662
+ return `${label}.keys must be an object`;
3663
+ }
3664
+ for (const [k, v] of Object.entries(s.keys)) {
3665
+ if (k !== "email") return `${label}.keys.${k}: only "email" is a recognized identity key type`;
3666
+ const reason = invalidFieldPathReason(`${label}.keys.${k}`, v);
3667
+ if (reason) return reason;
3668
+ }
3669
+ }
3670
+ if (s.chain !== void 0) {
3671
+ if (s.chain === null || typeof s.chain !== "object" || Array.isArray(s.chain)) {
3672
+ return `${label}.chain must be an object`;
3673
+ }
3674
+ const c = s.chain;
3675
+ if (!isNonEmptyString(c.field)) return `${label}.chain.field must be a non-empty string`;
3676
+ if (typeof c.max !== "number" || c.max <= 0) return `${label}.chain.max must be a positive number`;
3677
+ }
3678
+ return null;
3679
+ }
3680
+ function invalidContextHistoryReason(history) {
3681
+ const label = "context.history";
3682
+ if (history === null || typeof history !== "object" || Array.isArray(history)) {
3683
+ return `${label} must be an object`;
3684
+ }
3685
+ const h = history;
3686
+ const isKeyType = (v) => v === "email" || v === "email_domain";
3687
+ if (Array.isArray(h.byKey)) {
3688
+ if (h.byKey.length === 0) return `${label}.byKey must not be an empty array`;
3689
+ for (const [i, v] of h.byKey.entries()) {
3690
+ if (!isKeyType(v)) return `${label}.byKey[${i}] must be "email" or "email_domain"`;
3691
+ }
3692
+ } else if (!isKeyType(h.byKey)) {
3693
+ return `${label}.byKey must be "email" or "email_domain" (or an array of those, in preference order)`;
3694
+ }
3695
+ const readReason = invalidContextReadReason(`${label}.read`, h.read);
3696
+ if (readReason) return readReason;
3697
+ if (typeof h.limit !== "number" || h.limit <= 0) return `${label}.limit must be a positive number`;
3698
+ if (typeof h.maxAgeDays !== "number" || h.maxAgeDays <= 0) return `${label}.maxAgeDays must be a positive number`;
3699
+ if (h.item === null || typeof h.item !== "object" || Array.isArray(h.item)) {
3700
+ return `${label}.item must be an object`;
3701
+ }
3702
+ for (const [k, v] of Object.entries(h.item)) {
3703
+ if (k === "kind") {
3704
+ if (!isNonEmptyString(v)) return `${label}.item.kind must be a non-empty string`;
3705
+ continue;
3706
+ }
3707
+ const reason = invalidFieldPathReason(`${label}.item.${k}`, v);
3708
+ if (reason) return reason;
3709
+ }
3710
+ return null;
3711
+ }
3712
+ function invalidContextRecordKeyReason(name, entry) {
3713
+ const label = `context.recordKeys.${name}`;
3714
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
3715
+ return `${label} must be an object`;
3716
+ }
3717
+ const rk = entry;
3718
+ const readReason = invalidContextReadReason(`${label}.read`, rk.read);
3719
+ if (readReason) return readReason;
3720
+ if (rk.keys !== void 0) {
3721
+ if (rk.keys === null || typeof rk.keys !== "object" || Array.isArray(rk.keys)) {
3722
+ return `${label}.keys must be an object`;
3723
+ }
3724
+ for (const [k, v] of Object.entries(rk.keys)) {
3725
+ if (k !== "email") return `${label}.keys.${k}: only "email" is a recognized identity key type`;
3726
+ const reason = invalidFieldPathReason(`${label}.keys.${k}`, v);
3727
+ if (reason) return reason;
3728
+ }
3729
+ }
3730
+ if (rk.next !== void 0) {
3731
+ if (rk.next === null || typeof rk.next !== "object" || Array.isArray(rk.next)) {
3732
+ return `${label}.next must be an object`;
3733
+ }
3734
+ const next = rk.next;
3735
+ if (!isNonEmptyString(next.recordKey)) return `${label}.next.recordKey must be a non-empty string`;
3736
+ if (!isNonEmptyString(next.field)) return `${label}.next.field must be a non-empty string`;
3737
+ }
3738
+ if (rk.keys === void 0 && rk.next === void 0) {
3739
+ return `${label} must declare "keys", "next", or both \u2014 an entry with neither learns nothing from the record it reads`;
3740
+ }
3741
+ return null;
3742
+ }
3743
+ function recordKeyChainLength(name, recordKeys, seen) {
3744
+ if (seen.has(name)) return "cycle";
3745
+ const entry = recordKeys[name];
3746
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return "missing";
3747
+ const next = entry.next;
3748
+ if (!next || typeof next !== "object") return 1;
3749
+ const nextName = next.recordKey;
3750
+ if (typeof nextName !== "string" || !(nextName in recordKeys)) return "missing";
3751
+ const rest = recordKeyChainLength(nextName, recordKeys, /* @__PURE__ */ new Set([...seen, name]));
3752
+ if (rest === "cycle" || rest === "missing") return rest;
3753
+ return 1 + rest;
3754
+ }
3755
+ function invalidManifestContextReason(manifest) {
3756
+ const ctx = manifest.context;
3757
+ const recordKeyNames = new Set(Object.keys(ctx?.recordKeys ?? {}));
3758
+ if (ctx) {
3759
+ if (ctx.sources !== void 0) {
3760
+ if (ctx.sources === null || typeof ctx.sources !== "object" || Array.isArray(ctx.sources)) {
3761
+ return "context.sources must be an object";
3762
+ }
3763
+ for (const [kind, entry] of Object.entries(ctx.sources)) {
3764
+ const reason = invalidContextSourceReason(kind, entry);
3765
+ if (reason) return reason;
3766
+ }
3767
+ }
3768
+ if (ctx.history !== void 0) {
3769
+ const reason = invalidContextHistoryReason(ctx.history);
3770
+ if (reason) return reason;
3771
+ }
3772
+ if (ctx.recordKeys !== void 0) {
3773
+ if (ctx.recordKeys === null || typeof ctx.recordKeys !== "object" || Array.isArray(ctx.recordKeys)) {
3774
+ return "context.recordKeys must be an object";
3775
+ }
3776
+ for (const [name, entry] of Object.entries(ctx.recordKeys)) {
3777
+ const reason = invalidContextRecordKeyReason(name, entry);
3778
+ if (reason) return reason;
3779
+ }
3780
+ for (const name of Object.keys(ctx.recordKeys)) {
3781
+ const length = recordKeyChainLength(name, ctx.recordKeys, /* @__PURE__ */ new Set());
3782
+ if (length === "cycle") {
3783
+ return `context.recordKeys.${name}.next forms a cycle`;
3784
+ }
3785
+ if (length === "missing") {
3786
+ return `context.recordKeys.${name}.next.recordKey names an entry this manifest does not declare`;
3787
+ }
3788
+ if (length > MAX_RECORD_KEY_STEPS) {
3789
+ return `context.recordKeys.${name}.next chain is ${length} steps long \u2014 max ${MAX_RECORD_KEY_STEPS}`;
3790
+ }
3791
+ }
3792
+ }
3793
+ }
3794
+ const sourceKinds = new Set(Object.keys(ctx?.sources ?? {}));
3795
+ const overrides = manifest.toolGating?.overrides ?? {};
3796
+ for (const [toolName, entry] of Object.entries(overrides)) {
3797
+ if (entry === null || typeof entry !== "object") continue;
3798
+ const e = entry;
3799
+ if (e.contextKeys !== void 0) {
3800
+ if (e.contextKeys === null || typeof e.contextKeys !== "object" || Array.isArray(e.contextKeys)) {
3801
+ return `override "${toolName}".contextKeys must be an object`;
3802
+ }
3803
+ for (const [name, argName] of Object.entries(e.contextKeys)) {
3804
+ if (!isNonEmptyString(argName)) {
3805
+ return `override "${toolName}".contextKeys.${name} must be a non-empty string naming an action argument`;
3806
+ }
3807
+ if (name !== "email" && !recordKeyNames.has(name)) {
3808
+ return `override "${toolName}".contextKeys.${name}: "${name}" is neither "email" nor a context.recordKeys entry on this manifest`;
3809
+ }
3810
+ }
3811
+ }
3812
+ if (e.sourceFromArg !== void 0) {
3813
+ if (e.sourceFromArg === null || typeof e.sourceFromArg !== "object" || Array.isArray(e.sourceFromArg)) {
3814
+ return `override "${toolName}".sourceFromArg must be an object`;
3815
+ }
3816
+ const sfa = e.sourceFromArg;
3817
+ if (!isNonEmptyString(sfa.kind)) return `override "${toolName}".sourceFromArg.kind must be a non-empty string`;
3818
+ if (!isNonEmptyString(sfa.arg)) return `override "${toolName}".sourceFromArg.arg must be a non-empty string`;
3819
+ if (!sourceKinds.has(sfa.kind)) {
3820
+ return `override "${toolName}".sourceFromArg.kind "${String(sfa.kind)}" is not declared in this manifest's context.sources`;
3821
+ }
3822
+ }
3823
+ }
3824
+ return null;
3825
+ }
3600
3826
  var manifests = /* @__PURE__ */ new Map();
3601
3827
  function loadManifests(integrationsDir) {
3602
3828
  const dir = resolve(
@@ -3629,6 +3855,11 @@ function loadManifests(integrationsDir) {
3629
3855
  console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${previewReason}`);
3630
3856
  continue;
3631
3857
  }
3858
+ const contextReason = invalidManifestContextReason(manifest);
3859
+ if (contextReason) {
3860
+ console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${contextReason}`);
3861
+ continue;
3862
+ }
3632
3863
  manifests.set(id, manifest);
3633
3864
  loaded++;
3634
3865
  } catch (err) {
@@ -3891,26 +4122,625 @@ async function buildInternalPreview(integrationManager2, integrationId, toolName
3891
4122
  if (typeof approved !== "string" && typeof approved !== "number") {
3892
4123
  return ok();
3893
4124
  }
3894
- const { [preview.version.arg]: _omit, ...withoutVersion } = actionArgs;
3895
- const mappedCurrentArgs = mapPreviewArgs(preview, withoutVersion);
3896
- const read2 = await readPreview(integrationManager2, integrationId, preview.tool, mappedCurrentArgs);
3897
- if (read2.status !== "ok" || read2.body.structured === void 0) {
3898
- return ok();
4125
+ const { [preview.version.arg]: _omit, ...withoutVersion } = actionArgs;
4126
+ const mappedCurrentArgs = mapPreviewArgs(preview, withoutVersion);
4127
+ const read2 = await readPreview(integrationManager2, integrationId, preview.tool, mappedCurrentArgs);
4128
+ if (read2.status !== "ok" || read2.body.structured === void 0) {
4129
+ return ok();
4130
+ }
4131
+ const current = read2.body.structured[preview.version.field];
4132
+ if (typeof current !== "string" && typeof current !== "number") {
4133
+ return ok();
4134
+ }
4135
+ const stale = String(current) !== String(approved);
4136
+ return ok({
4137
+ version: {
4138
+ field: preview.version.field,
4139
+ approved,
4140
+ current,
4141
+ stale,
4142
+ ...stale ? { currentBody: read2.body } : {}
4143
+ }
4144
+ });
4145
+ }
4146
+
4147
+ // src/lib/scope-specificity.ts
4148
+ function tokenSet(value) {
4149
+ if (value === void 0 || value === null) return /* @__PURE__ */ new Set();
4150
+ return new Set(
4151
+ String(value).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
4152
+ );
4153
+ }
4154
+ function subsetOrEqual(a, b) {
4155
+ if (b.size === 0) return true;
4156
+ if (a.size === 0) return false;
4157
+ for (const x of a) if (!b.has(x)) return false;
4158
+ return true;
4159
+ }
4160
+ function atLeastAsSpecific(keys, a, b) {
4161
+ for (const k of keys) {
4162
+ if (!subsetOrEqual(tokenSet(a[k]), tokenSet(b[k]))) return false;
4163
+ }
4164
+ return true;
4165
+ }
4166
+ function strictlyMoreSpecific(keys, a, b) {
4167
+ return atLeastAsSpecific(keys, a, b) && !atLeastAsSpecific(keys, b, a);
4168
+ }
4169
+ function selectAuthorization(contextKeys, passers) {
4170
+ if (passers.length === 1) {
4171
+ return { chosen: passers[0], superseded: [], reason: "sole" };
4172
+ }
4173
+ const dominators = passers.filter(
4174
+ (a) => passers.every((b) => b === a || strictlyMoreSpecific(contextKeys, a.context, b.context))
4175
+ );
4176
+ if (dominators.length === 1) {
4177
+ const chosen2 = dominators[0];
4178
+ return { chosen: chosen2, superseded: passers.filter((p) => p !== chosen2), reason: "most-specific" };
4179
+ }
4180
+ const approvers = passers.filter((p) => p.requiresApproval);
4181
+ const pool = approvers.length > 0 ? approvers : passers;
4182
+ const chosen = pool.slice().sort((a, b) => a.id.localeCompare(b.id))[0];
4183
+ return {
4184
+ chosen,
4185
+ superseded: passers.filter((p) => p !== chosen),
4186
+ reason: approvers.length > 0 ? "fail-safe-approval" : "fail-safe-tiebreak"
4187
+ };
4188
+ }
4189
+
4190
+ // src/lib/read-gate.ts
4191
+ function boundsSatisfyReadGate(bounds, gate) {
4192
+ if (!gate.boundField) return true;
4193
+ const actual = bounds?.[gate.boundField];
4194
+ return actual !== void 0 && String(actual) === gate.requiredValue;
4195
+ }
4196
+ function readToolIsGoverned(g) {
4197
+ if (!g) return false;
4198
+ return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
4199
+ }
4200
+ function allowedResources(contexts, boundField) {
4201
+ const out = /* @__PURE__ */ new Set();
4202
+ for (const ctx of contexts) {
4203
+ for (const v of tokenSet(ctx?.[boundField])) out.add(v);
4204
+ }
4205
+ return out;
4206
+ }
4207
+ function deniedResources(requested, allowed) {
4208
+ return requested.filter((r) => !allowed.has(r));
4209
+ }
4210
+ function filterItemsByResource(items, idPath, allowed) {
4211
+ return items.filter((it) => {
4212
+ const id = getByDottedPath(it, idPath);
4213
+ return typeof id === "string" && allowed.has(id);
4214
+ });
4215
+ }
4216
+ function hasBlockedValue(item, valuesPath, blocked) {
4217
+ const v = getByDottedPath(item, valuesPath);
4218
+ const arr = Array.isArray(v) ? v : v != null ? [v] : [];
4219
+ return arr.some((x) => typeof x === "string" && blocked.has(x));
4220
+ }
4221
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
4222
+ function parseMessageTimestamp(value) {
4223
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4224
+ if (typeof value === "string" && value.trim() !== "") {
4225
+ const n = Number(value);
4226
+ if (Number.isFinite(n)) return n;
4227
+ const iso = Date.parse(value);
4228
+ if (Number.isFinite(iso)) return iso;
4229
+ }
4230
+ return null;
4231
+ }
4232
+ function ageInDays(timestampMs, nowMs) {
4233
+ return Math.floor((nowMs - timestampMs) / MS_PER_DAY);
4234
+ }
4235
+ function maxReadAgeDays(boundsList, field) {
4236
+ let max = null;
4237
+ for (const b of boundsList) {
4238
+ const raw = b?.[field];
4239
+ const n = typeof raw === "number" ? raw : raw !== void 0 ? Number(raw) : NaN;
4240
+ if (Number.isFinite(n)) max = max === null ? n : Math.max(max, n);
4241
+ }
4242
+ return max;
4243
+ }
4244
+ function isOlderThanMaxAge(timestampMs, maxAgeDays, nowMs) {
4245
+ if (timestampMs === null) return true;
4246
+ return ageInDays(timestampMs, nowMs) > maxAgeDays;
4247
+ }
4248
+ var TRAILING_OPERATOR = /(?:^|\s)(?:OR|AND|NOT)$|[-+|&:]$/i;
4249
+ function parensBalanced(s) {
4250
+ let depth = 0;
4251
+ let inQuote = false;
4252
+ for (const c of s) {
4253
+ if (c === '"') {
4254
+ inQuote = !inQuote;
4255
+ continue;
4256
+ }
4257
+ if (inQuote) continue;
4258
+ if (c === "(") depth++;
4259
+ else if (c === ")" && --depth < 0) return false;
4260
+ }
4261
+ return !inQuote && depth === 0;
4262
+ }
4263
+ function composeReadQuery(agentQuery, clauses) {
4264
+ const injected = clauses.filter((c) => typeof c === "string" && c.trim() !== "").map((c) => c.trim());
4265
+ const base = (agentQuery ?? "").trim();
4266
+ if (base === "") return { ok: true, query: injected.join(" ") };
4267
+ if (!parensBalanced(base)) {
4268
+ return { ok: false, reason: "the requested search query has unbalanced parentheses" };
4269
+ }
4270
+ if (TRAILING_OPERATOR.test(base)) {
4271
+ return { ok: false, reason: "the requested search query ends in a dangling operator" };
4272
+ }
4273
+ if (injected.length === 0) return { ok: true, query: base };
4274
+ return { ok: true, query: `(${base}) ${injected.join(" ")}` };
4275
+ }
4276
+ function detectAgeConflict(agentQuery, conflictPattern, maxAgeDays) {
4277
+ if (!agentQuery || !conflictPattern || maxAgeDays === null) return null;
4278
+ let re;
4279
+ try {
4280
+ re = new RegExp(conflictPattern, "gi");
4281
+ } catch {
4282
+ return null;
4283
+ }
4284
+ let worst = null;
4285
+ for (const match of agentQuery.matchAll(re)) {
4286
+ const days = Number(match[1]);
4287
+ if (!Number.isFinite(days)) continue;
4288
+ if (days >= maxAgeDays && (worst === null || days > worst)) worst = days;
4289
+ }
4290
+ return worst;
4291
+ }
4292
+ function resolveAgeBoundField(boundsSchema, ageExecField) {
4293
+ const fields = boundsSchema?.fields;
4294
+ if (!fields) return null;
4295
+ for (const [name, def] of Object.entries(fields)) {
4296
+ if (def?.boundType?.kind === "per_transaction" && def.boundType.of === ageExecField) return name;
4297
+ }
4298
+ return null;
4299
+ }
4300
+ function getByDottedPath(obj, path) {
4301
+ return path.split(".").reduce(
4302
+ (o, k) => o == null ? void 0 : o[k],
4303
+ obj
4304
+ );
4305
+ }
4306
+ function firstParsableDate(obj, paths) {
4307
+ for (const path of Array.isArray(paths) ? paths : [paths]) {
4308
+ const parsed = parseMessageTimestamp(getByDottedPath(obj, path));
4309
+ if (parsed !== null) return parsed;
4310
+ }
4311
+ return null;
4312
+ }
4313
+ function renderAgeConstraint(template, maxAgeDays, nowMs) {
4314
+ const floorIso = new Date(nowMs - maxAgeDays * MS_PER_DAY).toISOString();
4315
+ return template.replace(/\{days\}/g, String(maxAgeDays)).replace(/\{date\}/g, floorIso.slice(0, 10));
4316
+ }
4317
+ function ageFloorValue(maxAgeDays, nowMs, format = "iso") {
4318
+ const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
4319
+ if (format === "epoch_ms") return floorMs;
4320
+ if (format === "epoch_s") return String(Math.floor(floorMs / 1e3));
4321
+ return new Date(floorMs).toISOString();
4322
+ }
4323
+ function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
4324
+ const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
4325
+ const asked = parseMessageTimestamp(requested);
4326
+ if (asked !== null && asked >= floorMs) return requested;
4327
+ return ageFloorValue(maxAgeDays, nowMs, format);
4328
+ }
4329
+
4330
+ // src/lib/field-path.ts
4331
+ function matchesWhere(element, where) {
4332
+ if (element === null || typeof element !== "object" || Array.isArray(element)) return false;
4333
+ const row = element;
4334
+ return Object.entries(where).every(([key, expected]) => {
4335
+ const actual = row[key];
4336
+ if (typeof expected === "string" && typeof actual === "string") {
4337
+ return expected.toLowerCase() === actual.toLowerCase();
4338
+ }
4339
+ return actual === expected;
4340
+ });
4341
+ }
4342
+ function parseAddress(value, which) {
4343
+ const m = value.match(/^(.*?)<([^<>]+)>\s*$/);
4344
+ if (m) {
4345
+ const email = m[2].trim();
4346
+ if (which === "address.email") return email || void 0;
4347
+ const name = m[1].trim().replace(/^"(.*)"$/, "$1").trim();
4348
+ return name || void 0;
4349
+ }
4350
+ if (which === "address.email") return value.trim() || void 0;
4351
+ return void 0;
4352
+ }
4353
+ function parseEpochMs(value) {
4354
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
4355
+ if (!Number.isFinite(n)) return void 0;
4356
+ return new Date(n).toISOString();
4357
+ }
4358
+ function applyParse(value, parse2) {
4359
+ if (value === void 0 || value === null) return void 0;
4360
+ if (parse2 === "epoch-ms") return parseEpochMs(value);
4361
+ if (typeof value !== "string") return void 0;
4362
+ return parseAddress(value, parse2);
4363
+ }
4364
+ function resolveFieldPath(record, fieldPath) {
4365
+ if (fieldPath === void 0) return void 0;
4366
+ if (typeof fieldPath === "string") return getByDottedPath(record, fieldPath);
4367
+ let value = getByDottedPath(record, fieldPath.path);
4368
+ if (fieldPath.where) {
4369
+ if (!Array.isArray(value)) return void 0;
4370
+ value = value.find((el) => matchesWhere(el, fieldPath.where));
4371
+ if (value === void 0) return void 0;
4372
+ }
4373
+ if (fieldPath.pick !== void 0) {
4374
+ value = getByDottedPath(value, fieldPath.pick);
4375
+ }
4376
+ if (fieldPath.parse) {
4377
+ value = applyParse(value, fieldPath.parse);
4378
+ }
4379
+ return value;
4380
+ }
4381
+
4382
+ // src/lib/request-context.ts
4383
+ var SOURCE_LINK_ARG = "_source";
4384
+ function withSourceLink(schema) {
4385
+ const props = schema.properties ?? {};
4386
+ if ("_source" in props) return schema;
4387
+ return {
4388
+ ...schema,
4389
+ properties: {
4390
+ ...props,
4391
+ _source: {
4392
+ type: "object",
4393
+ description: 'Optional. Link the request you are answering \u2014 the ONE record (e.g. an inbound email) this action responds to, so the human approver can see why it was proposed. Only a reference: {"connector": "<integration id>", "kind": "<declared kind>", "id": "<record id>"}. Example: {"connector":"mail","kind":"message","id":"m-17"}. The gateway reads the record itself \u2014 never write a summary or quote here.',
4394
+ properties: {
4395
+ connector: { type: "string" },
4396
+ kind: { type: "string" },
4397
+ id: { type: "string" }
4398
+ }
4399
+ }
4400
+ }
4401
+ };
4402
+ }
4403
+ function stripSourceLink(args) {
4404
+ if (!(SOURCE_LINK_ARG in args)) return args;
4405
+ const { [SOURCE_LINK_ARG]: _omit, ...rest } = args;
4406
+ return rest;
4407
+ }
4408
+ var FREE_MAIL_DOMAINS = /* @__PURE__ */ new Set([
4409
+ "gmail.com",
4410
+ "googlemail.com",
4411
+ "outlook.com",
4412
+ "hotmail.com",
4413
+ "hotmail.co.uk",
4414
+ "hotmail.fr",
4415
+ "hotmail.de",
4416
+ "live.com",
4417
+ "msn.com",
4418
+ "yahoo.com",
4419
+ "yahoo.co.uk",
4420
+ "yahoo.de",
4421
+ "yahoo.fr",
4422
+ "ymail.com",
4423
+ "rocketmail.com",
4424
+ "gmx.de",
4425
+ "gmx.net",
4426
+ "gmx.at",
4427
+ "gmx.ch",
4428
+ "gmx.com",
4429
+ "web.de",
4430
+ "t-online.de",
4431
+ "icloud.com",
4432
+ "me.com",
4433
+ "mac.com",
4434
+ "aol.com",
4435
+ "protonmail.com",
4436
+ "proton.me",
4437
+ "mail.com",
4438
+ "zoho.com",
4439
+ "yandex.com"
4440
+ ]);
4441
+ function isFreeMailDomain(domain) {
4442
+ return FREE_MAIL_DOMAINS.has(domain.toLowerCase());
4443
+ }
4444
+ function deriveIdentityKeysFromEmail(email) {
4445
+ const lower = email.toLowerCase().trim();
4446
+ const at = lower.lastIndexOf("@");
4447
+ if (at <= 0 || at === lower.length - 1) return [];
4448
+ const domain = lower.slice(at + 1);
4449
+ const keys = [{ type: "email", value: lower }];
4450
+ if (!isFreeMailDomain(domain)) keys.push({ type: "email_domain", value: domain });
4451
+ return keys;
4452
+ }
4453
+ function deriveIdentityKeysFromValue(value) {
4454
+ if (typeof value === "string") return deriveIdentityKeysFromEmail(value);
4455
+ if (Array.isArray(value)) {
4456
+ return value.flatMap((v) => typeof v === "string" ? deriveIdentityKeysFromEmail(v) : []);
4457
+ }
4458
+ return [];
4459
+ }
4460
+ function dedupeKeys(keys) {
4461
+ const seen = /* @__PURE__ */ new Set();
4462
+ const out = [];
4463
+ for (const k of keys) {
4464
+ const sig = `${k.type}:${k.value}`;
4465
+ if (seen.has(sig)) continue;
4466
+ seen.add(sig);
4467
+ out.push(k);
4468
+ }
4469
+ return out;
4470
+ }
4471
+ function substitute(template, values) {
4472
+ const out = {};
4473
+ for (const [arg, token] of Object.entries(template)) {
4474
+ if (token.startsWith("$")) {
4475
+ const key = token.slice(1);
4476
+ if (key in values) out[arg] = values[key];
4477
+ } else {
4478
+ out[arg] = token;
4479
+ }
4480
+ }
4481
+ return out;
4482
+ }
4483
+ function parseContextBody(body) {
4484
+ if (body.structured !== void 0) return body.structured;
4485
+ if (body.text !== void 0) {
4486
+ try {
4487
+ return JSON.parse(body.text);
4488
+ } catch {
4489
+ return void 0;
4490
+ }
4491
+ }
4492
+ return void 0;
4493
+ }
4494
+ async function readContextRecord(integrationManager2, connectorId, tool, args) {
4495
+ const read = await readPreview(integrationManager2, connectorId, tool, args);
4496
+ if (read.status === "ok") {
4497
+ const record = parseContextBody(read.body);
4498
+ if (record === void 0) return { status: "unavailable", reason: "connector_error" };
4499
+ return { status: "ok", record };
4500
+ }
4501
+ if (read.status === "not_found") return { status: "not_found" };
4502
+ return { status: "unavailable", reason: read.reason };
4503
+ }
4504
+ function parseExplicitSourceLink(raw) {
4505
+ if (raw === void 0 || raw === null) return void 0;
4506
+ if (typeof raw !== "object" || Array.isArray(raw)) return "invalid";
4507
+ const o = raw;
4508
+ if (typeof o.connector !== "string" || !o.connector) return "invalid";
4509
+ if (typeof o.kind !== "string" || !o.kind) return "invalid";
4510
+ if (typeof o.id !== "string" || !o.id) return "invalid";
4511
+ return { connector: o.connector, kind: o.kind, id: o.id };
4512
+ }
4513
+ function resolveSourceLink(toolArgs, connectorId, sourceFromArg) {
4514
+ const explicit = parseExplicitSourceLink(toolArgs._source);
4515
+ if (explicit !== void 0) return explicit;
4516
+ if (sourceFromArg) {
4517
+ const argVal = toolArgs[sourceFromArg.arg];
4518
+ if (typeof argVal === "string" && argVal) {
4519
+ return { connector: connectorId, kind: sourceFromArg.kind, id: argVal };
4520
+ }
4521
+ }
4522
+ return void 0;
4523
+ }
4524
+ async function readSourceOnce(integrationManager2, link, sourceConfig) {
4525
+ const args = substitute(sourceConfig.read.args, { id: link.id });
4526
+ const read = await readContextRecord(integrationManager2, link.connector, sourceConfig.read.tool, args);
4527
+ if (read.status === "not_found") return { status: "not_found" };
4528
+ if (read.status === "unavailable") return { status: "unreadable", reason: read.reason };
4529
+ const keys = [];
4530
+ if (sourceConfig.keys?.email) {
4531
+ const v = resolveFieldPath(read.record, sourceConfig.keys.email);
4532
+ if (typeof v === "string" && v) keys.push(...deriveIdentityKeysFromEmail(v));
4533
+ }
4534
+ return { record: read.record, keys };
4535
+ }
4536
+ async function resolveRequestContext(integrationManager2, toolArgs, actionConnectorId, sourceFromArg, configuredConnectorIds) {
4537
+ const link = resolveSourceLink(toolArgs, actionConnectorId, sourceFromArg);
4538
+ if (link === void 0) return { result: { status: "none" }, keys: [] };
4539
+ if (link === "invalid") {
4540
+ return { result: { status: "unreadable", reason: "malformed link" }, keys: [] };
4541
+ }
4542
+ if (!configuredConnectorIds.has(link.connector)) {
4543
+ return { result: { status: "unreadable", reason: "connector not installed" }, keys: [] };
4544
+ }
4545
+ const connectorManifest = getManifest(link.connector);
4546
+ if (!connectorManifest) {
4547
+ return { result: { status: "unreadable", reason: "connector not installed" }, keys: [] };
4548
+ }
4549
+ const sourceConfig = connectorManifest.context?.sources?.[link.kind];
4550
+ if (!sourceConfig) {
4551
+ return { result: { status: "unreadable", reason: `connector "${link.connector}" declares no "${link.kind}" source` }, keys: [] };
4552
+ }
4553
+ const primary = await readSourceOnce(integrationManager2, link, sourceConfig);
4554
+ if ("status" in primary) {
4555
+ if (primary.status === "not_found") return { result: { status: "not_found" }, keys: [] };
4556
+ return { result: { status: "unreadable", reason: primary.reason ?? "connector unreachable" }, keys: [] };
4557
+ }
4558
+ const implicit = toolArgs._source === void 0;
4559
+ const base = {
4560
+ connector: link.connector,
4561
+ kind: link.kind,
4562
+ implicit,
4563
+ ...sourceConfig.title ? { title: asString(resolveFieldPath(primary.record, sourceConfig.title)) } : {},
4564
+ ...sourceConfig.author ? { author: asString(resolveFieldPath(primary.record, sourceConfig.author)) } : {},
4565
+ ...sourceConfig.at ? { at: asString(resolveFieldPath(primary.record, sourceConfig.at)) } : {},
4566
+ ...sourceConfig.body ? { body: asBody(resolveFieldPath(primary.record, sourceConfig.body)) } : {}
4567
+ };
4568
+ const chain = [];
4569
+ if (sourceConfig.chain) {
4570
+ let currentId = asString(getByDottedPath(primary.record, sourceConfig.chain.field));
4571
+ let hops = 0;
4572
+ const seen = /* @__PURE__ */ new Set([link.id]);
4573
+ while (currentId && hops < sourceConfig.chain.max && !seen.has(currentId)) {
4574
+ seen.add(currentId);
4575
+ const hopLink = { connector: link.connector, kind: link.kind, id: currentId };
4576
+ const hop = await readSourceOnce(integrationManager2, hopLink, sourceConfig);
4577
+ hops++;
4578
+ if ("status" in hop) break;
4579
+ chain.push({
4580
+ ...sourceConfig.author ? { author: asString(resolveFieldPath(hop.record, sourceConfig.author)) } : {},
4581
+ ...sourceConfig.at ? { at: asString(resolveFieldPath(hop.record, sourceConfig.at)) } : {},
4582
+ ...sourceConfig.title ? { title: asString(resolveFieldPath(hop.record, sourceConfig.title)) } : {},
4583
+ ...sourceConfig.body ? { body: asBody(resolveFieldPath(hop.record, sourceConfig.body)) } : {}
4584
+ });
4585
+ currentId = sourceConfig.chain ? asString(getByDottedPath(hop.record, sourceConfig.chain.field)) : void 0;
4586
+ }
3899
4587
  }
3900
- const current = read2.body.structured[preview.version.field];
3901
- if (typeof current !== "string" && typeof current !== "number") {
3902
- return ok();
4588
+ return { result: { status: "ok", ...base, chain }, keys: primary.keys };
4589
+ }
4590
+ function asString(v) {
4591
+ return typeof v === "string" && v.length > 0 ? v : void 0;
4592
+ }
4593
+ var REQUEST_BODY_CAP = 2e4;
4594
+ function asBody(v) {
4595
+ const s = asString(v);
4596
+ return s === void 0 ? void 0 : s.length > REQUEST_BODY_CAP ? s.slice(0, REQUEST_BODY_CAP) : s;
4597
+ }
4598
+ async function resolveRecordKeyChain(integrationManager2, connectorId, manifest, recordKeyName, idValue, stepsLeft) {
4599
+ if (stepsLeft <= 0) return [];
4600
+ const recordKey = manifest.context?.recordKeys?.[recordKeyName];
4601
+ if (!recordKey) return [];
4602
+ const args = substitute(recordKey.read.args, { id: idValue });
4603
+ const read = await readContextRecord(integrationManager2, connectorId, recordKey.read.tool, args);
4604
+ if (read.status !== "ok") return [];
4605
+ const out = [];
4606
+ const emailField = recordKey.keys?.email;
4607
+ if (emailField) {
4608
+ const v = resolveFieldPath(read.record, emailField);
4609
+ if (typeof v === "string" && v) out.push(...deriveIdentityKeysFromEmail(v));
4610
+ }
4611
+ if (recordKey.next) {
4612
+ const nextId = asString(getByDottedPath(read.record, recordKey.next.field));
4613
+ if (nextId) {
4614
+ out.push(
4615
+ ...await resolveRecordKeyChain(
4616
+ integrationManager2,
4617
+ connectorId,
4618
+ manifest,
4619
+ recordKey.next.recordKey,
4620
+ nextId,
4621
+ stepsLeft - 1
4622
+ )
4623
+ );
4624
+ }
3903
4625
  }
3904
- const stale = String(current) !== String(approved);
3905
- return ok({
3906
- version: {
3907
- field: preview.version.field,
3908
- approved,
3909
- current,
3910
- stale,
3911
- ...stale ? { currentBody: read2.body } : {}
4626
+ return out;
4627
+ }
4628
+ async function gatherContextKeysFromAction(integrationManager2, connectorId, contextKeys, toolArgs) {
4629
+ if (!contextKeys) return [];
4630
+ const manifest = getManifest(connectorId);
4631
+ const out = [];
4632
+ for (const [name, argName] of Object.entries(contextKeys)) {
4633
+ const raw = toolArgs[argName];
4634
+ if (raw === void 0 || raw === null) continue;
4635
+ if (name === "email") {
4636
+ out.push(...deriveIdentityKeysFromValue(raw));
4637
+ continue;
3912
4638
  }
4639
+ if (!manifest?.context?.recordKeys?.[name] || typeof raw !== "string" || !raw) continue;
4640
+ out.push(...await resolveRecordKeyChain(integrationManager2, connectorId, manifest, name, raw, MAX_RECORD_KEY_STEPS));
4641
+ }
4642
+ return out;
4643
+ }
4644
+ function mapHistoryItem(raw, mapping) {
4645
+ return {
4646
+ // `kind` is a literal label, copied as-is — never a path (doc §8.1).
4647
+ ...mapping.kind ? { kind: mapping.kind } : {},
4648
+ ...mapping.title ? { title: asString(resolveFieldPath(raw, mapping.title)) } : {},
4649
+ ...mapping.body ? { body: asBody(resolveFieldPath(raw, mapping.body)) } : {},
4650
+ ...mapping.at ? { at: asString(resolveFieldPath(raw, mapping.at)) } : {},
4651
+ ...mapping.author ? { author: asString(resolveFieldPath(raw, mapping.author)) } : {},
4652
+ ...mapping.type ? { type: asString(resolveFieldPath(raw, mapping.type)) } : {}
4653
+ };
4654
+ }
4655
+ function withinWindow(at, cutoffMs) {
4656
+ if (!at) return true;
4657
+ const ms = Date.parse(at);
4658
+ if (Number.isNaN(ms)) return true;
4659
+ return ms >= cutoffMs;
4660
+ }
4661
+ function chooseHistoryKey(byKey, keys) {
4662
+ const order = Array.isArray(byKey) ? byKey : [byKey];
4663
+ for (const type of order) {
4664
+ const found = keys.find((k) => k.type === type);
4665
+ if (found) return found;
4666
+ }
4667
+ return void 0;
4668
+ }
4669
+ function resolveHistoryArgsTemplate(template, keyType) {
4670
+ const out = {};
4671
+ for (const [argName, token] of Object.entries(template)) {
4672
+ out[argName === "$keyType" ? keyType : argName] = token;
4673
+ }
4674
+ return out;
4675
+ }
4676
+ async function readConnectorHistory(integrationManager2, connectorId, history, keys, executionJournal) {
4677
+ const key = chooseHistoryKey(history.byKey, keys);
4678
+ if (!key) return { status: "no_match" };
4679
+ const sinceMs = Date.now() - history.maxAgeDays * 24 * 60 * 60 * 1e3;
4680
+ const args = substitute(resolveHistoryArgsTemplate(history.read.args, key.type), {
4681
+ key: key.value,
4682
+ since: new Date(sinceMs).toISOString(),
4683
+ limit: history.limit
3913
4684
  });
4685
+ const read = await readContextRecord(integrationManager2, connectorId, history.read.tool, args);
4686
+ if (read.status !== "ok") return { status: "unreachable" };
4687
+ const rows = Array.isArray(read.record) ? read.record : [];
4688
+ const items = [];
4689
+ for (const row of rows.slice(0, history.limit)) {
4690
+ const item = mapHistoryItem(row, history.item);
4691
+ if (!withinWindow(item.at, sinceMs)) continue;
4692
+ if (history.item.ticket) {
4693
+ const ticketId = asString(resolveFieldPath(row, history.item.ticket));
4694
+ if (ticketId) {
4695
+ const entry = executionJournal.get(ticketId);
4696
+ if (entry?.state === "done") {
4697
+ item.aiWritten = { at: entry.finishedAt ? entry.finishedAt * 1e3 : void 0 };
4698
+ }
4699
+ }
4700
+ }
4701
+ items.push(item);
4702
+ }
4703
+ return { status: "ok", match: { connectorId, matchedKeyType: key.type, matchedKeyValue: key.value, items } };
4704
+ }
4705
+ async function buildHistory(integrationManager2, keys, executionJournal, configuredConnectorIds) {
4706
+ if (keys.length === 0) return { status: "no_customer_link" };
4707
+ const connectors = getAllManifests().filter((m) => m.context?.history && configuredConnectorIds.has(m.id));
4708
+ if (connectors.length === 0) return { status: "no_match" };
4709
+ const matches = [];
4710
+ const unreachable = [];
4711
+ let anyMatchedType = false;
4712
+ for (const manifest of connectors) {
4713
+ const history = manifest.context.history;
4714
+ const outcome = await readConnectorHistory(integrationManager2, manifest.id, history, keys, executionJournal);
4715
+ if (outcome.status === "no_match") continue;
4716
+ anyMatchedType = true;
4717
+ if (outcome.status === "unreachable") {
4718
+ unreachable.push(manifest.id);
4719
+ continue;
4720
+ }
4721
+ matches.push(outcome.match);
4722
+ }
4723
+ if (!anyMatchedType) return { status: "no_match" };
4724
+ if (matches.length === 0 && unreachable.length > 0) return { status: "unreachable", connectorIds: unreachable };
4725
+ return { status: "ok", matches, unreachableConnectorIds: unreachable };
4726
+ }
4727
+ async function buildRequestContext(integrationManager2, executionJournal, actionConnectorId, toolArgs, actionGating, configuredConnectorIds) {
4728
+ const { result: request, keys: sourceKeys } = await resolveRequestContext(
4729
+ integrationManager2,
4730
+ toolArgs,
4731
+ actionConnectorId,
4732
+ actionGating?.sourceFromArg,
4733
+ configuredConnectorIds
4734
+ );
4735
+ const actionKeys = await gatherContextKeysFromAction(
4736
+ integrationManager2,
4737
+ actionConnectorId,
4738
+ actionGating?.contextKeys,
4739
+ toolArgs
4740
+ );
4741
+ const keys = dedupeKeys([...actionKeys, ...sourceKeys]);
4742
+ const history = await buildHistory(integrationManager2, keys, executionJournal, configuredConnectorIds);
4743
+ return { request, history };
3914
4744
  }
3915
4745
 
3916
4746
  // src/lib/ticket-verify.ts
@@ -4116,191 +4946,34 @@ function normalizeIncomingArgs(tool, args) {
4116
4946
  return out ?? args;
4117
4947
  }
4118
4948
 
4119
- // src/lib/scope-specificity.ts
4120
- function tokenSet(value) {
4121
- if (value === void 0 || value === null) return /* @__PURE__ */ new Set();
4122
- return new Set(
4123
- String(value).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
4124
- );
4125
- }
4126
- function subsetOrEqual(a, b) {
4127
- if (b.size === 0) return true;
4128
- if (a.size === 0) return false;
4129
- for (const x of a) if (!b.has(x)) return false;
4130
- return true;
4131
- }
4132
- function atLeastAsSpecific(keys, a, b) {
4133
- for (const k of keys) {
4134
- if (!subsetOrEqual(tokenSet(a[k]), tokenSet(b[k]))) return false;
4135
- }
4136
- return true;
4137
- }
4138
- function strictlyMoreSpecific(keys, a, b) {
4139
- return atLeastAsSpecific(keys, a, b) && !atLeastAsSpecific(keys, b, a);
4140
- }
4141
- function selectAuthorization(contextKeys, passers) {
4142
- if (passers.length === 1) {
4143
- return { chosen: passers[0], superseded: [], reason: "sole" };
4144
- }
4145
- const dominators = passers.filter(
4146
- (a) => passers.every((b) => b === a || strictlyMoreSpecific(contextKeys, a.context, b.context))
4147
- );
4148
- if (dominators.length === 1) {
4149
- const chosen2 = dominators[0];
4150
- return { chosen: chosen2, superseded: passers.filter((p) => p !== chosen2), reason: "most-specific" };
4151
- }
4152
- const approvers = passers.filter((p) => p.requiresApproval);
4153
- const pool = approvers.length > 0 ? approvers : passers;
4154
- const chosen = pool.slice().sort((a, b) => a.id.localeCompare(b.id))[0];
4155
- return {
4156
- chosen,
4157
- superseded: passers.filter((p) => p !== chosen),
4158
- reason: approvers.length > 0 ? "fail-safe-approval" : "fail-safe-tiebreak"
4159
- };
4160
- }
4161
-
4162
- // src/lib/read-gate.ts
4163
- function boundsSatisfyReadGate(bounds, gate) {
4164
- if (!gate.boundField) return true;
4165
- const actual = bounds?.[gate.boundField];
4166
- return actual !== void 0 && String(actual) === gate.requiredValue;
4167
- }
4168
- function readToolIsGoverned(g) {
4169
- if (!g) return false;
4170
- return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
4171
- }
4172
- function allowedResources(contexts, boundField) {
4949
+ // src/lib/numeric-execution.ts
4950
+ import { getProfile as getProfile2 } from "@hap/core";
4951
+ function parseNumericString(value) {
4952
+ if (!/^-?\d+(\.\d+)?$/.test(value)) return void 0;
4953
+ const parsed = Number(value);
4954
+ return Number.isFinite(parsed) ? parsed : void 0;
4955
+ }
4956
+ function declaredNumericFields(profile) {
4173
4957
  const out = /* @__PURE__ */ new Set();
4174
- for (const ctx of contexts) {
4175
- for (const v of tokenSet(ctx?.[boundField])) out.add(v);
4176
- }
4177
- return out;
4178
- }
4179
- function deniedResources(requested, allowed) {
4180
- return requested.filter((r) => !allowed.has(r));
4181
- }
4182
- function filterItemsByResource(items, idPath, allowed) {
4183
- return items.filter((it) => {
4184
- const id = getByDottedPath(it, idPath);
4185
- return typeof id === "string" && allowed.has(id);
4186
- });
4187
- }
4188
- function hasBlockedValue(item, valuesPath, blocked) {
4189
- const v = getByDottedPath(item, valuesPath);
4190
- const arr = Array.isArray(v) ? v : v != null ? [v] : [];
4191
- return arr.some((x) => typeof x === "string" && blocked.has(x));
4192
- }
4193
- var MS_PER_DAY = 24 * 60 * 60 * 1e3;
4194
- function parseMessageTimestamp(value) {
4195
- if (typeof value === "number" && Number.isFinite(value)) return value;
4196
- if (typeof value === "string" && value.trim() !== "") {
4197
- const n = Number(value);
4198
- if (Number.isFinite(n)) return n;
4199
- const iso = Date.parse(value);
4200
- if (Number.isFinite(iso)) return iso;
4201
- }
4202
- return null;
4203
- }
4204
- function ageInDays(timestampMs, nowMs) {
4205
- return Math.floor((nowMs - timestampMs) / MS_PER_DAY);
4206
- }
4207
- function maxReadAgeDays(boundsList, field) {
4208
- let max = null;
4209
- for (const b of boundsList) {
4210
- const raw = b?.[field];
4211
- const n = typeof raw === "number" ? raw : raw !== void 0 ? Number(raw) : NaN;
4212
- if (Number.isFinite(n)) max = max === null ? n : Math.max(max, n);
4213
- }
4214
- return max;
4215
- }
4216
- function isOlderThanMaxAge(timestampMs, maxAgeDays, nowMs) {
4217
- if (timestampMs === null) return true;
4218
- return ageInDays(timestampMs, nowMs) > maxAgeDays;
4219
- }
4220
- var TRAILING_OPERATOR = /(?:^|\s)(?:OR|AND|NOT)$|[-+|&:]$/i;
4221
- function parensBalanced(s) {
4222
- let depth = 0;
4223
- let inQuote = false;
4224
- for (const c of s) {
4225
- if (c === '"') {
4226
- inQuote = !inQuote;
4227
- continue;
4228
- }
4229
- if (inQuote) continue;
4230
- if (c === "(") depth++;
4231
- else if (c === ")" && --depth < 0) return false;
4232
- }
4233
- return !inQuote && depth === 0;
4234
- }
4235
- function composeReadQuery(agentQuery, clauses) {
4236
- const injected = clauses.filter((c) => typeof c === "string" && c.trim() !== "").map((c) => c.trim());
4237
- const base = (agentQuery ?? "").trim();
4238
- if (base === "") return { ok: true, query: injected.join(" ") };
4239
- if (!parensBalanced(base)) {
4240
- return { ok: false, reason: "the requested search query has unbalanced parentheses" };
4241
- }
4242
- if (TRAILING_OPERATOR.test(base)) {
4243
- return { ok: false, reason: "the requested search query ends in a dangling operator" };
4244
- }
4245
- if (injected.length === 0) return { ok: true, query: base };
4246
- return { ok: true, query: `(${base}) ${injected.join(" ")}` };
4247
- }
4248
- function detectAgeConflict(agentQuery, conflictPattern, maxAgeDays) {
4249
- if (!agentQuery || !conflictPattern || maxAgeDays === null) return null;
4250
- let re;
4251
- try {
4252
- re = new RegExp(conflictPattern, "gi");
4253
- } catch {
4254
- return null;
4255
- }
4256
- let worst = null;
4257
- for (const match of agentQuery.matchAll(re)) {
4258
- const days = Number(match[1]);
4259
- if (!Number.isFinite(days)) continue;
4260
- if (days >= maxAgeDays && (worst === null || days > worst)) worst = days;
4261
- }
4262
- return worst;
4263
- }
4264
- function resolveAgeBoundField(boundsSchema, ageExecField) {
4265
- const fields = boundsSchema?.fields;
4266
- if (!fields) return null;
4958
+ const fields = profile?.executionContextSchema?.fields;
4959
+ if (!fields) return out;
4267
4960
  for (const [name, def] of Object.entries(fields)) {
4268
- if (def?.boundType?.kind === "per_transaction" && def.boundType.of === ageExecField) return name;
4961
+ if (def.source === "declared" && def.constraint?.type === "number") out.add(name);
4269
4962
  }
4270
- return null;
4271
- }
4272
- function getByDottedPath(obj, path) {
4273
- return path.split(".").reduce(
4274
- (o, k) => o == null ? void 0 : o[k],
4275
- obj
4276
- );
4963
+ return out;
4277
4964
  }
4278
- function firstParsableDate(obj, paths) {
4279
- for (const path of Array.isArray(paths) ? paths : [paths]) {
4280
- const parsed = parseMessageTimestamp(getByDottedPath(obj, path));
4281
- if (parsed !== null) return parsed;
4965
+ function coerceDeclaredNumericFields(profileId, execution) {
4966
+ const fields = declaredNumericFields(getProfile2(profileId));
4967
+ for (const field of fields) {
4968
+ const value = execution[field];
4969
+ if (typeof value !== "string") continue;
4970
+ const parsed = parseNumericString(value);
4971
+ if (parsed !== void 0) execution[field] = parsed;
4282
4972
  }
4283
- return null;
4284
- }
4285
- function renderAgeConstraint(template, maxAgeDays, nowMs) {
4286
- const floorIso = new Date(nowMs - maxAgeDays * MS_PER_DAY).toISOString();
4287
- return template.replace(/\{days\}/g, String(maxAgeDays)).replace(/\{date\}/g, floorIso.slice(0, 10));
4288
- }
4289
- function ageFloorValue(maxAgeDays, nowMs, format = "iso") {
4290
- const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
4291
- if (format === "epoch_ms") return floorMs;
4292
- if (format === "epoch_s") return String(Math.floor(floorMs / 1e3));
4293
- return new Date(floorMs).toISOString();
4294
- }
4295
- function clampAgeFloor(requested, maxAgeDays, nowMs, format = "iso") {
4296
- const floorMs = nowMs - maxAgeDays * MS_PER_DAY;
4297
- const asked = parseMessageTimestamp(requested);
4298
- if (asked !== null && asked >= floorMs) return requested;
4299
- return ageFloorValue(maxAgeDays, nowMs, format);
4300
4973
  }
4301
4974
 
4302
4975
  // src/lib/tool-proxy.ts
4303
- import { getProfile as getProfile2, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
4976
+ import { getProfile as getProfile3, ContentBindingError, boundActionTypes as boundActionTypes2 } from "@hap/core";
4304
4977
  import { readFile } from "fs/promises";
4305
4978
  import { extname } from "path";
4306
4979
  import { randomUUID } from "crypto";
@@ -4372,7 +5045,7 @@ function profileMatches(profileId, shortName2) {
4372
5045
  return profileId === shortName2 || profileId.includes("/" + shortName2 + "@") || profileId.endsWith("/" + shortName2);
4373
5046
  }
4374
5047
  function wireExecutionContext(profileId, execution) {
4375
- const profile = getProfile2(profileId);
5048
+ const profile = getProfile3(profileId);
4376
5049
  const allowed = /* @__PURE__ */ new Set(["action_type"]);
4377
5050
  const fields = profile?.boundsSchema?.fields;
4378
5051
  if (fields) {
@@ -4390,7 +5063,7 @@ function wireExecutionContext(profileId, execution) {
4390
5063
  return out;
4391
5064
  }
4392
5065
  function countBoundsFor(profileId, actionType) {
4393
- const fields = getProfile2(profileId)?.boundsSchema?.fields;
5066
+ const fields = getProfile3(profileId)?.boundsSchema?.fields;
4394
5067
  if (!fields) return null;
4395
5068
  return Object.entries(fields).filter(([, def]) => def.boundType?.kind === "cumulative_count").filter(([name, def]) => {
4396
5069
  const applies = boundActionTypes2(name, def);
@@ -4465,7 +5138,7 @@ function recordBlocked(state2, tool, rec) {
4465
5138
  function recordGatekeeperRefusal(state2, tool, refusals) {
4466
5139
  if (refusals.length === 0) return;
4467
5140
  const pick = refusals.find((r) => r.bound !== void 0 && r.actual !== void 0) ?? refusals[0];
4468
- const scoped = pick.field ? Boolean(getProfile2(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
5141
+ const scoped = pick.field ? Boolean(getProfile3(pick.profileId)?.scopeSchema?.fields?.[pick.field]) : false;
4469
5142
  const asValueOrString = (v) => typeof v === "number" || typeof v === "string" ? v : void 0;
4470
5143
  recordBlocked(state2, tool, {
4471
5144
  kind: scoped ? "scope" : "bound",
@@ -4644,7 +5317,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4644
5317
  );
4645
5318
  }
4646
5319
  }
4647
- const readProfile = readAdapter ? getProfile2(matchingAuths[0].profileId) : void 0;
5320
+ const readProfile = readAdapter ? getProfile3(matchingAuths[0].profileId) : void 0;
4648
5321
  const boundsSchema = readProfile?.boundsSchema;
4649
5322
  const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
4650
5323
  const hasAgeDimension = Boolean(readAdapter?.ageField);
@@ -4789,6 +5462,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4789
5462
  }
4790
5463
  }
4791
5464
  }
5465
+ coerceDeclaredNumericFields(profile, execution);
4792
5466
  const declaredActionType = typeof execution.action_type === "string" && execution.action_type.length > 0 ? execution.action_type : void 0;
4793
5467
  if (!declaredActionType) {
4794
5468
  recordBlocked(state2, tool, {
@@ -4807,7 +5481,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4807
5481
  };
4808
5482
  }
4809
5483
  {
4810
- const profileDef = getProfile2(profile);
5484
+ const profileDef = getProfile3(profile);
4811
5485
  const registry = profileDef?.boundsSchema?.actionTypes;
4812
5486
  if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
4813
5487
  recordBlocked(state2, tool, {
@@ -4892,7 +5566,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4892
5566
  isError: true
4893
5567
  };
4894
5568
  }
4895
- const contextKeys = getProfile2(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
5569
+ const contextKeys = getProfile3(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
4896
5570
  return attemptWithCandidates(passers);
4897
5571
  async function attemptWithCandidates(candidates) {
4898
5572
  const selection = selectAuthorization(
@@ -5194,6 +5868,7 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.` + a
5194
5868
  let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
5195
5869
  if (receiptId) outgoingArgs = attachTicketId(tool, outgoingArgs, receiptId);
5196
5870
  outgoingArgs = encodeOutgoingArgs(tool, outgoingArgs);
5871
+ outgoingArgs = stripSourceLink(outgoingArgs);
5197
5872
  if (receiptId) {
5198
5873
  const begun = state2.executionJournal.begin({
5199
5874
  ticketId: receiptId,
@@ -5270,7 +5945,8 @@ function agentVisibleAuthorizations(authorizations, integrationManager2) {
5270
5945
  // src/lib/agent-rules.ts
5271
5946
  var AGENT_RULES = [
5272
5947
  "Never open or operate the Suveren Gateway's web page \u2014 not in a browser you control, not with computer use. Never ask for, read or type the person's API key. Never approve a proposal: approvals are the person's alone.",
5273
- "Use only the Suveren tools. No terminal, HTTP or browser calls to the gateway's ports (for example `curl localhost:3500/mcp`)."
5948
+ "Use only the Suveren tools. No terminal, HTTP or browser calls to the gateway's ports (for example `curl localhost:3500/mcp`).",
5949
+ "When a tool offers an optional `_source` argument, set it to link the ONE record (e.g. an inbound email) you are acting on \u2014 just the reference ({connector, kind, id}), never a summary or quote; the gateway reads the record itself for the approver."
5274
5950
  ];
5275
5951
 
5276
5952
  // src/lib/mandate-brief.ts
@@ -5350,7 +6026,7 @@ function buildMandateBrief(opts) {
5350
6026
  const parts = [];
5351
6027
  if (boundsDesc) parts.push(boundsDesc);
5352
6028
  if (executionLog) {
5353
- const profile = getProfile3(auth.profileId) ?? getProfile3(shortName2);
6029
+ const profile = getProfile4(auth.profileId) ?? getProfile4(shortName2);
5354
6030
  const consumption = getConsumptionState(auth, executionLog, profile);
5355
6031
  const compact = formatConsumptionCompact(consumption);
5356
6032
  if (compact) parts.push(compact);
@@ -5388,7 +6064,7 @@ function buildMandateBrief(opts) {
5388
6064
  }
5389
6065
 
5390
6066
  // src/tools/authorizations.ts
5391
- import { getProfile as getProfile4 } from "@hap/core";
6067
+ import { getProfile as getProfile5 } from "@hap/core";
5392
6068
  function shortProfileName2(profileId) {
5393
6069
  const withoutVersion = profileId.replace(/@.*$/, "");
5394
6070
  const parts = withoutVersion.split("/");
@@ -5506,7 +6182,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
5506
6182
  } catch {
5507
6183
  }
5508
6184
  const shortName2 = shortProfileName2(auth.profileId);
5509
- const profile = getProfile4(auth.profileId) ?? getProfile4(shortName2);
6185
+ const profile = getProfile5(auth.profileId) ?? getProfile5(shortName2);
5510
6186
  if (profile) {
5511
6187
  const consumption = getConsumptionState(auth, state2.executionLog, profile);
5512
6188
  const consumptionText = formatConsumptionFull(consumption);
@@ -5566,7 +6242,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
5566
6242
  lines.push(" Mode: review (each action requires your approval)");
5567
6243
  }
5568
6244
  const shortName2 = shortProfileName2(auth.profileId);
5569
- const profile = getProfile4(auth.profileId) ?? getProfile4(shortName2);
6245
+ const profile = getProfile5(auth.profileId) ?? getProfile5(shortName2);
5570
6246
  if (profile) {
5571
6247
  const consumption = getConsumptionState(auth, state2.executionLog, profile);
5572
6248
  const compact = formatConsumptionCompact(consumption);
@@ -6020,6 +6696,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
6020
6696
  outgoingArgs = attachTicketId(discovered, outgoingArgs, receiptId);
6021
6697
  outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
6022
6698
  }
6699
+ outgoingArgs = stripSourceLink(outgoingArgs);
6023
6700
  let result;
6024
6701
  try {
6025
6702
  result = await integrationManager2.callTool(integrationId, toolName, outgoingArgs);
@@ -7118,19 +7795,19 @@ var IntegrationManager = class {
7118
7795
  const profileGating = config.toolGating ?? null;
7119
7796
  const tools = (toolsResult.tools ?? []).map((tool) => {
7120
7797
  const gating = this.resolveToolGating(config.profile, profileGating, tool.name);
7798
+ let inputSchema = withoutBlockedArgs(
7799
+ tool.inputSchema ?? {},
7800
+ gating?.blockedArgs
7801
+ );
7802
+ if (gating && gating.category !== "read" && gating.category !== "disabled") {
7803
+ inputSchema = withSourceLink(inputSchema);
7804
+ }
7121
7805
  return {
7122
7806
  originalName: tool.name,
7123
7807
  namespacedName: `${config.id}__${tool.name}`,
7124
7808
  integrationId: config.id,
7125
7809
  description: tool.description ?? "",
7126
- // A downstream schema otherwise reaches the agent verbatim, so a
7127
- // connector can offer a control-bypassing argument and we would pass it
7128
- // straight on. Anything the manifest blocks is removed here, before the
7129
- // agent ever sees it exists.
7130
- inputSchema: withoutBlockedArgs(
7131
- tool.inputSchema ?? {},
7132
- gating?.blockedArgs
7133
- ),
7810
+ inputSchema,
7134
7811
  outputSchema: tool.outputSchema,
7135
7812
  gating
7136
7813
  };
@@ -7525,7 +8202,9 @@ var IntegrationManager = class {
7525
8202
  argNormalization: ext.argNormalization,
7526
8203
  hideUnlessAuthorized: ext.hideUnlessAuthorized,
7527
8204
  simulationOnly: ext.simulationOnly,
7528
- approvalView: ext.approvalView
8205
+ approvalView: ext.approvalView,
8206
+ contextKeys: ext.contextKeys,
8207
+ sourceFromArg: ext.sourceFromArg
7529
8208
  };
7530
8209
  }
7531
8210
  return {
@@ -7656,7 +8335,7 @@ async function buildTicketDetails(archive, ticketIds) {
7656
8335
  }
7657
8336
 
7658
8337
  // src/lib/report/window.ts
7659
- import { getProfile as getProfile5 } from "@hap/core";
8338
+ import { getProfile as getProfile6 } from "@hap/core";
7660
8339
  var REPORT_AGE_FIELD = "read_age_days";
7661
8340
  var REPORTING_PROFILE = "reporting";
7662
8341
  var END_SKEW_SECONDS = 120;
@@ -7670,7 +8349,7 @@ function windowLabel(start, days, loadedAt) {
7670
8349
  if (loadedAt !== null && loadedAt === start) return `${since} (when the test data was loaded)`;
7671
8350
  return `${since} (the last ${days} ${days === 1 ? "day" : "days"})`;
7672
8351
  }
7673
- var defaultLookup = (id) => getProfile5(id);
8352
+ var defaultLookup = (id) => getProfile6(id);
7674
8353
  function reportingMandateRefusal(m, lookup = defaultLookup) {
7675
8354
  const schema = lookup(m.profileId)?.boundsSchema;
7676
8355
  const field = resolveAgeBoundField(schema, REPORT_AGE_FIELD);
@@ -8408,7 +9087,7 @@ function unifiedLineDiff(before, after) {
8408
9087
  }
8409
9088
 
8410
9089
  // src/lib/builtins/setup.ts
8411
- import { getProfile as getProfile6 } from "@hap/core";
9090
+ import { getProfile as getProfile7 } from "@hap/core";
8412
9091
  var DELEGATION_PROFILE = "delegation";
8413
9092
  var DELEGATION_INTENT_HINT = [
8414
9093
  "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.",
@@ -8460,7 +9139,7 @@ function delegationItselfRefusal(args) {
8460
9139
  }
8461
9140
  function delegationMandateRefusal(auth) {
8462
9141
  if (isSimulationMode()) return void 0;
8463
- const profile = getProfile6(auth.profileId);
9142
+ const profile = getProfile7(auth.profileId);
8464
9143
  if (profile?.scopeSchema?.fields?.allowed_profiles) return void 0;
8465
9144
  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.`;
8466
9145
  }
@@ -8518,7 +9197,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
8518
9197
  );
8519
9198
  const first = governing[0];
8520
9199
  if (first) {
8521
- const delegationProfile = getProfile6(first.profileId);
9200
+ const delegationProfile = getProfile7(first.profileId);
8522
9201
  const entry = getConsumptionState(first, deps.state.executionLog, delegationProfile).find((e) => e.field === "mandate_daily_max");
8523
9202
  if (entry && entry.limit !== null) {
8524
9203
  checks.push(`Proposals today: ${entry.current} of ${entry.limit}`);
@@ -8528,7 +9207,7 @@ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
8528
9207
  }
8529
9208
  function resolveTargetProfile(input) {
8530
9209
  if (typeof input !== "string" || !input.trim()) return void 0;
8531
- const profile = getProfile6(input.trim());
9210
+ const profile = getProfile7(input.trim());
8532
9211
  if (!profile) return void 0;
8533
9212
  return { profile, shortName: shortName(profile.id) };
8534
9213
  }
@@ -8679,7 +9358,7 @@ Read each topic with get_guide(topic) before doing that step.`);
8679
9358
  return { ...builtinText(`Unknown topic "${topic}". Topics: ${guides.map((g) => g.topic).join(", ") || "none installed"}.`), isError: true };
8680
9359
  }
8681
9360
  const body = fillGenerated(guide.body.trim(), {
8682
- limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile6(s.profile) }))),
9361
+ limits: () => limitsSection(systems.map((s) => ({ id: s.id, profile: getProfile7(s.profile) }))),
8683
9362
  package: () => packageSection(
8684
9363
  deps.integrationManager.getAllTools().find((t) => !deps.integrationManager.isBuiltin(t.integrationId) && t.originalName === "load_simulation")
8685
9364
  )
@@ -9166,6 +9845,32 @@ app.post("/internal/preview", internalOnly, async (req, res) => {
9166
9845
  });
9167
9846
  res.json(result);
9168
9847
  });
9848
+ app.post("/internal/request-context", internalOnly, async (req, res) => {
9849
+ const { tool, toolArgs } = req.body;
9850
+ if (!tool || typeof toolArgs !== "object" || toolArgs === null) {
9851
+ res.status(400).json({ error: "Missing tool or toolArgs" });
9852
+ return;
9853
+ }
9854
+ const sep = tool.indexOf("__");
9855
+ if (sep < 0) {
9856
+ res.status(400).json({ error: `Invalid tool name: ${tool}` });
9857
+ return;
9858
+ }
9859
+ const integrationId = tool.slice(0, sep);
9860
+ const toolName = tool.slice(sep + 2);
9861
+ const manifest = getManifest(integrationId);
9862
+ const actionGating = manifest?.toolGating?.overrides?.[toolName];
9863
+ const configuredConnectorIds = new Set(integrationRegistry.getAll().map((c) => c.id));
9864
+ const result = await buildRequestContext(
9865
+ integrationManager,
9866
+ state.executionJournal,
9867
+ integrationId,
9868
+ toolArgs,
9869
+ actionGating ?? void 0,
9870
+ configuredConnectorIds
9871
+ );
9872
+ res.json(result);
9873
+ });
9169
9874
  app.post("/internal/add-integration", internalOnly, async (req, res) => {
9170
9875
  try {
9171
9876
  const config = req.body;