theorum 0.1.2

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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/docs/AGENT_PROFILE_CONTRACT.md +161 -0
  4. package/docs/CLI_SPEC.md +183 -0
  5. package/docs/SECRETS.md +55 -0
  6. package/esm/_dnt.polyfills.d.ts +11 -0
  7. package/esm/_dnt.polyfills.js +15 -0
  8. package/esm/_dnt.shims.d.ts +5 -0
  9. package/esm/_dnt.shims.js +61 -0
  10. package/esm/mod.d.ts +37 -0
  11. package/esm/mod.js +35 -0
  12. package/esm/package.json +3 -0
  13. package/esm/src/guardrails/error.d.ts +35 -0
  14. package/esm/src/guardrails/error.js +116 -0
  15. package/esm/src/guardrails/injection.d.ts +12 -0
  16. package/esm/src/guardrails/injection.js +220 -0
  17. package/esm/src/guardrails/keys.d.ts +12 -0
  18. package/esm/src/guardrails/keys.js +132 -0
  19. package/esm/src/guardrails/mod.d.ts +14 -0
  20. package/esm/src/guardrails/mod.js +14 -0
  21. package/esm/src/guardrails/sanitize.d.ts +22 -0
  22. package/esm/src/guardrails/sanitize.js +133 -0
  23. package/esm/src/guardrails/sensitive.d.ts +12 -0
  24. package/esm/src/guardrails/sensitive.js +88 -0
  25. package/esm/src/kernel/engine/boundary.d.ts +10 -0
  26. package/esm/src/kernel/engine/boundary.js +55 -0
  27. package/esm/src/kernel/engine/delta.d.ts +8 -0
  28. package/esm/src/kernel/engine/delta.js +362 -0
  29. package/esm/src/kernel/engine/hash.d.ts +1 -0
  30. package/esm/src/kernel/engine/hash.js +9 -0
  31. package/esm/src/kernel/engine/record.d.ts +2 -0
  32. package/esm/src/kernel/engine/record.js +7 -0
  33. package/esm/src/kernel/engine/repair.d.ts +9 -0
  34. package/esm/src/kernel/engine/repair.js +38 -0
  35. package/esm/src/kernel/engine/runner.d.ts +14 -0
  36. package/esm/src/kernel/engine/runner.js +731 -0
  37. package/esm/src/kernel/engine/tree.d.ts +2 -0
  38. package/esm/src/kernel/engine/tree.js +17 -0
  39. package/esm/src/kernel/mod.d.ts +16 -0
  40. package/esm/src/kernel/mod.js +15 -0
  41. package/esm/src/kernel/registry/catalog.d.ts +24 -0
  42. package/esm/src/kernel/registry/catalog.js +213 -0
  43. package/esm/src/kernel/registry/profiles.d.ts +36 -0
  44. package/esm/src/kernel/registry/profiles.js +111 -0
  45. package/esm/src/kernel/registry/resolve.d.ts +20 -0
  46. package/esm/src/kernel/registry/resolve.js +235 -0
  47. package/esm/src/kernel/registry/schemas.d.ts +14 -0
  48. package/esm/src/kernel/registry/schemas.js +23 -0
  49. package/esm/src/kernel/registry/tools.d.ts +12 -0
  50. package/esm/src/kernel/registry/tools.js +36 -0
  51. package/esm/src/kernel/types.d.ts +497 -0
  52. package/esm/src/kernel/types.js +10 -0
  53. package/esm/src/observability/mod.d.ts +12 -0
  54. package/esm/src/observability/mod.js +10 -0
  55. package/esm/src/observability/spans.d.ts +16 -0
  56. package/esm/src/observability/spans.js +56 -0
  57. package/esm/src/observability/trace-attach.d.ts +16 -0
  58. package/esm/src/observability/trace-attach.js +81 -0
  59. package/esm/src/observability/trace-record.d.ts +112 -0
  60. package/esm/src/observability/trace-record.js +140 -0
  61. package/esm/src/observability/trace-usage.d.ts +3 -0
  62. package/esm/src/observability/trace-usage.js +32 -0
  63. package/esm/src/observability/trace.d.ts +23 -0
  64. package/esm/src/observability/trace.js +121 -0
  65. package/esm/src/providers/attachments.d.ts +17 -0
  66. package/esm/src/providers/attachments.js +156 -0
  67. package/esm/src/providers/gemini-tape.d.ts +3 -0
  68. package/esm/src/providers/gemini-tape.js +46 -0
  69. package/esm/src/providers/google-tap.d.ts +3 -0
  70. package/esm/src/providers/google-tap.js +48 -0
  71. package/esm/src/providers/interactions.d.ts +5 -0
  72. package/esm/src/providers/interactions.js +127 -0
  73. package/esm/src/providers/media.d.ts +5 -0
  74. package/esm/src/providers/media.js +125 -0
  75. package/esm/src/providers/mod.d.ts +15 -0
  76. package/esm/src/providers/mod.js +13 -0
  77. package/esm/src/providers/openrouter-payload.d.ts +24 -0
  78. package/esm/src/providers/openrouter-payload.js +177 -0
  79. package/esm/src/providers/openrouter.d.ts +17 -0
  80. package/esm/src/providers/openrouter.js +332 -0
  81. package/esm/src/providers/provider.d.ts +13 -0
  82. package/esm/src/providers/provider.js +123 -0
  83. package/esm/src/providers/sse.d.ts +7 -0
  84. package/esm/src/providers/sse.js +53 -0
  85. package/esm/src/providers/tts.d.ts +24 -0
  86. package/esm/src/providers/tts.js +144 -0
  87. package/package.json +48 -0
