@clien-ai/mcp 0.15.0 → 0.15.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.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * FUL-1219 / FUL-1225: which stored source URLs and platform labels an MCP channel may SHOW, and how a
3
+ * receipt whose URL the projection withheld still answers the questions its URL used to answer.
4
+ *
5
+ * Imports nothing, so `hypothesis-semantics.ts` (which `report.ts` imports) can read it too.
6
+ *
7
+ * ⚠️ THE MARKERS BELOW ARE TRUSTED ONLY ON PROJECTED DATA. `projectReportDataForMcp` deletes any
8
+ * stored copy before it sets them, so a reader must be handed the output of exactly one projection:
9
+ * on unprojected report JSON a forged `urlWithheld` would read as a reachable receipt.
10
+ */
11
+ /**
12
+ * The `href` of a stored source URL an MCP channel may show, or null.
13
+ *
14
+ * The producer writes each URL through `normalizeUrl` (`agent/src/exa-fetch-tool.ts`), i.e. a WHATWG
15
+ * `URL` serialisation, which is always printable ASCII. So a stored URL with any other character —
16
+ * a newline, a tab, a bidi or zero-width mark, a `←` — was not written by the producer and is
17
+ * refused. `new URL()` alone would NOT refuse it: the parser silently strips tabs and newlines, so
18
+ * the raw string would pass and reach `structuredContent` unchanged. Also refuses non-http(s) and
19
+ * credential-carrying URLs.
20
+ */
21
+ export function safeSourceUrl(value) {
22
+ if (typeof value !== 'string' || !/^https?:\/\/[\x21-\x7e]+$/i.test(value))
23
+ return null;
24
+ return isSafeHttpDestination(value) ? new URL(value).href : null;
25
+ }
26
+ /**
27
+ * FUL-1225: a stored `platform` label an MCP channel may show: printable ASCII on one line (space
28
+ * allowed), else null. Anything else (a newline, a bidi or zero-width mark) was not a host label.
29
+ */
30
+ export function safeSourcePlatform(value) {
31
+ return typeof value === 'string' && /^[\x20-\x7e]*$/.test(value) ? value : null;
32
+ }
33
+ /**
34
+ * FUL-534's reachability test: a non-credentialed http(s) URL the WHATWG parser accepts. Decides
35
+ * whether a receipt earns its GROUNDED / Cited presentation. It is deliberately NOT the display
36
+ * test ({@link safeSourceUrl}): tightening it would downgrade claims, which FUL-1225 must not do.
37
+ */
38
+ export function isSafeHttpDestination(value) {
39
+ if (typeof value !== 'string' || !/^https?:\/\//i.test(value))
40
+ return false;
41
+ try {
42
+ const url = new URL(value);
43
+ return (url.protocol === 'http:' || url.protocol === 'https:') && !url.username && !url.password;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ /**
50
+ * Set by the projection on a `reportEvidence` or persona-source row whose URL it blanked
51
+ * (`url: ''`) because the URL is unsafe to show, while the stored URL still passed
52
+ * {@link isSafeHttpDestination}. It means "still a receipt, link withheld".
53
+ */
54
+ export const RECEIPT_URL_WITHHELD_KEY = 'urlWithheld';
55
+ /**
56
+ * Set beside {@link RECEIPT_URL_WITHHELD_KEY}: the withheld URL's hostname (ASCII by WHATWG
57
+ * construction, and re-checked). Integrity checks that compare hosts (`sameNamedVendor`) read it,
58
+ * so withholding a URL for display never changes an integrity or coverage decision.
59
+ */
60
+ export const RECEIPT_URL_WITHHELD_HOST_KEY = 'urlWithheldHost';
61
+ /** The hostname the projection preserves for a withheld URL, or null. */
62
+ export function withheldUrlHost(value) {
63
+ if (!isSafeHttpDestination(value))
64
+ return null;
65
+ const host = new URL(value).hostname;
66
+ return /^[a-z0-9.-]+$/.test(host) ? host : null;
67
+ }
68
+ function asRow(row) {
69
+ return row !== null && typeof row === 'object' && !Array.isArray(row) ? row : null;
70
+ }
71
+ function isWithheld(record) {
72
+ return record[RECEIPT_URL_WITHHELD_KEY] === true && record.url === '';
73
+ }
74
+ /**
75
+ * FUL-534 + FUL-1225: does a receipt row have a safe destination, shown or withheld? Every
76
+ * receipt-granting reader calls this instead of testing `row.url`, so blanking an unprintable URL
77
+ * never changes a claim's state or tally.
78
+ */
79
+ export function receiptRowHasSafeDestination(row) {
80
+ const record = asRow(row);
81
+ if (!record)
82
+ return false;
83
+ if (isWithheld(record))
84
+ return true;
85
+ return isSafeHttpDestination(record.url);
86
+ }
87
+ /** The hostname behind a receipt row's URL, shown or withheld, or null. */
88
+ export function receiptRowHostname(row) {
89
+ const record = asRow(row);
90
+ if (!record)
91
+ return null;
92
+ if (isWithheld(record)) {
93
+ const host = record[RECEIPT_URL_WITHHELD_HOST_KEY];
94
+ return typeof host === 'string' && /^[a-z0-9.-]+$/.test(host) ? host : null;
95
+ }
96
+ if (typeof record.url !== 'string')
97
+ return null;
98
+ try {
99
+ return new URL(record.url).hostname;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ //# sourceMappingURL=receipt-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt-url.js","sourceRoot":"","sources":["../../src/types/receipt-url.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { z } from 'zod';
15
15
  import { CONFIDENCE_BASES, CONFIDENCE_LEVELS, EVIDENCE_SUFFICIENCY_LEVELS, HYPOTHESIS_PRIORITIES, HYPOTHESIS_SEMANTICS_VERSION, LEGACY_HYPOTHESIS_SEMANTICS_VERSION, ROBUSTNESS_OUTCOMES, VERDICT_STATUSES, finalClauseCoverageMatchesIntegrity, hasCoherentRobustnessWorking, hypothesisWebReceiptAuthenticates, normalizeFinalHypothesisState, readRobustnessCounts, reportClaimNeedsIndependentMarketAuthority, robustnessWorkingContradictsSummary, sourceAuthenticatesMarketEconomics, } from '../hypothesis-semantics.js';
16
+ import { RECEIPT_URL_WITHHELD_HOST_KEY, RECEIPT_URL_WITHHELD_KEY, receiptRowHasSafeDestination, safeSourcePlatform, safeSourceUrl, withheldUrlHost, } from './receipt-url.js';
16
17
  // ⚠️ A types module importing from `tools/` reads backwards, and it is deliberate.
17
18
  // `content-type-display.ts` imports NOTHING — it is the package's content-type vocabulary, which
18
19
  // happens to live beside the two renderers that were its first callers, and the FUL-341 coupling
@@ -696,29 +697,9 @@ export function isModelAttestedReportClaim(claim) {
696
697
  return typeof sourceId === 'string' && sourceId.trim().length > 0 &&
697
698
  typeof quote === 'string' && quote.trim().length > 0;
698
699
  }
699
- /**
700
- * FUL-1219: the `href` of an `unverifiedEvidence` row's URL, or null.
701
- *
702
- * The producer writes each URL through `normalizeUrl` (`agent/src/exa-fetch-tool.ts`), i.e. a WHATWG
703
- * `URL` serialisation, which is always printable ASCII. So a stored URL with any other character —
704
- * a newline, a tab, a bidi or zero-width mark, a `←` — was not written by the producer and is
705
- * refused. `new URL()` alone would NOT refuse it: the parser silently strips tabs and newlines, so
706
- * the raw string would pass and reach `structuredContent` unchanged. Also refuses non-http(s) and
707
- * credential-carrying URLs.
708
- */
709
- export function safeUnverifiedEvidenceUrl(value) {
710
- if (typeof value !== 'string' || !/^https?:\/\/[\x21-\x7e]+$/i.test(value))
711
- return null;
712
- try {
713
- const url = new URL(value);
714
- if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password)
715
- return null;
716
- return url.href;
717
- }
718
- catch {
719
- return null;
720
- }
721
- }
700
+ // FUL-1219 / FUL-1225: the URL display and reachability helpers live in `receipt-url.ts` (it imports
701
+ // nothing, so `hypothesis-semantics.ts` can read them too); re-exported for this module's callers.
702
+ export { RECEIPT_URL_WITHHELD_HOST_KEY, RECEIPT_URL_WITHHELD_KEY, isSafeHttpDestination, receiptRowHasSafeDestination, safeSourcePlatform, safeSourceUrl, } from './receipt-url.js';
722
703
  /**
723
704
  * A single REPORT-level (market/competitor) claim. Shape mirrors {@link ClaimSchema}
724
705
  * but carries a `section` instead of a `personaId` — report claims are not attributed
@@ -1208,7 +1189,7 @@ export const ResearchReportDataSchema = z
1208
1189
  reportEvidence: z
1209
1190
  .array(ReportEvidenceSchema)
1210
1191
  .optional()
1211
- .describe('The reportClaims receipt pool: cached market/competitor evidence pages ("RRCP-s{n}" indexes this array) with the verbatim highlight a claim\'s span was matched against.'),
1192
+ .describe('The reportClaims receipt pool: cached market/competitor evidence pages ("RRCP-s{n}" indexes this array) with the verbatim highlight a claim\'s span was matched against. A row with `url: ""` and `urlWithheld: true` (plus `urlWithheldHost`) is still a receipt whose stored URL was unsafe to show; its claims keep their state.'),
1212
1193
  /**
1213
1194
  * FUL-1219: the pool behind `reportClaims[].unverifiedSources`. Soft per row so one bad row
1214
1195
  * becomes a hole and every other "RUVS-s{n}" keeps its index.
@@ -1387,18 +1368,6 @@ export function projectReportDataForMcp(raw) {
1387
1368
  const asProjectionRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
1388
1369
  ? value
1389
1370
  : null;
1390
- const safeProjectionUrl = (value) => {
1391
- if (typeof value !== 'string' || !/^https?:\/\//i.test(value))
1392
- return false;
1393
- try {
1394
- const url = new URL(value);
1395
- return (url.protocol === 'http:' || url.protocol === 'https:') &&
1396
- !url.username && !url.password;
1397
- }
1398
- catch {
1399
- return false;
1400
- }
1401
- };
1402
1371
  const hasProjectionVisibleContent = (value) => typeof value === 'string' &&
1403
1372
  value.replace(/[\p{C}\p{Z}\p{Default_Ignorable_Code_Point}]/gu, '').length > 0;
1404
1373
  const hasHighlights = Object.prototype.hasOwnProperty.call(record, 'interviewHighlights');
@@ -1451,6 +1420,27 @@ export function projectReportDataForMcp(raw) {
1451
1420
  if (!source)
1452
1421
  return rawSource;
1453
1422
  const next = { ...source };
1423
+ // FUL-1225: a URL the display check refuses is blanked, with the host-derived `platform`, so
1424
+ // no channel carries it. The row itself stays, so every RRCP-s{n} / RCP-p{i}-s{j} keeps its
1425
+ // index, and the markers keep FUL-534's reachability answer and the hostname for every
1426
+ // receipt-granting or host-comparing reader (`receiptRowHasSafeDestination`,
1427
+ // `receiptRowHostname`): no claim or integrity decision changes. Stored markers are untrusted.
1428
+ delete next[RECEIPT_URL_WITHHELD_KEY];
1429
+ delete next[RECEIPT_URL_WITHHELD_HOST_KEY];
1430
+ if (Object.prototype.hasOwnProperty.call(source, 'url') && source.url !== '' && !safeSourceUrl(source.url)) {
1431
+ next.url = '';
1432
+ if (Object.prototype.hasOwnProperty.call(source, 'platform'))
1433
+ next.platform = '';
1434
+ const host = withheldUrlHost(source.url);
1435
+ if (host !== null) {
1436
+ next[RECEIPT_URL_WITHHELD_KEY] = true;
1437
+ next[RECEIPT_URL_WITHHELD_HOST_KEY] = host;
1438
+ }
1439
+ }
1440
+ else if (Object.prototype.hasOwnProperty.call(source, 'platform') && safeSourcePlatform(source.platform) === null) {
1441
+ // A platform label that is not printable ASCII on one line reaches no channel either.
1442
+ next.platform = '';
1443
+ }
1454
1444
  if (Object.prototype.hasOwnProperty.call(source, 'quote')) {
1455
1445
  if (typeof source.quote === 'string' && !isDisplaySuitableExcerpt(source.quote)) {
1456
1446
  // `undefined` is absent on the JSON wire but remains distinguishable from a historical
@@ -1550,6 +1540,14 @@ export function projectReportDataForMcp(raw) {
1550
1540
  if (!thread)
1551
1541
  return rawThread;
1552
1542
  const nextThread = { ...thread };
1543
+ // FUL-1225: a thread URL is display-only (every forum receipt renders from its cached
1544
+ // excerpt without one), so an unsafe one is simply dropped.
1545
+ if (Object.prototype.hasOwnProperty.call(thread, 'url') && thread.url !== '' && !safeSourceUrl(thread.url)) {
1546
+ delete nextThread.url;
1547
+ }
1548
+ if (Object.prototype.hasOwnProperty.call(thread, 'platform') && safeSourcePlatform(thread.platform) === null) {
1549
+ delete nextThread.platform;
1550
+ }
1553
1551
  if (Array.isArray(thread.relevantQuotes)) {
1554
1552
  nextThread.relevantQuotes = thread.relevantQuotes.filter(isDisplaySuitableExcerpt);
1555
1553
  }
@@ -1615,9 +1613,9 @@ export function projectReportDataForMcp(raw) {
1615
1613
  const unverifiedEvidence = Array.isArray(record.unverifiedEvidence)
1616
1614
  ? record.unverifiedEvidence.map((rawRow) => {
1617
1615
  const row = asProjectionRecord(rawRow);
1618
- if (!row || !safeUnverifiedEvidenceUrl(row.url))
1616
+ if (!row || !safeSourceUrl(row.url))
1619
1617
  return undefined;
1620
- return Object.fromEntries(Object.keys(UnverifiedEvidenceSchema.shape).filter((key) => key in row).map((key) => [key, row[key]]));
1618
+ return Object.fromEntries(Object.keys(UnverifiedEvidenceSchema.shape).filter((key) => key in row).map((key) => [key, key === 'platform' ? safeSourcePlatform(row[key]) ?? '' : row[key]]));
1621
1619
  })
1622
1620
  : [];
1623
1621
  if (hasUnverifiedEvidence) {
@@ -1653,7 +1651,7 @@ export function projectReportDataForMcp(raw) {
1653
1651
  const row = index === null ? undefined : unverifiedEvidence[index];
1654
1652
  if (!parsed.success || !row || row.section !== claim.section)
1655
1653
  return [];
1656
- const href = safeUnverifiedEvidenceUrl(row.url);
1654
+ const href = safeSourceUrl(row.url);
1657
1655
  if (!href || seen.has(href))
1658
1656
  return [];
1659
1657
  seen.add(href);
@@ -1897,7 +1895,7 @@ export function projectReportDataForMcp(raw) {
1897
1895
  ? projected.reportEvidence[Number(match[1])]
1898
1896
  : undefined);
1899
1897
  return Boolean(source &&
1900
- safeProjectionUrl(source.url) &&
1898
+ receiptRowHasSafeDestination(source) &&
1901
1899
  (!Object.prototype.hasOwnProperty.call(source, 'quote') ||
1902
1900
  isDisplaySuitableExcerpt(source.quote)) &&
1903
1901
  (source.section === 'market' || source.section === 'competitor' || source.section === 'summary'));