@suveren/gateway 0.27.2 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,7 +54,17 @@
54
54
  "staticExecution": {
55
55
  "action_type": "write"
56
56
  },
57
- "actionLabel": "Contact created"
57
+ "actionLabel": "Contact created",
58
+ "preview": [
59
+ { "tool": "find_contacts", "args": { "query": "email" } }
60
+ ],
61
+ "display": {
62
+ "order": ["name", "email", "company", "role", "type", "stage", "tags", "notes"],
63
+ "labels": {
64
+ "name": "Name", "email": "Email", "company": "Company", "role": "Role",
65
+ "type": "Type", "stage": "Stage", "tags": "Tags", "notes": "Notes"
66
+ }
67
+ }
58
68
  },
59
69
  "update_contact": {
60
70
  "executionMapping": {
@@ -330,7 +340,8 @@
330
340
  "action_type": "setup"
331
341
  },
332
342
  "actionLabel": "Test data loaded",
333
- "hideUnlessAuthorized": true
343
+ "hideUnlessAuthorized": true,
344
+ "simulationOnly": true
334
345
  },
335
346
  "clear_simulation": {
336
347
  "executionMapping": {},
@@ -339,7 +350,8 @@
339
350
  "action_type": "setup"
340
351
  },
341
352
  "actionLabel": "Test data cleared",
342
- "hideUnlessAuthorized": true
353
+ "hideUnlessAuthorized": true,
354
+ "simulationOnly": true
343
355
  }
344
356
  }
345
357
  },
@@ -95,7 +95,29 @@
95
95
  "staticExecution": {
96
96
  "action_type": "quote"
97
97
  },
98
- "actionLabel": "Quote drafted"
98
+ "actionLabel": "Quote drafted",
99
+ "preview": [
100
+ { "tool": "get_customer", "args": { "id": "customer_id" }, "attach": "customer_id" },
101
+ { "tool": "get_item", "forEach": "lines", "args": { "id": "item_id" }, "attach": "item" }
102
+ ],
103
+ "display": {
104
+ "order": ["customer_id", "lines", "discount_pct", "value", "valid_until", "notes"],
105
+ "money": ["value", "customer_id.credit_limit", "customer_id.open_balance", "customer_id.available_credit", "lines[].item.list_price"],
106
+ "percent": ["discount_pct"],
107
+ "currencyField": "currency",
108
+ "labels": {
109
+ "customer_id": "Customer",
110
+ "discount_pct": "Discount",
111
+ "value": "Total (net)",
112
+ "customer_id.credit_limit": "Credit limit",
113
+ "customer_id.open_balance": "Open balance",
114
+ "customer_id.available_credit": "Available credit",
115
+ "customer_id.payment_terms": "Payment terms",
116
+ "lines[].item": "Item",
117
+ "lines[].item.list_price": "List price",
118
+ "lines[].item.stock": "In stock"
119
+ }
120
+ }
99
121
  },
100
122
  "update_quote": {
101
123
  "executionMapping": {
@@ -106,7 +128,23 @@
106
128
  "staticExecution": {
107
129
  "action_type": "quote"
108
130
  },
109
- "actionLabel": "Quote updated"
131
+ "actionLabel": "Quote updated",
132
+ "preview": [
133
+ { "tool": "get_item", "forEach": "lines", "args": { "id": "item_id" }, "attach": "item" }
134
+ ],
135
+ "display": {
136
+ "order": ["lines", "discount_pct", "value", "valid_until", "notes"],
137
+ "money": ["value", "lines[].item.list_price"],
138
+ "percent": ["discount_pct"],
139
+ "currencyField": "currency",
140
+ "labels": {
141
+ "discount_pct": "Discount",
142
+ "value": "Total (net)",
143
+ "lines[].item": "Item",
144
+ "lines[].item.list_price": "List price",
145
+ "lines[].item.stock": "In stock"
146
+ }
147
+ }
110
148
  },
111
149
  "send_quote": {
112
150
  "executionMapping": {
@@ -148,7 +186,8 @@
148
186
  "action_type": "setup"
149
187
  },
150
188
  "actionLabel": "Test data loaded",
151
- "hideUnlessAuthorized": true
189
+ "hideUnlessAuthorized": true,
190
+ "simulationOnly": true
152
191
  },
153
192
  "clear_simulation": {
154
193
  "executionMapping": {},
@@ -156,7 +195,8 @@
156
195
  "action_type": "setup"
157
196
  },
158
197
  "actionLabel": "Test data cleared",
159
- "hideUnlessAuthorized": true
198
+ "hideUnlessAuthorized": true,
199
+ "simulationOnly": true
160
200
  }
