@vizuh/sabi 0.1.3 → 0.1.5
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/mod/sabi.mjs +507 -57
- package/package.json +1 -1
- package/sabi.config.json +3 -0
package/mod/sabi.mjs
CHANGED
|
@@ -1,6 +1,260 @@
|
|
|
1
|
-
// @vizuh/sabi 0.1.
|
|
1
|
+
// @vizuh/sabi 0.1.5 — generated by pack.mjs from packages/adapters/command-code/mod/sabi.ts.
|
|
2
2
|
// Source and docs: https://github.com/vizuh/sabi
|
|
3
3
|
|
|
4
|
+
// packages/core/src/telemetry.ts
|
|
5
|
+
var ALLOWLIST = /* @__PURE__ */ new Set([
|
|
6
|
+
"error-line",
|
|
7
|
+
"python-traceback",
|
|
8
|
+
"panic",
|
|
9
|
+
"exception",
|
|
10
|
+
"typescript-error",
|
|
11
|
+
"fail-marker",
|
|
12
|
+
"command-failed",
|
|
13
|
+
"nonzero-exit",
|
|
14
|
+
"failure-count",
|
|
15
|
+
"command-not-found",
|
|
16
|
+
"permission-denied",
|
|
17
|
+
"missing-file",
|
|
18
|
+
"soft-warning",
|
|
19
|
+
"soft-deprecated",
|
|
20
|
+
"soft-retrying",
|
|
21
|
+
"soft-timeout",
|
|
22
|
+
"tool-error",
|
|
23
|
+
"permission-denial",
|
|
24
|
+
"rate-limited",
|
|
25
|
+
"quota-exceeded",
|
|
26
|
+
"timeout",
|
|
27
|
+
"mutation",
|
|
28
|
+
"verification-receipt",
|
|
29
|
+
"summary-claim",
|
|
30
|
+
"scope-observed",
|
|
31
|
+
"constraint",
|
|
32
|
+
"prior-failure",
|
|
33
|
+
"context-boundary",
|
|
34
|
+
"observation"
|
|
35
|
+
]);
|
|
36
|
+
function allowlisted(value) {
|
|
37
|
+
const code = value.split(":")[0]?.trim();
|
|
38
|
+
return ALLOWLIST.has(code);
|
|
39
|
+
}
|
|
40
|
+
function telemetryPolicy(config) {
|
|
41
|
+
const captureSnippets = config?.captureSnippets === true;
|
|
42
|
+
const captureChars = config?.captureChars ?? 800;
|
|
43
|
+
return {
|
|
44
|
+
captureSnippets,
|
|
45
|
+
allowlisted: (value) => allowlisted(value),
|
|
46
|
+
snippet: (value) => captureSnippets ? String(value).slice(0, captureChars) : ""
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function sanitizeReason(reason, policy) {
|
|
50
|
+
if (!policy.captureSnippets) {
|
|
51
|
+
if (!reason || allowlisted(reason)) return reason;
|
|
52
|
+
const segments = reason.split(":").map((segment) => segment.trim());
|
|
53
|
+
const last = segments[segments.length - 1] ?? "";
|
|
54
|
+
if (allowlisted(last)) return reason;
|
|
55
|
+
return "reason withheld (telemetry.allowlistOnly)";
|
|
56
|
+
}
|
|
57
|
+
return policy.snippet(reason);
|
|
58
|
+
}
|
|
59
|
+
var KEYWORD_REDACT = /\b(bearer|authorization|api[-_]?\s*key|token|secret|password|passwd)\b["']?\s*[:=\s]\s*["']?[^\s"'{},\]]+/gi;
|
|
60
|
+
var BARE_TOKEN_REDACT = [
|
|
61
|
+
[/\bsk-[A-Za-z0-9_-]{8,}\b/g, "sk-[REDACTED]"],
|
|
62
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "AKIA[REDACTED]"],
|
|
63
|
+
[/\bAIza[0-9A-Za-z_-]{30,}\b/g, "AIza[REDACTED]"],
|
|
64
|
+
[/\bghp_[A-Za-z0-9]{8,}\b/g, "ghp_[REDACTED]"],
|
|
65
|
+
[/\bghu_[A-Za-z0-9]{8,}\b/g, "ghu_[REDACTED]"],
|
|
66
|
+
[/\bghs_[A-Za-z0-9]{8,}\b/g, "ghs_[REDACTED]"],
|
|
67
|
+
[/\bgithub_pat_[A-Za-z0-9_]{8,}\b/g, "github_pat_[REDACTED]"]
|
|
68
|
+
];
|
|
69
|
+
var CREDENTIAL_URL_REDACT = /([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^:@\s/]+:[^@\s/]+@/g;
|
|
70
|
+
function sanitizeError(error) {
|
|
71
|
+
const first = String(error ?? "").split("\n")[0]?.slice(0, 200) ?? "";
|
|
72
|
+
if (!first.trim()) return "unknown error";
|
|
73
|
+
let redacted = first.replace(KEYWORD_REDACT, "$1=REDACTED");
|
|
74
|
+
for (const [pattern, replacement] of BARE_TOKEN_REDACT) redacted = redacted.replace(pattern, replacement);
|
|
75
|
+
redacted = redacted.replace(CREDENTIAL_URL_REDACT, "$1REDACTED@");
|
|
76
|
+
if (looksLikeCanary(redacted)) return "upstream error redacted (possible secret)";
|
|
77
|
+
return redacted || "unknown error";
|
|
78
|
+
}
|
|
79
|
+
var CANARY_PATTERNS = [
|
|
80
|
+
/\b(BEGIN|END)\s+(RSA|OPENSSH|EC|DSA)\s+PRIVATE\s+KEY/i,
|
|
81
|
+
/\bsk-[A-Za-z0-9_-]{8,}\b/,
|
|
82
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
83
|
+
/\bAIza[0-9A-Za-z_-]{30,}\b/,
|
|
84
|
+
/\bghp_[A-Za-z0-9]{8,}\b/,
|
|
85
|
+
/\bghu_[A-Za-z0-9]{8,}\b/,
|
|
86
|
+
/\bghs_[A-Za-z0-9]{8,}\b/,
|
|
87
|
+
/\bgithub_pat_[A-Za-z0-9_]{8,}\b/,
|
|
88
|
+
/[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:@\s/]+:[^@\s/]+@/,
|
|
89
|
+
/(?:password|passwd|pwd|secret)\s*[:=]\s*\S+/i
|
|
90
|
+
];
|
|
91
|
+
function looksLikeCanary(text) {
|
|
92
|
+
return CANARY_PATTERNS.some((re) => re.test(text));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// packages/core/src/evidence.ts
|
|
96
|
+
var MAX_EVIDENCE_ITEMS = 16;
|
|
97
|
+
var MAX_EVIDENCE_DETAIL_CHARS = 240;
|
|
98
|
+
var MAX_SCOPE_ITEMS = 32;
|
|
99
|
+
var MAX_SCOPE_LABEL_CHARS = 120;
|
|
100
|
+
var MAX_RECEIPT_ID_CHARS = 128;
|
|
101
|
+
var EVIDENCE_CODES = /* @__PURE__ */ new Set([
|
|
102
|
+
"error-line",
|
|
103
|
+
"python-traceback",
|
|
104
|
+
"panic",
|
|
105
|
+
"exception",
|
|
106
|
+
"typescript-error",
|
|
107
|
+
"fail-marker",
|
|
108
|
+
"command-failed",
|
|
109
|
+
"nonzero-exit",
|
|
110
|
+
"failure-count",
|
|
111
|
+
"command-not-found",
|
|
112
|
+
"permission-denied",
|
|
113
|
+
"missing-file",
|
|
114
|
+
"soft-warning",
|
|
115
|
+
"soft-deprecated",
|
|
116
|
+
"soft-retrying",
|
|
117
|
+
"soft-timeout",
|
|
118
|
+
"tool-error",
|
|
119
|
+
"permission-denial",
|
|
120
|
+
"rate-limited",
|
|
121
|
+
"quota-exceeded",
|
|
122
|
+
"timeout",
|
|
123
|
+
"mutation",
|
|
124
|
+
"verification-receipt",
|
|
125
|
+
"summary-claim",
|
|
126
|
+
"scope-observed",
|
|
127
|
+
"constraint",
|
|
128
|
+
"prior-failure",
|
|
129
|
+
"context-boundary",
|
|
130
|
+
"observation"
|
|
131
|
+
]);
|
|
132
|
+
var EVIDENCE_SOURCES = /* @__PURE__ */ new Set(["tool", "user", "harness", "judge", "summary"]);
|
|
133
|
+
var EVIDENCE_STATUSES = /* @__PURE__ */ new Set(["observed", "verified", "unverified", "contradicted"]);
|
|
134
|
+
function boundedText(value, maxChars) {
|
|
135
|
+
if (typeof value !== "string") return void 0;
|
|
136
|
+
const text = value.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
|
|
137
|
+
if (!text || looksLikeCanary(text)) return void 0;
|
|
138
|
+
return text.slice(0, maxChars);
|
|
139
|
+
}
|
|
140
|
+
function generationOf(value) {
|
|
141
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
142
|
+
}
|
|
143
|
+
function isEvidenceCode(value) {
|
|
144
|
+
return typeof value === "string" && EVIDENCE_CODES.has(value);
|
|
145
|
+
}
|
|
146
|
+
function isEvidenceSource(value) {
|
|
147
|
+
return typeof value === "string" && EVIDENCE_SOURCES.has(value);
|
|
148
|
+
}
|
|
149
|
+
function isEvidenceStatus(value) {
|
|
150
|
+
return typeof value === "string" && EVIDENCE_STATUSES.has(value);
|
|
151
|
+
}
|
|
152
|
+
function normalizeTrajectoryEvidence(value, fallbackGeneration = 0) {
|
|
153
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
154
|
+
const item = value;
|
|
155
|
+
if (!isEvidenceCode(item.code) || !isEvidenceSource(item.source) || !isEvidenceStatus(item.status)) return void 0;
|
|
156
|
+
const detail = boundedText(item.detail, MAX_EVIDENCE_DETAIL_CHARS);
|
|
157
|
+
return {
|
|
158
|
+
code: item.code,
|
|
159
|
+
source: item.source,
|
|
160
|
+
status: item.status,
|
|
161
|
+
contextGeneration: generationOf(item.contextGeneration ?? fallbackGeneration),
|
|
162
|
+
...detail ? { detail } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function boundTrajectoryEvidence(values, maxItems = MAX_EVIDENCE_ITEMS) {
|
|
166
|
+
const result = [];
|
|
167
|
+
if (maxItems <= 0) return result;
|
|
168
|
+
const seen = /* @__PURE__ */ new Set();
|
|
169
|
+
for (const value of values ?? []) {
|
|
170
|
+
const item = normalizeTrajectoryEvidence(value);
|
|
171
|
+
if (!item) continue;
|
|
172
|
+
const key = JSON.stringify(item);
|
|
173
|
+
if (seen.has(key)) continue;
|
|
174
|
+
seen.add(key);
|
|
175
|
+
result.push(item);
|
|
176
|
+
if (result.length >= Math.max(0, Math.min(maxItems, MAX_EVIDENCE_ITEMS))) break;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
function countScope(value) {
|
|
181
|
+
if (typeof value === "number") return Number.isSafeInteger(value) && value >= 0 ? { count: value } : {};
|
|
182
|
+
if (!Array.isArray(value)) return {};
|
|
183
|
+
const labels = [...new Set(value.map((item) => boundedText(item, MAX_SCOPE_LABEL_CHARS)).filter((item) => Boolean(item)))].slice(0, MAX_SCOPE_ITEMS);
|
|
184
|
+
return { count: labels.length, labels };
|
|
185
|
+
}
|
|
186
|
+
function buildScopeCoverage(input = {}) {
|
|
187
|
+
const expected = countScope(input.expected);
|
|
188
|
+
const observed = countScope(input.observed);
|
|
189
|
+
const explicit = input.source === "explicit" || input.expected !== void 0;
|
|
190
|
+
const source = input.source ?? (explicit ? "explicit" : input.observed !== void 0 ? "inferred" : "unknown");
|
|
191
|
+
const expectedCount = expected.count;
|
|
192
|
+
const observedCount = observed.count;
|
|
193
|
+
const ratio = expectedCount !== void 0 && expectedCount > 0 && observedCount !== void 0 ? Math.min(1, observedCount / expectedCount) : void 0;
|
|
194
|
+
const missing = input.missing ? [...new Set(input.missing.map((item) => boundedText(item, MAX_SCOPE_LABEL_CHARS)).filter((item) => Boolean(item)))].slice(0, MAX_SCOPE_ITEMS) : expected.labels && observed.labels ? expected.labels.filter((item) => !observed.labels.includes(item)).slice(0, MAX_SCOPE_ITEMS) : void 0;
|
|
195
|
+
return {
|
|
196
|
+
...expectedCount !== void 0 ? { expected: expectedCount } : {},
|
|
197
|
+
...observedCount !== void 0 ? { observed: observedCount } : {},
|
|
198
|
+
...ratio !== void 0 ? { ratio } : {},
|
|
199
|
+
source,
|
|
200
|
+
...missing && missing.length > 0 ? { missing } : {}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function validReceipt(value, generation) {
|
|
204
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
205
|
+
const item = value;
|
|
206
|
+
const id = boundedText(item.id, MAX_RECEIPT_ID_CHARS);
|
|
207
|
+
const status = item.status === "passed" || item.status === "failed" ? item.status : void 0;
|
|
208
|
+
const source = item.source === "tool" || item.source === "user" || item.source === "harness" ? item.source : void 0;
|
|
209
|
+
const receiptGeneration = generationOf(item.generation);
|
|
210
|
+
if (!id || !status || !source || item.valid === false || receiptGeneration !== generation) return void 0;
|
|
211
|
+
return { id, status, source, generation: receiptGeneration, ...item.valid === true ? { valid: true } : {} };
|
|
212
|
+
}
|
|
213
|
+
function deriveVerificationState(input = {}) {
|
|
214
|
+
const generation = generationOf(input.generation);
|
|
215
|
+
const receiptCandidate = input.receipt;
|
|
216
|
+
if (receiptCandidate !== void 0) {
|
|
217
|
+
const receipt = validReceipt(receiptCandidate, generation);
|
|
218
|
+
if (receipt) return { status: receipt.status, receiptId: receipt.id, generation };
|
|
219
|
+
return { status: "unknown", reason: "invalid-receipt", generation };
|
|
220
|
+
}
|
|
221
|
+
if (input.summaryClaim) return { status: "unknown", reason: "summary-without-receipt", generation };
|
|
222
|
+
if (input.mutation) return { status: "needed", reason: "mutation-without-receipt", generation };
|
|
223
|
+
if (input.verificationAttempted) return { status: "attempted", reason: "missing-receipt", generation };
|
|
224
|
+
return { status: "not-required", generation };
|
|
225
|
+
}
|
|
226
|
+
function evidenceForTrajectory(state, options = {}) {
|
|
227
|
+
const generation = generationOf(state.contextGeneration);
|
|
228
|
+
const evidence = [];
|
|
229
|
+
if (state.roundKind === "implementation") evidence.push({ code: "mutation", source: "tool", status: "observed", contextGeneration: generation });
|
|
230
|
+
for (const code of state.failureEvidence) {
|
|
231
|
+
if (isEvidenceCode(code)) evidence.push({ code, source: "tool", status: "observed", contextGeneration: generation });
|
|
232
|
+
}
|
|
233
|
+
if (options.verificationReceipt) evidence.push({ code: "verification-receipt", source: "harness", status: "verified", contextGeneration: generation });
|
|
234
|
+
if (options.summaryClaim) evidence.push({ code: "summary-claim", source: "summary", status: "unverified", contextGeneration: generation });
|
|
235
|
+
if (generation > 0) evidence.push({ code: "context-boundary", source: "harness", status: "observed", contextGeneration: generation });
|
|
236
|
+
return boundTrajectoryEvidence(evidence);
|
|
237
|
+
}
|
|
238
|
+
function decorateTrajectoryState(state, metadata = {}) {
|
|
239
|
+
const generation = generationOf(metadata.generation ?? state.contextGeneration);
|
|
240
|
+
const receipt = metadata.verificationReceipt;
|
|
241
|
+
const verification = deriveVerificationState({
|
|
242
|
+
mutation: state.roundKind === "implementation",
|
|
243
|
+
verificationAttempted: state.roundKind === "verification",
|
|
244
|
+
summaryClaim: metadata.summaryClaim,
|
|
245
|
+
generation,
|
|
246
|
+
receipt
|
|
247
|
+
});
|
|
248
|
+
const scopeCoverage = metadata.scope ? buildScopeCoverage(metadata.scope) : void 0;
|
|
249
|
+
const evidence = evidenceForTrajectory(state, { summaryClaim: metadata.summaryClaim, verificationReceipt: receipt !== void 0 });
|
|
250
|
+
return {
|
|
251
|
+
...state,
|
|
252
|
+
...evidence.length > 0 ? { evidence } : {},
|
|
253
|
+
verification,
|
|
254
|
+
...scopeCoverage ? { scopeCoverage } : {}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
4
258
|
// packages/core/src/state.ts
|
|
5
259
|
var EXPLORE_TOOLS = /* @__PURE__ */ new Set([
|
|
6
260
|
"read",
|
|
@@ -331,8 +585,8 @@ function servesInputModalities(declared, required) {
|
|
|
331
585
|
return required.every((modality) => declared.includes(modality));
|
|
332
586
|
}
|
|
333
587
|
function firstServingTier(tiers, required, declared) {
|
|
334
|
-
for (const
|
|
335
|
-
if (servesInputModalities(declared(
|
|
588
|
+
for (const name of Object.keys(tiers).sort()) {
|
|
589
|
+
if (servesInputModalities(declared(tiers[name]), required)) return name;
|
|
336
590
|
}
|
|
337
591
|
return void 0;
|
|
338
592
|
}
|
|
@@ -441,6 +695,9 @@ function validateModelMetadata(value, label) {
|
|
|
441
695
|
if (!Array.isArray(items) || items.some((item) => typeof item !== "string" || !item.trim())) {
|
|
442
696
|
fail(`capabilities.${field}`, "must be an array of nonempty strings");
|
|
443
697
|
}
|
|
698
|
+
if ((field === "inputModalities" || field === "outputModalities") && items.length === 0) {
|
|
699
|
+
fail(`capabilities.${field}`, "must not be empty");
|
|
700
|
+
}
|
|
444
701
|
const strings = items;
|
|
445
702
|
if (new Set(strings).size !== strings.length) fail(`capabilities.${field}`, "must not contain duplicates");
|
|
446
703
|
const allowed = field.endsWith("Modalities") ? MODALITIES : field === "structuredOutput" ? ["json_object", "json_schema"] : void 0;
|
|
@@ -530,11 +787,30 @@ function validateConfig(value, source = "<inline>") {
|
|
|
530
787
|
throw new Error(`Sabi config ${source}: alias '${alias}' targets unknown tier '${target}'`);
|
|
531
788
|
}
|
|
532
789
|
}
|
|
790
|
+
const knownRules = new Set(POLICY_ORDER);
|
|
533
791
|
for (const [condition, tier] of Object.entries(policy)) {
|
|
792
|
+
if (!knownRules.has(condition)) {
|
|
793
|
+
throw new Error(`Sabi config ${source}: policy rule '${condition}' is not a known rule (${POLICY_ORDER.join(", ")})`);
|
|
794
|
+
}
|
|
534
795
|
if (typeof tier !== "string" || tier !== "off" && !Object.hasOwn(models, tier)) {
|
|
535
796
|
throw new Error(`Sabi config ${source}: policy rule '${condition}' targets unknown tier '${tier}'`);
|
|
536
797
|
}
|
|
537
798
|
}
|
|
799
|
+
if (Object.values(aliases).includes("auto")) {
|
|
800
|
+
const fallbackTier = typeof policy.unclassified === "string" && policy.unclassified !== "off" ? policy.unclassified : "cheap";
|
|
801
|
+
if (!Object.hasOwn(models, fallbackTier)) {
|
|
802
|
+
throw new Error(`Sabi config ${source}: policy.unclassified must resolve to a declared tier (got '${String(policy.unclassified)}')`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const transportFallback = config.transportFallback;
|
|
806
|
+
if (transportFallback !== void 0) {
|
|
807
|
+
if (!isObject(transportFallback)) {
|
|
808
|
+
throw new Error(`Sabi config ${source}: transportFallback must be an object`);
|
|
809
|
+
}
|
|
810
|
+
if (transportFallback.enabled !== void 0 && typeof transportFallback.enabled !== "boolean") {
|
|
811
|
+
throw new Error(`Sabi config ${source}: transportFallback.enabled must be a boolean`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
538
814
|
const judge = config.judge;
|
|
539
815
|
if (judge !== void 0) {
|
|
540
816
|
if (typeof judge !== "object" || judge === null || typeof judge.enabled !== "boolean") {
|
|
@@ -556,6 +832,14 @@ function validateConfig(value, source = "<inline>") {
|
|
|
556
832
|
if (!Array.isArray(judge.callOn) || judge.callOn.some((rule) => typeof rule !== "string")) {
|
|
557
833
|
throw new Error(`Sabi config ${source}: judge.callOn must be an array of policy rule names`);
|
|
558
834
|
}
|
|
835
|
+
for (const rule of judge.callOn) {
|
|
836
|
+
if (!knownRules.has(rule)) {
|
|
837
|
+
throw new Error(`Sabi config ${source}: judge.callOn rule '${rule}' is not a known rule (${POLICY_ORDER.join(", ")})`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (judge.includeSnippets !== void 0 && typeof judge.includeSnippets !== "boolean") {
|
|
842
|
+
throw new Error(`Sabi config ${source}: judge.includeSnippets must be a boolean`);
|
|
559
843
|
}
|
|
560
844
|
for (const [name, value2] of Object.entries(judge.thresholds ?? {})) {
|
|
561
845
|
if (typeof value2 !== "number" || value2 < 0 || value2 > 1) {
|
|
@@ -632,25 +916,233 @@ function validateConfig(value, source = "<inline>") {
|
|
|
632
916
|
if (tier.minPlan !== void 0 && typeof tier.minPlan !== "string") {
|
|
633
917
|
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.minPlan must be a string`);
|
|
634
918
|
}
|
|
919
|
+
if (tier.inputModalities !== void 0) {
|
|
920
|
+
if (!Array.isArray(tier.inputModalities) || tier.inputModalities.length === 0 || tier.inputModalities.some((modality) => typeof modality !== "string" || !MODALITIES.includes(modality))) {
|
|
921
|
+
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.inputModalities must be a nonempty array of: ${MODALITIES.join(", ")}`);
|
|
922
|
+
}
|
|
923
|
+
if (new Set(tier.inputModalities).size !== tier.inputModalities.length) {
|
|
924
|
+
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.inputModalities must not contain duplicates`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (tier.contextWindow !== void 0 && (typeof tier.contextWindow !== "number" || !Number.isSafeInteger(tier.contextWindow) || tier.contextWindow < 1)) {
|
|
928
|
+
throw new Error(`Sabi config ${source}: harness.tiers.${tierName}.contextWindow must be a safe integer >= 1`);
|
|
929
|
+
}
|
|
635
930
|
}
|
|
636
931
|
}
|
|
637
932
|
return { ...config, upstreams, models, aliases, policy };
|
|
638
933
|
}
|
|
639
934
|
|
|
935
|
+
// packages/core/src/recovery-actions.ts
|
|
936
|
+
var RECOVERY_ACTIONS = [
|
|
937
|
+
"continue",
|
|
938
|
+
"retry-same",
|
|
939
|
+
"retry-with-feedback",
|
|
940
|
+
"gather-evidence",
|
|
941
|
+
"escalate-model",
|
|
942
|
+
"fresh-context",
|
|
943
|
+
"rollback-with-reflection",
|
|
944
|
+
"ask-user"
|
|
945
|
+
];
|
|
946
|
+
function isRecoveryAction(value) {
|
|
947
|
+
return typeof value === "string" && RECOVERY_ACTIONS.includes(value);
|
|
948
|
+
}
|
|
949
|
+
function normalizeRecoveryAction(value, fallback = "ask-user") {
|
|
950
|
+
return isRecoveryAction(value) ? value : fallback;
|
|
951
|
+
}
|
|
952
|
+
|
|
640
953
|
// packages/core/src/log.ts
|
|
641
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
642
|
-
import {
|
|
954
|
+
import { appendFileSync, chmodSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
955
|
+
import { createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
956
|
+
import os2 from "node:os";
|
|
643
957
|
import path2 from "node:path";
|
|
644
958
|
function defaultLogPath() {
|
|
645
959
|
return process.env.SABI_LOG?.trim() || path2.join(process.cwd(), ".sabi", "decisions.jsonl");
|
|
646
960
|
}
|
|
961
|
+
function appendPrivateLine(logFile, line) {
|
|
962
|
+
const dir = path2.dirname(logFile);
|
|
963
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
964
|
+
appendFileSync(logFile, line, { mode: 384 });
|
|
965
|
+
if (process.platform !== "win32") {
|
|
966
|
+
chmodSync(dir, 448);
|
|
967
|
+
chmodSync(logFile, 384);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function identitySaltPath(env = process.env) {
|
|
971
|
+
const override = env.SABI_ID_SALT_FILE?.trim();
|
|
972
|
+
if (override) return override;
|
|
973
|
+
const base = env.XDG_CONFIG_HOME?.trim() || path2.join(os2.homedir(), ".config");
|
|
974
|
+
return path2.join(base, "sabi", ".identity-salt");
|
|
975
|
+
}
|
|
976
|
+
var cachedSalt;
|
|
977
|
+
var cachedSaltSource;
|
|
978
|
+
var ephemeralSaltWarned = false;
|
|
979
|
+
function loadOrCreateSalt(source) {
|
|
980
|
+
try {
|
|
981
|
+
if (existsSync2(source)) {
|
|
982
|
+
const stored = readFileSync2(source, "utf8").trim();
|
|
983
|
+
if (stored.length >= 16) return stored;
|
|
984
|
+
}
|
|
985
|
+
const fresh = randomBytes(32).toString("hex");
|
|
986
|
+
mkdirSync(path2.dirname(source), { recursive: true });
|
|
987
|
+
writeFileSync(source, `${fresh}
|
|
988
|
+
`, { mode: 384 });
|
|
989
|
+
try {
|
|
990
|
+
chmodSync(source, 384);
|
|
991
|
+
} catch {
|
|
992
|
+
}
|
|
993
|
+
return fresh;
|
|
994
|
+
} catch {
|
|
995
|
+
return void 0;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function getIdentitySalt(env = process.env) {
|
|
999
|
+
const override = env.SABI_ID_SALT?.trim();
|
|
1000
|
+
if (override) return override;
|
|
1001
|
+
const source = identitySaltPath(env);
|
|
1002
|
+
if (cachedSalt !== void 0 && cachedSaltSource === source) return cachedSalt;
|
|
1003
|
+
const salt = loadOrCreateSalt(source);
|
|
1004
|
+
if (salt === void 0) {
|
|
1005
|
+
if (!ephemeralSaltWarned) {
|
|
1006
|
+
ephemeralSaltWarned = true;
|
|
1007
|
+
console.warn(`Sabi: identity salt at ${source} is unreadable \u2014 using an ephemeral per-process salt`);
|
|
1008
|
+
}
|
|
1009
|
+
cachedSalt = randomBytes(32).toString("hex");
|
|
1010
|
+
cachedSaltSource = source;
|
|
1011
|
+
return cachedSalt;
|
|
1012
|
+
}
|
|
1013
|
+
cachedSalt = salt;
|
|
1014
|
+
cachedSaltSource = source;
|
|
1015
|
+
return salt;
|
|
1016
|
+
}
|
|
1017
|
+
var logWriteFailures = 0;
|
|
1018
|
+
var warnedLogFiles = /* @__PURE__ */ new Set();
|
|
1019
|
+
function logWriteFailurePath(logFile) {
|
|
1020
|
+
return `${logFile}.write-failures.json`;
|
|
1021
|
+
}
|
|
1022
|
+
function recordLogWriteFailure(logFile, error) {
|
|
1023
|
+
logWriteFailures += 1;
|
|
1024
|
+
if (!warnedLogFiles.has(logFile)) {
|
|
1025
|
+
warnedLogFiles.add(logFile);
|
|
1026
|
+
console.warn(`Sabi: could not append to the decision log at ${logFile}: ${error?.message ?? error} (telemetry degraded, will retry next round)`);
|
|
1027
|
+
}
|
|
1028
|
+
try {
|
|
1029
|
+
const sidecar = logWriteFailurePath(logFile);
|
|
1030
|
+
let failures = 0;
|
|
1031
|
+
try {
|
|
1032
|
+
const prior = JSON.parse(readFileSync2(sidecar, "utf8"));
|
|
1033
|
+
if (typeof prior.failures === "number" && Number.isFinite(prior.failures)) failures = Math.floor(prior.failures);
|
|
1034
|
+
} catch {
|
|
1035
|
+
}
|
|
1036
|
+
const state = {
|
|
1037
|
+
failures: failures + 1,
|
|
1038
|
+
lastTs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1039
|
+
lastError: String(error?.message ?? error).slice(0, 120)
|
|
1040
|
+
};
|
|
1041
|
+
mkdirSync(path2.dirname(sidecar), { recursive: true });
|
|
1042
|
+
writeFileSync(sidecar, `${JSON.stringify(state)}
|
|
1043
|
+
`);
|
|
1044
|
+
} catch {
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
647
1047
|
function appendDecision(record, logFile = defaultLogPath()) {
|
|
648
|
-
|
|
649
|
-
|
|
1048
|
+
try {
|
|
1049
|
+
appendPrivateLine(logFile, `${serializeDecisionRecord(record)}
|
|
650
1050
|
`);
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
recordLogWriteFailure(logFile, error);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function boundedStringList(values, maxItems, maxChars) {
|
|
1056
|
+
if (!Array.isArray(values)) return [];
|
|
1057
|
+
return values.filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, maxChars)).slice(0, maxItems);
|
|
1058
|
+
}
|
|
1059
|
+
function isPlainObject(value) {
|
|
1060
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1061
|
+
}
|
|
1062
|
+
function sanitizeState(state, config) {
|
|
1063
|
+
const policy = telemetryPolicy(config);
|
|
1064
|
+
const failureEvidence = boundedStringList(state.failureEvidence, 8, 64).filter((value) => policy.allowlisted(value));
|
|
1065
|
+
const evidence = boundTrajectoryEvidence(state.evidence);
|
|
1066
|
+
const clean = {
|
|
1067
|
+
messageCount: state.messageCount,
|
|
1068
|
+
assistantTurns: state.assistantTurns,
|
|
1069
|
+
toolMessages: state.toolMessages,
|
|
1070
|
+
lastRole: String(state.lastRole ?? "").slice(0, 32),
|
|
1071
|
+
contextChars: state.contextChars,
|
|
1072
|
+
estimatedTokens: state.estimatedTokens,
|
|
1073
|
+
hasTools: state.hasTools,
|
|
1074
|
+
toolNames: boundedStringList(state.toolNames, 40, 120),
|
|
1075
|
+
lastToolNames: boundedStringList(state.lastToolNames, 12, 120),
|
|
1076
|
+
roundKind: state.roundKind,
|
|
1077
|
+
failure: state.failure,
|
|
1078
|
+
failureEvidence,
|
|
1079
|
+
...state.contextTokens !== void 0 ? { contextTokens: state.contextTokens } : {},
|
|
1080
|
+
...state.contextKnown !== void 0 ? { contextKnown: state.contextKnown } : {},
|
|
1081
|
+
...state.contextGeneration !== void 0 ? { contextGeneration: state.contextGeneration } : {},
|
|
1082
|
+
...state.repeatedFailure !== void 0 ? { repeatedFailure: state.repeatedFailure } : {},
|
|
1083
|
+
...state.failureStreak !== void 0 ? { failureStreak: state.failureStreak } : {},
|
|
1084
|
+
...state.contextWindow !== void 0 ? { contextWindow: state.contextWindow } : {},
|
|
1085
|
+
...state.inputModalities ? { inputModalities: state.inputModalities.slice(0, 5) } : {},
|
|
1086
|
+
...state.mediaCounts ? { mediaCounts: state.mediaCounts } : {},
|
|
1087
|
+
...evidence.length > 0 ? { evidence } : {},
|
|
1088
|
+
...state.verification ? { verification: { ...state.verification, receiptId: state.verification.receiptId?.slice(0, 128) } } : {},
|
|
1089
|
+
...state.scopeCoverage ? {
|
|
1090
|
+
scopeCoverage: {
|
|
1091
|
+
...state.scopeCoverage,
|
|
1092
|
+
...state.scopeCoverage.missing ? { missing: boundedStringList(state.scopeCoverage.missing, 32, 120) } : {}
|
|
1093
|
+
}
|
|
1094
|
+
} : {}
|
|
1095
|
+
};
|
|
1096
|
+
return clean;
|
|
1097
|
+
}
|
|
1098
|
+
function sanitizeDecisionRecord(record, config) {
|
|
1099
|
+
const policy = telemetryPolicy(config);
|
|
1100
|
+
const state = isPlainObject(record.state) ? sanitizeState(record.state, config) : void 0;
|
|
1101
|
+
const sanitized = {
|
|
1102
|
+
ts: record.ts,
|
|
1103
|
+
sessionId: record.sessionId,
|
|
1104
|
+
sessionKnown: record.sessionKnown,
|
|
1105
|
+
requestId: record.requestId,
|
|
1106
|
+
client: record.client,
|
|
1107
|
+
turnId: record.turnId,
|
|
1108
|
+
servedModel: record.servedModel,
|
|
1109
|
+
alias: record.alias,
|
|
1110
|
+
mode: record.mode,
|
|
1111
|
+
rule: record.rule,
|
|
1112
|
+
tier: record.tier,
|
|
1113
|
+
reason: sanitizeReason(String(record.reason ?? ""), policy),
|
|
1114
|
+
upstream: record.upstream,
|
|
1115
|
+
upstreamModel: record.upstreamModel,
|
|
1116
|
+
stream: record.stream,
|
|
1117
|
+
state,
|
|
1118
|
+
judge: record.judge,
|
|
1119
|
+
usage: record.usage,
|
|
1120
|
+
cost: record.cost,
|
|
1121
|
+
latencyMs: record.latencyMs,
|
|
1122
|
+
ttftMs: record.ttftMs,
|
|
1123
|
+
outcome: record.outcome,
|
|
1124
|
+
...record.error ? { error: sanitizeError(record.error) } : {},
|
|
1125
|
+
transport: record.transport,
|
|
1126
|
+
fallback: record.fallback,
|
|
1127
|
+
...record.recovery ? {
|
|
1128
|
+
recovery: {
|
|
1129
|
+
...record.recovery,
|
|
1130
|
+
action: normalizeRecoveryAction(record.recovery.action),
|
|
1131
|
+
evidenceGrade: record.recovery.evidenceGrade === "matched" || record.recovery.evidenceGrade === "replayed" ? record.recovery.evidenceGrade : "observed",
|
|
1132
|
+
failureSignature: String(record.recovery.failureSignature).slice(0, 64),
|
|
1133
|
+
stateFingerprint: String(record.recovery.stateFingerprint).slice(0, 64),
|
|
1134
|
+
...record.recovery.route ? { route: String(record.recovery.route).slice(0, 160) } : {},
|
|
1135
|
+
...record.recovery.receiptId ? { receiptId: String(record.recovery.receiptId).slice(0, 128) } : {}
|
|
1136
|
+
}
|
|
1137
|
+
} : {}
|
|
1138
|
+
};
|
|
1139
|
+
return sanitized;
|
|
1140
|
+
}
|
|
1141
|
+
function serializeDecisionRecord(record, config) {
|
|
1142
|
+
return JSON.stringify(sanitizeDecisionRecord(record, config));
|
|
651
1143
|
}
|
|
652
1144
|
function hashIdentity(kind, ...parts) {
|
|
653
|
-
return
|
|
1145
|
+
return createHmac("sha256", getIdentitySalt()).update(JSON.stringify([kind, ...parts])).digest("hex");
|
|
654
1146
|
}
|
|
655
1147
|
function sessionIdFor(session, client = "unknown") {
|
|
656
1148
|
return session === void 0 ? randomUUID() : hashIdentity("session", client, session);
|
|
@@ -673,7 +1165,7 @@ function trajectoryFromRound(round, previous) {
|
|
|
673
1165
|
const sameFailure = previous?.failure === "hard" && failure === "hard";
|
|
674
1166
|
const repeatedFailure = sameFailure === true;
|
|
675
1167
|
const failureStreak = repeatedFailure ? 2 : failure === "hard" ? 1 : 0;
|
|
676
|
-
|
|
1168
|
+
const state = {
|
|
677
1169
|
messageCount: round.messageCount,
|
|
678
1170
|
assistantTurns: round.assistantTurns,
|
|
679
1171
|
toolMessages: round.calls.length,
|
|
@@ -694,6 +1186,12 @@ function trajectoryFromRound(round, previous) {
|
|
|
694
1186
|
...round.inputModalities ? { inputModalities: round.inputModalities } : {},
|
|
695
1187
|
...round.mediaCounts && Object.keys(round.mediaCounts).length ? { inputModalities: round.inputModalities ?? modalitiesOf(round.mediaCounts), mediaCounts: round.mediaCounts } : {}
|
|
696
1188
|
};
|
|
1189
|
+
return decorateTrajectoryState(state, {
|
|
1190
|
+
generation: round.contextGeneration,
|
|
1191
|
+
verificationReceipt: round.verificationReceipt,
|
|
1192
|
+
summaryClaim: round.summaryClaim,
|
|
1193
|
+
scope: round.scope
|
|
1194
|
+
});
|
|
697
1195
|
}
|
|
698
1196
|
function roundMedia(round) {
|
|
699
1197
|
return { counts: round.mediaCounts ?? {}, payloadChars: 0 };
|
|
@@ -725,54 +1223,6 @@ function planRound(state, policy, tiers, options = {}) {
|
|
|
725
1223
|
};
|
|
726
1224
|
}
|
|
727
1225
|
|
|
728
|
-
// packages/core/src/telemetry.ts
|
|
729
|
-
var ALLOWLIST = /* @__PURE__ */ new Set([
|
|
730
|
-
"error-line",
|
|
731
|
-
"python-traceback",
|
|
732
|
-
"panic",
|
|
733
|
-
"exception",
|
|
734
|
-
"typescript-error",
|
|
735
|
-
"fail-marker",
|
|
736
|
-
"command-failed",
|
|
737
|
-
"nonzero-exit",
|
|
738
|
-
"failure-count",
|
|
739
|
-
"command-not-found",
|
|
740
|
-
"permission-denied",
|
|
741
|
-
"missing-file",
|
|
742
|
-
"soft-warning",
|
|
743
|
-
"soft-deprecated",
|
|
744
|
-
"soft-retrying",
|
|
745
|
-
"soft-timeout",
|
|
746
|
-
"tool-error",
|
|
747
|
-
"permission-denial",
|
|
748
|
-
"rate-limited",
|
|
749
|
-
"quota-exceeded",
|
|
750
|
-
"timeout"
|
|
751
|
-
]);
|
|
752
|
-
function allowlisted(value) {
|
|
753
|
-
const code = value.split(":")[0]?.trim();
|
|
754
|
-
return ALLOWLIST.has(code);
|
|
755
|
-
}
|
|
756
|
-
function telemetryPolicy(config) {
|
|
757
|
-
const captureSnippets = config?.captureSnippets === true;
|
|
758
|
-
const captureChars = config?.captureChars ?? 800;
|
|
759
|
-
return {
|
|
760
|
-
captureSnippets,
|
|
761
|
-
allowlisted: (value) => allowlisted(value),
|
|
762
|
-
snippet: (value) => captureSnippets ? String(value).slice(0, captureChars) : ""
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
function sanitizeReason(reason, policy) {
|
|
766
|
-
if (!policy.captureSnippets) {
|
|
767
|
-
if (!reason || allowlisted(reason)) return reason;
|
|
768
|
-
const segments = reason.split(":").map((segment) => segment.trim());
|
|
769
|
-
const last = segments[segments.length - 1] ?? "";
|
|
770
|
-
if (allowlisted(last)) return reason;
|
|
771
|
-
return "reason withheld (telemetry.allowlistOnly)";
|
|
772
|
-
}
|
|
773
|
-
return policy.snippet(reason);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
1226
|
// packages/core/src/prompt.ts
|
|
777
1227
|
import * as readline from "node:readline/promises";
|
|
778
1228
|
|
package/package.json
CHANGED