@tangle-network/traces 0.23.1 → 1.0.1
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/README.md +61 -1078
- package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
- package/dist/ask-2KF4KSPB.js +39 -0
- package/dist/check-YF6ZHPGG.js +322 -0
- package/dist/chunk-2XXABIRL.js +25 -0
- package/dist/chunk-4VKPN432.js +138 -0
- package/dist/chunk-6GOG5KIK.js +53 -0
- package/dist/chunk-7USMM4MA.js +5695 -0
- package/dist/chunk-CG5QHQO4.js +492 -0
- package/dist/chunk-CQ6UYZG6.js +1217 -0
- package/dist/chunk-DU534HAX.js +263 -0
- package/dist/chunk-FC4V25CG.js +503 -0
- package/dist/chunk-GLUDGUGJ.js +232 -0
- package/dist/chunk-GNRXKQEQ.js +120 -0
- package/dist/chunk-K2KH2HHD.js +228 -0
- package/dist/chunk-M3W6TZHQ.js +381 -0
- package/dist/chunk-O52XK7OB.js +19 -0
- package/dist/chunk-PYH3PL3N.js +62 -0
- package/dist/chunk-QLVP37YB.js +782 -0
- package/dist/chunk-QUHUMZ4W.js +759 -0
- package/dist/chunk-REO2JQJY.js +535 -0
- package/dist/chunk-RZL5GAWH.js +60 -0
- package/dist/chunk-TOCNGML5.js +273 -0
- package/dist/chunk-UQSAGXPH.js +208 -0
- package/dist/chunk-VFOLC4OX.js +407 -0
- package/dist/chunk-WF7PTDCZ.js +3921 -0
- package/dist/chunk-YAX72TST.js +259 -0
- package/dist/cli.js +2086 -2050
- package/dist/diff-J4RAIOXO.js +16 -0
- package/dist/improvement-LCCVGGUS.js +25 -0
- package/dist/index.d.ts +1235 -4357
- package/dist/index.js +53 -1204
- package/dist/live-XXL7VZWB.js +14 -0
- package/dist/mcp-EYIDKBEK.js +85 -0
- package/dist/report-E3G2T5TJ.js +338 -0
- package/dist/run-watch-RAOOAXOF.js +649 -0
- package/dist/supervisor-report-RMPLLBGZ.js +29 -0
- package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
- package/dist/upload-3R7GRSKS.js +20 -0
- package/dist/verify-safe-C5ZXZOZ2.js +8 -0
- package/docs/analyze.md +76 -0
- package/docs/ask.md +88 -0
- package/docs/bundle.md +40 -0
- package/docs/check.md +77 -0
- package/docs/convert.md +44 -0
- package/docs/develop.md +32 -0
- package/docs/diff.md +38 -0
- package/docs/facts.md +114 -0
- package/docs/mcp.md +36 -0
- package/docs/migrate-1.0.md +33 -0
- package/docs/parsers.md +66 -0
- package/docs/release-1.0.0.md +147 -0
- package/docs/release-1.0.1.md +4 -0
- package/docs/sdk.md +77 -0
- package/docs/select.md +70 -0
- package/docs/skills.md +46 -0
- package/docs/upload.md +47 -0
- package/docs/watch-jsonl.v1.example.jsonl +5 -0
- package/docs/watch.md +66 -0
- package/examples/README.md +9 -0
- package/examples/custom-analyst.ts +65 -74
- package/install.sh +22 -2
- package/package.json +10 -9
- package/skills/inspect-agent-traces/SKILL.md +47 -110
- package/dist/chunk-7DVZCCZI.js +0 -20983
- package/docs/replay-verify.md +0 -123
- package/docs/stream-findings.svg +0 -39
- package/docs/trace-analysts.md +0 -643
- package/examples/custom-backend.ts +0 -21
- package/examples/external-engines.ts +0 -62
- package/examples/improvement-config.mjs +0 -63
- package/examples/observe-and-alert.ts +0 -19
- package/examples/register-harness.ts +0 -26
- package/skills/build-trace-analyst/SKILL.md +0 -76
- package/skills/build-trace-analyst/agents/openai.yaml +0 -4
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANALYST_MAX_OUTPUT_TOKENS,
|
|
3
|
+
UnservableModelError,
|
|
4
|
+
analysisEngineFromEnv,
|
|
5
|
+
bridgeVersion,
|
|
6
|
+
createAnalystModelOwner,
|
|
7
|
+
missingAskRequirements
|
|
8
|
+
} from "./chunk-GLUDGUGJ.js";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_ANALYST_MODEL
|
|
11
|
+
} from "./chunk-O52XK7OB.js";
|
|
12
|
+
export {
|
|
13
|
+
ANALYST_MAX_OUTPUT_TOKENS,
|
|
14
|
+
DEFAULT_ANALYST_MODEL,
|
|
15
|
+
UnservableModelError,
|
|
16
|
+
analysisEngineFromEnv,
|
|
17
|
+
bridgeVersion,
|
|
18
|
+
createAnalystModelOwner,
|
|
19
|
+
missingAskRequirements
|
|
20
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTraceQuestionCorpus,
|
|
3
|
+
engineCallReservationFloorUsd,
|
|
4
|
+
isBridgeMismatchError,
|
|
5
|
+
loadTraceQuestionsFile,
|
|
6
|
+
normalizeTraceQuestions,
|
|
7
|
+
renderTraceQuestionInstructions,
|
|
8
|
+
renderTraceQuestionPrompt,
|
|
9
|
+
renderTraceQuestionsReport,
|
|
10
|
+
runTraceQuestions,
|
|
11
|
+
traceCitationsInText,
|
|
12
|
+
writeTraceQuestionsArtifacts
|
|
13
|
+
} from "./chunk-QUHUMZ4W.js";
|
|
14
|
+
import "./chunk-PYH3PL3N.js";
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_ASK_CONCURRENCY,
|
|
17
|
+
MAX_TRACE_QUESTION_CHARS
|
|
18
|
+
} from "./chunk-O52XK7OB.js";
|
|
19
|
+
import "./chunk-2XXABIRL.js";
|
|
20
|
+
import "./chunk-WF7PTDCZ.js";
|
|
21
|
+
import "./chunk-7USMM4MA.js";
|
|
22
|
+
import "./chunk-UQSAGXPH.js";
|
|
23
|
+
import "./chunk-CQ6UYZG6.js";
|
|
24
|
+
import "./chunk-DU534HAX.js";
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_ASK_CONCURRENCY,
|
|
27
|
+
MAX_TRACE_QUESTION_CHARS,
|
|
28
|
+
createTraceQuestionCorpus,
|
|
29
|
+
engineCallReservationFloorUsd,
|
|
30
|
+
isBridgeMismatchError,
|
|
31
|
+
loadTraceQuestionsFile,
|
|
32
|
+
normalizeTraceQuestions,
|
|
33
|
+
renderTraceQuestionInstructions,
|
|
34
|
+
renderTraceQuestionPrompt,
|
|
35
|
+
renderTraceQuestionsReport,
|
|
36
|
+
runTraceQuestions,
|
|
37
|
+
traceCitationsInText,
|
|
38
|
+
writeTraceQuestionsArtifacts
|
|
39
|
+
};
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatVerdict
|
|
3
|
+
} from "./chunk-4VKPN432.js";
|
|
4
|
+
import {
|
|
5
|
+
redactSpans
|
|
6
|
+
} from "./chunk-RZL5GAWH.js";
|
|
7
|
+
import {
|
|
8
|
+
parseIsoToEpochMs,
|
|
9
|
+
toolArgumentsFromAttributes
|
|
10
|
+
} from "./chunk-DU534HAX.js";
|
|
11
|
+
|
|
12
|
+
// src/check.ts
|
|
13
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import {
|
|
16
|
+
compileTraceContractSpec,
|
|
17
|
+
evaluateTraceContract,
|
|
18
|
+
explainTraceContract,
|
|
19
|
+
lintTraceContractSpec
|
|
20
|
+
} from "@tangle-network/agent-eval";
|
|
21
|
+
var CONFORMANCE = "conformance";
|
|
22
|
+
var CHECK_EXIT = Object.freeze({
|
|
23
|
+
pass: 0,
|
|
24
|
+
fail: 1,
|
|
25
|
+
/** A rule or verdict could not run, the contract is malformed, or the trace is unreadable or ambiguous. */
|
|
26
|
+
couldNotDecide: 2
|
|
27
|
+
});
|
|
28
|
+
var AmbiguousTraceInputError = class extends Error {
|
|
29
|
+
name = "AmbiguousTraceInputError";
|
|
30
|
+
};
|
|
31
|
+
var ContractFileError = class extends Error {
|
|
32
|
+
name = "ContractFileError";
|
|
33
|
+
};
|
|
34
|
+
async function loadContract(path) {
|
|
35
|
+
let text;
|
|
36
|
+
try {
|
|
37
|
+
text = await readFile(path, "utf8");
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new ContractFileError(`cannot read contract ${path}: ${errorText(error)}`);
|
|
40
|
+
}
|
|
41
|
+
let json;
|
|
42
|
+
try {
|
|
43
|
+
json = JSON.parse(text);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new ContractFileError(`contract ${path} is not JSON: ${errorText(error)}`);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const findings = lintTraceContractSpec(json);
|
|
49
|
+
const errors = findings.filter((f) => f.level === "error");
|
|
50
|
+
if (errors.length > 0) {
|
|
51
|
+
throw new ContractFileError(
|
|
52
|
+
`contract ${path} contradicts itself:
|
|
53
|
+
${errors.map((f) => ` ${f.path}: ${f.message} [${f.code}]`).join("\n")}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
path,
|
|
58
|
+
contract: compileTraceContractSpec(json),
|
|
59
|
+
warnings: findings.map((f) => `${f.path}: ${f.message} [${f.code}]`)
|
|
60
|
+
};
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof ContractFileError) throw error;
|
|
63
|
+
throw new ContractFileError(`contract ${path}: ${errorText(error)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function contractSpansFromOtlp(spans) {
|
|
67
|
+
return spans.map((span) => {
|
|
68
|
+
const startedAt = epochMs(span.start_time);
|
|
69
|
+
const endedAt = epochMs(span.end_time);
|
|
70
|
+
const args = toolArgumentsFromAttributes(span.attributes);
|
|
71
|
+
const truncated = span.attributes["traces.input.truncated"] === true;
|
|
72
|
+
return {
|
|
73
|
+
spanId: span.span_id,
|
|
74
|
+
parentSpanId: span.parent_span_id,
|
|
75
|
+
name: span.name,
|
|
76
|
+
...startedAt === void 0 ? {} : { startedAt },
|
|
77
|
+
...endedAt === void 0 ? {} : { endedAt },
|
|
78
|
+
...span.status.code === "ERROR" ? { status: "error" } : span.status.code === "OK" ? { status: "ok" } : {},
|
|
79
|
+
// A truncated argument capture is not the call's arguments.
|
|
80
|
+
...args.argsCaptured && !truncated ? { args: args.args } : { argsCaptured: false },
|
|
81
|
+
attributes: span.attributes
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function epochMs(value) {
|
|
86
|
+
try {
|
|
87
|
+
return parseIsoToEpochMs(value);
|
|
88
|
+
} catch {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function groupByTrace(spans) {
|
|
93
|
+
const groups = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const span of spans) {
|
|
95
|
+
const list = groups.get(span.trace_id) ?? [];
|
|
96
|
+
list.push(span);
|
|
97
|
+
groups.set(span.trace_id, list);
|
|
98
|
+
}
|
|
99
|
+
return [...groups].map(([traceId, list]) => ({ traceId, spans: list }));
|
|
100
|
+
}
|
|
101
|
+
async function runCheck(contract, spans, opts = {}) {
|
|
102
|
+
const traces = [];
|
|
103
|
+
for (const group of groupByTrace(spans)) {
|
|
104
|
+
const verdict = evaluateTraceContract(contract, contractSpansFromOtlp(group.spans));
|
|
105
|
+
const check = { traceId: group.traceId, spanCount: group.spans.length, verdict };
|
|
106
|
+
if (verdict.status !== "pass" && opts.evidenceRoot !== void 0) {
|
|
107
|
+
check.evidenceDir = await writeEvidence(opts.evidenceRoot, contract, group.traceId, group.spans, verdict);
|
|
108
|
+
}
|
|
109
|
+
traces.push(check);
|
|
110
|
+
}
|
|
111
|
+
const status = traces.some((t) => t.verdict.status === "error") ? "error" : traces.every((t) => t.verdict.status === "pass") ? "pass" : "fail";
|
|
112
|
+
const exitCode = status === "pass" ? CHECK_EXIT.pass : status === "fail" ? CHECK_EXIT.fail : CHECK_EXIT.couldNotDecide;
|
|
113
|
+
return { contract: contract.name, status, exitCode, traces };
|
|
114
|
+
}
|
|
115
|
+
async function writeEvidence(root, contract, traceId, spans, verdict) {
|
|
116
|
+
const dir = join(root, `${safeName(contract.name)}-${safeName(traceId).slice(0, 40)}`);
|
|
117
|
+
await mkdir(dir, { recursive: true });
|
|
118
|
+
const cited = new Set(verdict.violations.map((v) => v.spanId).filter((id) => id !== void 0));
|
|
119
|
+
const redacted = redactSpans(spans.filter((s) => cited.has(s.span_id)));
|
|
120
|
+
await writeFile(join(dir, "verdict.json"), `${JSON.stringify({ traceId, verdict }, null, 2)}
|
|
121
|
+
`);
|
|
122
|
+
await writeFile(join(dir, "contract.json"), `${JSON.stringify(contract, null, 2)}
|
|
123
|
+
`);
|
|
124
|
+
await writeFile(join(dir, "explain.txt"), `${explainTraceContract(contract)}
|
|
125
|
+
`);
|
|
126
|
+
await writeFile(join(dir, "cited-spans.jsonl"), redacted.spans.map((s) => JSON.stringify(s)).join("\n") + (redacted.spans.length ? "\n" : ""));
|
|
127
|
+
return dir;
|
|
128
|
+
}
|
|
129
|
+
function safeName(value) {
|
|
130
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
131
|
+
}
|
|
132
|
+
function ruleLines(verdict) {
|
|
133
|
+
const branchPassed = /* @__PURE__ */ new Map();
|
|
134
|
+
for (const e of verdict.ruleExecutions) {
|
|
135
|
+
if (e.alternative === void 0) continue;
|
|
136
|
+
branchPassed.set(e.alternative, (branchPassed.get(e.alternative) ?? true) && e.status === "pass");
|
|
137
|
+
}
|
|
138
|
+
const anyBranchPassed = [...branchPassed.values()].some(Boolean);
|
|
139
|
+
return verdict.ruleExecutions.map((e) => {
|
|
140
|
+
const rule = e.alternative ? `${e.alternative}/${e.rule}` : e.rule;
|
|
141
|
+
const status = e.alternative && anyBranchPassed && !branchPassed.get(e.alternative) ? "skipped" : e.status;
|
|
142
|
+
return {
|
|
143
|
+
rule,
|
|
144
|
+
status,
|
|
145
|
+
...e.error === void 0 ? {} : { error: e.error },
|
|
146
|
+
details: verdict.violations.filter((x) => x.rule === rule).map((x) => x.detail)
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function renderCheckText(report) {
|
|
151
|
+
const lines = [];
|
|
152
|
+
for (const t of report.traces) {
|
|
153
|
+
const v = t.verdict;
|
|
154
|
+
lines.push(`${v.status.toUpperCase()} contract ${report.contract} trace ${t.traceId} (${t.spanCount} spans, ${v.notes})`);
|
|
155
|
+
for (const r of ruleLines(v)) {
|
|
156
|
+
lines.push(` ${r.status.padEnd(7)} ${r.rule}${r.error ? ` ${r.error}` : ""}`);
|
|
157
|
+
if (r.status === "fail") for (const detail of r.details.slice(0, 5)) lines.push(` ${detail}`);
|
|
158
|
+
}
|
|
159
|
+
if (t.evidenceDir) lines.push(` evidence \u2192 ${t.evidenceDir}`);
|
|
160
|
+
}
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
function contractSuites(report) {
|
|
164
|
+
return report.traces.map((t) => {
|
|
165
|
+
const rules = ruleLines(t.verdict);
|
|
166
|
+
const count = (status) => rules.filter((r) => r.status === status).length;
|
|
167
|
+
const cases = rules.map((r) => {
|
|
168
|
+
const open = ` <testcase classname="${xml(report.contract)}" name="${xml(r.rule)}">`;
|
|
169
|
+
if (r.status === "pass") return `${open}</testcase>`;
|
|
170
|
+
if (r.status === "skipped") return `${open}
|
|
171
|
+
<skipped message="another alternative passed"/>
|
|
172
|
+
</testcase>`;
|
|
173
|
+
if (r.status === "error") {
|
|
174
|
+
return `${open}
|
|
175
|
+
<error message="${xml(r.error ?? "rule could not be evaluated")}"/>
|
|
176
|
+
</testcase>`;
|
|
177
|
+
}
|
|
178
|
+
return `${open}
|
|
179
|
+
<failure message="${xml(r.details[0] ?? "rule failed")}">${xml(r.details.join("\n"))}</failure>
|
|
180
|
+
</testcase>`;
|
|
181
|
+
});
|
|
182
|
+
return [
|
|
183
|
+
` <testsuite name="${xml(`${report.contract} @ ${t.traceId}`)}" tests="${rules.length}" failures="${count("fail")}" errors="${count("error")}" skipped="${count("skipped")}">`,
|
|
184
|
+
...cases,
|
|
185
|
+
" </testsuite>"
|
|
186
|
+
].join("\n");
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function renderGithubAnnotations(report) {
|
|
190
|
+
const lines = [];
|
|
191
|
+
for (const t of report.traces) {
|
|
192
|
+
for (const r of ruleLines(t.verdict)) {
|
|
193
|
+
if (r.status === "pass" || r.status === "skipped") continue;
|
|
194
|
+
const detail = r.status === "error" ? `could not be evaluated: ${r.error ?? "unknown"}` : r.details.join("\n");
|
|
195
|
+
lines.push(`::error title=${ghProperty(`${report.contract}: ${r.rule}`)}::${ghData(`trace ${t.traceId}: ${detail}`)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
|
200
|
+
function contractGate(report) {
|
|
201
|
+
return { gate: "contract", status: report.status === "error" ? "unknown" : report.status, report };
|
|
202
|
+
}
|
|
203
|
+
function conformanceGate(subject, validation, text) {
|
|
204
|
+
return { gate: "conformance", status: validation.ok ? "pass" : "fail", subject, validation, text };
|
|
205
|
+
}
|
|
206
|
+
function shareSafeGate(verdict) {
|
|
207
|
+
const status = verdict.status === "UNSAFE" ? "fail" : verdict.status === "UNKNOWN" ? "unknown" : "pass";
|
|
208
|
+
return { gate: "share-safe", status, verdict };
|
|
209
|
+
}
|
|
210
|
+
function combineGates(gates) {
|
|
211
|
+
const status = gates.some((gate) => gate.status === "fail") ? "fail" : gates.some((gate) => gate.status === "unknown") ? "unknown" : "pass";
|
|
212
|
+
const exitCode = status === "pass" ? CHECK_EXIT.pass : status === "fail" ? CHECK_EXIT.fail : CHECK_EXIT.couldNotDecide;
|
|
213
|
+
return { status, exitCode, gates };
|
|
214
|
+
}
|
|
215
|
+
async function writeGateEvidence(root, verdict) {
|
|
216
|
+
for (const gate of verdict.gates) {
|
|
217
|
+
if (gate.status === "pass" || gate.gate === "contract") continue;
|
|
218
|
+
await mkdir(root, { recursive: true });
|
|
219
|
+
if (gate.gate === "conformance") await writeFile(join(root, "conformance.md"), `${gate.text}
|
|
220
|
+
`);
|
|
221
|
+
else await writeFile(join(root, "share-safe.json"), `${JSON.stringify(gate.verdict, null, 2)}
|
|
222
|
+
`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
var CONFORMANCE_EXPLAINED = [
|
|
226
|
+
"conformance: whether the input can be read as a trace at all, and which analyses its spans can support.",
|
|
227
|
+
" fail not one entry could be read as a span (an empty export, or every line unreadable)",
|
|
228
|
+
" pass spans were read; each capability a field they lack costs is listed with what it empties"
|
|
229
|
+
].join("\n");
|
|
230
|
+
function renderVerdictText(verdict) {
|
|
231
|
+
return verdict.gates.map((gate) => {
|
|
232
|
+
if (gate.gate === "contract") return renderCheckText(gate.report);
|
|
233
|
+
if (gate.gate === "conformance") return gate.text;
|
|
234
|
+
return formatVerdict(gate.verdict);
|
|
235
|
+
}).join("\n\n");
|
|
236
|
+
}
|
|
237
|
+
function renderVerdictJUnit(verdict) {
|
|
238
|
+
const suites = verdict.gates.flatMap((gate) => {
|
|
239
|
+
if (gate.gate === "contract") return contractSuites(gate.report);
|
|
240
|
+
if (gate.gate === "conformance") {
|
|
241
|
+
const readable = gate.validation.ok ? ' <testcase classname="conformance" name="readable"></testcase>' : ` <testcase classname="conformance" name="readable">
|
|
242
|
+
<failure message="not one entry could be read as a span"/>
|
|
243
|
+
</testcase>`;
|
|
244
|
+
const capabilities = gate.validation.capabilities.map((capability) => capability.available ? ` <testcase classname="conformance" name="${xml(capability.name)}"></testcase>` : ` <testcase classname="conformance" name="${xml(capability.name)}">
|
|
245
|
+
<skipped message="${xml(capability.reason ?? "unavailable")}"/>
|
|
246
|
+
</testcase>`);
|
|
247
|
+
const unavailable = gate.validation.capabilities.filter((capability) => !capability.available).length;
|
|
248
|
+
return [[
|
|
249
|
+
` <testsuite name="${xml(`conformance @ ${gate.subject}`)}" tests="${capabilities.length + 1}" failures="${gate.validation.ok ? 0 : 1}" errors="0" skipped="${unavailable}">`,
|
|
250
|
+
readable,
|
|
251
|
+
...capabilities,
|
|
252
|
+
" </testsuite>"
|
|
253
|
+
].join("\n")];
|
|
254
|
+
}
|
|
255
|
+
const result = gate.verdict;
|
|
256
|
+
const findings = result.findings.map((finding) => `${finding.severity} ${finding.category}/${finding.detector} x${finding.count} at ${finding.paths.join(", ")}`);
|
|
257
|
+
const body = gate.status === "fail" ? `
|
|
258
|
+
<failure message="${xml(`${result.status}: ${findings[0] ?? ""}`)}">${xml(findings.join("\n"))}</failure>
|
|
259
|
+
` : gate.status === "unknown" ? `
|
|
260
|
+
<error message="${xml(`UNKNOWN: ${result.unreadable[0] ?? "part of the input could not be read"}`)}">${xml(result.unreadable.join("\n"))}</error>
|
|
261
|
+
` : findings.length > 0 ? `
|
|
262
|
+
<system-out>${xml(findings.join("\n"))}</system-out>
|
|
263
|
+
` : "";
|
|
264
|
+
return [[
|
|
265
|
+
` <testsuite name="${xml(`share-safe @ ${result.target}`)}" tests="1" failures="${gate.status === "fail" ? 1 : 0}" errors="${gate.status === "unknown" ? 1 : 0}" skipped="0">`,
|
|
266
|
+
` <testcase classname="share-safe" name="${xml(result.target)}">${body}</testcase>`,
|
|
267
|
+
" </testsuite>"
|
|
268
|
+
].join("\n")];
|
|
269
|
+
});
|
|
270
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
271
|
+
<testsuites name="traces check">
|
|
272
|
+
${suites.join("\n")}
|
|
273
|
+
</testsuites>
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
function renderVerdictGithub(verdict) {
|
|
277
|
+
const lines = [];
|
|
278
|
+
for (const gate of verdict.gates) {
|
|
279
|
+
if (gate.gate === "contract") {
|
|
280
|
+
const annotations = renderGithubAnnotations(gate.report);
|
|
281
|
+
if (annotations) lines.push(annotations);
|
|
282
|
+
} else if (gate.gate === "conformance") {
|
|
283
|
+
if (!gate.validation.ok) lines.push(`::error title=${ghProperty("conformance")}::${ghData(`${gate.subject}: not one entry could be read as a span`)}`);
|
|
284
|
+
} else {
|
|
285
|
+
const result = gate.verdict;
|
|
286
|
+
for (const finding of result.findings) {
|
|
287
|
+
const kind = finding.severity === "error" ? "error" : "warning";
|
|
288
|
+
lines.push(`::${kind} title=${ghProperty(`share-safe: ${finding.category}/${finding.detector}`)}::${ghData(`${result.target}: x${finding.count} at ${finding.paths.join(", ")}`)}`);
|
|
289
|
+
}
|
|
290
|
+
for (const entry of result.unreadable) lines.push(`::error title=${ghProperty("share-safe: unreadable")}::${ghData(entry)}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return lines.join("\n");
|
|
294
|
+
}
|
|
295
|
+
function xml(value) {
|
|
296
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
|
|
297
|
+
}
|
|
298
|
+
function ghData(value) {
|
|
299
|
+
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
300
|
+
}
|
|
301
|
+
function ghProperty(value) {
|
|
302
|
+
return ghData(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
303
|
+
}
|
|
304
|
+
function errorText(error) {
|
|
305
|
+
return error instanceof Error ? error.message : String(error);
|
|
306
|
+
}
|
|
307
|
+
export {
|
|
308
|
+
AmbiguousTraceInputError,
|
|
309
|
+
CHECK_EXIT,
|
|
310
|
+
CONFORMANCE,
|
|
311
|
+
CONFORMANCE_EXPLAINED,
|
|
312
|
+
combineGates,
|
|
313
|
+
conformanceGate,
|
|
314
|
+
contractGate,
|
|
315
|
+
loadContract,
|
|
316
|
+
renderVerdictGithub,
|
|
317
|
+
renderVerdictJUnit,
|
|
318
|
+
renderVerdictText,
|
|
319
|
+
runCheck,
|
|
320
|
+
shareSafeGate,
|
|
321
|
+
writeGateEvidence
|
|
322
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
iterSerializedSpans
|
|
3
|
+
} from "./chunk-CQ6UYZG6.js";
|
|
4
|
+
|
|
5
|
+
// src/analysis-store.ts
|
|
6
|
+
import { otlpTextToTraceAnalysisStore } from "@tangle-network/agent-eval/traces";
|
|
7
|
+
var DETERMINISTIC_PER_CALL_CEILING = 512 * 1024 * 1024;
|
|
8
|
+
function openAgenticTraceStore(spans) {
|
|
9
|
+
return otlpTextToTraceAnalysisStore(spans instanceof Uint8Array ? spans : exportBytes(spans));
|
|
10
|
+
}
|
|
11
|
+
function openDeterministicTraceStore(spans) {
|
|
12
|
+
const bytes = spans instanceof Uint8Array ? spans : exportBytes(spans);
|
|
13
|
+
return otlpTextToTraceAnalysisStore(bytes, { perCallByteCeiling: DETERMINISTIC_PER_CALL_CEILING });
|
|
14
|
+
}
|
|
15
|
+
function exportBytes(spans) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
for (const line of iterSerializedSpans(spans)) lines.push(Buffer.from(line, "utf8"));
|
|
18
|
+
return Buffer.concat(lines);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
openAgenticTraceStore,
|
|
23
|
+
openDeterministicTraceStore,
|
|
24
|
+
exportBytes
|
|
25
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/verify-safe.ts
|
|
2
|
+
import { lstat, readdir, readFile } from "fs/promises";
|
|
3
|
+
import { basename, extname, join, relative } from "path";
|
|
4
|
+
import {
|
|
5
|
+
assessShareSafety,
|
|
6
|
+
classifyKey,
|
|
7
|
+
combineVerdicts
|
|
8
|
+
} from "@tangle-network/agent-eval/traces";
|
|
9
|
+
var PROFILE = "share";
|
|
10
|
+
var JSON_LINES = /* @__PURE__ */ new Set([".jsonl", ".ndjson"]);
|
|
11
|
+
async function verifySafe(target, options = {}) {
|
|
12
|
+
const assess = (value) => assessShareSafety(numbersAreNotContent(value), { profile: PROFILE, knownSecrets: options.knownSecrets });
|
|
13
|
+
const verdicts = [];
|
|
14
|
+
const unreadable = [];
|
|
15
|
+
const files = await listFiles(target, unreadable);
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const label = relative(target, file) || basename(file);
|
|
18
|
+
let bytes;
|
|
19
|
+
try {
|
|
20
|
+
bytes = await readFile(file);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
unreadable.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const text = decodeUtf8(bytes);
|
|
26
|
+
if (text === void 0) {
|
|
27
|
+
unreadable.push(`${label}: not UTF-8 text`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const extension = extname(file).toLowerCase();
|
|
31
|
+
if (JSON_LINES.has(extension)) {
|
|
32
|
+
text.split("\n").forEach((line, index) => {
|
|
33
|
+
if (!line.trim()) return;
|
|
34
|
+
const parsed = parseJson(line);
|
|
35
|
+
if (parsed === void 0) unreadable.push(`${label}:${index + 1}: not JSON`);
|
|
36
|
+
else verdicts.push(located(assess(parsed.value), `${label}:${index + 1}`));
|
|
37
|
+
});
|
|
38
|
+
} else if (extension === ".json") {
|
|
39
|
+
const parsed = parseJson(text);
|
|
40
|
+
if (parsed === void 0) unreadable.push(`${label}: not JSON`);
|
|
41
|
+
else verdicts.push(located(assess(parsed.value), label));
|
|
42
|
+
} else {
|
|
43
|
+
verdicts.push(located(assess(text), label));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (files.length === 0 && unreadable.length === 0) unreadable.push(`${target}: no files to check`);
|
|
47
|
+
return { ...combineVerdicts(PROFILE, verdicts, unreadable), target, files: files.length };
|
|
48
|
+
}
|
|
49
|
+
function onlyNumbers(value) {
|
|
50
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") return true;
|
|
51
|
+
if (Array.isArray(value)) return value.every(onlyNumbers);
|
|
52
|
+
if (typeof value === "object") return Object.values(value).every(onlyNumbers);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function noFlaggedKeys(value) {
|
|
56
|
+
if (Array.isArray(value)) return value.every(noFlaggedKeys);
|
|
57
|
+
if (value === null || typeof value !== "object") return true;
|
|
58
|
+
return Object.entries(value).every(([key, item]) => {
|
|
59
|
+
const category = classifyKey(key);
|
|
60
|
+
return (category === void 0 || category === "raw-content") && noFlaggedKeys(item);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function numbersAreNotContent(value) {
|
|
64
|
+
if (Array.isArray(value)) return value.map(numbersAreNotContent);
|
|
65
|
+
if (value === null || typeof value !== "object") return value;
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [key, item] of Object.entries(value)) {
|
|
68
|
+
out[key] = classifyKey(key) === "raw-content" && item !== null && typeof item === "object" && onlyNumbers(item) && noFlaggedKeys(item) ? 0 : numbersAreNotContent(item);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
async function listFiles(target, unreadable) {
|
|
73
|
+
let stat;
|
|
74
|
+
try {
|
|
75
|
+
stat = await lstat(target);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
unreadable.push(`${target}: ${error instanceof Error ? error.message : String(error)}`);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
if (stat.isFile()) return [target];
|
|
81
|
+
if (!stat.isDirectory()) {
|
|
82
|
+
unreadable.push(`${target}: not a regular file or directory`);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const out = [];
|
|
86
|
+
const walk = async (dir) => {
|
|
87
|
+
for (const entry of (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
88
|
+
const path = join(dir, entry.name);
|
|
89
|
+
if (entry.isDirectory()) await walk(path);
|
|
90
|
+
else if (entry.isFile()) out.push(path);
|
|
91
|
+
else unreadable.push(`${relative(target, path)}: not a regular file`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
await walk(target);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
function decodeUtf8(bytes) {
|
|
98
|
+
if (bytes.includes(0)) return void 0;
|
|
99
|
+
try {
|
|
100
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
101
|
+
} catch {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function parseJson(text) {
|
|
106
|
+
try {
|
|
107
|
+
return { value: JSON.parse(text) };
|
|
108
|
+
} catch {
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function located(verdict, where) {
|
|
113
|
+
return {
|
|
114
|
+
...verdict,
|
|
115
|
+
findings: verdict.findings.map((finding) => ({
|
|
116
|
+
...finding,
|
|
117
|
+
paths: finding.paths.map((path) => `${where}#${path}`)
|
|
118
|
+
})),
|
|
119
|
+
unreadable: verdict.unreadable.map((entry) => `${where}#${entry}`)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function formatVerdict(result) {
|
|
123
|
+
const lines = [`${result.status} ${result.target} (${result.files} file(s), share profile)`];
|
|
124
|
+
for (const finding of result.findings) {
|
|
125
|
+
lines.push(
|
|
126
|
+
` ${finding.severity.padEnd(7)} ${finding.category}/${finding.detector} x${finding.count} ${finding.paths.join(", ")}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
for (const entry of result.unreadable) lines.push(` unread ${entry}`);
|
|
130
|
+
if (result.status === "UNSAFE") lines.push("Refuse to share: remove what each finding names, then run traces check <path> --share-safe again.");
|
|
131
|
+
if (result.status === "UNKNOWN") lines.push("Refuse to share: part of the input could not be read, so its safety is unknown.");
|
|
132
|
+
return lines.join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export {
|
|
136
|
+
verifySafe,
|
|
137
|
+
formatVerdict
|
|
138
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ATTR,
|
|
3
|
+
SESSION_CWD_ATTR,
|
|
4
|
+
SUBAGENT_SPAWN_TASK_ATTR,
|
|
5
|
+
SUBAGENT_TASK_ATTR
|
|
6
|
+
} from "./chunk-UQSAGXPH.js";
|
|
7
|
+
import {
|
|
8
|
+
SOURCE_ATTRIBUTE_PREFIX
|
|
9
|
+
} from "./chunk-DU534HAX.js";
|
|
10
|
+
|
|
11
|
+
// src/metadata-only.ts
|
|
12
|
+
import { isContentAttribute } from "@tangle-network/agent-eval/diagnosis";
|
|
13
|
+
var PAYLOAD_DERIVED = /^(?:input|output)\.mime_type$|^traces\.(?:input|output)\.(?:bytes|sha256|truncated)$/i;
|
|
14
|
+
var WITHHELD = /* @__PURE__ */ new Set([
|
|
15
|
+
"tool.args_captured",
|
|
16
|
+
"cwd",
|
|
17
|
+
SESSION_CWD_ATTR,
|
|
18
|
+
ATTR.CWD,
|
|
19
|
+
ATTR.ENVIRONMENT_CWD,
|
|
20
|
+
ATTR.SESSION_FILE,
|
|
21
|
+
ATTR.CORRUPTION_SOURCE_PATH,
|
|
22
|
+
ATTR.SUBJECT_KEY,
|
|
23
|
+
ATTR.GIT_REPOSITORY,
|
|
24
|
+
ATTR.GIT_BRANCH,
|
|
25
|
+
ATTR.GIT_BRANCH_NAME,
|
|
26
|
+
SUBAGENT_TASK_ATTR,
|
|
27
|
+
SUBAGENT_SPAWN_TASK_ATTR
|
|
28
|
+
]);
|
|
29
|
+
function isMetadataAttribute(key, value) {
|
|
30
|
+
return !(isContentAttribute(key) || PAYLOAD_DERIVED.test(key) || WITHHELD.has(key) || key.startsWith(SOURCE_ATTRIBUTE_PREFIX) || value !== null && typeof value === "object");
|
|
31
|
+
}
|
|
32
|
+
function metadataOnly(spans) {
|
|
33
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
34
|
+
const keep = (attributes) => {
|
|
35
|
+
const kept = {};
|
|
36
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
37
|
+
if (isMetadataAttribute(key, value)) kept[key] = value;
|
|
38
|
+
else dropped.add(key);
|
|
39
|
+
}
|
|
40
|
+
return kept;
|
|
41
|
+
};
|
|
42
|
+
const out = spans.map((span) => {
|
|
43
|
+
if (span.status.message) dropped.add("status.message");
|
|
44
|
+
const links = span.links?.map((link) => link.attributes ? { ...link, attributes: keep(link.attributes) } : link);
|
|
45
|
+
return { ...span, attributes: keep(span.attributes), status: { code: span.status.code }, ...links ? { links } : {} };
|
|
46
|
+
});
|
|
47
|
+
return { spans: out, dropped: [...dropped].sort() };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
isMetadataAttribute,
|
|
52
|
+
metadataOnly
|
|
53
|
+
};
|