161
201
  }
162
202
  },
@@ -83,7 +83,8 @@
83
83
  "action_type": "setup"
84
84
  },
85
85
  "actionLabel": "Test data loaded",
86
- "hideUnlessAuthorized": true
86
+ "hideUnlessAuthorized": true,
87
+ "simulationOnly": true
87
88
  },
88
89
  "clear_simulation": {
89
90
  "executionMapping": {},
@@ -91,7 +92,8 @@
91
92
  "action_type": "setup"
92
93
  },
93
94
  "actionLabel": "Test data cleared",
94
- "hideUnlessAuthorized": true
95
+ "hideUnlessAuthorized": true,
96
+ "simulationOnly": true
95
97
  }
96
98
  }
97
99
  },
@@ -3469,6 +3469,12 @@ import { computeContentHash as computeContentHash2 } from "@hap/core";
3469
3469
  // src/lib/manifest-loader.ts
3470
3470
  import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
3471
3471
  import { join as join14, resolve } from "path";
3472
+
3473
+ // src/lib/tool-gating-types.ts
3474
+ var MAX_PREVIEW_READS = 10;
3475
+ var MAX_FOREACH_ELEMENTS = 50;
3476
+
3477
+ // src/lib/manifest-loader.ts
3472
3478
  var EXACT_SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
3473
3479
  function isExactSemver(value) {
3474
3480
  return typeof value === "string" && EXACT_SEMVER_RE.test(value);
@@ -3483,44 +3489,101 @@ function invalidNpmPinReason(manifest) {
3483
3489
  }
3484
3490
  return null;
3485
3491
  }
