@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.
- package/CHANGELOG.md +35 -0
- package/README.md +34 -7
- package/dist/server.js +1114 -323
- package/package.json +6 -5
- package/src/amplify.ts +32 -41
- package/src/evidence_metadata.ts +191 -0
- package/src/guidance.ts +57 -0
- package/src/ingest/html.ts +129 -19
- package/src/profile.ts +15 -0
- package/src/public_safety.ts +110 -0
- package/src/response.ts +73 -0
- package/src/server.ts +304 -108
- package/src/tool_policy.ts +58 -0
- package/src/types.ts +4 -3
- package/src/vault.ts +300 -75
- package/src/vault_get.ts +109 -0
- package/src/vault_write.ts +78 -112
|
@@ -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
|
+
}
|
package/src/response.ts
ADDED
|
@@ -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
|
+
}
|