@syndash/research-vault-mcp 1.1.2 → 1.1.3

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,110 @@
1
+ export interface PublicSafetyScan {
2
+ public_safe: boolean
3
+ redacted: unknown
4
+ reasons: string[]
5
+ }
6
+
7
+ const REDACTIONS = [
8
+ {
9
+ reason: 'local_home_path',
10
+ marker: '[REDACTED_LOCAL_PATH]',
11
+ pattern: /\/Users\/[^/\s]+\/[^"'`]+?(?=["'`])/g,
12
+ },
13
+ {
14
+ reason: 'local_home_path',
15
+ marker: '[REDACTED_LOCAL_PATH]',
16
+ pattern: /\/Users\/[^/\s]+\/(?:[^/\s"'`),;]+(?: [^/\s"'`),;]+)*\/)*[^/\s"'`),;]+/g,
17
+ },
18
+ {
19
+ reason: 'operator_command',
20
+ marker: '[REDACTED_OPERATOR_COMMAND]',
21
+ pattern: /(^|[\s;|&])(?:ssh|scp|rsync)\s+[^\n"'`]*/g,
22
+ keepPrefix: true,
23
+ },
24
+ {
25
+ reason: 'token',
26
+ marker: '[REDACTED_TOKEN]',
27
+ pattern: /\b(?:sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9_]{12,}|xox[A-Za-z0-9-]{12,})\b/g,
28
+ },
29
+ {
30
+ reason: 'ipv4',
31
+ marker: '[REDACTED_IPV4]',
32
+ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
33
+ },
34
+ ] as const
35
+
36
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
37
+ if (!value || typeof value !== 'object') return false
38
+ const prototype = Object.getPrototypeOf(value)
39
+ return prototype === Object.prototype || prototype === null
40
+ }
41
+
42
+ function hasEnumerableEntries(value: unknown): value is Record<string, unknown> {
43
+ return !!value && typeof value === 'object' && Object.keys(value).length > 0
44
+ }
45
+
46
+ function redactStringWithReasons(input: string): { redacted: string; reasons: string[] } {
47
+ const reasons = new Set<string>()
48
+ let redacted = input
49
+
50
+ for (const rule of REDACTIONS) {
51
+ rule.pattern.lastIndex = 0
52
+ redacted = redacted.replace(rule.pattern, (...match) => {
53
+ reasons.add(rule.reason)
54
+ if ('keepPrefix' in rule && rule.keepPrefix) {
55
+ const prefix = String(match[1] || '')
56
+ return `${prefix}${rule.marker}`
57
+ }
58
+ return rule.marker
59
+ })
60
+ rule.pattern.lastIndex = 0
61
+ }
62
+
63
+ return { redacted, reasons: [...reasons] }
64
+ }
65
+
66
+ export function redactUnsafeText(input: string): string {
67
+ return redactStringWithReasons(input).redacted
68
+ }
69
+
70
+ export function sanitizePublicData<T>(value: T): T {
71
+ if (typeof value === 'string') return redactUnsafeText(value) as T
72
+ if (Array.isArray(value)) return value.map(item => sanitizePublicData(item)) as T
73
+ if (!isPlainObject(value) && !hasEnumerableEntries(value)) return value
74
+
75
+ const entries = Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
76
+ redactUnsafeText(key),
77
+ sanitizePublicData(entry),
78
+ ])
79
+ return Object.fromEntries(entries) as T
80
+ }
81
+
82
+ function collectReasons(value: unknown, reasons: Set<string>): void {
83
+ if (typeof value === 'string') {
84
+ for (const reason of redactStringWithReasons(value).reasons) reasons.add(reason)
85
+ return
86
+ }
87
+
88
+ if (Array.isArray(value)) {
89
+ for (const item of value) collectReasons(item, reasons)
90
+ return
91
+ }
92
+
93
+ if (!isPlainObject(value) && !hasEnumerableEntries(value)) return
94
+
95
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
96
+ collectReasons(key, reasons)
97
+ collectReasons(entry, reasons)
98
+ }
99
+ }
100
+
101
+ export function scanPublicSafety(value: unknown): PublicSafetyScan {
102
+ const reasons = new Set<string>()
103
+ collectReasons(value, reasons)
104
+
105
+ return {
106
+ public_safe: reasons.size === 0,
107
+ redacted: sanitizePublicData(value),
108
+ reasons: [...reasons],
109
+ }
110
+ }
@@ -0,0 +1,73 @@
1
+ import { getActiveProfile, type McpProfile } from './profile.ts'
2
+ import { blockGuidance, passGuidance, type AgentGuidance } from './guidance.ts'
3
+ import { sanitizePublicData, scanPublicSafety } from './public_safety.ts'
4
+
5
+ export interface EvidenceMetadata {
6
+ as_of: string
7
+ profile: McpProfile
8
+ public_safe: boolean
9
+ safety_reasons?: string[]
10
+ freshness?: string
11
+ provenance?: string
12
+ release?: string
13
+ }
14
+
15
+ export interface ToolEnvelope<T> {
16
+ ok: boolean
17
+ data: T
18
+ agent_guidance: AgentGuidance
19
+ evidence: EvidenceMetadata
20
+ }
21
+
22
+ type EvidenceInput = Partial<EvidenceMetadata>
23
+
24
+ function buildEvidence(evidence: EvidenceInput = {}, public_safe = true, safety_reasons?: string[]): EvidenceMetadata {
25
+ return {
26
+ ...evidence,
27
+ as_of: evidence.as_of || new Date().toISOString(),
28
+ profile: evidence.profile || getActiveProfile(),
29
+ public_safe,
30
+ safety_reasons: safety_reasons || evidence.safety_reasons,
31
+ }
32
+ }
33
+
34
+ export function okEnvelope<T>(
35
+ data: T,
36
+ guidance: AgentGuidance = passGuidance('Request completed.', 'Use the returned evidence.'),
37
+ evidence: EvidenceInput = {},
38
+ ): ToolEnvelope<T> {
39
+ const scan = scanPublicSafety(data)
40
+ const sanitized = sanitizePublicData(data)
41
+
42
+ if (!scan.public_safe) {
43
+ return {
44
+ ok: false,
45
+ data: sanitized,
46
+ agent_guidance: blockGuidance(
47
+ 'Public-surface leak candidates were redacted from the tool response.',
48
+ 'Use a private diagnostic profile or sanitize the source data before sharing this result.',
49
+ ),
50
+ evidence: buildEvidence(evidence, false, scan.reasons),
51
+ }
52
+ }
53
+
54
+ return {
55
+ ok: true,
56
+ data: sanitized,
57
+ agent_guidance: guidance,
58
+ evidence: buildEvidence(evidence, true),
59
+ }
60
+ }
61
+
62
+ export function errorEnvelope(
63
+ reason: string,
64
+ next_step: string,
65
+ evidence: EvidenceInput = {},
66
+ ): ToolEnvelope<null> {
67
+ return {
68
+ ok: false,
69
+ data: null,
70
+ agent_guidance: blockGuidance(reason, next_step),
71
+ evidence: buildEvidence(evidence, evidence.public_safe ?? true, evidence.safety_reasons),
72
+ }
73
+ }