@suveren/gateway 0.23.2 → 0.25.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.
@@ -3257,7 +3257,10 @@ function capText(text, max = PREVIEW_MESSAGE_CAP) {
3257
3257
  if (text === void 0) return void 0;
3258
3258
  return text.length > max ? text.slice(0, max) : text;
3259
3259
  }
3260
- function getToolPreviewConfig(integrationId, toolName) {
3260
+ function getToolPreviewConfig(integrationManager2, integrationId, toolName) {
3261
+ if (integrationManager2.isBuiltin(integrationId)) {
3262
+ return integrationManager2.getBuiltinToolPreview(integrationId, toolName);
3263
+ }
3261
3264
  const manifest = getManifest(integrationId);
3262
3265
  const overrides = manifest?.toolGating?.overrides ?? {};
3263
3266
  const entry = overrides[toolName];
@@ -3322,7 +3325,8 @@ function hashPreviewBody(body) {
3322
3325
  return computeContentHash2({ kind: "text", version: "1" }, body.text ?? "");
3323
3326
  }
3324
3327
  async function computeSubmissionPreviewHash(integrationManager2, integrationId, toolName, actionArgs) {
3325
- const preview = getToolPreviewConfig(integrationId, toolName);
3328
+ if (integrationManager2.isBuiltin(integrationId)) return void 0;
3329
+ const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
3326
3330
  if (!preview || preview.version) return void 0;
3327
3331
  const mapped = mapPreviewArgs(preview, actionArgs);
3328
3332
  if (previewHasNoTarget(preview, mapped)) return void 0;
@@ -3331,7 +3335,7 @@ async function computeSubmissionPreviewHash(integrationManager2, integrationId,
3331
3335
  return hashPreviewBody(read.body);
3332
3336
  }
3333
3337
  async function previewChangedSinceSubmission(integrationManager2, integrationId, toolName, actionArgs, previousHash) {
3334
- const preview = getToolPreviewConfig(integrationId, toolName);
3338
+ const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
3335
3339
  if (!preview) return true;
3336
3340
  const mapped = mapPreviewArgs(preview, actionArgs);
3337
3341
  const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
@@ -3339,7 +3343,7 @@ async function previewChangedSinceSubmission(integrationManager2, integrationId,
3339
3343
  return hashPreviewBody(read.body) !== previousHash;
3340
3344
  }
3341
3345
  async function buildInternalPreview(integrationManager2, integrationId, toolName, actionArgs) {
3342
- const preview = getToolPreviewConfig(integrationId, toolName);
3346
+ const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
3343
3347
  if (!preview) return { status: "none" };
3344
3348
  const mappedArgs = mapPreviewArgs(preview, actionArgs);
3345
3349
  if (previewHasNoTarget(preview, mappedArgs)) return { status: "none", reason: "no_target" };
@@ -3815,8 +3819,8 @@ function applyMapping(m, value, execution) {
3815
3819
  execution[m.field] = typeof value === "number" ? value : String(value);
3816
3820
  }
3817
3821
  }
3818
- function profileMatches(profileId, shortName) {
3819
- return profileId === shortName || profileId.includes("/" + shortName + "@") || profileId.endsWith("/" + shortName);
3822
+ function profileMatches(profileId, shortName2) {
3823
+ return profileId === shortName2 || profileId.includes("/" + shortName2 + "@") || profileId.endsWith("/" + shortName2);
3820
3824
  }
3821
3825
  function wireExecutionContext(profileId, execution) {
3822
3826
  const profile = getProfile2(profileId);
@@ -3854,6 +3858,11 @@ function zeroCappedBound(profileId, bounds, actionType) {
3854
3858
  }
3855
3859
  return null;
3856
3860
  }
3861
+ function toolAlwaysListed(tool, isBuiltin) {
3862
+ if (tool.internal || !tool.gating?.profile) return false;
3863
+ if (tool.gating.hideUnlessAuthorized || tool.gating.category === "disabled") return false;
3864
+ return !isBuiltin(tool.integrationId);
3865
+ }
3857
3866
  function toolIsAuthorizedForDisplay(tool, matchingAuths) {
3858
3867
  if (!tool.gating?.hideUnlessAuthorized) return true;
3859
3868
  if (matchingAuths.length === 0) return false;
@@ -3933,6 +3942,12 @@ function recordAsRefusal(state2, tool, authorizationId, err) {
3933
3942
  });
3934
3943
  }
3935
3944
  function createGatedToolHandler(tool, integrationManager2, state2) {
3945
+ if (tool.internal) {
3946
+ return async () => ({
3947
+ content: [{ type: "text", text: `Refused: "${tool.namespacedName}" is internal and cannot be called directly. No ticket was requested.` }],
3948
+ isError: true
3949
+ });
3950
+ }
3936
3951
  if (isSimulationMode() && !integrationManager2.isSimulationSafe(tool.integrationId)) {
3937
3952
  return async () => {
3938
3953
  recordBlocked(state2, tool, {
@@ -4023,7 +4038,7 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4023
4038
  return {
4024
4039
  content: [{
4025
4040
  type: "text",
4026
- text: refusals.length > 0 ? `Refused: ${reasons.charAt(0).toUpperCase() + reasons.slice(1)} No ticket was requested.` : `No active authorization matching profile "${profile}". A decision owner must grant authority via the Authority UI before this tool can be used.`
4041
+ text: refusals.length > 0 ? `Refused: ${reasons.charAt(0).toUpperCase() + reasons.slice(1)} No ticket was requested.` : noMandateRefusal(tool.namespacedName, profile)
4027
4042
  }],
4028
4043
  isError: true
4029
4044
  };
@@ -4635,20 +4650,21 @@ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
4635
4650
  }
4636
4651
  });
4637
4652
  }
4638
- function buildProxiedToolDescription(tool, state2) {
4653
+ function noMandateRefusal(toolName, profile) {
4654
+ return `Refused: no mandate covers ${toolName} (${shortProfile(profile)}). Nothing was done. Ask the person to grant a ${shortProfile(profile)} mandate in the gateway, then try again. No ticket was requested.`;
4655
+ }
4656
+ function shortProfile(profileId) {
4657
+ const parts = profileId.replace(/@.*$/, "").split("/");
4658
+ return parts[parts.length - 1];
4659
+ }
4660
+ function buildProxiedToolDescription(tool) {
4639
4661
  if (!tool.gating || !tool.gating.profile) {
4640
4662
  return `[Suveren: no gating config] ${tool.description}`;
4641
4663
  }
4642
4664
  const profile = tool.gating.profile;
4643
- const auths = state2.getEnrichedAuthorizations();
4644
- const hasAuth = auths.some(
4645
- (a) => a.complete && profileMatches(a.profileId, profile)
4646
- );
4647
- if (!hasAuth) {
4648
- return `[Suveren: ${profile} \u2014 no active authorization] ${tool.description}`;
4649
- }
4665
+ const condition = `works only under a ${shortProfile(profile)} mandate, check list-authorizations`;
4650
4666
  if (tool.gating.category === "read") {
4651
- return `[Suveren: ${profile} \u2014 read] ${tool.description}`;
4667
+ return `[Suveren: ${profile} \u2014 read; ${condition}] ${tool.description}`;
4652
4668
  }
4653
4669
  const parts = [];
4654
4670
  if (tool.gating.staticExecution?.action_type) {
@@ -4661,7 +4677,7 @@ function buildProxiedToolDescription(tool, state2) {
4661
4677
  parts.push(`${mappedFields.join(", ")} checked`);
4662
4678
  }
4663
4679
  const tag = parts.length > 0 ? parts.join(", ") : "gated";
4664
- return `[Suveren: ${profile} \u2014 ${tag}] ${tool.description}`;
4680
+ return `[Suveren: ${profile} \u2014 ${tag}; ${condition}] ${tool.description}`;
4665
4681
  }
4666
4682
 
4667
4683
  // src/lib/agent-visibility.ts
@@ -4697,6 +4713,7 @@ function countToolsByGating(profileId, integrationManager2) {
4697
4713
  let gated = 0;
4698
4714
  let readOnly = 0;
4699
4715
  for (const tool of allTools) {
4716
+ if (tool.internal) continue;
4700
4717
  if (!tool.gating || !tool.gating.profile) {
4701
4718
  readOnly++;
4702
4719
  } else if (tool.gating.profile === profileId || tool.gating.profile === shortProfileName(profileId)) {
@@ -4716,7 +4733,8 @@ function buildMandateBrief(opts) {
4716
4733
  "You are connected to Suveren \u2014 the gateway that gates every privileged tool call you make.",
4717
4734
  "Suveren implements the bounded-authority model from the open Human Agency Protocol (HAP).",
4718
4735
  "You have bounded authorities granted by human decision owners.",
4719
- "You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them."
4736
+ "You MUST stay within these bounds \u2014 the Gatekeeper will reject actions that exceed them.",
4737
+ "Tools are listed even where no mandate covers them yet. If a call is refused because no mandate covers it, tell the person and ask them to grant one \u2014 do not retry it or work around it with another tool."
4720
4738
  ];
4721
4739
  if (opts.asVersionRefusal) {
4722
4740
  lines.push("");
@@ -4753,14 +4771,14 @@ function buildMandateBrief(opts) {
4753
4771
  for (const auth of active) {
4754
4772
  const earliestExpiry = Math.min(...auth.attestations.map((a) => a.expiresAt));
4755
4773
  const remainingMin = Math.max(0, Math.round((earliestExpiry - now) / 60));
4756
- const shortName = shortProfileName(auth.profileId);
4774
+ const shortName2 = shortProfileName(auth.profileId);
4757
4775
  const version = auth.profileId.match(/@(.+)$/)?.[1] ?? "";
4758
- const shortId = version ? `${shortName}@${version}` : shortName;
4776
+ const shortId = version ? `${shortName2}@${version}` : shortName2;
4759
4777
  const boundsDesc = Object.entries(auth.frame).filter(([key]) => key !== "profile" && key !== "path").map(([key, value]) => `${key}:${value}`).join(" \xB7 ");
4760
4778
  const parts = [];
4761
4779
  if (boundsDesc) parts.push(boundsDesc);
4762
4780
  if (executionLog) {
4763
- const profile = getProfile3(auth.profileId) ?? getProfile3(shortName);
4781
+ const profile = getProfile3(auth.profileId) ?? getProfile3(shortName2);
4764
4782
  const consumption = getConsumptionState(auth, executionLog, profile);
4765
4783
  const compact = formatConsumptionCompact(consumption);
4766
4784
  if (compact) parts.push(compact);
@@ -4769,7 +4787,7 @@ function buildMandateBrief(opts) {
4769
4787
  if (gated > 0 || readOnly > 0) parts.push(`${gated} gated tools`);
4770
4788
  parts.push(`${remainingMin} min remaining`);
4771
4789
  lines.push(`[${shortId}] ${parts.join(" \xB7 ")}`);
4772
- lines.push(` \u2192 list-authorizations(domain: "${shortName}") for intent & details`);
4790
+ lines.push(` \u2192 list-authorizations(domain: "${shortName2}") for intent & details`);
4773
4791
  lines.push("");
4774
4792
  }
4775
4793
  }
@@ -4807,15 +4825,15 @@ function shortProfileName2(profileId) {
4807
4825
  function buildCapabilityMap(profileId, integrationManager2) {
4808
4826
  if (!integrationManager2) return "";
4809
4827
  const allTools = integrationManager2.getAllTools();
4810
- const shortName = shortProfileName2(profileId);
4828
+ const shortName2 = shortProfileName2(profileId);
4811
4829
  const gated = [];
4812
4830
  const readOnly = [];
4813
4831
  const undescribed = [];
4814
4832
  for (const tool of allTools) {
4815
- if (!tool.gating || !tool.gating.profile) {
4833
+ if (tool.internal || !tool.gating || !tool.gating.profile) {
4816
4834
  continue;
4817
4835
  }
4818
- if (!profileMatches(tool.gating.profile, shortName) && tool.gating.profile !== profileId) {
4836
+ if (!profileMatches(tool.gating.profile, shortName2) && tool.gating.profile !== profileId) {
4819
4837
  continue;
4820
4838
  }
4821
4839
  const gating = tool.gating;
@@ -4853,6 +4871,20 @@ function buildCapabilityMap(profileId, integrationManager2) {
4853
4871
  }
4854
4872
  return lines.join("\n");
4855
4873
  }
4874
+ function listedWithoutMandate(authorizations, integrationManager2) {
4875
+ if (!integrationManager2) return [];
4876
+ const isBuiltin = (id) => integrationManager2.isBuiltin?.(id) ?? false;
4877
+ const byProfile = /* @__PURE__ */ new Map();
4878
+ for (const tool of integrationManager2.getAllTools()) {
4879
+ if (!toolAlwaysListed(tool, isBuiltin)) continue;
4880
+ const profile = tool.gating.profile;
4881
+ if (authorizations.some((a) => a.complete && profileMatches(a.profileId, profile))) continue;
4882
+ const short = shortProfileName2(profile);
4883
+ byProfile.set(short, [...byProfile.get(short) ?? [], tool.namespacedName]);
4884
+ }
4885
+ return [...byProfile.entries()].map(([profile, tools]) => ` ${profile}: ${tools.join(", ")}`);
4886
+ }
4887
+ var LISTED_WITHOUT_MANDATE_HEADING = "Listed but not usable yet \u2014 no mandate covers these; every call is refused until the person grants one:";
4856
4888
  function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4857
4889
  return async (args) => {
4858
4890
  if (!state2.spClient.isUnlocked()) {
@@ -4861,18 +4893,16 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4861
4893
  const authorizations = agentVisibleAuthorizations(state2.getEnrichedAuthorizations(), integrationManager2);
4862
4894
  const now = Math.floor(Date.now() / 1e3);
4863
4895
  const domain = args?.domain;
4896
+ const notYet = listedWithoutMandate(authorizations, integrationManager2);
4864
4897
  if (authorizations.length === 0) {
4865
- return {
4866
- content: [{
4867
- type: "text",
4868
- text: "No active authorizations. A decision owner must grant authority via the Authority UI."
4869
- }]
4870
- };
4898
+ const text = ["No active authorizations \u2014 no tool is usable yet. Ask the person to grant a mandate in the gateway."];
4899
+ if (notYet.length > 0) text.push("", LISTED_WITHOUT_MANDATE_HEADING, ...notYet);
4900
+ return { content: [{ type: "text", text: text.join("\n") }] };
4871
4901
  }
4872
4902
  if (domain) {
4873
4903
  const matching = authorizations.filter((auth) => {
4874
- const shortName = shortProfileName2(auth.profileId);
4875
- return shortName === domain || profileMatches(auth.profileId, domain);
4904
+ const shortName2 = shortProfileName2(auth.profileId);
4905
+ return shortName2 === domain || profileMatches(auth.profileId, domain);
4876
4906
  });
4877
4907
  if (matching.length === 0) {
4878
4908
  return {
@@ -4903,8 +4933,8 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4903
4933
  }
4904
4934
  } catch {
4905
4935
  }
4906
- const shortName = shortProfileName2(auth.profileId);
4907
- const profile = getProfile4(auth.profileId) ?? getProfile4(shortName);
4936
+ const shortName2 = shortProfileName2(auth.profileId);
4937
+ const profile = getProfile4(auth.profileId) ?? getProfile4(shortName2);
4908
4938
  if (profile) {
4909
4939
  const consumption = getConsumptionState(auth, state2.executionLog, profile);
4910
4940
  const consumptionText = formatConsumptionFull(consumption);
@@ -4963,8 +4993,8 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4963
4993
  if ((auth.deferredCommitmentDomains ?? []).length > 0) {
4964
4994
  lines.push(" Mode: review (each action requires your approval)");
4965
4995
  }
4966
- const shortName = shortProfileName2(auth.profileId);
4967
- const profile = getProfile4(auth.profileId) ?? getProfile4(shortName);
4996
+ const shortName2 = shortProfileName2(auth.profileId);
4997
+ const profile = getProfile4(auth.profileId) ?? getProfile4(shortName2);
4968
4998
  if (profile) {
4969
4999
  const consumption = getConsumptionState(auth, state2.executionLog, profile);
4970
5000
  const compact = formatConsumptionCompact(consumption);
@@ -4988,7 +5018,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4988
5018
  output.push("");
4989
5019
  }
4990
5020
  if (active.length > 0) {
4991
- output.push("Active authorizations:");
5021
+ output.push("Active authorizations (usable now):");
4992
5022
  output.push(...active);
4993
5023
  }
4994
5024
  if (pending.length > 0) {
@@ -4996,6 +5026,11 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
4996
5026
  output.push("Pending (missing owners):");
4997
5027
  output.push(...pending);
4998
5028
  }
5029
+ if (notYet.length > 0) {
5030
+ if (output.length > 0) output.push("");
5031
+ output.push(LISTED_WITHOUT_MANDATE_HEADING);
5032
+ output.push(...notYet);
5033
+ }
4999
5034
  return {
5000
5035
  content: [{
5001
5036
  type: "text",
@@ -5603,6 +5638,7 @@ function createMcpServer(state2, integrationManager2) {
5603
5638
  }
5604
5639
  }
5605
5640
  for (const tool of allTools) {
5641
+ if (tool.internal) continue;
5606
5642
  const existing = proxiedTools.get(tool.namespacedName);
5607
5643
  if (existing) {
5608
5644
  if (existing.tool === tool) continue;
@@ -5611,7 +5647,7 @@ function createMcpServer(state2, integrationManager2) {
5611
5647
  }
5612
5648
  const handler = createGatedToolHandler(tool, integrationManager2, state2);
5613
5649
  const zodShape = jsonSchemaToZodShape(tool.inputSchema);
5614
- const description = buildProxiedToolDescription(tool, state2);
5650
+ const description = buildProxiedToolDescription(tool);
5615
5651
  try {
5616
5652
  const registered = server.registerTool(
5617
5653
  tool.namespacedName,
@@ -5629,10 +5665,11 @@ function createMcpServer(state2, integrationManager2) {
5629
5665
  }
5630
5666
  function refreshTools() {
5631
5667
  const auths = state2.getEnrichedAuthorizations();
5668
+ const isBuiltin = (id) => integrationManager2?.isBuiltin?.(id) ?? false;
5632
5669
  for (const [, { tool, registered }] of proxiedTools) {
5633
- const description = buildProxiedToolDescription(tool, state2);
5634
- registered.update({ description });
5635
- if (tool.gating?.profile) {
5670
+ if (toolAlwaysListed(tool, isBuiltin)) {
5671
+ registered.enable();
5672
+ } else if (tool.gating?.profile) {
5636
5673
  const matchingAuths = auths.filter(
5637
5674
  (a) => a.complete && profileMatches(a.profileId, tool.gating.profile)
5638
5675
  );
@@ -6443,6 +6480,8 @@ var IntegrationManager = class {
6443
6480
  integrationId: def.id,
6444
6481
  description: tool.description,
6445
6482
  inputSchema: withoutBlockedArgs(tool.inputSchema, gating?.blockedArgs),
6483
+ ...tool.outputSchema ? { outputSchema: tool.outputSchema } : {},
6484
+ ...tool.internal ? { internal: true } : {},
6446
6485
  gating
6447
6486
  };
6448
6487
  });
@@ -6488,19 +6527,39 @@ var IntegrationManager = class {
6488
6527
  }
6489
6528
  /**
6490
6529
  * The built-in's own refusal for this call, checked before the gate (no proposal,
6491
- * no ticket): `simulationOnly` outside simulation mode, then the tool's `validate`.
6492
- * Null for connector tools and for calls that may proceed.
6530
+ * no ticket): an `internal` tool (defence in depth — `index.ts` already never
6531
+ * registers one with the MCP server, so an agent cannot reach this in the first
6532
+ * place), `simulationOnly` outside simulation mode (group-level OR this one
6533
+ * tool's — DL1: a group may now mix test-setup-only tools with ones that work
6534
+ * outside simulation), then the tool's `validate`. Null for connector tools and
6535
+ * for calls that may proceed.
6493
6536
  */
6494
6537
  async precheckBuiltin(tool, args) {
6495
6538
  const builtin = this.builtins.get(tool.integrationId);
6496
6539
  if (!builtin) return null;
6497
- if (builtin.def.simulationOnly && !isSimulationMode()) {
6540
+ const def = builtin.def.tools.find((t) => t.name === tool.originalName);
6541
+ if (def?.internal) {
6542
+ return `Refused: "${tool.namespacedName}" is internal and cannot be called directly. No ticket was requested.`;
6543
+ }
6544
+ if ((builtin.def.simulationOnly || def?.simulationOnly) && !isSimulationMode()) {
6498
6545
  return `Refused: "${tool.namespacedName}" is not available. No ticket was requested.`;
6499
6546
  }
6500
- const def = builtin.def.tools.find((t) => t.name === tool.originalName);
6501
6547
  const refusal = await def?.validate?.(args);
6502
6548
  return refusal ? `Refused: ${refusal} No ticket was requested.` : null;
6503
6549
  }
6550
+ /**
6551
+ * A built-in's own declared preview (AU3 — `BuiltinTool`/toolGating override's
6552
+ * `preview`, same shape a manifest declares for a connector tool). `preview.ts`'s
6553
+ * `getToolPreviewConfig` reads a connector's declared preview from its loaded
6554
+ * manifest (`getManifest`); a built-in has no manifest, so it reads it from here
6555
+ * instead — same override object, widened the same way `resolveToolGating`
6556
+ * already widens it for `approvalView`/`hideUnlessAuthorized`.
6557
+ */
6558
+ getBuiltinToolPreview(integrationId, toolName) {
6559
+ const builtin = this.builtins.get(integrationId);
6560
+ const override = builtin?.def.toolGating.overrides?.[toolName];
6561
+ return override?.preview;
6562
+ }
6504
6563
  /**
6505
6564
  * Why this tool's integration refuses the held mandate `auth`, or null when
6506
6565
  * it accepts it (always null for connectors and built-ins that declare no
@@ -6522,7 +6581,14 @@ var IntegrationManager = class {
6522
6581
  }
6523
6582
  try {
6524
6583
  const result = await tool.handler(args);
6525
- return { content: result.content.map((c) => ({ type: c.type, text: c.text })), isError: result.isError };
6584
+ return {
6585
+ content: result.content.map((c) => ({ type: c.type, text: c.text })),
6586
+ isError: result.isError,
6587
+ // DL1: a built-in's internal preview tools (describe_mandate_proposal /
6588
+ // describe_brief_proposal) return structuredContent, same as a connector's
6589
+ // declared-output-schema read — see the field's doc comment above.
6590
+ ...result.structuredContent ? { structuredContent: result.structuredContent } : {}
6591
+ };
6526
6592
  } catch (err) {
6527
6593
  const message = err instanceof Error ? err.message : String(err);
6528
6594
  return { content: [{ type: "text", text: `Tool call failed: ${message}` }], isError: true };
@@ -7117,9 +7183,9 @@ function loadProfiles(profilesDir) {
7117
7183
  try {
7118
7184
  const profile = JSON.parse(readFileSync18(profilePath, "utf-8"));
7119
7185
  registerProfile(profileId, profile);
7120
- const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
7121
- if (shortName && shortName !== profileId) {
7122
- registerProfile(shortName, profile);
7186
+ const shortName2 = profileId.split("/").pop()?.replace(/@.*$/, "");
7187
+ if (shortName2 && shortName2 !== profileId) {
7188
+ registerProfile(shortName2, profile);
7123
7189
  }
7124
7190
  loaded++;
7125
7191
  } catch (err) {
@@ -7151,6 +7217,9 @@ function warnNoProfiles(reason) {
7151
7217
  function builtinText(text) {
7152
7218
  return { content: [{ type: "text", text }] };
7153
7219
  }
7220
+ function builtinStructured(structuredContent) {
7221
+ return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent };
7222
+ }
7154
7223
 
7155
7224
  // src/lib/builtins/report.ts
7156
7225
  var MAX_REPORT_HTML_BYTES = 512 * 1024;
@@ -7579,6 +7648,45 @@ function fillGenerated(body, sections) {
7579
7648
  return body.replace(/<!--\s*generated:\s*([a-z-]+)\s*-->/g, (m, key) => sections[key] ? sections[key]() : m);
7580
7649
  }
7581
7650
 
7651
+ // src/lib/unified-diff.ts
7652
+ function unifiedLineDiff(before, after) {
7653
+ const a = before.split("\n");
7654
+ const b = after.split("\n");
7655
+ const n = a.length;
7656
+ const m = b.length;
7657
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
7658
+ for (let i2 = n - 1; i2 >= 0; i2--) {
7659
+ for (let j2 = m - 1; j2 >= 0; j2--) {
7660
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
7661
+ }
7662
+ }
7663
+ const lines = [];
7664
+ let i = 0;
7665
+ let j = 0;
7666
+ while (i < n && j < m) {
7667
+ if (a[i] === b[j]) {
7668
+ lines.push(a[i]);
7669
+ i++;
7670
+ j++;
7671
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
7672
+ lines.push(`-${a[i]}`);
7673
+ i++;
7674
+ } else {
7675
+ lines.push(`+${b[j]}`);
7676
+ j++;
7677
+ }
7678
+ }
7679
+ while (i < n) {
7680
+ lines.push(`-${a[i]}`);
7681
+ i++;
7682
+ }
7683
+ while (j < m) {
7684
+ lines.push(`+${b[j]}`);
7685
+ j++;
7686
+ }
7687
+ return lines.join("\n");
7688
+ }
7689
+
7582
7690
  // src/lib/builtins/setup.ts
7583
7691
  import { getProfile as getProfile6 } from "@hap/core";
7584
7692
  var DELEGATION_PROFILE = "delegation";
@@ -7587,7 +7695,12 @@ var DELEGATION_INTENT_HINT = [
7587
7695
  "",
7588
7696
  "Goal \u2014 Follow the setup guide: interview me about the company, the team and the work; prepare test data from real cases, renamed to .example names; propose the mandates and the working instructions (agent brief) that the AI doing the work will need.",
7589
7697
  "",
7590
- "Watch out \u2014 You only propose; nothing happens until I approve it in the Suveren Gateway. Ask, don't guess: never invent a rule, a limit or a step. One proposal at a time. Steps customers would see start with my review. Never use real names or customer data \u2014 proposals are stored readable on the Suveren server. Never give the working AI a reporting mandate during a test run. Keep the setup and the working AI in separate conversations."
7698
+ "Watch out \u2014 You only propose; nothing happens until I approve it in the Suveren Gateway. Ask, don't guess: never invent a rule, a limit or a step. One proposal at a time. Steps customers would see start with my review. Never use real names or customer data \u2014 proposals are stored readable on the Suveren server. Never give the working AI a reporting mandate during a test run. Keep the setup and the working AI in separate conversations.",
7699
+ "",
7700
+ // DL1 — outside simulation, the same mandate also covers ongoing, real-system
7701
+ // use: the Scope above (which profiles, which mode) is what actually bounds
7702
+ // the AI from here, not simulation mode. Plain words, per the brief.
7703
+ "Outside simulation \u2014 once you have connected real systems: the AI proposes mandates and its own working instructions (agent brief) for the profiles you allow above; you approve each one before it takes effect. Delegation itself can never be proposed."
7591
7704
  ].join("\n");
7592
7705
  function briefRefusal(args) {
7593
7706
  if (typeof args.content !== "string" || args.content.trim() === "") return "`content` must be the complete new brief (markdown text).";
@@ -7611,6 +7724,149 @@ function connectedSystems(im) {
7611
7724
  }
7612
7725
  return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
7613
7726
  }
7727
+ function shortName(idOrName) {
7728
+ const stripped = idOrName.replace(/@.*$/, "").split("/").pop() ?? idOrName;
7729
+ return stripped.toLowerCase();
7730
+ }
7731
+ function versionOf(id) {
7732
+ return id.split("@")[1] ?? "";
7733
+ }
7734
+ function delegationItselfRefusal(args) {
7735
+ const requested = typeof args.profile === "string" ? args.profile : "";
7736
+ if (shortName(requested) === DELEGATION_PROFILE) {
7737
+ return "Delegation can never be proposed \u2014 the AI cannot widen its own delegation.";
7738
+ }
7739
+ return void 0;
7740
+ }
7741
+ function delegationMandateRefusal(auth) {
7742
+ if (isSimulationMode()) return void 0;
7743
+ const profile = getProfile6(auth.profileId);
7744
+ if (profile?.scopeSchema?.fields?.allowed_profiles) return void 0;
7745
+ 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.`;
7746
+ }
7747
+ function formatUnit(unit) {
7748
+ if (!unit || unit === "count") return "";
7749
+ if (unit === "minutes") return "min";
7750
+ if (unit === "hours") return "hr";
7751
+ if (unit === "days") return "days";
7752
+ if (unit === "percent") return "%";
7753
+ if (unit.startsWith("currency:")) return unit.slice("currency:".length);
7754
+ return unit;
7755
+ }
7756
+ function formatBoundValue2(value, unit) {
7757
+ if (typeof value !== "number" && typeof value !== "string") return String(value);
7758
+ const u = formatUnit(unit);
7759
+ if (!u) return String(value);
7760
+ if (unit === "percent") return `${value}${u}`;
7761
+ if (unit?.startsWith("currency:")) return `${u} ${value}`;
7762
+ return `${value} ${u}`;
7763
+ }
7764
+ function labeledRecord(values, fields) {
7765
+ const out = {};
7766
+ if (!values || typeof values !== "object" || Array.isArray(values)) return out;
7767
+ for (const [key, value] of Object.entries(values)) {
7768
+ const def = fields?.[key];
7769
+ out[def?.displayName ?? key] = formatBoundValue2(value, def?.unit);
7770
+ }
7771
+ return out;
7772
+ }
7773
+ function formatValidity(args, profile) {
7774
+ const hours = typeof args.duration_hours === "number" && args.duration_hours > 0 ? args.duration_hours : void 0;
7775
+ const totalSeconds = hours !== void 0 ? Math.round(hours * 3600) : profile.ttl?.default ?? 0;
7776
+ if (totalSeconds <= 0) return "unknown";
7777
+ if (totalSeconds % 86400 === 0) {
7778
+ const days = totalSeconds / 86400;
7779
+ return `${days} day${days === 1 ? "" : "s"}`;
7780
+ }
7781
+ if (totalSeconds % 3600 === 0) {
7782
+ const hrs = totalSeconds / 3600;
7783
+ return `${hrs} hour${hrs === 1 ? "" : "s"}`;
7784
+ }
7785
+ return `${Math.round(totalSeconds / 3600)} hours`;
7786
+ }
7787
+ function delegationChecks(deps, args, targetShortName, targetDisplayName) {
7788
+ const governing = deps.state.getEnrichedAuthorizations().filter((a) => a.complete && shortName(a.profileId) === DELEGATION_PROFILE);
7789
+ const listed = (ctx, field) => typeof ctx?.[field] === "string" ? ctx[field].split(",").map((s) => s.trim().toLowerCase()).filter(Boolean) : [];
7790
+ const profileOk = governing.some((g) => listed(g.context, "allowed_profiles").includes(targetShortName.toLowerCase()));
7791
+ const mode = typeof args.mode === "string" ? args.mode : "";
7792
+ const modeOk = mode !== "" && governing.some((g) => listed(g.context, "allowed_modes").includes(mode.toLowerCase()));
7793
+ const checks = [
7794
+ profileOk ? `${targetDisplayName} is allowed by your Delegation mandate` : `${targetDisplayName} is not confirmed as allowed by an active Delegation mandate`
7795
+ ];
7796
+ checks.push(
7797
+ mode === "" ? "No mode was given" : modeOk ? `${mode} \u2014 within the modes your Delegation mandate allows` : `${mode} is not among the modes your Delegation mandate allows`
7798
+ );
7799
+ const first = governing[0];
7800
+ if (first) {
7801
+ const delegationProfile = getProfile6(first.profileId);
7802
+ const entry = getConsumptionState(first, deps.state.executionLog, delegationProfile).find((e) => e.field === "mandate_daily_max");
7803
+ if (entry && entry.limit !== null) {
7804
+ checks.push(`Proposals today: ${entry.current} of ${entry.limit}`);
7805
+ }
7806
+ }
7807
+ return checks;
7808
+ }
7809
+ function resolveTargetProfile(input) {
7810
+ if (typeof input !== "string" || !input.trim()) return void 0;
7811
+ const profile = getProfile6(input.trim());
7812
+ if (!profile) return void 0;
7813
+ return { profile, shortName: shortName(profile.id) };
7814
+ }
7815
+ var CREATE_MANDATE_INPUT_SCHEMA = {
7816
+ type: "object",
7817
+ properties: {
7818
+ team: { type: "string", description: 'Team name or id, or "personal". Omit to use the workspace of your Delegation mandate.' },
7819
+ // Short name only (e.g. "sales") — a full profile id is refused here, at input
7820
+ // validation, before the tool ever runs (DL1 Interface contract).
7821
+ profile: { type: "string", pattern: "^[a-z][a-z0-9-]*$", description: 'Profile short name, e.g. "sales" (newest version).' },
7822
+ limits: { type: "object", description: "The limits (bounds) by field name, as the profile defines them." },
7823
+ scope: { type: "object", description: "The scope (context) by field name, as the profile defines it." },
7824
+ intent: { type: "string", description: "Why, goal and watch-outs \u2014 signed with the mandate (at most 2000 characters)." },
7825
+ mode: { type: "string", enum: ["review", "automatic"], description: "review = each action needs approval; automatic = within the limits." },
7826
+ duration_hours: { type: "number", description: "How long the mandate is valid, in hours. Omit for the profile default." },
7827
+ title: { type: "string", description: "A short name for the mandate." },
7828
+ ticket_id: {
7829
+ type: "string",
7830
+ description: "Authorization reference for this call, set by the governing gateway \u2014 agents do not set this."
7831
+ }
7832
+ },
7833
+ required: ["profile", "limits", "intent", "mode"]
7834
+ };
7835
+ var SET_AGENT_BRIEF_INPUT_SCHEMA = {
7836
+ type: "object",
7837
+ properties: {
7838
+ content: { type: "string", description: "The complete new agent brief, in markdown (at most 16 KB)." },
7839
+ ticket_id: {
7840
+ type: "string",
7841
+ description: "Authorization reference for this call, set by the governing gateway \u2014 agents do not set this."
7842
+ }
7843
+ },
7844
+ required: ["content"]
7845
+ };
7846
+ var DESCRIBE_MANDATE_PROPOSAL_OUTPUT_SCHEMA = {
7847
+ type: "object",
7848
+ properties: {
7849
+ proposal: { type: "string", title: "Proposal" },
7850
+ profile_version: { type: "string", title: "Version" },
7851
+ workspace: { type: "string", title: "Workspace" },
7852
+ mode: { type: "string", title: "Mode" },
7853
+ valid: { type: "string", title: "Valid" },
7854
+ limits: { type: "object", title: "Limits" },
7855
+ scope: { type: "object", title: "Scope" },
7856
+ title: { type: ["string", "null"], title: "Title" },
7857
+ intent: { type: "string", title: "Intent", description: "Written by the AI \u2014 becomes your words when you sign" },
7858
+ checks: { type: "array", title: "Checks", items: { type: "string" } }
7859
+ },
7860
+ required: ["proposal", "profile_version", "workspace", "mode", "valid", "limits", "scope", "title", "intent", "checks"]
7861
+ };
7862
+ var DESCRIBE_BRIEF_PROPOSAL_OUTPUT_SCHEMA = {
7863
+ type: "object",
7864
+ properties: {
7865
+ change: { type: "string", title: "Change", contentMediaType: "text/x-diff" },
7866
+ words: { type: "number", title: "Words" }
7867
+ },
7868
+ required: ["change", "words"]
7869
+ };
7614
7870
  function setupBuiltin(deps) {
7615
7871
  return {
7616
7872
  id: "setup",
@@ -7619,18 +7875,24 @@ function setupBuiltin(deps) {
7619
7875
  // name they'll see everywhere else (Mandates page, dashboard first-run
7620
7876
  // card), not the internal "test setup" framing. Description is unchanged.
7621
7877
  name: "Delegation",
7622
- description: "Your AI sets up a test of itself: it reads the setup guides and proposes its agent brief and its mandates. Every proposal waits for your approval. Simulation mode only.",
7878
+ description: "Your AI proposes mandates and its agent brief \u2014 in a test (simulation mode) and, within the profiles and modes you allow, on your real systems too. Every proposal waits for your approval.",
7623
7879
  intentHint: DELEGATION_INTENT_HINT,
7624
7880
  profile: DELEGATION_PROFILE,
7625
7881
  simulation: true,
7626
- simulationOnly: true,
7882
+ // DL1: no longer simulationOnly at the GROUP level — create_mandate and
7883
+ // set_agent_brief now work outside simulation too (decision 1); only
7884
+ // get_guide keeps the per-tool flag below (decision 2).
7885
+ mandateRefusal: delegationMandateRefusal,
7627
7886
  toolGating: {
7628
7887
  overrides: {
7629
7888
  set_agent_brief: {
7630
7889
  executionMapping: {},
7631
7890
  staticExecution: { action_type: "brief" },
7632
7891
  hideUnlessAuthorized: true,
7633
- approvalView: { content: { label: "New agent brief", kind: "markdown" } }
7892
+ approvalView: { content: { label: "New agent brief", kind: "markdown" } },
7893
+ // AU3 — the approval card's preview of this pending proposal (DL1
7894
+ // Interface contract). Identity arg mapping: same field name either side.
7895
+ preview: { tool: "describe_brief_proposal", args: { content: "content" } }
7634
7896
  },
7635
7897
  get_guide: {
7636
7898
  category: "read",
@@ -7638,7 +7900,7 @@ function setupBuiltin(deps) {
7638
7900
  requiredValue: "unlimited"
7639
7901
  },
7640
7902
  create_mandate: {
7641
- executionMapping: {},
7903
+ executionMapping: { profile: "allowed_profiles", mode: "allowed_modes" },
7642
7904
  staticExecution: { action_type: "mandate" },
7643
7905
  hideUnlessAuthorized: true,
7644
7906
  // Shown like the mandate screen: limits and scope by the profile's own names.
@@ -7651,13 +7913,23 @@ function setupBuiltin(deps) {
7651
7913
  mode: { label: "Mode" },
7652
7914
  duration_hours: { label: "Valid for (hours)" },
7653
7915
  title: { label: "Title" }
7654
- }
7655
- }
7916
+ },
7917
+ preview: { tool: "describe_mandate_proposal", args: { team: "team", profile: "profile", limits: "limits", scope: "scope", intent: "intent", mode: "mode", duration_hours: "duration_hours", title: "title" } }
7918
+ },
7919
+ // Internal preview tools (DL1): never listed, never callable by an
7920
+ // agent (BuiltinTool.internal — enforced in index.ts and
7921
+ // tool-proxy.ts); explicitly disabled here too, so a direct call
7922
+ // through the normal gate (defence in depth) refuses cleanly instead
7923
+ // of falling through to the "not described in manifest" default.
7924
+ describe_mandate_proposal: { category: "disabled" },
7925
+ describe_brief_proposal: { category: "disabled" }
7656
7926
  }
7657
7927
  },
7658
7928
  tools: [
7659
7929
  {
7660
7930
  name: "get_guide",
7931
+ // DL1 decision 2: stays simulation-only, per-tool (the group no longer is).
7932
+ simulationOnly: true,
7661
7933
  description: "Start here when the person asks how to begin with Suveren or how to set up their work with it. Simulation mode only: the setup guides \u2014 how to interview the person, build the test data, propose mandates and the agent brief, set up the working AI's process, and what can make a test misleading. Call without `topic` for the list and the order to follow; then read each topic before doing that step.",
7662
7934
  inputSchema: {
7663
7935
  type: "object",
@@ -7701,21 +7973,10 @@ ${body}`);
7701
7973
  },
7702
7974
  {
7703
7975
  name: "set_agent_brief",
7704
- description: "Simulation mode only: propose a new agent brief \u2014 the standing instructions every agent session starts with. `content` replaces the whole brief (markdown, at most 16 KB). A person approves or rejects the proposal; only after approval is the brief replaced, and it applies from the next session. Write the complete brief, not a change to the current one.",
7705
- inputSchema: {
7706
- type: "object",
7707
- properties: {
7708
- content: { type: "string", description: "The complete new agent brief, in markdown (at most 16 KB)." },
7709
- ticket_id: {
7710
- type: "string",
7711
- description: "Authorization reference for this call, set by the governing gateway \u2014 agents do not set this."
7712
- }
7713
- },
7714
- required: ["content"]
7715
- },
7976
+ description: "Propose a new agent brief \u2014 the standing instructions every agent session starts with. `content` replaces the whole brief (markdown, at most 16 KB). A person approves or rejects the proposal; only after approval is the brief replaced, and it applies from the next session. Write the complete brief, not a change to the current one.",
7977
+ inputSchema: SET_AGENT_BRIEF_INPUT_SCHEMA,
7716
7978
  validate: briefRefusal,
7717
7979
  handler: async (args) => {
7718
- if (!isSimulationMode()) return { ...builtinText("Refused: not available outside simulation mode."), isError: true };
7719
7980
  const refusal = briefRefusal(args);
7720
7981
  if (refusal) return { ...builtinText(`Refused: ${refusal}`), isError: true };
7721
7982
  writeContextFile(args.content);
@@ -7726,37 +7987,86 @@ ${body}`);
7726
7987
  },
7727
7988
  {
7728
7989
  name: "create_mandate",
7729
- description: "Simulation mode only: propose a new mandate for the person running this gateway \u2014 the authority another tool needs (e.g. sales quotes up to a value). A person approves or rejects the proposal; only after approval is the mandate created, exactly as if they had signed it on the mandate screen. Checked against the profile before the proposal: unknown limits, a mode the profile does not allow, or a team where the person may not give this mandate are refused at once. Create only.",
7730
- inputSchema: {
7731
- type: "object",
7732
- properties: {
7733
- team: { type: "string", description: 'Team name or id, or "personal". Omit to use the workspace of your Delegation mandate.' },
7734
- profile: { type: "string", description: 'Profile, e.g. "sales" (newest version) or a full profile id.' },
7735
- limits: { type: "object", description: "The limits (bounds) by field name, as the profile defines them." },
7736
- scope: { type: "object", description: "The scope (context) by field name, as the profile defines it." },
7737
- intent: { type: "string", description: "Why, goal and watch-outs \u2014 signed with the mandate (at most 2000 characters)." },
7738
- mode: { type: "string", enum: ["review", "automatic"], description: "review = each action needs approval; automatic = within the limits." },
7739
- duration_hours: { type: "number", description: "How long the mandate is valid, in hours. Omit for the profile default." },
7740
- title: { type: "string", description: "A short name for the mandate." },
7741
- ticket_id: {
7742
- type: "string",
7743
- description: "Authorization reference for this call, set by the governing gateway \u2014 agents do not set this."
7744
- }
7745
- },
7746
- required: ["profile", "limits", "intent", "mode"]
7747
- },
7990
+ description: "Propose a new mandate for the person running this gateway \u2014 the authority another tool needs (e.g. sales quotes up to a value). A person approves or rejects the proposal; only after approval is the mandate created, exactly as if they had signed it on the mandate screen. Checked against the profile before the proposal: unknown limits, a mode the profile does not allow, or a team where the person may not give this mandate are refused at once. Outside simulation mode, the profile and mode you may propose are themselves limited by your Delegation mandate's scope \u2014 Delegation itself can never be proposed. Create only.",
7991
+ inputSchema: CREATE_MANDATE_INPUT_SCHEMA,
7748
7992
  validate: async (args) => {
7993
+ const fixed = delegationItselfRefusal(args);
7994
+ if (fixed) {
7995
+ deps.state.denialLog?.record({
7996
+ ts: Date.now(),
7997
+ tool: "create_mandate",
7998
+ integrationId: "setup",
7999
+ profile: DELEGATION_PROFILE,
8000
+ kind: "bound",
8001
+ detail: fixed,
8002
+ who: "gateway",
8003
+ code: "DELEGATION_NOT_PROPOSABLE"
8004
+ });
8005
+ return fixed;
8006
+ }
7749
8007
  const check = await controlPlaneMandate(true, mandateRequest(args));
7750
8008
  return check.ok ? void 0 : check.message;
7751
8009
  },
7752
8010
  handler: async (args) => {
7753
- if (!isSimulationMode()) return { ...builtinText("Refused: not available outside simulation mode."), isError: true };
8011
+ const fixed = delegationItselfRefusal(args);
8012
+ if (fixed) return { ...builtinText(`Refused: ${fixed}`), isError: true };
7754
8013
  const r = await controlPlaneMandate(false, mandateRequest(args));
7755
8014
  if (!r.ok) return { ...builtinText(`Refused: ${r.message}`), isError: true };
7756
8015
  return builtinText(
7757
8016
  `Mandate created: ${r.authorizationId} \u2014 profile ${r.profileId}, ${r.groupName}, mode ${r.mode}, valid for ${Math.round((r.ttlSeconds ?? 0) / 3600)} h. It is active now.`
7758
8017
  );
7759
8018
  }
8019
+ },
8020
+ {
8021
+ // AU3 internal preview (DL1 Interface contract) — never listed, never
8022
+ // callable by an agent. Builds the "What you would sign" card for a
8023
+ // pending create_mandate proposal from the SAME args, local data only.
8024
+ name: "describe_mandate_proposal",
8025
+ internal: true,
8026
+ description: "Internal \u2014 the approval card's preview of a pending create_mandate proposal. Not for agent use.",
8027
+ inputSchema: CREATE_MANDATE_INPUT_SCHEMA,
8028
+ outputSchema: DESCRIBE_MANDATE_PROPOSAL_OUTPUT_SCHEMA,
8029
+ handler: async (args) => {
8030
+ const resolved = resolveTargetProfile(args.profile);
8031
+ if (!resolved) {
8032
+ return { ...builtinText(`Refused: unknown profile "${String(args.profile)}".`), isError: true };
8033
+ }
8034
+ const { profile, shortName: targetShortName } = resolved;
8035
+ const displayName = profile.name ?? targetShortName;
8036
+ const rawTeam = typeof args.team === "string" ? args.team.trim() : "";
8037
+ const workspace = rawTeam && rawTeam.toLowerCase() !== "personal" ? rawTeam : "Personal";
8038
+ const mode = typeof args.mode === "string" ? args.mode : "";
8039
+ const structured = {
8040
+ proposal: `New mandate \xB7 ${displayName}`,
8041
+ profile_version: `${targetShortName} ${profile.version}`,
8042
+ workspace,
8043
+ mode,
8044
+ valid: formatValidity(args, profile),
8045
+ limits: labeledRecord(args.limits, profile.boundsSchema?.fields),
8046
+ scope: labeledRecord(args.scope, profile.scopeSchema?.fields),
8047
+ title: typeof args.title === "string" && args.title.trim() ? args.title.trim() : null,
8048
+ intent: typeof args.intent === "string" ? args.intent : "",
8049
+ checks: delegationChecks(deps, args, targetShortName, displayName)
8050
+ };
8051
+ return builtinStructured(structured);
8052
+ }
8053
+ },
8054
+ {
8055
+ // AU3 internal preview (DL1 Interface contract) — never listed, never
8056
+ // callable by an agent. Builds the "New agent brief" diff card.
8057
+ name: "describe_brief_proposal",
8058
+ internal: true,
8059
+ description: "Internal \u2014 the approval card's preview of a pending set_agent_brief proposal. Not for agent use.",
8060
+ inputSchema: SET_AGENT_BRIEF_INPUT_SCHEMA,
8061
+ outputSchema: DESCRIBE_BRIEF_PROPOSAL_OUTPUT_SCHEMA,
8062
+ handler: async (args) => {
8063
+ const proposed = typeof args.content === "string" ? args.content : "";
8064
+ const current = readContextFile() ?? "";
8065
+ const change = unifiedLineDiff(current, proposed);
8066
+ const trimmed = proposed.trim();
8067
+ const words = trimmed === "" ? 0 : trimmed.split(/\s+/).length;
8068
+ return builtinStructured({ change, words });
8069
+ }
7760
8070
  }
7761
8071
  ]
7762
8072
  };