@tangle-network/agent-app 0.1.6 → 0.1.7
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/dist/chunk-KWHLTXLE.js +95 -0
- package/dist/chunk-KWHLTXLE.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -2
- package/dist/redact/index.d.ts +109 -17
- package/dist/redact/index.js +11 -3
- package/package.json +1 -1
- package/dist/chunk-C5CREGT2.js +0 -45
- package/dist/chunk-C5CREGT2.js.map +0 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/redact/index.ts
|
|
2
|
+
var DEFAULT_REDACTION_PATTERNS = [
|
|
3
|
+
{ kind: "ssn", pattern: /\d{3}-\d{2}-\d{4}/ },
|
|
4
|
+
{ kind: "ein", pattern: /\d{2}-\d{7}/ }
|
|
5
|
+
];
|
|
6
|
+
var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
|
|
7
|
+
"ssn",
|
|
8
|
+
"ein",
|
|
9
|
+
"password",
|
|
10
|
+
"apikey",
|
|
11
|
+
"token",
|
|
12
|
+
"secret",
|
|
13
|
+
"authorization",
|
|
14
|
+
"email",
|
|
15
|
+
"phone"
|
|
16
|
+
]);
|
|
17
|
+
function redactString(value, patterns) {
|
|
18
|
+
for (const { kind, pattern } of patterns) {
|
|
19
|
+
if (pattern.test(value)) return `[REDACTED:${kind}]`;
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function isPlainObject(value) {
|
|
24
|
+
if (value === null || typeof value !== "object") return false;
|
|
25
|
+
const proto = Object.getPrototypeOf(value);
|
|
26
|
+
return proto === Object.prototype || proto === null;
|
|
27
|
+
}
|
|
28
|
+
function redactForIngestion(value, options = {}) {
|
|
29
|
+
const patterns = options.extraPatterns ? [...DEFAULT_REDACTION_PATTERNS, ...options.extraPatterns] : DEFAULT_REDACTION_PATTERNS;
|
|
30
|
+
const walk = (v) => {
|
|
31
|
+
if (typeof v === "string") return redactString(v, patterns);
|
|
32
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
33
|
+
if (isPlainObject(v)) {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [k, val] of Object.entries(v)) {
|
|
36
|
+
out[k] = SENSITIVE_KEYS.has(k.toLowerCase()) ? "[REDACTED:field]" : walk(val);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
return v;
|
|
41
|
+
};
|
|
42
|
+
return walk(value);
|
|
43
|
+
}
|
|
44
|
+
function detectSpans(text, patterns = DEFAULT_REDACTION_PATTERNS) {
|
|
45
|
+
const raw = [];
|
|
46
|
+
for (const { kind, pattern } of patterns) {
|
|
47
|
+
const g = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
48
|
+
for (const m of text.matchAll(g)) {
|
|
49
|
+
if (m.index === void 0 || m[0].length === 0) continue;
|
|
50
|
+
raw.push({ kind, start: m.index, end: m.index + m[0].length, text: m[0] });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
raw.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
54
|
+
const spans = [];
|
|
55
|
+
let cursor = -1;
|
|
56
|
+
let i = 0;
|
|
57
|
+
for (const s of raw) {
|
|
58
|
+
if (s.start < cursor) continue;
|
|
59
|
+
spans.push({ id: `span-${i++}`, ...s });
|
|
60
|
+
cursor = s.end;
|
|
61
|
+
}
|
|
62
|
+
return spans;
|
|
63
|
+
}
|
|
64
|
+
async function buildRedactedDocument(text, options) {
|
|
65
|
+
const spans = detectSpans(text, options.patterns);
|
|
66
|
+
const segments = [];
|
|
67
|
+
let pos = 0;
|
|
68
|
+
for (const span of spans) {
|
|
69
|
+
if (span.start > pos) segments.push({ type: "text", text: text.slice(pos, span.start) });
|
|
70
|
+
segments.push({ type: "redacted", id: span.id, kind: span.kind, cipher: await options.encrypt(span.text) });
|
|
71
|
+
pos = span.end;
|
|
72
|
+
}
|
|
73
|
+
if (pos < text.length) segments.push({ type: "text", text: text.slice(pos) });
|
|
74
|
+
return { segments };
|
|
75
|
+
}
|
|
76
|
+
async function revealSpan(doc, spanId, options) {
|
|
77
|
+
const seg = doc.segments.find(
|
|
78
|
+
(s) => s.type === "redacted" && s.id === spanId
|
|
79
|
+
);
|
|
80
|
+
if (!seg) return { ok: false, reason: "not_found" };
|
|
81
|
+
const allowed = await options.canReveal({ id: seg.id, kind: seg.kind });
|
|
82
|
+
if (!allowed) return { ok: false, reason: "forbidden" };
|
|
83
|
+
const value = await options.decrypt(seg.cipher);
|
|
84
|
+
if (options.onReveal) await options.onReveal({ id: seg.id, kind: seg.kind });
|
|
85
|
+
return { ok: true, value };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
DEFAULT_REDACTION_PATTERNS,
|
|
90
|
+
redactForIngestion,
|
|
91
|
+
detectSpans,
|
|
92
|
+
buildRedactedDocument,
|
|
93
|
+
revealSpan
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=chunk-KWHLTXLE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/redact/index.ts"],"sourcesContent":["/**\n * PII redaction — two complementary modes.\n *\n * 1. ONE-WAY scrub (`redactForIngestion`): for production trace payloads. Tool\n * args + results (and once, the LLM span's prompt) cross the wire into the\n * ingestion store, which also feeds the analyst-loop's LLM prompts, so\n * personal identifiers MUST be stripped before they leave the request path.\n * Destructive — the original is gone, replaced by a sentinel.\n *\n * 2. REVERSIBLE redaction (`buildRedactedDocument` / `revealSpan`): for the UI.\n * A document is split into text + redacted segments; each redacted original\n * is kept ENCRYPTED (via a caller-supplied `encrypt` seam → `agent-app/crypto`)\n * so a viewer can reveal a single span on demand, gated by an authorization\n * callback and an audit hook. The mask is presentation; the original is\n * recoverable by an authorized reveal, not lost.\n *\n * Discipline: cheap deterministic string patterns + well-known sensitive object\n * keys (value replaced, key kept, so the shape stays debuggable); recurse arrays\n * + plain objects only; NEVER throw on the one-way path.\n */\n\n/** A named PII pattern. `pattern` is matched case-insensitively at the string\n * level; keep it non-global (global instances are derived where needed). */\nexport interface RedactionPattern {\n kind: string\n pattern: RegExp\n}\n\n/** The default deterministic patterns. Extend via the `extraPatterns` /\n * `patterns` options rather than forking this module (the seam that lets a\n * product add e.g. a credit-card matcher without a local copy). */\nexport const DEFAULT_REDACTION_PATTERNS: readonly RedactionPattern[] = [\n { kind: 'ssn', pattern: /\\d{3}-\\d{2}-\\d{4}/ },\n { kind: 'ein', pattern: /\\d{2}-\\d{7}/ },\n]\n\nconst SENSITIVE_KEYS = new Set([\n 'ssn',\n 'ein',\n 'password',\n 'apikey',\n 'token',\n 'secret',\n 'authorization',\n 'email',\n 'phone',\n])\n\nexport interface RedactForIngestionOptions {\n /** Extra patterns appended to {@link DEFAULT_REDACTION_PATTERNS} for the\n * string-level scrub (e.g. credit-card). Additive — defaults still apply. */\n extraPatterns?: readonly RedactionPattern[]\n}\n\nfunction redactString(value: string, patterns: readonly RedactionPattern[]): string {\n for (const { kind, pattern } of patterns) {\n if (pattern.test(value)) return `[REDACTED:${kind}]`\n }\n return value\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object') return false\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n}\n\n/**\n * One-way PII scrub for telemetry/ingestion. Backward-compatible: called with no\n * options it behaves exactly as before (SSN/EIN strings + sensitive object keys\n * → sentinels). `extraPatterns` lets a product add matchers (e.g. credit-card)\n * without forking this module.\n */\nexport function redactForIngestion(value: unknown, options: RedactForIngestionOptions = {}): unknown {\n const patterns = options.extraPatterns\n ? [...DEFAULT_REDACTION_PATTERNS, ...options.extraPatterns]\n : DEFAULT_REDACTION_PATTERNS\n const walk = (v: unknown): unknown => {\n if (typeof v === 'string') return redactString(v, patterns)\n if (Array.isArray(v)) return v.map(walk)\n if (isPlainObject(v)) {\n const out: Record<string, unknown> = {}\n for (const [k, val] of Object.entries(v)) {\n out[k] = SENSITIVE_KEYS.has(k.toLowerCase()) ? '[REDACTED:field]' : walk(val)\n }\n return out\n }\n return v\n }\n return walk(value)\n}\n\n// ── Reversible document redaction (the UI path) ─────────────────────────────\n\n/** A detected PII span in a source string. */\nexport interface RedactionSpan {\n /** Stable within a document (index-derived) — used for reveal + audit. */\n id: string\n kind: string\n start: number\n end: number\n text: string\n}\n\n/**\n * Find non-overlapping PII spans in `text`. Matches every pattern, sorts by\n * position, and drops overlaps (first match wins). Deterministic — no ids that\n * vary per call.\n */\nexport function detectSpans(\n text: string,\n patterns: readonly RedactionPattern[] = DEFAULT_REDACTION_PATTERNS,\n): RedactionSpan[] {\n const raw: Array<{ kind: string; start: number; end: number; text: string }> = []\n for (const { kind, pattern } of patterns) {\n const g = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)\n for (const m of text.matchAll(g)) {\n if (m.index === undefined || m[0].length === 0) continue\n raw.push({ kind, start: m.index, end: m.index + m[0].length, text: m[0] })\n }\n }\n raw.sort((a, b) => a.start - b.start || b.end - a.end)\n const spans: RedactionSpan[] = []\n let cursor = -1\n let i = 0\n for (const s of raw) {\n if (s.start < cursor) continue // overlaps an earlier (higher-priority) span\n spans.push({ id: `span-${i++}`, ...s })\n cursor = s.end\n }\n return spans\n}\n\n/** A redacted document segment: literal text, or a masked span with the\n * original kept ENCRYPTED for an authorized reveal. */\nexport type RedactedDocSegment =\n | { type: 'text'; text: string }\n | { type: 'redacted'; id: string; kind: string; cipher: string }\n\nexport interface RedactedDocument {\n segments: RedactedDocSegment[]\n}\n\nexport interface BuildRedactedDocumentOptions {\n /** Encrypt one original span value. Wire it to `agent-app/crypto`\n * (`encryptWithKey` / `createFieldCrypto`). The cipher is what's stored. */\n encrypt: (plaintext: string) => string | Promise<string>\n /** Patterns to detect (default: {@link DEFAULT_REDACTION_PATTERNS}). */\n patterns?: readonly RedactionPattern[]\n}\n\n/**\n * Split `text` into text + redacted segments, encrypting each redacted span's\n * original. The result carries NO plaintext PII — only the masked structure and\n * ciphertext — so it is safe to ship to a client; reveal happens server-side via\n * {@link revealSpan}.\n */\nexport async function buildRedactedDocument(\n text: string,\n options: BuildRedactedDocumentOptions,\n): Promise<RedactedDocument> {\n const spans = detectSpans(text, options.patterns)\n const segments: RedactedDocSegment[] = []\n let pos = 0\n for (const span of spans) {\n if (span.start > pos) segments.push({ type: 'text', text: text.slice(pos, span.start) })\n segments.push({ type: 'redacted', id: span.id, kind: span.kind, cipher: await options.encrypt(span.text) })\n pos = span.end\n }\n if (pos < text.length) segments.push({ type: 'text', text: text.slice(pos) })\n return { segments }\n}\n\nexport interface RevealSpanOptions {\n /** Decrypt a span cipher. Wire to `agent-app/crypto` (`decryptWithKey`). */\n decrypt: (cipher: string) => string | Promise<string>\n /** Authorization gate — return false to deny the reveal (fail-closed). */\n canReveal: (segment: { id: string; kind: string }) => boolean | Promise<boolean>\n /** Audit hook — invoked only on a granted reveal (the caller records who/when). */\n onReveal?: (segment: { id: string; kind: string }) => void | Promise<void>\n}\n\nexport interface RevealResult {\n ok: boolean\n value?: string\n /** `not_found` | `forbidden` when `ok` is false. */\n reason?: string\n}\n\n/**\n * Reveal one redacted span's original, gated + audited. Fail-closed: an unknown\n * id or a denied `canReveal` returns `{ ok: false }` and never decrypts; a\n * granted reveal decrypts, fires `onReveal` for the audit trail, and returns the\n * value.\n */\nexport async function revealSpan(\n doc: RedactedDocument,\n spanId: string,\n options: RevealSpanOptions,\n): Promise<RevealResult> {\n const seg = doc.segments.find((s): s is Extract<RedactedDocSegment, { type: 'redacted' }> =>\n s.type === 'redacted' && s.id === spanId,\n )\n if (!seg) return { ok: false, reason: 'not_found' }\n const allowed = await options.canReveal({ id: seg.id, kind: seg.kind })\n if (!allowed) return { ok: false, reason: 'forbidden' }\n const value = await options.decrypt(seg.cipher)\n if (options.onReveal) await options.onReveal({ id: seg.id, kind: seg.kind })\n return { ok: true, value }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { DeriveKeyOptions, createFieldCrypto, decodeHexKey, decryptAesGcm, decry
|
|
|
14
14
|
export { JsonRecord, PersistedChatMessageForTurn, ResolvedChatTurn, StreamEvent, asRecord, asString, buildUserTextParts, encodeEvent, finalizeAssistantParts, getPartKey, mergePersistedPart, messageHasTurnId, normalizeClientTurnId, normalizePersistedPart, normalizeTime, normalizeToolEvent, resolveChatTurn, resolveToolId, resolveToolName } from './stream/index.js';
|
|
15
15
|
export { HubExecClient, HubExecClientOptions, HubExecErrorCode, HubExecResult, HubInvokeDeps, HubInvokeInput, HubInvokeOutcome, ParsedIntegrationAction, invokeIntegrationHub, resolveIntegrationAction } from './integrations/index.js';
|
|
16
16
|
export { JsonObject, KvLike, RateLimitResult, RequestContext, SecurityHeaderOptions, addSecurityHeaders, checkRateLimit, extractRequestContext, parseJsonObjectBody, requireString } from './web/index.js';
|
|
17
|
-
export { redactForIngestion } from './redact/index.js';
|
|
17
|
+
export { BuildRedactedDocumentOptions, DEFAULT_REDACTION_PATTERNS, RedactForIngestionOptions, RedactedDocSegment, RedactedDocument, RedactionPattern, RedactionSpan, RevealResult, RevealSpanOptions, buildRedactedDocument, detectSpans, redactForIngestion, revealSpan } from './redact/index.js';
|
|
18
18
|
export { CompletionRequirement, CompletionVerdict, CorrectnessChecker, ProducedState, RuntimeEventLike, SatisfiedBy, TaskGold, createLlmCorrectnessChecker, extractProducedState, verifyCompletion, weightedComposite } from '@tangle-network/agent-eval';
|
|
19
19
|
export { D as DEFAULT_TANGLE_ROUTER_BASE_URL, R as ResolveModelOptions, T as TangleModelConfig, r as resolveTangleModelConfig } from './model-BOP69mVu.js';
|
|
20
20
|
import '@tangle-network/agent-knowledge';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
DEFAULT_REDACTION_PATTERNS,
|
|
3
|
+
buildRedactedDocument,
|
|
4
|
+
detectSpans,
|
|
5
|
+
redactForIngestion,
|
|
6
|
+
revealSpan
|
|
7
|
+
} from "./chunk-KWHLTXLE.js";
|
|
4
8
|
import {
|
|
5
9
|
DEFAULT_HARNESS,
|
|
6
10
|
KNOWN_HARNESSES,
|
|
@@ -124,6 +128,7 @@ export {
|
|
|
124
128
|
DEFAULT_APP_TOOL_PATHS,
|
|
125
129
|
DEFAULT_HARNESS,
|
|
126
130
|
DEFAULT_HEADER_NAMES,
|
|
131
|
+
DEFAULT_REDACTION_PATTERNS,
|
|
127
132
|
DEFAULT_TANGLE_ROUTER_BASE_URL,
|
|
128
133
|
DELEGATION_MCP_SERVER_KEY,
|
|
129
134
|
DELEGATION_TOOLS,
|
|
@@ -143,6 +148,7 @@ export {
|
|
|
143
148
|
buildDelegationMcpServer,
|
|
144
149
|
buildHttpMcpServer,
|
|
145
150
|
buildKnowledgeRequirements,
|
|
151
|
+
buildRedactedDocument,
|
|
146
152
|
buildUserTextParts,
|
|
147
153
|
checkRateLimit,
|
|
148
154
|
coerceHarness,
|
|
@@ -171,6 +177,7 @@ export {
|
|
|
171
177
|
delegationMcpForConfig,
|
|
172
178
|
deriveKey,
|
|
173
179
|
deriveSignals,
|
|
180
|
+
detectSpans,
|
|
174
181
|
dispatchAppTool,
|
|
175
182
|
encodeEvent,
|
|
176
183
|
encryptAesGcm,
|
|
@@ -202,6 +209,7 @@ export {
|
|
|
202
209
|
resolveTangleModelConfig,
|
|
203
210
|
resolveToolId,
|
|
204
211
|
resolveToolName,
|
|
212
|
+
revealSpan,
|
|
205
213
|
reviewCandidate,
|
|
206
214
|
runAppToolLoop,
|
|
207
215
|
streamAppToolLoop,
|
package/dist/redact/index.d.ts
CHANGED
|
@@ -1,22 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PII redaction
|
|
2
|
+
* PII redaction — two complementary modes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* 1. ONE-WAY scrub (`redactForIngestion`): for production trace payloads. Tool
|
|
5
|
+
* args + results (and once, the LLM span's prompt) cross the wire into the
|
|
6
|
+
* ingestion store, which also feeds the analyst-loop's LLM prompts, so
|
|
7
|
+
* personal identifiers MUST be stripped before they leave the request path.
|
|
8
|
+
* Destructive — the original is gone, replaced by a sentinel.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* 2. REVERSIBLE redaction (`buildRedactedDocument` / `revealSpan`): for the UI.
|
|
11
|
+
* A document is split into text + redacted segments; each redacted original
|
|
12
|
+
* is kept ENCRYPTED (via a caller-supplied `encrypt` seam → `agent-app/crypto`)
|
|
13
|
+
* so a viewer can reveal a single span on demand, gated by an authorization
|
|
14
|
+
* callback and an audit hook. The mask is presentation; the original is
|
|
15
|
+
* recoverable by an authorized reveal, not lost.
|
|
16
|
+
*
|
|
17
|
+
* Discipline: cheap deterministic string patterns + well-known sensitive object
|
|
18
|
+
* keys (value replaced, key kept, so the shape stays debuggable); recurse arrays
|
|
19
|
+
* + plain objects only; NEVER throw on the one-way path.
|
|
20
|
+
*/
|
|
21
|
+
/** A named PII pattern. `pattern` is matched case-insensitively at the string
|
|
22
|
+
* level; keep it non-global (global instances are derived where needed). */
|
|
23
|
+
interface RedactionPattern {
|
|
24
|
+
kind: string;
|
|
25
|
+
pattern: RegExp;
|
|
26
|
+
}
|
|
27
|
+
/** The default deterministic patterns. Extend via the `extraPatterns` /
|
|
28
|
+
* `patterns` options rather than forking this module (the seam that lets a
|
|
29
|
+
* product add e.g. a credit-card matcher without a local copy). */
|
|
30
|
+
declare const DEFAULT_REDACTION_PATTERNS: readonly RedactionPattern[];
|
|
31
|
+
interface RedactForIngestionOptions {
|
|
32
|
+
/** Extra patterns appended to {@link DEFAULT_REDACTION_PATTERNS} for the
|
|
33
|
+
* string-level scrub (e.g. credit-card). Additive — defaults still apply. */
|
|
34
|
+
extraPatterns?: readonly RedactionPattern[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One-way PII scrub for telemetry/ingestion. Backward-compatible: called with no
|
|
38
|
+
* options it behaves exactly as before (SSN/EIN strings + sensitive object keys
|
|
39
|
+
* → sentinels). `extraPatterns` lets a product add matchers (e.g. credit-card)
|
|
40
|
+
* without forking this module.
|
|
41
|
+
*/
|
|
42
|
+
declare function redactForIngestion(value: unknown, options?: RedactForIngestionOptions): unknown;
|
|
43
|
+
/** A detected PII span in a source string. */
|
|
44
|
+
interface RedactionSpan {
|
|
45
|
+
/** Stable within a document (index-derived) — used for reveal + audit. */
|
|
46
|
+
id: string;
|
|
47
|
+
kind: string;
|
|
48
|
+
start: number;
|
|
49
|
+
end: number;
|
|
50
|
+
text: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Find non-overlapping PII spans in `text`. Matches every pattern, sorts by
|
|
54
|
+
* position, and drops overlaps (first match wins). Deterministic — no ids that
|
|
55
|
+
* vary per call.
|
|
56
|
+
*/
|
|
57
|
+
declare function detectSpans(text: string, patterns?: readonly RedactionPattern[]): RedactionSpan[];
|
|
58
|
+
/** A redacted document segment: literal text, or a masked span with the
|
|
59
|
+
* original kept ENCRYPTED for an authorized reveal. */
|
|
60
|
+
type RedactedDocSegment = {
|
|
61
|
+
type: 'text';
|
|
62
|
+
text: string;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'redacted';
|
|
65
|
+
id: string;
|
|
66
|
+
kind: string;
|
|
67
|
+
cipher: string;
|
|
68
|
+
};
|
|
69
|
+
interface RedactedDocument {
|
|
70
|
+
segments: RedactedDocSegment[];
|
|
71
|
+
}
|
|
72
|
+
interface BuildRedactedDocumentOptions {
|
|
73
|
+
/** Encrypt one original span value. Wire it to `agent-app/crypto`
|
|
74
|
+
* (`encryptWithKey` / `createFieldCrypto`). The cipher is what's stored. */
|
|
75
|
+
encrypt: (plaintext: string) => string | Promise<string>;
|
|
76
|
+
/** Patterns to detect (default: {@link DEFAULT_REDACTION_PATTERNS}). */
|
|
77
|
+
patterns?: readonly RedactionPattern[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Split `text` into text + redacted segments, encrypting each redacted span's
|
|
81
|
+
* original. The result carries NO plaintext PII — only the masked structure and
|
|
82
|
+
* ciphertext — so it is safe to ship to a client; reveal happens server-side via
|
|
83
|
+
* {@link revealSpan}.
|
|
84
|
+
*/
|
|
85
|
+
declare function buildRedactedDocument(text: string, options: BuildRedactedDocumentOptions): Promise<RedactedDocument>;
|
|
86
|
+
interface RevealSpanOptions {
|
|
87
|
+
/** Decrypt a span cipher. Wire to `agent-app/crypto` (`decryptWithKey`). */
|
|
88
|
+
decrypt: (cipher: string) => string | Promise<string>;
|
|
89
|
+
/** Authorization gate — return false to deny the reveal (fail-closed). */
|
|
90
|
+
canReveal: (segment: {
|
|
91
|
+
id: string;
|
|
92
|
+
kind: string;
|
|
93
|
+
}) => boolean | Promise<boolean>;
|
|
94
|
+
/** Audit hook — invoked only on a granted reveal (the caller records who/when). */
|
|
95
|
+
onReveal?: (segment: {
|
|
96
|
+
id: string;
|
|
97
|
+
kind: string;
|
|
98
|
+
}) => void | Promise<void>;
|
|
99
|
+
}
|
|
100
|
+
interface RevealResult {
|
|
101
|
+
ok: boolean;
|
|
102
|
+
value?: string;
|
|
103
|
+
/** `not_found` | `forbidden` when `ok` is false. */
|
|
104
|
+
reason?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reveal one redacted span's original, gated + audited. Fail-closed: an unknown
|
|
108
|
+
* id or a denied `canReveal` returns `{ ok: false }` and never decrypts; a
|
|
109
|
+
* granted reveal decrypts, fires `onReveal` for the audit trail, and returns the
|
|
110
|
+
* value.
|
|
19
111
|
*/
|
|
20
|
-
declare function
|
|
112
|
+
declare function revealSpan(doc: RedactedDocument, spanId: string, options: RevealSpanOptions): Promise<RevealResult>;
|
|
21
113
|
|
|
22
|
-
export { redactForIngestion };
|
|
114
|
+
export { type BuildRedactedDocumentOptions, DEFAULT_REDACTION_PATTERNS, type RedactForIngestionOptions, type RedactedDocSegment, type RedactedDocument, type RedactionPattern, type RedactionSpan, type RevealResult, type RevealSpanOptions, buildRedactedDocument, detectSpans, redactForIngestion, revealSpan };
|
package/dist/redact/index.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
DEFAULT_REDACTION_PATTERNS,
|
|
3
|
+
buildRedactedDocument,
|
|
4
|
+
detectSpans,
|
|
5
|
+
redactForIngestion,
|
|
6
|
+
revealSpan
|
|
7
|
+
} from "../chunk-KWHLTXLE.js";
|
|
4
8
|
export {
|
|
5
|
-
|
|
9
|
+
DEFAULT_REDACTION_PATTERNS,
|
|
10
|
+
buildRedactedDocument,
|
|
11
|
+
detectSpans,
|
|
12
|
+
redactForIngestion,
|
|
13
|
+
revealSpan
|
|
6
14
|
};
|
|
7
15
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"packageManager": "pnpm@10.33.4",
|
|
5
5
|
"description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
|
|
6
6
|
"keywords": [
|
package/dist/chunk-C5CREGT2.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// src/redact/index.ts
|
|
2
|
-
var SSN_PATTERN = /\d{3}-\d{2}-\d{4}/;
|
|
3
|
-
var EIN_PATTERN = /\d{2}-\d{7}/;
|
|
4
|
-
var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
|
|
5
|
-
"ssn",
|
|
6
|
-
"ein",
|
|
7
|
-
"password",
|
|
8
|
-
"apikey",
|
|
9
|
-
"token",
|
|
10
|
-
"secret",
|
|
11
|
-
"authorization",
|
|
12
|
-
"email",
|
|
13
|
-
"phone"
|
|
14
|
-
]);
|
|
15
|
-
function redactString(value) {
|
|
16
|
-
if (SSN_PATTERN.test(value)) return "[REDACTED:ssn]";
|
|
17
|
-
if (EIN_PATTERN.test(value)) return "[REDACTED:ein]";
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
function isPlainObject(value) {
|
|
21
|
-
if (value === null || typeof value !== "object") return false;
|
|
22
|
-
const proto = Object.getPrototypeOf(value);
|
|
23
|
-
return proto === Object.prototype || proto === null;
|
|
24
|
-
}
|
|
25
|
-
function redactForIngestion(value) {
|
|
26
|
-
if (typeof value === "string") return redactString(value);
|
|
27
|
-
if (Array.isArray(value)) return value.map(redactForIngestion);
|
|
28
|
-
if (isPlainObject(value)) {
|
|
29
|
-
const out = {};
|
|
30
|
-
for (const [k, v] of Object.entries(value)) {
|
|
31
|
-
if (SENSITIVE_KEYS.has(k.toLowerCase())) {
|
|
32
|
-
out[k] = "[REDACTED:field]";
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
out[k] = redactForIngestion(v);
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
return value;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export {
|
|
43
|
-
redactForIngestion
|
|
44
|
-
};
|
|
45
|
-
//# sourceMappingURL=chunk-C5CREGT2.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/redact/index.ts"],"sourcesContent":["/**\n * PII redaction for production trace payloads.\n *\n * The chat-trace emission sites pass tool args + results — and at one\n * point the LLM span carried the system prompt + user message verbatim\n * — through the wire. Anything that lands in the ingestion store is\n * also fair game for the analyst-loop's LLM prompts, so personal\n * identifiers MUST be stripped before they leave the request path.\n *\n * Discipline:\n * - Match cheap, deterministic patterns at the string level (SSN, EIN).\n * - Match well-known sensitive object keys (case-insensitive) and\n * replace the value, never the key, so the shape of the object\n * remains debuggable.\n * - Recurse arrays + plain objects only; pass through everything else\n * unchanged (numbers, booleans, null, undefined, functions, etc).\n * - NEVER throw — a redaction failure must not crash the chat handler.\n * Unrecognized inputs round-trip as-is.\n */\n\nconst SSN_PATTERN = /\\d{3}-\\d{2}-\\d{4}/\nconst EIN_PATTERN = /\\d{2}-\\d{7}/\n\nconst SENSITIVE_KEYS = new Set([\n 'ssn',\n 'ein',\n 'password',\n 'apikey',\n 'token',\n 'secret',\n 'authorization',\n 'email',\n 'phone',\n])\n\nfunction redactString(value: string): string {\n if (SSN_PATTERN.test(value)) return '[REDACTED:ssn]'\n if (EIN_PATTERN.test(value)) return '[REDACTED:ein]'\n return value\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object') return false\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n}\n\nexport function redactForIngestion(value: unknown): unknown {\n if (typeof value === 'string') return redactString(value)\n if (Array.isArray(value)) return value.map(redactForIngestion)\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value)) {\n if (SENSITIVE_KEYS.has(k.toLowerCase())) {\n out[k] = '[REDACTED:field]'\n continue\n }\n out[k] = redactForIngestion(v)\n }\n return out\n }\n return value\n}\n"],"mappings":";AAoBA,IAAM,cAAc;AACpB,IAAM,cAAc;AAEpB,IAAM,iBAAiB,oBAAI,IAAI;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,aAAa,OAAuB;AAC3C,MAAI,YAAY,KAAK,KAAK,EAAG,QAAO;AACpC,MAAI,YAAY,KAAK,KAAK,EAAG,QAAO;AACpC,SAAO;AACT;AAEA,SAAS,cAAc,OAAkD;AACvE,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,QAAM,QAAQ,OAAO,eAAe,KAAK;AACzC,SAAO,UAAU,OAAO,aAAa,UAAU;AACjD;AAEO,SAAS,mBAAmB,OAAyB;AAC1D,MAAI,OAAO,UAAU,SAAU,QAAO,aAAa,KAAK;AACxD,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,kBAAkB;AAC7D,MAAI,cAAc,KAAK,GAAG;AACxB,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC1C,UAAI,eAAe,IAAI,EAAE,YAAY,CAAC,GAAG;AACvC,YAAI,CAAC,IAAI;AACT;AAAA,MACF;AACA,UAAI,CAAC,IAAI,mBAAmB,CAAC;AAAA,IAC/B;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
|