3486
- function invalidPreviewReason(toolName, entry) {
3487
- if (entry === null || typeof entry !== "object") return null;
3488
- const preview = entry.preview;
3489
- if (preview === void 0) return null;
3490
- if (preview === null || typeof preview !== "object" || Array.isArray(preview)) {
3491
- return `override "${toolName}".preview must be an object`;
3492
+ function invalidReadConfigReason(toolName, idx, entry) {
3493
+ const label = idx === null ? `override "${toolName}".preview` : `override "${toolName}".preview[${idx}]`;
3494
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
3495
+ return `${label} must be an object`;
3492
3496
  }
3493
- const p = preview;
3497
+ const p = entry;
3494
3498
  if (typeof p.tool !== "string" || p.tool.length === 0) {
3495
- return `override "${toolName}".preview.tool must be a non-empty string`;
3499
+ return `${label}.tool must be a non-empty string`;
3496
3500
  }
3497
3501
  if (p.args === void 0 || p.args === null || typeof p.args !== "object" || Array.isArray(p.args)) {
3498
- return `override "${toolName}".preview.args must be an object mapping preview-tool arg names to action arg names`;
3502
+ return `${label}.args must be an object mapping preview-tool arg names to action arg names`;
3499
3503
  }
3500
3504
  for (const [argName, source] of Object.entries(p.args)) {
3501
3505
  if (typeof source !== "string" || source.length === 0) {
3502
- return `override "${toolName}".preview.args.${argName} must name a non-empty source argument (string)`;
3506
+ return `${label}.args.${argName} must name a non-empty source argument (string)`;
3503
3507
  }
3504
3508
  }
3509
+ if (p.forEach !== void 0 && (typeof p.forEach !== "string" || p.forEach.length === 0)) {
3510
+ return `${label}.forEach must be a non-empty string naming an action argument`;
3511
+ }
3512
+ if (p.attach !== void 0 && (typeof p.attach !== "string" || p.attach.length === 0)) {
3513
+ return `${label}.attach must be a non-empty string`;
3514
+ }
3505
3515
  if (p.version !== void 0) {
3506
3516
  if (p.version === null || typeof p.version !== "object" || Array.isArray(p.version)) {
3507
- return `override "${toolName}".preview.version must be an object`;
3517
+ return `${label}.version must be an object`;
3508
3518
  }
3509
3519
  const v = p.version;
3510
3520
  if (typeof v.arg !== "string" || v.arg.length === 0) {
3511
- return `override "${toolName}".preview.version.arg must be a non-empty string`;
3521
+ return `${label}.version.arg must be a non-empty string`;
3512
3522
  }
3513
3523
  if (typeof v.field !== "string" || v.field.length === 0) {
3514
- return `override "${toolName}".preview.version.field must be a non-empty string`;
3524
+ return `${label}.version.field must be a non-empty string`;
3515
3525
  }
3516
3526
  }
3517
3527
  if (p.fields !== void 0) {
3518
3528
  if (!Array.isArray(p.fields)) {
3519
- return `override "${toolName}".preview.fields must be an array of field names`;
3529
+ return `${label}.fields must be an array of field names`;
3520
3530
  }
3521
3531
  for (const [i, f] of p.fields.entries()) {
3522
3532
  if (typeof f !== "string" || f.length === 0) {
3523
- return `override "${toolName}".preview.fields[${i}] must be a non-empty string`;
3533
+ return `${label}.fields[${i}] must be a non-empty string`;
3534
+ }
3535
+ }
3536
+ }
3537
+ return null;
3538
+ }
3539
+ function invalidPreviewReason(toolName, entry) {
3540
+ if (entry === null || typeof entry !== "object") return null;
3541
+ const preview = entry.preview;
3542
+ if (preview === void 0) return null;
3543
+ if (Array.isArray(preview)) {
3544
+ if (preview.length === 0) {
3545
+ return `override "${toolName}".preview must not be an empty list \u2014 omit it, or use the single-object form`;
3546
+ }
3547
+ if (preview.length > MAX_PREVIEW_READS) {
3548
+ return `override "${toolName}".preview declares ${preview.length} reads \u2014 max ${MAX_PREVIEW_READS}`;
3549
+ }
3550
+ for (const [i, read] of preview.entries()) {
3551
+ const reason = invalidReadConfigReason(toolName, i, read);
3552
+ if (reason) return reason;
3553
+ }
3554
+ return null;
3555
+ }
3556
+ return invalidReadConfigReason(toolName, null, preview);
3557
+ }
3558
+ function invalidDisplayHintsReason(toolName, entry) {
3559
+ if (entry === null || typeof entry !== "object") return null;
3560
+ const display = entry.display;
3561
+ if (display === void 0) return null;
3562
+ if (display === null || typeof display !== "object" || Array.isArray(display)) {
3563
+ return `override "${toolName}".display must be an object`;
3564
+ }
3565
+ const d = display;
3566
+ for (const key of ["order", "money", "percent", "hidden"]) {
3567
+ if (d[key] === void 0) continue;
3568
+ if (!Array.isArray(d[key])) {
3569
+ return `override "${toolName}".display.${key} must be an array of field paths`;
3570
+ }
3571
+ for (const [i, v] of d[key].entries()) {
3572
+ if (typeof v !== "string" || v.length === 0) {
3573
+ return `override "${toolName}".display.${key}[${i}] must be a non-empty string`;
3574
+ }
3575
+ }
3576
+ }
3577
+ if (d.currencyField !== void 0 && (typeof d.currencyField !== "string" || d.currencyField.length === 0)) {
3578
+ return `override "${toolName}".display.currencyField must be a non-empty string`;
3579
+ }
3580
+ if (d.labels !== void 0) {
3581
+ if (d.labels === null || typeof d.labels !== "object" || Array.isArray(d.labels)) {
3582
+ return `override "${toolName}".display.labels must be an object mapping field paths to labels`;
3583
+ }
3584
+ for (const [path, label] of Object.entries(d.labels)) {
3585
+ if (typeof label !== "string" || label.length === 0) {
3586
+ return `override "${toolName}".display.labels.${path} must be a non-empty string`;
3524
3587
  }
3525
3588
  }
3526
3589
  }
@@ -3529,7 +3592,7 @@ function invalidPreviewReason(toolName, entry) {
3529
3592
  function invalidManifestPreviewReason(manifest) {
3530
3593
  const overrides = manifest.toolGating?.overrides ?? {};
3531
3594
  for (const [toolName, entry] of Object.entries(overrides)) {
3532
- const reason = invalidPreviewReason(toolName, entry);
3595
+ const reason = invalidPreviewReason(toolName, entry) ?? invalidDisplayHintsReason(toolName, entry);
3533
3596
  if (reason) return reason;
3534
3597
  }
3535
3598
  return null;
@@ -3598,6 +3661,13 @@ function getToolPreviewConfig(integrationManager2, integrationId, toolName) {
3598
3661
  const entry = overrides[toolName];
3599
3662
  return entry?.preview;
3600
3663
  }
3664
+ function getToolDisplayHints(integrationManager2, integrationId, toolName) {
3665
+ if (integrationManager2.isBuiltin(integrationId)) return void 0;
3666
+ const manifest = getManifest(integrationId);
3667
+ const overrides = manifest?.toolGating?.overrides ?? {};
3668
+ const entry = overrides[toolName];
3669
+ return entry?.display;
3670
+ }
3601
3671
  function mapPreviewArgs(preview, actionArgs) {
3602
3672
  const mapped = {};
3603
3673
  for (const [targetArg, sourceArg] of Object.entries(preview.args)) {
@@ -3656,10 +3726,105 @@ function hashPreviewBody(body) {
3656
3726
  }
3657
3727
  return computeContentHash2({ kind: "text", version: "1" }, body.text ?? "");
3658
3728
  }
3729
+ function isPlainRecord(v) {
3730
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3731
+ }
3732
+ function planPreviewReads(declaration, actionArgs) {
3733
+ const reads = Array.isArray(declaration) ? declaration : [declaration];
3734
+ return reads.map((config) => {
3735
+ const base = {
3736
+ tool: config.tool,
3737
+ attach: config.attach,
3738
+ fields: config.fields,
3739
+ version: config.version,
3740
+ argsMapping: config.args
3741
+ };
3742
+ if (config.forEach) {
3743
+ const source = actionArgs[config.forEach];
3744
+ const list = Array.isArray(source) ? source : [];
3745
+ const capped = list.slice(0, MAX_FOREACH_ELEMENTS);
3746
+ const elements = capped.filter(isPlainRecord).map((el) => ({ args: mapPreviewArgs(config, el) }));
3747
+ return {
3748
+ ...base,
3749
+ forEach: config.forEach,
3750
+ elements,
3751
+ ...list.length > MAX_FOREACH_ELEMENTS ? { overflow: { shown: MAX_FOREACH_ELEMENTS, total: list.length } } : {}
3752
+ };
3753
+ }
3754
+ const mapped = mapPreviewArgs(config, actionArgs);
3755
+ return { ...base, args: mapped, rawSource: actionArgs, noTarget: previewHasNoTarget(config, mapped) };
3756
+ });
3757
+ }
3758
+ async function runPreviewReadPlan(integrationManager2, integrationId, plan) {
3759
+ const sections = [];
3760
+ for (const item of plan) {
3761
+ const sourceArgNames = Object.values(item.argsMapping);
3762
+ const base = {
3763
+ tool: item.tool,
3764
+ ...item.attach ? { attach: item.attach } : {},
3765
+ ...sourceArgNames.length === 1 ? { sourceArg: sourceArgNames[0] } : {},
3766
+ ...item.fields ? { fields: item.fields } : {}
3767
+ };
3768
+ if (item.elements !== void 0) {
3769
+ const elements = [];
3770
+ for (const el of item.elements) {
3771
+ elements.push(await readPreview(integrationManager2, integrationId, item.tool, el.args));
3772
+ }
3773
+ sections.push({
3774
+ ...base,
3775
+ forEach: item.forEach,
3776
+ elements,
3777
+ ...item.overflow ? { overflow: item.overflow } : {}
3778
+ });
3779
+ continue;
3780
+ }
3781
+ if (item.noTarget) {
3782
+ sections.push({ ...base, result: { status: "none", reason: "no_target" } });
3783
+ continue;
3784
+ }
3785
+ const read1 = await readPreview(integrationManager2, integrationId, item.tool, item.args);
3786
+ if (read1.status !== "ok" || !item.version) {
3787
+ sections.push({ ...base, result: read1 });
3788
+ continue;
3789
+ }
3790
+ const approved = item.rawSource?.[item.version.arg];
3791
+ if (typeof approved !== "string" && typeof approved !== "number") {
3792
+ sections.push({ ...base, result: read1 });
3793
+ continue;
3794
+ }
3795
+ const { [item.version.arg]: _omit, ...withoutVersion } = item.rawSource;
3796
+ const mappedCurrentArgs = mapPreviewArgs({ args: item.argsMapping }, withoutVersion);
3797
+ const read2 = await readPreview(integrationManager2, integrationId, item.tool, mappedCurrentArgs);
3798
+ if (read2.status !== "ok" || read2.body.structured === void 0) {
3799
+ sections.push({ ...base, result: read1 });
3800
+ continue;
3801
+ }
3802
+ const current = read2.body.structured[item.version.field];
3803
+ if (typeof current !== "string" && typeof current !== "number") {
3804
+ sections.push({ ...base, result: read1 });
3805
+ continue;
3806
+ }
3807
+ const stale = String(current) !== String(approved);
3808
+ sections.push({
3809
+ ...base,
3810
+ result: read1,
3811
+ version: {
3812
+ field: item.version.field,
3813
+ approved,
3814
+ current,
3815
+ stale,
3816
+ ...stale ? { currentBody: read2.body } : {}
3817
+ }
3818
+ });
3819
+ }
3820
+ return sections;
3821
+ }
3659
3822
  async function computeSubmissionPreviewHash(integrationManager2, integrationId, toolName, actionArgs) {
3660
3823
  if (integrationManager2.isBuiltin(integrationId)) return void 0;
3661
3824
  const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
3662
- if (!preview || preview.version) return void 0;
3825
+ if (!preview) return void 0;
3826
+ if (Array.isArray(preview)) return combinedPreviewReadsHash(integrationManager2, integrationId, preview, actionArgs);
3827
+ if (preview.version) return void 0;
3663
3828
  const mapped = mapPreviewArgs(preview, actionArgs);
3664
3829
  if (previewHasNoTarget(preview, mapped)) return void 0;
3665
3830
  const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
@@ -3669,14 +3834,44 @@ async function computeSubmissionPreviewHash(integrationManager2, integrationId,
3669
3834
  async function previewChangedSinceSubmission(integrationManager2, integrationId, toolName, actionArgs, previousHash) {
3670
3835
  const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
3671
3836
  if (!preview) return true;
3837
+ if (Array.isArray(preview)) {
3838
+ const hash = await combinedPreviewReadsHash(integrationManager2, integrationId, preview, actionArgs);
3839
+ if (hash === void 0) return true;
3840
+ return hash !== previousHash;
3841
+ }
3672
3842
  const mapped = mapPreviewArgs(preview, actionArgs);
3673
3843
  const read = await readPreview(integrationManager2, integrationId, preview.tool, mapped);
3674
3844
  if (read.status !== "ok") return true;
3675
3845
  return hashPreviewBody(read.body) !== previousHash;
3676
3846
  }
3847
+ async function combinedPreviewReadsHash(integrationManager2, integrationId, reads, actionArgs) {
3848
+ const plan = planPreviewReads(reads, actionArgs).filter((item) => !item.version);
3849
+ const hashes = [];
3850
+ for (const item of plan) {
3851
+ if (item.elements !== void 0) {
3852
+ for (const el of item.elements) {
3853
+ const read2 = await readPreview(integrationManager2, integrationId, item.tool, el.args);
3854
+ if (read2.status !== "ok") return void 0;
3855
+ hashes.push(hashPreviewBody(read2.body));
3856
+ }
3857
+ continue;
3858
+ }
3859
+ if (item.noTarget) continue;
3860
+ const read = await readPreview(integrationManager2, integrationId, item.tool, item.args);
3861
+ if (read.status !== "ok") return void 0;
3862
+ hashes.push(hashPreviewBody(read.body));
3863
+ }
3864
+ if (hashes.length === 0) return void 0;
3865
+ return computeContentHash2({ kind: "jcs", version: "1" }, { reads: hashes });
3866
+ }
3677
3867
  async function buildInternalPreview(integrationManager2, integrationId, toolName, actionArgs) {
3678
3868
  const preview = getToolPreviewConfig(integrationManager2, integrationId, toolName);
3679
3869
  if (!preview) return { status: "none" };
3870
+ if (Array.isArray(preview)) {
3871
+ const reads = await runPreviewReadPlan(integrationManager2, integrationId, planPreviewReads(preview, actionArgs));
3872
+ const display = getToolDisplayHints(integrationManager2, integrationId, toolName);
3873
+ return { status: "ok", integration: integrationId, reads, ...display ? { display } : {} };
3874
+ }
3680
3875
  const mappedArgs = mapPreviewArgs(preview, actionArgs);
3681
3876
  if (previewHasNoTarget(preview, mappedArgs)) return { status: "none", reason: "no_target" };
3682
3877
  const read1 = await readPreview(integrationManager2, integrationId, preview.tool, mappedArgs);
@@ -4214,8 +4409,10 @@ function zeroCappedBound(profileId, bounds, actionType) {
4214
4409
  }
4215
4410
  function toolAlwaysListed(tool, isBuiltin) {
4216
4411
  if (tool.internal || !tool.gating?.profile) return false;
4217
- if (tool.gating.hideUnlessAuthorized || tool.gating.category === "disabled") return false;
4218
- return !isBuiltin(tool.integrationId);
4412
+ if (tool.gating.category === "disabled") return false;
4413
+ if (isBuiltin(tool.integrationId)) return false;
4414
+ if (!tool.gating.hideUnlessAuthorized) return true;
4415
+ return Boolean(tool.gating.simulationOnly) && isSimulationMode();
4219
4416
  }
4220
4417
  function toolIsAuthorizedForDisplay(tool, matchingAuths) {
4221
4418
  if (!tool.gating?.hideUnlessAuthorized) return true;
@@ -4631,6 +4828,18 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
4631
4828
  }
4632
4829
  }
4633
4830
  const { matching: matchingAuths, refusals } = eligibleMandates();
4831
+ if (tool.gating?.simulationOnly && isSimulationMode() && !toolIsAuthorizedForDisplay(tool, matchingAuths)) {
4832
+ const actionType = staticExecution?.action_type;
4833
+ const mandateLabel = typeof actionType === "string" ? actionType : profile;
4834
+ recordBlocked(state2, tool, {
4835
+ kind: "not_authorized",
4836
+ detail: `No active authorization grants "${mandateLabel}" under profile "${profile}".`,
4837
+ field: mandateLabel,
4838
+ who: "gateway",
4839
+ code: "NO_MATCHING_MANDATE"
4840
+ });
4841
+ return { content: [{ type: "text", text: noMandateRefusal(tool.namespacedName, mandateLabel) }], isError: true };
4842
+ }
4634
4843
  if (matchingAuths.length === 0) return noEligibleMandate(refusals);
4635
4844
  const errors = [];
4636
4845
  const passers = [];
@@ -7298,6 +7507,7 @@ var IntegrationManager = class {
7298
7507
  readGovernanceReason: ext.readGovernanceReason,
7299
7508
  blockedArgs: ext.blockedArgs,
7300
7509
  hideUnlessAuthorized: ext.hideUnlessAuthorized,
7510
+ simulationOnly: ext.simulationOnly,
7301
7511
  approvalView: ext.approvalView
7302
7512
  };
7303
7513
  }
@@ -7314,6 +7524,7 @@ var IntegrationManager = class {
7314
7524
  argEncoding: ext.argEncoding,
7315
7525
  argNormalization: ext.argNormalization,
7316
7526
  hideUnlessAuthorized: ext.hideUnlessAuthorized,
7527
+ simulationOnly: ext.simulationOnly,
7317
7528
  approvalView: ext.approvalView
7318
7529
  };
7319
7530
  }