@@ -0,0 +1,81 @@
1
+ import { modelEntry } from '../kernel/registry/catalog.js';
2
+ import { tapeGemini } from '../providers/gemini-tape.js';
3
+ import { toInteractionsBody } from '../providers/interactions.js';
4
+ import { httpStatus } from './trace-usage.js';
5
+ function completeRequest(generation, system) {
6
+ return {
7
+ model: generation.model,
8
+ previousInteractionId: generation.previousInteractionId,
9
+ store: generation.store,
10
+ thinking: generation.thinking,
11
+ summaries: generation.summaries,
12
+ maxOutputTokens: generation.maxOutputTokens,
13
+ temperature: generation.temperature,
14
+ builtins: generation.builtins,
15
+ system,
16
+ input: generation.input,
17
+ structured: generation.structured,
18
+ image: generation.image,
19
+ geminiBucket: generation.geminiBucket,
20
+ };
21
+ }
22
+ function attachResolved(record, args) {
23
+ const { safe, model, bucket, generation } = args;
24
+ if (safe.projectId) {
25
+ record.projectId = safe.projectId;
26
+ }
27
+ if (safe.select) {
28
+ record.select = safe.select;
29
+ }
30
+ if (safe.thinking !== undefined) {
31
+ record.thinking = safe.thinking;
32
+ }
33
+ if (safe.tools) {
34
+ record.tools = safe.tools;
35
+ }
36
+ if (safe.metadata) {
37
+ record.metadata = safe.metadata;
38
+ }
39
+ if (model) {
40
+ record.model = { id: model, apiId: modelEntry(model).apiId };
41
+ }
42
+ if (bucket) {
43
+ record.bucket = bucket;
44
+ }
45
+ if (generation) {
46
+ record.generation = {
47
+ thinking: generation.thinking,
48
+ summaries: generation.summaries,
49
+ temperature: generation.temperature,
50
+ maxOutputTokens: generation.maxOutputTokens,
51
+ builtins: generation.builtins,
52
+ custom: generation.custom,
53
+ structured: generation.structured,
54
+ image: generation.image,
55
+ };
56
+ }
57
+ }
58
+ async function attachTape(record, args) {
59
+ const { gemini, canary, system, generation } = args;
60
+ if (gemini !== undefined) {
61
+ record.gemini = await tapeGemini(gemini, canary ?? '');
62
+ }
63
+ if (generation && system !== undefined) {
64
+ record.wire = await tapeGemini(toInteractionsBody(completeRequest(generation, system)), canary ?? '');
65
+ }
66
+ }
67
+ function attachUsage(record, gemini, done) {
68
+ if (done?.usage !== undefined) {
69
+ record.usage = done.usage;
70
+ }
71
+ const upStatus = httpStatus(gemini);
72
+ if (upStatus !== undefined || done) {
73
+ record.upstream = {
74
+ status: upStatus,
75
+ id: done?.id,
76
+ finish: done?.status,
77
+ serviceTier: done?.service_tier ?? done?.serviceTier,
78
+ };
79
+ }
80
+ }
81
+ export { attachResolved, attachTape, attachUsage };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Trace record construction types.
3
+ *
4
+ * Trace records preserve useful execution evidence while hashing or omitting
5
+ * unsafe media bytes and canary-sensitive content.
6
+ *
7
+ * @module
8
+ */
9
+ import type { ResolvedGeneration, TurnEvent, TurnRequest } from '../kernel/types.js';
10
+ /** Hash-only image reference stored in trace records. */
11
+ export interface TraceImage {
12
+ mimeType: string;
13
+ sha256: string;
14
+ }
15
+ /** Trace-safe copy of a public turn event. */
16
+ export interface TraceEvent {
17
+ type: string;
18
+ text?: string;
19
+ tool?: {
20
+ name: string;
21
+ arguments?: Record<string, unknown>;
22
+ result?: {
23
+ status: string;
24
+ finding?: string;
25
+ data?: Record<string, unknown>;
26
+ };
27
+ };
28
+ structured?: unknown;
29
+ media?: TraceImage;
30
+ grounding?: TurnEvent['grounding'];
31
+ evidence?: TurnEvent['evidence'];
32
+ error?: string;
33
+ }
34
+ /** Complete trace-safe record for one attempted turn. */
35
+ interface TraceRecord {
36
+ v: number;
37
+ id: string;
38
+ ts: number;
39
+ ms: number;
40
+ streamed: boolean;
41
+ cancelled: boolean;
42
+ previousInteractionId: string | null;
43
+ store: boolean | null;
44
+ profile: string;
45
+ title?: string;
46
+ projectId?: string;
47
+ select?: string;
48
+ thinking?: boolean;
49
+ tools?: TurnRequest['tools'];
50
+ metadata?: Record<string, unknown>;
51
+ model?: {
52
+ id: string;
53
+ apiId: string;
54
+ };
55
+ bucket?: string;
56
+ generation?: {
57
+ thinking: string;
58
+ summaries: string;
59
+ temperature: number;
60
+ maxOutputTokens: number;
61
+ builtins: string[];
62
+ custom: string[];
63
+ structured: string | null;
64
+ image: unknown;
65
+ };
66
+ input: {
67
+ text?: string;
68
+ role?: string;
69
+ slots?: Record<string, string>;
70
+ attachments: TraceImage[];
71
+ voice: TraceImage[];
72
+ images?: TraceImage[];
73
+ audio?: TraceImage[];
74
+ };
75
+ wire?: unknown;
76
+ events: TraceEvent[];
77
+ gemini?: unknown;
78
+ usage?: unknown;
79
+ upstream?: {
80
+ status?: unknown;
81
+ id?: unknown;
82
+ finish?: unknown;
83
+ serviceTier?: unknown;
84
+ };
85
+ ok: boolean;
86
+ error?: string;
87
+ errorInternal?: string;
88
+ app?: Record<string, unknown>;
89
+ cutout?: {
90
+ ok: boolean;
91
+ ms: number;
92
+ url?: string;
93
+ inSha256?: string;
94
+ outSha256?: string;
95
+ http?: unknown;
96
+ error?: string;
97
+ };
98
+ }
99
+ declare function buildRecord(args: {
100
+ req: TurnRequest;
101
+ events: TurnEvent[];
102
+ started: number;
103
+ model?: string;
104
+ bucket?: string;
105
+ thrown?: unknown;
106
+ gemini?: unknown;
107
+ canary?: string;
108
+ system?: string;
109
+ generation?: ResolvedGeneration;
110
+ }): Promise<TraceRecord>;
111
+ export type { TraceRecord };
112
+ export { buildRecord };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Trace record construction types.
3
+ *
4
+ * Trace records preserve useful execution evidence while hashing or omitting
5
+ * unsafe media bytes and canary-sensitive content.
6
+ *
7
+ * @module
8
+ */
9
+ import { publicError } from '../guardrails/error.js';
10
+ import { sanitizeText, sanitizeTurnRequest } from '../guardrails/sanitize.js';
11
+ import { OMIT_CANARY } from '../kernel/engine/boundary.js';
12
+ import { sha256 } from '../kernel/engine/hash.js';
13
+ import { attachResolved, attachTape, attachUsage } from './trace-attach.js';
14
+ import { completedInteraction } from './trace-usage.js';
15
+ const TRACE_VERSION = 2;
16
+ const TITLE_MAX = 80;
17
+ function hashBlobs(blobs) {
18
+ if (!blobs) {
19
+ return Promise.resolve([]);
20
+ }
21
+ return Promise.all(blobs.map(async (blob) => ({
22
+ mimeType: blob.mimeType,
23
+ sha256: await sha256(blob.data),
24
+ })));
25
+ }
26
+ async function snapshotEvent(event) {
27
+ const row = { type: event.type };
28
+ if (event.text) {
29
+ row.text = sanitizeText(event.text);
30
+ }
31
+ if (event.error) {
32
+ row.error = event.error;
33
+ }
34
+ if (event.structured !== undefined) {
35
+ row.structured = event.structured;
36
+ }
37
+ if (event.tool) {
38
+ const { name, arguments: args, result } = event.tool;
39
+ row.tool = { name, arguments: args };
40
+ if (result) {
41
+ row.tool.result = {
42
+ status: result.status,
43
+ ...(result.finding ? { finding: result.finding } : {}),
44
+ ...(result.data ? { data: result.data } : {}),
45
+ };
46
+ }
47
+ }
48
+ if (event.media) {
49
+ row.media = {
50
+ mimeType: event.media.mimeType,
51
+ sha256: await sha256(event.media.data),
52
+ };
53
+ }
54
+ if (event.grounding) {
55
+ row.grounding = event.grounding;
56
+ }
57
+ if (event.evidence) {
58
+ row.evidence = event.evidence;
59
+ }
60
+ return row;
61
+ }
62
+ function requestForTrace(req) {
63
+ try {
64
+ return sanitizeTurnRequest(req);
65
+ }
66
+ catch {
67
+ return { profile: req.profile, input: {} };
68
+ }
69
+ }
70
+ function internalError(err) {
71
+ if (typeof err === 'string') {
72
+ return sanitizeText(err);
73
+ }
74
+ if (err instanceof Error && err.message) {
75
+ return sanitizeText(err.message);
76
+ }
77
+ return undefined;
78
+ }
79
+ function titleFrom(text) {
80
+ if (!text) {
81
+ return undefined;
82
+ }
83
+ const trimmed = text.trim().replaceAll(/\s+/g, ' ');
84
+ if (!trimmed) {
85
+ return undefined;
86
+ }
87
+ return trimmed.slice(0, TITLE_MAX);
88
+ }
89
+ function attachFailure(record, thrown, lastErr, canary) {
90
+ if (!record.ok) {
91
+ record.error = publicError(thrown ?? lastErr?.error);
92
+ const inside = internalError(thrown) ?? lastErr?.error;
93
+ if (inside) {
94
+ record.errorInternal = inside;
95
+ }
96
+ }
97
+ if (canary && JSON.stringify(record).includes(canary)) {
98
+ record.errorInternal = OMIT_CANARY;
99
+ }
100
+ }
101
+ async function buildRecord(args) {
102
+ const { req, events, started, model, bucket, thrown, gemini, canary, system, generation } = args;
103
+ const safe = requestForTrace(req);
104
+ const input = safe.input ?? {};
105
+ const snapped = await Promise.all(events.map((event) => snapshotEvent(event)));
106
+ const lastErr = [...snapped].reverse().find((row) => row.type === 'error');
107
+ const done = completedInteraction(gemini);
108
+ const status = done?.status;
109
+ const ok = !(thrown || lastErr) && status !== 'cancelled';
110
+ const record = {
111
+ v: TRACE_VERSION,
112
+ id: crypto.randomUUID(),
113
+ ts: started,
114
+ ms: Date.now() - started,
115
+ streamed: true,
116
+ cancelled: status === 'cancelled',
117
+ previousInteractionId: safe.previousInteractionId ?? null,
118
+ store: safe.store ?? null,
119
+ profile: safe.profile,
120
+ input: {
121
+ text: input.text,
122
+ role: input.role,
123
+ slots: input.slots,
124
+ attachments: await hashBlobs(input.attachments),
125
+ voice: await hashBlobs(input.voice),
126
+ },
127
+ events: snapped,
128
+ ok,
129
+ };
130
+ const title = titleFrom(input.text);
131
+ if (title) {
132
+ record.title = title;
133
+ }
134
+ await attachTape(record, { gemini, canary, system, generation });
135
+ attachUsage(record, gemini, done);
136
+ attachResolved(record, { safe, model, bucket, generation });
137
+ attachFailure(record, thrown, lastErr, canary);
138
+ return record;
139
+ }
140
+ export { buildRecord };
@@ -0,0 +1,3 @@
1
+ declare function completedInteraction(gemini: unknown): Record<string, unknown> | undefined;
2
+ declare function httpStatus(gemini: unknown): unknown;
3
+ export { completedInteraction, httpStatus };
@@ -0,0 +1,32 @@
1
+ import { asRecord } from '../kernel/engine/record.js';
2
+ function eventName(row) {
3
+ return String(row.event_type ?? row.eventType ?? '');
4
+ }
5
+ function completedInteraction(gemini) {
6
+ if (!Array.isArray(gemini)) {
7
+ return undefined;
8
+ }
9
+ for (let i = gemini.length - 1; i >= 0; i -= 1) {
10
+ const row = asRecord(gemini[i]);
11
+ if (row) {
12
+ const name = eventName(row);
13
+ if (name === 'interaction.completed' || name === 'interaction.complete') {
14
+ return asRecord(row.interaction) ?? row;
15
+ }
16
+ }
17
+ }
18
+ return undefined;
19
+ }
20
+ function httpStatus(gemini) {
21
+ if (!Array.isArray(gemini)) {
22
+ return undefined;
23
+ }
24
+ for (const item of gemini) {
25
+ const row = asRecord(item);
26
+ if (row && eventName(row) === 'http_response') {
27
+ return row.status;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ export { completedInteraction, httpStatus };
@@ -0,0 +1,23 @@
1
+ import type { TraceRecord } from './trace-record.js';
2
+ /** Minimal async destination for completed turn trace records. */
3
+ interface TraceSink {
4
+ write: (record: TraceRecord) => Promise<void>;
5
+ }
6
+ /** Write a trace record without allowing trace failures to fail the turn. */
7
+ declare function writeTrace(sink: TraceSink, record: Promise<TraceRecord>): Promise<void>;
8
+ /** Trace sink that drops records. */
9
+ declare function noopSink(): TraceSink;
10
+ /** Trace sink that appends records to a caller-owned array. */
11
+ declare function memorySink(into: TraceRecord[]): TraceSink;
12
+ /** Trace sink that writes daily rotating JSONL files under a host-selected directory. */
13
+ declare function jsonlSink(dir: string, now?: () => number): TraceSink;
14
+ /** Resolve a trace directory while refusing relative paths or paths inside the clone. */
15
+ declare function resolveTraceDir(args: {
16
+ dir?: string;
17
+ fallbackDir?: string;
18
+ cwd?: string;
19
+ }): string | undefined;
20
+ /** Build a JSONL sink from a host-supplied directory or return a noop sink. */
21
+ declare function sinkFromDir(dir?: string, fallbackDir?: string): TraceSink;
22
+ export type { TraceSink };
23
+ export { jsonlSink, memorySink, noopSink, resolveTraceDir, sinkFromDir, writeTrace };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Trace sink primitives for THEORUM.
3
+ *
4
+ * Tracing is host-injected: the kernel can write to a provided sink, a memory
5
+ * sink, a JSONL directory, or a noop sink. It does not read environment
6
+ * variables or own a database destination.
7
+ *
8
+ * @module
9
+ */
10
+ import * as dntShim from "../../_dnt.shims.js";
11
+ const RETAIN_DAYS = 14;
12
+ const HOURS_PER_DAY = 24;
13
+ const MIN_PER_HOUR = 60;
14
+ const SEC_PER_MIN = 60;
15
+ const MS_PER_SEC = 1000;
16
+ const RETAIN_MS = RETAIN_DAYS * HOURS_PER_DAY * MIN_PER_HOUR * SEC_PER_MIN * MS_PER_SEC;
17
+ const KIB = 1024;
18
+ const MIB = KIB * KIB;
19
+ const ROTATE_MIB = 32;
20
+ const ROTATE_BYTES = ROTATE_MIB * MIB;
21
+ const FILE_DAY = /^turns-(\d{4}-\d{2}-\d{2})(?:-\d+)?\.jsonl$/;
22
+ /** Write a trace record without allowing trace failures to fail the turn. */
23
+ async function writeTrace(sink, record) {
24
+ try {
25
+ await sink.write(await record);
26
+ }
27
+ catch {
28
+ // Tracing must not fail the turn.
29
+ }
30
+ }
31
+ /** Trace sink that drops records. */
32
+ function noopSink() {
33
+ return { write: () => Promise.resolve() };
34
+ }
35
+ /** Trace sink that appends records to a caller-owned array. */
36
+ function memorySink(into) {
37
+ return {
38
+ write: (record) => {
39
+ into.push(record);
40
+ return Promise.resolve();
41
+ },
42
+ };
43
+ }
44
+ function dayStamp(ms) {
45
+ return new Date(ms).toISOString().slice(0, 10);
46
+ }
47
+ function fileDay(name) {
48
+ return FILE_DAY.exec(name)?.[1];
49
+ }
50
+ async function pruneTraces(dir, now) {
51
+ const cutoff = now - RETAIN_MS;
52
+ for await (const entry of dntShim.Deno.readDir(dir)) {
53
+ const day = fileDay(entry.name);
54
+ if (day && Date.parse(`${day}T00:00:00.000Z`) < cutoff) {
55
+ await dntShim.Deno.remove(`${dir}/${entry.name}`);
56
+ }
57
+ }
58
+ }
59
+ async function pickFile(dir, now) {
60
+ const day = dayStamp(now);
61
+ const base = `${dir}/turns-${day}.jsonl`;
62
+ try {
63
+ const info = await dntShim.Deno.stat(base);
64
+ if ((info.size ?? 0) < ROTATE_BYTES) {
65
+ return base;
66
+ }
67
+ }
68
+ catch {
69
+ return base;
70
+ }
71
+ return `${dir}/turns-${day}-${now}.jsonl`;
72
+ }
73
+ /** Trace sink that writes daily rotating JSONL files under a host-selected directory. */
74
+ function jsonlSink(dir, now = Date.now) {
75
+ return {
76
+ write: async (record) => {
77
+ const at = now();
78
+ await dntShim.Deno.mkdir(dir, { recursive: true });
79
+ await pruneTraces(dir, at);
80
+ const path = await pickFile(dir, at);
81
+ await dntShim.Deno.writeTextFile(path, `${JSON.stringify(record)}\n`, { append: true });
82
+ },
83
+ };
84
+ }
85
+ function insideDir(path, root) {
86
+ let base = root;
87
+ if (root.endsWith('/')) {
88
+ base = root.slice(0, -1);
89
+ }
90
+ if (path === base) {
91
+ return true;
92
+ }
93
+ return path.startsWith(`${base}/`);
94
+ }
95
+ /** Resolve a trace directory while refusing relative paths or paths inside the clone. */
96
+ function resolveTraceDir(args) {
97
+ const cwd = args.cwd ?? dntShim.Deno.cwd();
98
+ if (args.dir === '') {
99
+ return undefined;
100
+ }
101
+ let dir = args.dir?.trim() || args.fallbackDir;
102
+ if (!dir) {
103
+ return undefined;
104
+ }
105
+ if (!dir.startsWith('/') || insideDir(dir, cwd)) {
106
+ dir = args.fallbackDir;
107
+ }
108
+ if (!dir || insideDir(dir, cwd)) {
109
+ return undefined;
110
+ }
111
+ return dir;
112
+ }
113
+ /** Build a JSONL sink from a host-supplied directory or return a noop sink. */
114
+ function sinkFromDir(dir, fallbackDir) {
115
+ const resolved = resolveTraceDir({ dir, fallbackDir });
116
+ if (!resolved) {
117
+ return noopSink();
118
+ }
119
+ return jsonlSink(resolved);
120
+ }
121
+ export { jsonlSink, memorySink, noopSink, resolveTraceDir, sinkFromDir, writeTrace };
@@ -0,0 +1,17 @@
1
+ import type { MediaLimits, MimeInputs, Profile, TurnBlob } from '../kernel/types.js';
2
+ declare function tooManyFilesMessage(maxFiles: number): string;
3
+ declare function fileTooLargeMessage(maxBytes: number): string;
4
+ declare function turnTooLargeMessage(maxTurnBytes: number): string;
5
+ declare function mediaLimits(inputs: MimeInputs): MediaLimits | undefined;
6
+ declare function requireMediaLimits(profile: Profile): MediaLimits;
7
+ declare function sanitizeCsvText(text: string): string;
8
+ declare function assertAttachmentLimits(blobs: TurnBlob[], limits: MediaLimits): void;
9
+ declare function sanitizeTurnBlobs(attachments: TurnBlob[] | undefined, voice: TurnBlob[] | undefined, limits: MediaLimits | undefined): {
10
+ attachments?: TurnBlob[];
11
+ voice?: TurnBlob[];
12
+ };
13
+ declare function sanitizeTurnBlobsForProfile(profileId: string, attachments: TurnBlob[] | undefined, voice: TurnBlob[] | undefined): {
14
+ attachments?: TurnBlob[];
15
+ voice?: TurnBlob[];
16
+ };
17
+ export { assertAttachmentLimits, fileTooLargeMessage, mediaLimits, requireMediaLimits, sanitizeCsvText, sanitizeTurnBlobs, sanitizeTurnBlobsForProfile, tooManyFilesMessage, turnTooLargeMessage, };
@@ -0,0 +1,156 @@
1
+ import { TheorumError } from '../guardrails/error.js';
2
+ import { injectionSpans } from '../guardrails/injection.js';
3
+ import { sensitiveSpans } from '../guardrails/sensitive.js';
4
+ import { getProfile } from '../kernel/registry/profiles.js';
5
+ import { applySpans } from '../observability/spans.js';
6
+ const B64_PAD = 2;
7
+ const B64_WORD = 4;
8
+ const B64_TRIPLET = 3;
9
+ const CSV_FORMULA = /(^|,)(\s*)("?)(?:([=@])|([+-])(?![0-9."]))/gm;
10
+ const B64_BODY = /^[A-Za-z0-9+/]*={0,2}$/;
11
+ const TEXT_MIMES = new Set(['text/csv', 'text/plain', 'text/markdown']);
12
+ const BYTES_PER_KIB = 1024;
13
+ function formatMb(bytes) {
14
+ const mb = bytes / (BYTES_PER_KIB * BYTES_PER_KIB);
15
+ return Number.isInteger(mb) ? `${String(mb)} MB` : `${mb.toFixed(1)} MB`;
16
+ }
17
+ function tooManyFilesMessage(maxFiles) {
18
+ return maxFiles === 1
19
+ ? 'Only 1 file per message.'
20
+ : `Only ${String(maxFiles)} files per message.`;
21
+ }
22
+ function fileTooLargeMessage(maxBytes) {
23
+ return `Each file must be ${formatMb(maxBytes)} or smaller.`;
24
+ }
25
+ function turnTooLargeMessage(maxTurnBytes) {
26
+ return `Those files together are too large for one message (${formatMb(maxTurnBytes)} max).`;
27
+ }
28
+ function mediaLimits(inputs) {
29
+ const { maxFiles, maxBytes, maxTurnBytes, limitsByMime } = inputs;
30
+ if (maxFiles && maxBytes && maxTurnBytes) {
31
+ return { maxFiles, maxBytes, maxTurnBytes, limitsByMime };
32
+ }
33
+ return undefined;
34
+ }
35
+ function maxBytesForMime(mimeType, limits) {
36
+ if (limits.limitsByMime) {
37
+ const cleanMime = mimeType.split(';')[0]?.trim().toLowerCase() ?? '';
38
+ if (limits.limitsByMime[cleanMime]) {
39
+ return limits.limitsByMime[cleanMime];
40
+ }
41
+ const [category] = cleanMime.split('/');
42
+ const wildCard = `${category}/*`;
43
+ if (limits.limitsByMime[wildCard]) {
44
+ return limits.limitsByMime[wildCard];
45
+ }
46
+ }
47
+ return limits.maxBytes;
48
+ }
49
+ function requireMediaLimits(profile) {
50
+ const limits = mediaLimits(profile.inputs);
51
+ if (!limits) {
52
+ throw new TheorumError(`Profile ${profile.id} must set maxFiles, maxBytes, and maxTurnBytes`);
53
+ }
54
+ return limits;
55
+ }
56
+ function b64DecodedLen(data) {
57
+ let pad = 0;
58
+ if (data.endsWith('==')) {
59
+ pad = B64_PAD;
60
+ }
61
+ else if (data.endsWith('=')) {
62
+ pad = 1;
63
+ }
64
+ return Math.floor((data.length * B64_TRIPLET) / B64_WORD) - pad;
65
+ }
66
+ function decodeB64(data) {
67
+ const binary = atob(data);
68
+ const bytes = new Uint8Array(binary.length);
69
+ for (let i = 0; i < binary.length; i += 1) {
70
+ bytes[i] = binary.charCodeAt(i);
71
+ }
72
+ return bytes;
73
+ }
74
+ function encodeB64(bytes) {
75
+ let binary = '';
76
+ for (const byte of bytes) {
77
+ binary += String.fromCharCode(byte);
78
+ }
79
+ return btoa(binary);
80
+ }
81
+ function decodeText(bytes) {
82
+ try {
83
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
84
+ }
85
+ catch {
86
+ return new TextDecoder('latin1').decode(bytes);
87
+ }
88
+ }
89
+ function sanitizeCsvText(text) {
90
+ return text.replace(CSV_FORMULA, (_full, ...groups) => {
91
+ const [a, b, c, d, e] = groups;
92
+ return `${a}${b}${c}'${d ?? ''}${e ?? ''}`;
93
+ });
94
+ }
95
+ function sanitizeTextBytes(mime, bytes) {
96
+ let text = decodeText(bytes);
97
+ if (mime === 'text/csv') {
98
+ text = sanitizeCsvText(text);
99
+ }
100
+ return new TextEncoder().encode(applySpans(text, [...injectionSpans(text), ...sensitiveSpans(text)]));
101
+ }
102
+ function assertAttachmentLimits(blobs, limits) {
103
+ if (blobs.length > limits.maxFiles) {
104
+ throw new TheorumError(tooManyFilesMessage(limits.maxFiles));
105
+ }
106
+ let total = 0;
107
+ for (const blob of blobs) {
108
+ const { data, mimeType } = blob;
109
+ if (!B64_BODY.test(data)) {
110
+ throw new TheorumError('attachment data must be base64');
111
+ }
112
+ const size = b64DecodedLen(data);
113
+ const maxAllowed = maxBytesForMime(mimeType, limits);
114
+ if (size > maxAllowed) {
115
+ throw new TheorumError(fileTooLargeMessage(maxAllowed));
116
+ }
117
+ total += size;
118
+ }
119
+ if (total > limits.maxTurnBytes) {
120
+ throw new TheorumError(turnTooLargeMessage(limits.maxTurnBytes));
121
+ }
122
+ }
123
+ function sanitizeAttachment(blob) {
124
+ const { mimeType, data } = blob;
125
+ if (!TEXT_MIMES.has(mimeType.split(';')[0]?.trim().toLowerCase() ?? '')) {
126
+ return blob;
127
+ }
128
+ const bytes = sanitizeTextBytes(mimeType, decodeB64(data));
129
+ return { mimeType, data: encodeB64(bytes) };
130
+ }
131
+ function hasTurnBlobs(attachments, voice) {
132
+ return (attachments?.length ?? 0) > 0 || (voice?.length ?? 0) > 0;
133
+ }
134
+ function sanitizeTurnBlobs(attachments, voice, limits) {
135
+ if (!hasTurnBlobs(attachments, voice)) {
136
+ return { attachments, voice };
137
+ }
138
+ const files = attachments ?? [];
139
+ const clips = voice ?? [];
140
+ if (!limits) {
141
+ throw new TheorumError('This profile does not accept files.');
142
+ }
143
+ assertAttachmentLimits([...files, ...clips], limits);
144
+ return {
145
+ attachments: files.length > 0 ? files.map(sanitizeAttachment) : attachments,
146
+ voice: clips.length > 0 ? clips.map(sanitizeAttachment) : voice,
147
+ };
148
+ }
149
+ function sanitizeTurnBlobsForProfile(profileId, attachments, voice) {
150
+ if (!hasTurnBlobs(attachments, voice)) {
151
+ return { attachments, voice };
152
+ }
153
+ const limits = requireMediaLimits(getProfile(profileId));
154
+ return sanitizeTurnBlobs(attachments, voice, limits);
155
+ }
156
+ export { assertAttachmentLimits, fileTooLargeMessage, mediaLimits, requireMediaLimits, sanitizeCsvText, sanitizeTurnBlobs, sanitizeTurnBlobsForProfile, tooManyFilesMessage, turnTooLargeMessage, };
@@ -0,0 +1,3 @@
1
+ import { sha256 } from '../kernel/engine/hash.js';
2
+ declare function tapeGemini(value: unknown, canary: string): Promise<unknown>;
3
+ export { sha256, tapeGemini };