@tangle-network/agent-app 0.45.57 → 0.45.59
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/assistant/index.js +1 -1
- package/dist/chat-routes/attachment-store.d.ts +66 -26
- package/dist/chat-routes/attachment-upload.d.ts +59 -38
- package/dist/chat-routes/attachment-write-safety.d.ts +11 -0
- package/dist/chat-routes/index.js +387 -20
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-routes/promote-file-part.d.ts +28 -13
- package/dist/chat-store/index.js +1 -1
- package/dist/chunk-CDC5HFKL.js +165 -0
- package/dist/chunk-CDC5HFKL.js.map +1 -0
- package/dist/{chunk-5T5TSBM4.js → chunk-FOXGPGXF.js} +42 -4
- package/dist/chunk-FOXGPGXF.js.map +1 -0
- package/dist/{chunk-LRHVCVEW.js → chunk-Q4ZER4HI.js} +39 -1
- package/dist/chunk-Q4ZER4HI.js.map +1 -0
- package/dist/redact/index.d.ts +1 -0
- package/dist/redact/index.js +10 -116
- package/dist/redact/index.js.map +1 -1
- package/dist/store/index.d.ts +35 -0
- package/dist/store/index.js +3 -1
- package/dist/web-react/chat-composer.d.ts +6 -1
- package/dist/web-react/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5T5TSBM4.js.map +0 -1
- package/dist/chunk-LRHVCVEW.js.map +0 -1
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
* instead of losing the file silently.
|
|
11
11
|
*
|
|
12
12
|
* Storage-parameterized port of gtm-agent's `promote-file-parts.ts` with the
|
|
13
|
-
* refactor gtm never made: persistence goes through
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
13
|
+
* refactor gtm never made: persistence goes through an injected stable writer
|
|
14
|
+
* or ownership-safe adapter, the path strategy is the injected
|
|
15
|
+
* `buildAttachmentPath` (neutral `uploads/agent/<date>/` default, no domain
|
|
16
|
+
* bucket taxonomy baked), the MIME map is an injectable hook, and the date
|
|
17
|
+
* segment reads an injectable clock. The logical `hash8(id ?? url ?? filename)`
|
|
18
|
+
* naming is preserved. Atomic callers also receive a fresh ownership suffix so
|
|
19
|
+
* an ambiguous write cannot overwrite an older object.
|
|
20
20
|
*/
|
|
21
21
|
import { type SandboxExecChannel } from '../sandbox/binary-read';
|
|
22
22
|
import { type ChatAttachmentKind, type ChatAttachmentPart } from '../chat-store/parts';
|
|
23
|
-
import type
|
|
23
|
+
import { type AtomicAttachmentWriter, type WriteAttachmentFn } from './attachment-store';
|
|
24
24
|
/** Default ceiling on a promoted file's raw (pre-encoding) byte size. */
|
|
25
25
|
export declare const PROMOTE_MAX_FILE_BYTES: number;
|
|
26
26
|
/** Define the structure for a raw file part with optional metadata and media type information */
|
|
@@ -59,8 +59,8 @@ export interface AttachmentPathArgs {
|
|
|
59
59
|
}
|
|
60
60
|
/** Default MIME hook: extension → mime, or `text/plain` for the unknown. */
|
|
61
61
|
export declare function sniffMimeFromName(filename: string): string;
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
type PromoteFilePartLogger = Pick<Console, 'error'>;
|
|
63
|
+
interface PromoteFilePartCommonOptions {
|
|
64
64
|
raw: RawAgentFilePart;
|
|
65
65
|
/** The turn's box — required only to promote a sandbox-path part; a `data:`
|
|
66
66
|
* URI needs none. */
|
|
@@ -69,8 +69,6 @@ export interface PromoteAgentFilePartOptions {
|
|
|
69
69
|
scopeId: string;
|
|
70
70
|
/** The turn's session id, used for the sandbox stat/read exec calls. */
|
|
71
71
|
sessionId: string;
|
|
72
|
-
/** REQUIRED store writer — no default (the product owns its store). */
|
|
73
|
-
writeAttachment: WriteAttachmentFn;
|
|
74
72
|
/** Store-path strategy. Default {@link defaultBuildAttachmentPath}. */
|
|
75
73
|
buildAttachmentPath?: (args: AttachmentPathArgs) => string;
|
|
76
74
|
/** Raw-byte ceiling. Default {@link PROMOTE_MAX_FILE_BYTES}. */
|
|
@@ -79,6 +77,23 @@ export interface PromoteAgentFilePartOptions {
|
|
|
79
77
|
sniffMime?: (filename: string) => string;
|
|
80
78
|
/** Clock for the date path segment. Default `() => new Date()`. */
|
|
81
79
|
now?: () => Date;
|
|
80
|
+
/** Unique id source for tests or a product's id service. */
|
|
81
|
+
createWriteId?: () => string;
|
|
82
|
+
/** Server-side sink for redacted storage details. */
|
|
83
|
+
logger?: PromoteFilePartLogger;
|
|
84
|
+
}
|
|
85
|
+
/** Stable promotion options from the original writer contract. */
|
|
86
|
+
export interface PromoteAgentFilePartOptions extends PromoteFilePartCommonOptions {
|
|
87
|
+
/** REQUIRED store writer — no default (the product owns its store). */
|
|
88
|
+
writeAttachment: WriteAttachmentFn;
|
|
89
|
+
}
|
|
90
|
+
/** Ownership-safe promotion options for new products. */
|
|
91
|
+
export interface AtomicPromoteAgentFilePartOptions extends PromoteFilePartCommonOptions {
|
|
92
|
+
/** Complete writer + ambiguous-write cleanup adapter. */
|
|
93
|
+
attachmentWriter: AtomicAttachmentWriter;
|
|
82
94
|
}
|
|
83
|
-
/** Promote a part
|
|
95
|
+
/** Promote a part using the stable writer contract. */
|
|
84
96
|
export declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
|
|
97
|
+
/** Promote a part using an ownership-safe writer and cleanup adapter. */
|
|
98
|
+
export declare function promoteAgentFilePart(options: AtomicPromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
|
|
99
|
+
export {};
|
package/dist/chat-store/index.js
CHANGED
|
@@ -0,0 +1,165 @@
|
|
|
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, validate } of patterns) {
|
|
19
|
+
if (!validate) {
|
|
20
|
+
if (pattern.test(value)) return `[REDACTED:${kind}]`;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const g = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
24
|
+
for (const m of value.matchAll(g)) {
|
|
25
|
+
if (m[0].length > 0 && validate(m[0])) return `[REDACTED:${kind}]`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function maskSpans(text, patterns = DEFAULT_REDACTION_PATTERNS) {
|
|
31
|
+
const spans = detectSpans(text, patterns);
|
|
32
|
+
if (spans.length === 0) return text;
|
|
33
|
+
let out = "";
|
|
34
|
+
let pos = 0;
|
|
35
|
+
for (const s of spans) {
|
|
36
|
+
if (s.start > pos) out += text.slice(pos, s.start);
|
|
37
|
+
out += `[REDACTED:${s.kind}]`;
|
|
38
|
+
pos = s.end;
|
|
39
|
+
}
|
|
40
|
+
if (pos < text.length) out += text.slice(pos);
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
var ERROR_SECRET_PATTERNS = [
|
|
44
|
+
...DEFAULT_REDACTION_PATTERNS,
|
|
45
|
+
{ kind: "bearer", pattern: /Bearer\s+[^\s]+/i },
|
|
46
|
+
{ kind: "credential", pattern: /\b(?:sk|pk|tc|ghp|xoxb)[_-][A-Za-z0-9_-]{8,}\b/i },
|
|
47
|
+
{
|
|
48
|
+
kind: "credential",
|
|
49
|
+
pattern: /\b(?:access[_-]?key[_-]?id|secret[_-]?access[_-]?key|session[_-]?token|security[_-]?token|x-amz-(?:credential|signature|security-token)|aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key|aws[_-]?session[_-]?token|api[_-]?key|client[_-]?secret|credential|token|secret|password|signature|sig|authorization)\s*[:=]\s*[^\s,;&]+/i
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
function safeString(value) {
|
|
53
|
+
try {
|
|
54
|
+
return typeof value === "string" ? value : String(value);
|
|
55
|
+
} catch {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function safeErrorText(input) {
|
|
60
|
+
if (input !== null && (typeof input === "object" || typeof input === "function")) {
|
|
61
|
+
try {
|
|
62
|
+
const message = Reflect.get(input, "message");
|
|
63
|
+
const messageText = safeString(message);
|
|
64
|
+
if (messageText !== void 0) return messageText;
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return safeString(input) ?? "";
|
|
69
|
+
}
|
|
70
|
+
function redactErrorMessage(input, fallback = "unknown error") {
|
|
71
|
+
const fallbackText = safeString(fallback)?.trim() || "unknown error";
|
|
72
|
+
const raw = safeErrorText(input);
|
|
73
|
+
const message = raw.trim() || fallbackText;
|
|
74
|
+
try {
|
|
75
|
+
const redacted = maskSpans(message, ERROR_SECRET_PATTERNS).trim();
|
|
76
|
+
return redacted.length > 240 ? `${redacted.slice(0, 240)}\u2026` : redacted || fallbackText;
|
|
77
|
+
} catch {
|
|
78
|
+
return fallbackText;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isPlainObject(value) {
|
|
82
|
+
if (value === null || typeof value !== "object") return false;
|
|
83
|
+
const proto = Object.getPrototypeOf(value);
|
|
84
|
+
return proto === Object.prototype || proto === null;
|
|
85
|
+
}
|
|
86
|
+
function redactForIngestion(value, options = {}) {
|
|
87
|
+
const patterns = options.extraPatterns ? [...DEFAULT_REDACTION_PATTERNS, ...options.extraPatterns] : DEFAULT_REDACTION_PATTERNS;
|
|
88
|
+
const sensitiveKeys = options.extraSensitiveKeys ? /* @__PURE__ */ new Set([...SENSITIVE_KEYS, ...options.extraSensitiveKeys.map((k) => k.toLowerCase())]) : SENSITIVE_KEYS;
|
|
89
|
+
const maskString = options.stringMode === "mask-spans" ? (s) => maskSpans(s, patterns) : (s) => redactString(s, patterns);
|
|
90
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
91
|
+
const walk = (v) => {
|
|
92
|
+
if (typeof v === "string") return maskString(v);
|
|
93
|
+
if (Array.isArray(v)) {
|
|
94
|
+
if (seen.has(v)) return v;
|
|
95
|
+
seen.add(v);
|
|
96
|
+
return v.map(walk);
|
|
97
|
+
}
|
|
98
|
+
if (isPlainObject(v)) {
|
|
99
|
+
if (seen.has(v)) return v;
|
|
100
|
+
seen.add(v);
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const [k, val] of Object.entries(v)) {
|
|
103
|
+
out[k] = sensitiveKeys.has(k.toLowerCase()) ? "[REDACTED:field]" : walk(val);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
return v;
|
|
108
|
+
};
|
|
109
|
+
return walk(value);
|
|
110
|
+
}
|
|
111
|
+
function detectSpans(text, patterns = DEFAULT_REDACTION_PATTERNS) {
|
|
112
|
+
const raw = [];
|
|
113
|
+
for (const { kind, pattern, validate } of patterns) {
|
|
114
|
+
const g = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
115
|
+
for (const m of text.matchAll(g)) {
|
|
116
|
+
if (m.index === void 0 || m[0].length === 0) continue;
|
|
117
|
+
if (validate && !validate(m[0])) continue;
|
|
118
|
+
raw.push({ kind, start: m.index, end: m.index + m[0].length, text: m[0] });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
raw.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
122
|
+
const spans = [];
|
|
123
|
+
let cursor = -1;
|
|
124
|
+
let i = 0;
|
|
125
|
+
for (const s of raw) {
|
|
126
|
+
if (s.start < cursor) continue;
|
|
127
|
+
spans.push({ id: `span-${i++}`, ...s });
|
|
128
|
+
cursor = s.end;
|
|
129
|
+
}
|
|
130
|
+
return spans;
|
|
131
|
+
}
|
|
132
|
+
async function buildRedactedDocument(text, options) {
|
|
133
|
+
const spans = detectSpans(text, options.patterns);
|
|
134
|
+
const segments = [];
|
|
135
|
+
let pos = 0;
|
|
136
|
+
for (const span of spans) {
|
|
137
|
+
if (span.start > pos) segments.push({ type: "text", text: text.slice(pos, span.start) });
|
|
138
|
+
segments.push({ type: "redacted", id: span.id, kind: span.kind, cipher: await options.encrypt(span.text) });
|
|
139
|
+
pos = span.end;
|
|
140
|
+
}
|
|
141
|
+
if (pos < text.length) segments.push({ type: "text", text: text.slice(pos) });
|
|
142
|
+
return { segments };
|
|
143
|
+
}
|
|
144
|
+
async function revealSpan(doc, spanId, options) {
|
|
145
|
+
const seg = doc.segments.find(
|
|
146
|
+
(s) => s.type === "redacted" && s.id === spanId
|
|
147
|
+
);
|
|
148
|
+
if (!seg) return { ok: false, reason: "not_found" };
|
|
149
|
+
const allowed = await options.canReveal({ id: seg.id, kind: seg.kind });
|
|
150
|
+
if (!allowed) return { ok: false, reason: "forbidden" };
|
|
151
|
+
const value = await options.decrypt(seg.cipher);
|
|
152
|
+
if (options.onReveal) await options.onReveal({ id: seg.id, kind: seg.kind });
|
|
153
|
+
return { ok: true, value };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export {
|
|
157
|
+
DEFAULT_REDACTION_PATTERNS,
|
|
158
|
+
maskSpans,
|
|
159
|
+
redactErrorMessage,
|
|
160
|
+
redactForIngestion,
|
|
161
|
+
detectSpans,
|
|
162
|
+
buildRedactedDocument,
|
|
163
|
+
revealSpan
|
|
164
|
+
};
|
|
165
|
+
//# sourceMappingURL=chunk-CDC5HFKL.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 /** Optional predicate over each match — the pattern fires only when it returns\n * true. For matches a regex alone can't decide (e.g. a Luhn check on a\n * card-number candidate). When set, the value is scanned globally and the\n * first match that passes wins; when absent, a plain `pattern.test` decides. */\n validate?: (match: string) => boolean\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\n/** Define options to customize sensitive data redaction patterns and key names for ingestion */\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 /** Extra sensitive object-key names (case-insensitive) added to the built-in\n * set, e.g. the snake_case `api_key` an intake form uses. Additive. */\n extraSensitiveKeys?: readonly string[]\n /**\n * How a matched string is rewritten:\n * - `'collapse'` (default) — the whole string becomes `[REDACTED:<kind>]` on\n * the first matching pattern. Safest for telemetry: nothing of the original\n * survives.\n * - `'mask-spans'` — only the matched substrings are replaced (each with\n * `[REDACTED:<kind>]`), preserving surrounding text. Use when a downstream\n * reader needs the non-PII context (e.g. an analyst loop reading prose).\n */\n stringMode?: 'collapse' | 'mask-spans'\n}\n\nfunction redactString(value: string, patterns: readonly RedactionPattern[]): string {\n for (const { kind, pattern, validate } of patterns) {\n if (!validate) {\n if (pattern.test(value)) return `[REDACTED:${kind}]`\n continue\n }\n const g = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)\n for (const m of value.matchAll(g)) {\n if (m[0].length > 0 && validate(m[0])) return `[REDACTED:${kind}]`\n }\n }\n return value\n}\n\n/**\n * Replace only the PII substrings in `text`, preserving everything around them\n * (the `mask-spans` string mode). Built on {@link detectSpans} so matching,\n * non-overlap, and `validate` predicates behave identically to the reversible\n * path. Each span becomes `[REDACTED:<kind>]`.\n */\nexport function maskSpans(\n text: string,\n patterns: readonly RedactionPattern[] = DEFAULT_REDACTION_PATTERNS,\n): string {\n const spans = detectSpans(text, patterns)\n if (spans.length === 0) return text\n let out = ''\n let pos = 0\n for (const s of spans) {\n if (s.start > pos) out += text.slice(pos, s.start)\n out += `[REDACTED:${s.kind}]`\n pos = s.end\n }\n if (pos < text.length) out += text.slice(pos)\n return out\n}\n\nconst ERROR_SECRET_PATTERNS: readonly RedactionPattern[] = [\n ...DEFAULT_REDACTION_PATTERNS,\n { kind: 'bearer', pattern: /Bearer\\s+[^\\s]+/i },\n { kind: 'credential', pattern: /\\b(?:sk|pk|tc|ghp|xoxb)[_-][A-Za-z0-9_-]{8,}\\b/i },\n {\n kind: 'credential',\n pattern: /\\b(?:access[_-]?key[_-]?id|secret[_-]?access[_-]?key|session[_-]?token|security[_-]?token|x-amz-(?:credential|signature|security-token)|aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key|aws[_-]?session[_-]?token|api[_-]?key|client[_-]?secret|credential|token|secret|password|signature|sig|authorization)\\s*[:=]\\s*[^\\s,;&]+/i,\n },\n]\n\n/** Sanitize an untrusted backend error for server logs. This is deliberately\n * separate from public response text: callers should return an opaque message\n * and log this bounded, redacted value for operators. */\nfunction safeString(value: unknown): string | undefined {\n try {\n return typeof value === 'string' ? value : String(value)\n } catch {\n return undefined\n }\n}\n\nfunction safeErrorText(input: unknown): string {\n if (input !== null && (typeof input === 'object' || typeof input === 'function')) {\n try {\n const message = Reflect.get(input, 'message')\n const messageText = safeString(message)\n if (messageText !== undefined) return messageText\n } catch {\n // Hostile getters and proxies are still untrusted error values.\n }\n }\n return safeString(input) ?? ''\n}\n\nexport function redactErrorMessage(input: unknown, fallback = 'unknown error'): string {\n const fallbackText = safeString(fallback)?.trim() || 'unknown error'\n const raw = safeErrorText(input)\n const message = raw.trim() || fallbackText\n try {\n const redacted = maskSpans(message, ERROR_SECRET_PATTERNS).trim()\n return redacted.length > 240 ? `${redacted.slice(0, 240)}…` : redacted || fallbackText\n } catch {\n return fallbackText\n }\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 sensitiveKeys = options.extraSensitiveKeys\n ? new Set([...SENSITIVE_KEYS, ...options.extraSensitiveKeys.map((k) => k.toLowerCase())])\n : SENSITIVE_KEYS\n const maskString =\n options.stringMode === 'mask-spans'\n ? (s: string) => maskSpans(s, patterns)\n : (s: string) => redactString(s, patterns)\n // Cycle guard: a payload with a circular reference would otherwise recurse\n // forever. On re-encountering an object/array, return it untouched to break\n // the cycle (the same value was already redacted on its first visit).\n const seen = new WeakSet<object>()\n const walk = (v: unknown): unknown => {\n if (typeof v === 'string') return maskString(v)\n if (Array.isArray(v)) {\n if (seen.has(v)) return v\n seen.add(v)\n return v.map(walk)\n }\n if (isPlainObject(v)) {\n if (seen.has(v)) return v\n seen.add(v)\n const out: Record<string, unknown> = {}\n for (const [k, val] of Object.entries(v)) {\n out[k] = sensitiveKeys.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, validate } 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 if (validate && !validate(m[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\n/** Define a document composed of multiple redacted content segments */\nexport interface RedactedDocument {\n segments: RedactedDocSegment[]\n}\n\n/** Define options to encrypt text and specify patterns for redacting sensitive document content */\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\n/** Define options to decrypt, authorize, and audit the reveal of a span segment */\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\n/** Describe the outcome of a reveal operation including success status, value, and failure reason */\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":[]}
|
|
@@ -1421,6 +1421,9 @@ function SendGlyph({ className }) {
|
|
|
1421
1421
|
function StopGlyph({ className }) {
|
|
1422
1422
|
return /* @__PURE__ */ jsx7("svg", { className, viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx7("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) });
|
|
1423
1423
|
}
|
|
1424
|
+
function ArrowUpGlyph({ className }) {
|
|
1425
|
+
return /* @__PURE__ */ jsx7("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx7("path", { d: "M12 19V5M5 12l7-7 7 7" }) });
|
|
1426
|
+
}
|
|
1424
1427
|
function PaperclipGlyph({ className }) {
|
|
1425
1428
|
return /* @__PURE__ */ jsx7("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx7("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) });
|
|
1426
1429
|
}
|
|
@@ -1480,6 +1483,7 @@ function ChatComposer({
|
|
|
1480
1483
|
focusShortcut = true,
|
|
1481
1484
|
floating = false,
|
|
1482
1485
|
sendLabel = "Send",
|
|
1486
|
+
sendVariant = "pill",
|
|
1483
1487
|
className
|
|
1484
1488
|
}) {
|
|
1485
1489
|
const isControlled = value !== void 0;
|
|
@@ -1823,7 +1827,17 @@ function ChatComposer({
|
|
|
1823
1827
|
children: showInline && controls
|
|
1824
1828
|
}
|
|
1825
1829
|
),
|
|
1826
|
-
isStreaming ? /* @__PURE__ */
|
|
1830
|
+
isStreaming ? sendVariant === "icon" ? /* @__PURE__ */ jsx7(
|
|
1831
|
+
"button",
|
|
1832
|
+
{
|
|
1833
|
+
type: "button",
|
|
1834
|
+
onClick: onCancel,
|
|
1835
|
+
"aria-label": "Stop response",
|
|
1836
|
+
title: "Stop",
|
|
1837
|
+
className: "inline-flex h-[34px] w-[34px] shrink-0 items-center justify-center rounded-full border border-border bg-transparent text-foreground transition hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1838
|
+
children: /* @__PURE__ */ jsx7(StopGlyph, { className: "h-3 w-3" })
|
|
1839
|
+
}
|
|
1840
|
+
) : /* @__PURE__ */ jsxs5(
|
|
1827
1841
|
"button",
|
|
1828
1842
|
{
|
|
1829
1843
|
type: "button",
|
|
@@ -1835,6 +1849,17 @@ function ChatComposer({
|
|
|
1835
1849
|
/* @__PURE__ */ jsx7("span", { children: "Stop" })
|
|
1836
1850
|
]
|
|
1837
1851
|
}
|
|
1852
|
+
) : sendVariant === "icon" ? /* @__PURE__ */ jsx7(
|
|
1853
|
+
"button",
|
|
1854
|
+
{
|
|
1855
|
+
type: "button",
|
|
1856
|
+
onClick: send,
|
|
1857
|
+
disabled: !canSend,
|
|
1858
|
+
"aria-label": sendLabel,
|
|
1859
|
+
title: sendLabel,
|
|
1860
|
+
className: "inline-flex h-[34px] w-[34px] shrink-0 items-center justify-center rounded-full bg-foreground text-background transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-card",
|
|
1861
|
+
children: /* @__PURE__ */ jsx7(ArrowUpGlyph, { className: "h-4 w-4" })
|
|
1862
|
+
}
|
|
1838
1863
|
) : /* @__PURE__ */ jsxs5(
|
|
1839
1864
|
"button",
|
|
1840
1865
|
{
|
|
@@ -5882,6 +5907,11 @@ function formatModelCost(msg, models) {
|
|
|
5882
5907
|
if (!isFinite(cost) || cost <= 0) return null;
|
|
5883
5908
|
return cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}`;
|
|
5884
5909
|
}
|
|
5910
|
+
function reasoningPreview(reasoning) {
|
|
5911
|
+
const flat = reasoning.replace(/\s+/g, " ").trim();
|
|
5912
|
+
if (!flat) return void 0;
|
|
5913
|
+
return flat.length > 120 ? `${flat.slice(0, 119)}\u2026` : flat;
|
|
5914
|
+
}
|
|
5885
5915
|
function formatTokensPerSecond(msg) {
|
|
5886
5916
|
if (msg.completionTokens == null || !msg.durationMs) return null;
|
|
5887
5917
|
return `${Math.round(msg.completionTokens / (msg.durationMs / 1e3))} tok/s`;
|
|
@@ -6475,7 +6505,15 @@ function AssistantMessageImpl({
|
|
|
6475
6505
|
title: !hasAnswerText ? /* @__PURE__ */ jsxs13("span", { className: "agent-shimmer", "data-motion": "essential", children: [
|
|
6476
6506
|
"Thinking",
|
|
6477
6507
|
thinkingSeconds >= 1 ? ` \xB7 ${thinkingSeconds}s` : "\u2026"
|
|
6478
|
-
] }) : thinkMsRef.current != null ?
|
|
6508
|
+
] }) : thinkMsRef.current != null ? (
|
|
6509
|
+
// Words, not the abbreviated unit — "Thought for 4 seconds" reads
|
|
6510
|
+
// like a sentence; "4s" reads like a log line.
|
|
6511
|
+
`Thought for ${(() => {
|
|
6512
|
+
const s = Math.max(1, Math.round(thinkMsRef.current / 1e3));
|
|
6513
|
+
return `${s} second${s === 1 ? "" : "s"}`;
|
|
6514
|
+
})()}`
|
|
6515
|
+
) : "Thought process",
|
|
6516
|
+
description: hasAnswerText ? reasoningPreview(reasoning) : void 0,
|
|
6479
6517
|
status: hasAnswerText ? "idle" : "running",
|
|
6480
6518
|
open: reasoningOpen,
|
|
6481
6519
|
onOpenChange: (next) => setReasoningToggled(next),
|
|
@@ -6483,7 +6521,7 @@ function AssistantMessageImpl({
|
|
|
6483
6521
|
"div",
|
|
6484
6522
|
{
|
|
6485
6523
|
ref: reasoningScrollRef,
|
|
6486
|
-
className: "max-h-48 overflow-y-auto whitespace-pre-wrap px-3 py-2.5 text-
|
|
6524
|
+
className: "max-h-48 overflow-y-auto whitespace-pre-wrap px-3 py-2.5 text-sm leading-relaxed text-muted-foreground",
|
|
6487
6525
|
children: reasoning
|
|
6488
6526
|
}
|
|
6489
6527
|
)
|
|
@@ -6813,4 +6851,4 @@ export {
|
|
|
6813
6851
|
useThinkingSeconds,
|
|
6814
6852
|
ChatMessages
|
|
6815
6853
|
};
|
|
6816
|
-
//# sourceMappingURL=chunk-
|
|
6854
|
+
//# sourceMappingURL=chunk-FOXGPGXF.js.map
|