deepdebug-local-agent 1.3.0 → 1.4.0
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/.github/workflows/deploy-gcp.yml +58 -26
- package/CHANGELOG.md +6 -0
- package/docs/la-protocol-v2.md +68 -0
- package/package.json +2 -1
- package/scripts/sync-la-protocol-v2-contract.mjs +58 -0
- package/src/la-protocol-v2/chunks.js +45 -0
- package/src/la-protocol-v2/config.js +27 -0
- package/src/la-protocol-v2/content.js +57 -0
- package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +81 -0
- package/src/la-protocol-v2/contract/README.md +103 -0
- package/src/la-protocol-v2/contract/ack.schema.json +51 -0
- package/src/la-protocol-v2/contract/alert.schema.json +62 -0
- package/src/la-protocol-v2/contract/approval.decision.schema.json +57 -0
- package/src/la-protocol-v2/contract/audit.event.schema.json +86 -0
- package/src/la-protocol-v2/contract/config.update.schema.json +73 -0
- package/src/la-protocol-v2/contract/content.request.schema.json +110 -0
- package/src/la-protocol-v2/contract/content.response.schema.json +90 -0
- package/src/la-protocol-v2/contract/defs.schema.json +191 -0
- package/src/la-protocol-v2/contract/envelope.schema.json +89 -0
- package/src/la-protocol-v2/contract/error.schema.json +59 -0
- package/src/la-protocol-v2/contract/heartbeat.schema.json +100 -0
- package/src/la-protocol-v2/contract/hello.schema.json +111 -0
- package/src/la-protocol-v2/contract/incident.candidate.schema.json +102 -0
- package/src/la-protocol-v2/contract/index.json +166 -0
- package/src/la-protocol-v2/contract/job.assign.schema.json +212 -0
- package/src/la-protocol-v2/contract/job.cancel.schema.json +52 -0
- package/src/la-protocol-v2/contract/job.event.schema.json +115 -0
- package/src/la-protocol-v2/contract/job.result.schema.json +100 -0
- package/src/la-protocol-v2/contract/log.stats.schema.json +79 -0
- package/src/la-protocol-v2/contract/ready.schema.json +106 -0
- package/src/la-protocol-v2/contract/register.schema.json +92 -0
- package/src/la-protocol-v2/contract/scan.upload.schema.json +59 -0
- package/src/la-protocol-v2/contract/scan.uploaded.schema.json +51 -0
- package/src/la-protocol-v2/contract/webhook.forward.schema.json +69 -0
- package/src/la-protocol-v2/contract.js +90 -0
- package/src/la-protocol-v2/errors.js +34 -0
- package/src/la-protocol-v2/index.js +8 -0
- package/src/la-protocol-v2/link.js +93 -0
- package/src/la-protocol-v2/runtime.js +72 -0
- package/src/la-protocol-v2/session.js +650 -0
- package/src/la-protocol-v2/strict-json.js +47 -0
- package/src/server.js +22 -4
- package/test/contract/la-protocol-v2/vectors/ack.vectors.json +149 -0
- package/test/contract/la-protocol-v2/vectors/alert.vectors.json +101 -0
- package/test/contract/la-protocol-v2/vectors/approval.decision.vectors.json +165 -0
- package/test/contract/la-protocol-v2/vectors/audit.event.vectors.json +156 -0
- package/test/contract/la-protocol-v2/vectors/config.update.vectors.json +189 -0
- package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +325 -0
- package/test/contract/la-protocol-v2/vectors/content.response.vectors.json +413 -0
- package/test/contract/la-protocol-v2/vectors/cross-type.vectors.json +260 -0
- package/test/contract/la-protocol-v2/vectors/error.vectors.json +179 -0
- package/test/contract/la-protocol-v2/vectors/heartbeat.vectors.json +109 -0
- package/test/contract/la-protocol-v2/vectors/hello.vectors.json +224 -0
- package/test/contract/la-protocol-v2/vectors/incident.candidate.vectors.json +186 -0
- package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +631 -0
- package/test/contract/la-protocol-v2/vectors/job.cancel.vectors.json +133 -0
- package/test/contract/la-protocol-v2/vectors/job.event.vectors.json +233 -0
- package/test/contract/la-protocol-v2/vectors/job.result.vectors.json +137 -0
- package/test/contract/la-protocol-v2/vectors/log.stats.vectors.json +137 -0
- package/test/contract/la-protocol-v2/vectors/ready.vectors.json +177 -0
- package/test/contract/la-protocol-v2/vectors/register.vectors.json +399 -0
- package/test/contract/la-protocol-v2/vectors/scan.upload.vectors.json +137 -0
- package/test/contract/la-protocol-v2/vectors/scan.uploaded.vectors.json +101 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-agent-mismatch.json +140 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-after-last.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-byte-length-mismatch.json +174 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-last-flag.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-out-of-order.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-total-changes.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-clock-skew.json +46 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-event-before-ready.json +72 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-first-frame-not-register.json +48 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-frame-over-48kib.json +141 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-la-sends-job-assign.json +173 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-params-raw-differs-from-payload.json +172 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-payload-above-limit.json +174 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-resume-restarts-seq.json +144 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-gap.json +166 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-repeated.json +166 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-sig-tenant-mismatch.json +172 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-signature-warn-with-enforce-required.json +77 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-tenant-mismatch.json +140 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-type-outside-allowlist.json +140 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-content-chunks-in-order.json +224 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-handshake-and-job.json +266 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-resume-continues-seq.json +168 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-retransmit-is-idempotent.json +193 -0
- package/test/contract/la-protocol-v2/vectors/webhook.forward.vectors.json +170 -0
- package/test/la-protocol-v2/contract.test.js +131 -0
- package/test/la-protocol-v2/helpers/la-rig.js +83 -0
- package/test/la-protocol-v2/helpers/reference-cp.js +349 -0
- package/test/la-protocol-v2/helpers/spawn-agent.js +35 -0
- package/test/la-protocol-v2/link.test.js +141 -0
- package/test/la-protocol-v2/session-inbound.test.js +279 -0
- package/test/la-protocol-v2/session-outbound.test.js +382 -0
- package/test/la-protocol-v2/traces.test.js +270 -0
- package/test/la-protocol-v2/tunnel-e2e.test.js +124 -0
- package/tools/graphify-validation/extras/README.md +19 -0
- package/tools/graphify-validation/extras/bymethod.py +35 -0
- package/tools/graphify-validation/extras/docker/Dockerfile +16 -0
- package/tools/graphify-validation/extras/docker/inner.sh +7 -0
- package/tools/graphify-validation/extras/g10.py +59 -0
- package/tools/graphify-validation/extras/g3aug.py +49 -0
- package/tools/graphify-validation/extras/hybrid.py +34 -0
- package/tools/graphify-validation/extras/truth2.py +44 -0
- package/tools/interop/LaV2NodeInteropTest.java +266 -0
- package/tools/interop/README.md +21 -0
- package/tools/interop/la-interop-client.mjs +97 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// F3-05b: the 24 whole-session traces of the contract (F3-22), reproduced in Node. "Traces are language
|
|
2
|
+
// neutral; the LA must reproduce each expect.error." Three things are shown for them:
|
|
3
|
+
// 1. the reference CP (test helper) reproduces every trace: same error at the same frame (so the model the
|
|
4
|
+
// other tests use is the model of the contract);
|
|
5
|
+
// 2. the two traces whose error is in a CP->LA frame (params_mismatch, tenant_mismatch of the signature)
|
|
6
|
+
// are reproduced by the REAL LA session, same error at the same frame;
|
|
7
|
+
// 3. for the others (an LA frame that the CP refuses) the LA's own send path refuses to produce the same
|
|
8
|
+
// frame, with the same code; the valid traces are replayed with the LA as the LA.
|
|
9
|
+
import { test } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { ReferenceCp, uuid, waitFor } from "./helpers/reference-cp.js";
|
|
15
|
+
import { inMemory, signingFor } from "./helpers/la-rig.js";
|
|
16
|
+
import { V2Session } from "../../src/la-protocol-v2/session.js";
|
|
17
|
+
|
|
18
|
+
const DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "../contract/la-protocol-v2/vectors/traces");
|
|
19
|
+
const traces = fs.readdirSync(DIR).filter((f) => f.endsWith(".json")).sort().map((f) => ({ file: f, name: f.replace(".json", ""), ...JSON.parse(fs.readFileSync(path.join(DIR, f), "utf8")) }));
|
|
20
|
+
|
|
21
|
+
test("the suite has all 24 traces (20 invalid, 4 valid)", () => {
|
|
22
|
+
assert.equal(traces.length, 24);
|
|
23
|
+
assert.equal(traces.filter((t) => t.expect.ok).length, 4);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const textOf = (fr) => {
|
|
27
|
+
let text = JSON.stringify(fr.frame);
|
|
28
|
+
if (fr.inflateBytesTo) text += " ".repeat(Math.max(0, fr.inflateBytesTo - Buffer.byteLength(text)));
|
|
29
|
+
return text;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The reference CP in the state the trace starts from (a resumed session when the trace says so). */
|
|
33
|
+
function cpFor(trace) {
|
|
34
|
+
const s = trace.session;
|
|
35
|
+
const cp = new ReferenceCp({ tenantId: s.tenantId, agentId: s.agentId, requireEnforce: s.requireEnforce, now: () => Date.parse(s.now) });
|
|
36
|
+
const ready = trace.frames.map((f) => f.frame).find((f) => f.type === "ready");
|
|
37
|
+
const reg = trace.frames.map((f) => f.frame).find((f) => f.type === "register");
|
|
38
|
+
if (ready && ready.payload.resumed) {
|
|
39
|
+
cp.sessionStarted = true;
|
|
40
|
+
cp.sessionId = ready.payload.sessionId;
|
|
41
|
+
cp.laSeq = ready.payload.peerLastSeq;
|
|
42
|
+
cp.cpSeq = reg.payload.resume.lastRecvSeq;
|
|
43
|
+
}
|
|
44
|
+
cp.attach(() => {}, () => {});
|
|
45
|
+
return cp;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ------------------------------------------------------------------------------------------ 1. the reference CP
|
|
49
|
+
|
|
50
|
+
for (const t of traces) {
|
|
51
|
+
const cpSideError = t.expect.error && t.frames[t.expect.atFrame].dir === "cp->la";
|
|
52
|
+
test(`reference CP reproduces ${t.name}${cpSideError ? " (n/a: the error is in a CP frame, see 2)" : ""}`, { skip: cpSideError }, () => {
|
|
53
|
+
const cp = cpFor(t);
|
|
54
|
+
let failed = null;
|
|
55
|
+
for (let i = 0; i < t.frames.length && !failed; i++) {
|
|
56
|
+
const fr = t.frames[i];
|
|
57
|
+
if (fr.dir === "cp->la") { cp.cpSeq = Math.max(cp.cpSeq, fr.frame.seq); continue; }
|
|
58
|
+
const r = cp.feed(textOf(fr));
|
|
59
|
+
if (r) failed = { error: r.code, atFrame: i };
|
|
60
|
+
}
|
|
61
|
+
if (t.expect.ok) {
|
|
62
|
+
assert.equal(failed, null, JSON.stringify(failed));
|
|
63
|
+
if (t.expect.duplicateFrames) assert.equal(cp.duplicates.length, t.expect.duplicateFrames.length);
|
|
64
|
+
} else {
|
|
65
|
+
assert.deepEqual(failed, { error: t.expect.error, atFrame: t.expect.atFrame });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ------------------------------------------------------------------------------------------ 2. errors in CP frames, by the LA
|
|
71
|
+
|
|
72
|
+
for (const name of ["invalid-params-raw-differs-from-payload", "invalid-sig-tenant-mismatch"]) {
|
|
73
|
+
test(`the LA reproduces ${name}: ${traces.find((t) => t.name === name).expect.error} at frame ${traces.find((t) => t.name === name).expect.atFrame}`, async () => {
|
|
74
|
+
const t = traces.find((x) => x.name === name);
|
|
75
|
+
const cp = new ReferenceCp({ tenantId: t.session.tenantId, agentId: t.session.agentId });
|
|
76
|
+
// the trace's signatures are placeholders: mode off skips the cryptography, the structural checks still run
|
|
77
|
+
const env = inMemory(cp, { mode: "off", tenantId: t.session.tenantId, agentId: t.session.agentId });
|
|
78
|
+
env.session.attach(env.transport);
|
|
79
|
+
await waitFor(() => env.session.state === "ready");
|
|
80
|
+
const bad = t.frames[t.expect.atFrame].frame;
|
|
81
|
+
env.session.setHandler(bad.type, () => ({ accept: true }));
|
|
82
|
+
env.session.handleText(JSON.stringify(bad), env.transport);
|
|
83
|
+
await waitFor(() => cp.peerErrors.length > 0);
|
|
84
|
+
assert.deepEqual(cp.peerErrors, [t.expect.error]);
|
|
85
|
+
assert.equal(env.session.stats.dataOut, 0);
|
|
86
|
+
env.session.close();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ------------------------------------------------------------------------------------------ 3. the LA never produces them
|
|
91
|
+
|
|
92
|
+
const LA_FRAMES = (t) => t.frames.map((f, i) => ({ ...f, i })).filter((f) => f.dir === "la->cp");
|
|
93
|
+
|
|
94
|
+
async function readyLa() {
|
|
95
|
+
const cp = new ReferenceCp();
|
|
96
|
+
const env = inMemory(cp).start();
|
|
97
|
+
await waitFor(() => env.session.state === "ready");
|
|
98
|
+
return { cp, ...env };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const name of ["invalid-chunk-after-last", "invalid-chunk-byte-length-mismatch", "invalid-chunk-last-flag", "invalid-chunk-out-of-order", "invalid-chunk-total-changes", "invalid-payload-above-limit"]) {
|
|
102
|
+
test(`the LA refuses to send the chunks of ${name}: same code at the same frame`, async () => {
|
|
103
|
+
const t = traces.find((x) => x.name === name);
|
|
104
|
+
const { cp, session } = await readyLa();
|
|
105
|
+
let got = null;
|
|
106
|
+
for (const fr of LA_FRAMES(t)) {
|
|
107
|
+
if (fr.frame.type !== "content.response") continue;
|
|
108
|
+
try {
|
|
109
|
+
await session.enqueue("content.response", fr.frame.payload, { requestId: fr.frame.requestId, chunk: fr.frame.chunk }).then((h) => h.acked);
|
|
110
|
+
} catch (e) { got = { error: e.code, atFrame: fr.i }; break; }
|
|
111
|
+
}
|
|
112
|
+
// the oversized trace chunk is refused by the schema (payload maxLength), which the CP reports as payload_too_large
|
|
113
|
+
assert.deepEqual(got, { error: t.expect.error, atFrame: t.expect.atFrame });
|
|
114
|
+
assert.deepEqual(cp.errorsSent, [], "nothing invalid reached the CP");
|
|
115
|
+
session.close();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test("the LA never produces invalid-event-before-ready / invalid-first-frame-not-register: register and hello first, no data before ready", async () => {
|
|
120
|
+
const cp = new ReferenceCp();
|
|
121
|
+
const sent = [];
|
|
122
|
+
const env = inMemory(cp);
|
|
123
|
+
const swallow = { send: (t) => sent.push(JSON.parse(t)), close() {}, bufferedAmount: () => 0 };
|
|
124
|
+
env.session.attach(swallow);
|
|
125
|
+
assert.deepEqual(sent.map((f) => f.type), ["register", "hello"], "the first frame is register, then hello");
|
|
126
|
+
await assert.rejects(env.session.enqueue("job.event", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", state: "running" }), (e) => e.code === "not_ready");
|
|
127
|
+
assert.equal(sent.length, 2);
|
|
128
|
+
env.session.close();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("the LA never produces invalid-clock-skew: register.ts is the clock of the LA, hello carries clockEpochMs", async () => {
|
|
132
|
+
const cp = new ReferenceCp();
|
|
133
|
+
const sent = [];
|
|
134
|
+
const env = inMemory(cp);
|
|
135
|
+
env.session.attach({ send: (t) => sent.push(JSON.parse(t)), close() {}, bufferedAmount: () => 0 });
|
|
136
|
+
assert.ok(Math.abs(Date.parse(sent[0].ts) - Date.now()) < 2000);
|
|
137
|
+
assert.ok(Math.abs(sent[1].payload.clockEpochMs - Date.now()) < 2000);
|
|
138
|
+
env.session.close();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the LA never produces invalid-signature-warn-with-enforce-required: hello.signatureMode is the real mode, and a CP that requires enforce says so", async () => {
|
|
142
|
+
const cp = new ReferenceCp({ requireEnforce: true });
|
|
143
|
+
const env = inMemory(cp, { mode: "warn" }).start();
|
|
144
|
+
await waitFor(() => cp.errorsSent.length > 0);
|
|
145
|
+
assert.deepEqual(cp.errorsSent, ["signature_mode_insufficient"]);
|
|
146
|
+
assert.equal(env.session.state, "await_ready"); // not ready, no data possible
|
|
147
|
+
assert.equal(cp.hellos.length, 0);
|
|
148
|
+
env.session.close();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("the LA never produces invalid-agent-mismatch / invalid-tenant-mismatch: every frame carries the configured ids, never one from a payload", async () => {
|
|
152
|
+
const { cp, session } = await readyLa();
|
|
153
|
+
await session.send("job.event", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", state: "running" });
|
|
154
|
+
for (const f of cp.received) {
|
|
155
|
+
assert.equal(f.tenantId, cp.tenantId);
|
|
156
|
+
assert.equal(f.agentId, cp.agentId);
|
|
157
|
+
}
|
|
158
|
+
// there is no way to pass an identity to enqueue(); a payload that tries is refused by the closed schema
|
|
159
|
+
await assert.rejects(session.enqueue("job.event", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", state: "running", tenantId: "99999999-5a4d-4e7b-9c21-0a1b2c3d4e5f" }), (e) => e.code === "schema_violation");
|
|
160
|
+
session.close();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("the LA never produces invalid-la-sends-job-assign / invalid-type-outside-allowlist", async () => {
|
|
164
|
+
const { cp, session } = await readyLa();
|
|
165
|
+
await assert.rejects(session.enqueue("job.assign", {}), (e) => e.code === "wrong_direction");
|
|
166
|
+
await assert.rejects(session.enqueue("shell.exec", {}), (e) => e.code === "unknown_type");
|
|
167
|
+
assert.deepEqual(cp.errorsSent, []);
|
|
168
|
+
session.close();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("the LA never produces invalid-seq-gap / invalid-seq-repeated / invalid-resume-restarts-seq: consecutive seq, one msgId per seq, a resume continues", async () => {
|
|
172
|
+
const { cp, session } = await readyLa();
|
|
173
|
+
for (let i = 0; i < 4; i++) await session.send("job.event", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", state: "running", iteration: i });
|
|
174
|
+
assert.deepEqual(cp.received.map((f) => f.seq), [1, 2, 3, 4]);
|
|
175
|
+
assert.equal(new Set(cp.received.map((f) => f.msgId)).size, 4);
|
|
176
|
+
assert.deepEqual(cp.errorsSent, []);
|
|
177
|
+
session.close();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("the LA never produces invalid-frame-over-48kib: frames are compact JSON and the size is checked before sending", async () => {
|
|
181
|
+
const { cp, session } = await readyLa();
|
|
182
|
+
await assert.rejects(session.enqueue("job.event", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", state: "running", phase: "x".repeat(60_000) }), (e) => ["frame_too_large", "payload_too_large", "schema_violation"].includes(e.code));
|
|
183
|
+
// and the same frame as the trace has it (padded to 50000 bytes) is refused on the way IN as frame_too_large
|
|
184
|
+
const t = traces.find((x) => x.name === "invalid-frame-over-48kib");
|
|
185
|
+
const fr = t.frames[t.expect.atFrame];
|
|
186
|
+
const { transport } = { transport: session.transport };
|
|
187
|
+
session.handleText(textOf(fr), transport);
|
|
188
|
+
await waitFor(() => cp.peerErrors.length > 0);
|
|
189
|
+
assert.deepEqual(cp.peerErrors, ["frame_too_large"]);
|
|
190
|
+
session.close();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ------------------------------------------------------------------------------------------ valid traces, LA as the LA
|
|
194
|
+
|
|
195
|
+
test("valid-handshake-and-job: CP job.assign -> LA acks it (ackSeq 1) and then reports job.event x2 + job.result with seq 1..3", async () => {
|
|
196
|
+
const t = traces.find((x) => x.name === "valid-handshake-and-job");
|
|
197
|
+
const cp = new ReferenceCp({ tenantId: t.session.tenantId, agentId: t.session.agentId, requireEnforce: true });
|
|
198
|
+
const env = inMemory(cp, { tenantId: t.session.tenantId, agentId: t.session.agentId }).start();
|
|
199
|
+
await waitFor(() => env.session.state === "ready");
|
|
200
|
+
const assign = t.frames.find((f) => f.frame.type === "job.assign").frame;
|
|
201
|
+
env.session.setHandler("job.assign", (ctx) => { assert.deepEqual(ctx.params, assign.payload); return { accept: true }; });
|
|
202
|
+
cp.sendData("job.assign", assign.payload, { workspaceId: assign.workspaceId, requestId: assign.requestId });
|
|
203
|
+
await waitFor(() => cp.acksFromLa.length === 1);
|
|
204
|
+
assert.deepEqual(cp.acksFromLa[0], { ackSeq: 1, status: "accepted", errorCode: undefined });
|
|
205
|
+
for (const fr of LA_FRAMES(t).filter((f) => ["job.event", "job.result"].includes(f.frame.type))) {
|
|
206
|
+
await env.session.send(fr.frame.type, fr.frame.payload, { workspaceId: fr.frame.workspaceId });
|
|
207
|
+
}
|
|
208
|
+
assert.deepEqual(cp.received.map((f) => [f.type, f.seq]), [["job.event", 1], ["job.event", 2], ["job.result", 3]]);
|
|
209
|
+
assert.deepEqual(cp.errorsSent, []);
|
|
210
|
+
env.session.close();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("valid-content-chunks-in-order: the three chunks of the trace go out in order and the CP accepts them", async () => {
|
|
214
|
+
const t = traces.find((x) => x.name === "valid-content-chunks-in-order");
|
|
215
|
+
const cp = new ReferenceCp({ tenantId: t.session.tenantId, agentId: t.session.agentId });
|
|
216
|
+
const env = inMemory(cp, { tenantId: t.session.tenantId, agentId: t.session.agentId }).start();
|
|
217
|
+
await waitFor(() => env.session.state === "ready");
|
|
218
|
+
for (const fr of LA_FRAMES(t).filter((f) => f.frame.type === "content.response")) {
|
|
219
|
+
await env.session.send("content.response", fr.frame.payload, { requestId: fr.frame.requestId, chunk: fr.frame.chunk, workspaceId: fr.frame.workspaceId });
|
|
220
|
+
}
|
|
221
|
+
assert.deepEqual(cp.received.map((f) => [f.seq, f.chunk.index]), [[1, 0], [2, 1], [3, 2]]);
|
|
222
|
+
assert.deepEqual(cp.errorsSent, []);
|
|
223
|
+
env.session.close();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("valid-retransmit-is-idempotent: an ack lost -> the same frame again -> the CP re-acks and counts one event", async () => {
|
|
227
|
+
const cp = new ReferenceCp({ limits: { ackTimeoutMs: 100 } });
|
|
228
|
+
const env = inMemory(cp).start();
|
|
229
|
+
await waitFor(() => env.session.state === "ready");
|
|
230
|
+
cp.dropAcks = 1;
|
|
231
|
+
await env.session.send("job.event", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", state: "running" });
|
|
232
|
+
await env.session.send("job.event", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", state: "succeeded" });
|
|
233
|
+
assert.deepEqual(cp.duplicates, [1]);
|
|
234
|
+
assert.deepEqual(cp.received.map((f) => f.seq), [1, 2]);
|
|
235
|
+
assert.deepEqual(cp.errorsSent, []);
|
|
236
|
+
env.session.close();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("valid-resume-continues-seq: after a resumed ready the LA continues at peerLastSeq+1", async () => {
|
|
240
|
+
const t = traces.find((x) => x.name === "valid-resume-continues-seq");
|
|
241
|
+
const cp = new ReferenceCp({ tenantId: t.session.tenantId, agentId: t.session.agentId });
|
|
242
|
+
const env = inMemory(cp, { tenantId: t.session.tenantId, agentId: t.session.agentId });
|
|
243
|
+
// first life: 5 events delivered
|
|
244
|
+
env.session.attach(env.transport);
|
|
245
|
+
await waitFor(() => env.session.state === "ready");
|
|
246
|
+
for (let i = 0; i < 5; i++) await env.session.send("job.event", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", state: "running", iteration: i });
|
|
247
|
+
// the CP also sent 4 frames
|
|
248
|
+
env.session.setHandler("job.cancel", () => ({ accept: true }));
|
|
249
|
+
for (let i = 0; i < 4; i++) cp.sendData("job.cancel", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", reason: "user" });
|
|
250
|
+
await waitFor(() => env.session.lastRecvSeq === 4);
|
|
251
|
+
// second life: same session object (the LA process kept it), new connection
|
|
252
|
+
const t2 = { send: (x) => { env.wire.toCp.push(x); cp.feed(x); }, close() {}, bufferedAmount: () => 0 };
|
|
253
|
+
env.session.detach(env.transport);
|
|
254
|
+
cp.attach((x) => queueMicrotask(() => env.session.handleText(x, t2)), () => {});
|
|
255
|
+
env.session.attach(t2);
|
|
256
|
+
await waitFor(() => env.session.state === "ready");
|
|
257
|
+
assert.deepEqual(cp.registers.at(-1).resume, { sessionId: cp.sessionId, lastRecvSeq: 4 });
|
|
258
|
+
await env.session.send("job.event", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", state: "running", iteration: 5 });
|
|
259
|
+
assert.equal(cp.received.at(-1).seq, 6, "continues at peerLastSeq + 1");
|
|
260
|
+
assert.deepEqual(cp.errorsSent, []);
|
|
261
|
+
env.session.close();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("V2Session is exported and constructible without side effects (no timers left running)", () => {
|
|
265
|
+
const cp = new ReferenceCp();
|
|
266
|
+
const s = new V2Session({ tenantId: cp.tenantId, agentId: cp.agentId, signing: signingFor({ keyId: cp.keyId, publicKeyRaw: cp.publicKeyRaw }), laVersion: "1.3.0" });
|
|
267
|
+
assert.equal(s.state, "idle");
|
|
268
|
+
s.close();
|
|
269
|
+
assert.equal(uuid().length, 36);
|
|
270
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// F3-05b end to end: the real agent process (src/server.js) against the reference CP.
|
|
2
|
+
// - flag unset / false: the v1 client, nothing of v2 on the wire (characterization; the frames are the v1 ones)
|
|
3
|
+
// - flag true: v2 handshake with a signed ready, v1 commands still answered on the same connection
|
|
4
|
+
// - flag true against a gateway without v2: falls back to the v1 register on the same connection
|
|
5
|
+
// - flag true without an agent id: stays v1
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { ReferenceCpServer, TENANT, AGENT, waitFor } from "./helpers/reference-cp.js";
|
|
9
|
+
import { runAgent } from "./helpers/spawn-agent.js";
|
|
10
|
+
|
|
11
|
+
const baseEnv = (srv) => ({
|
|
12
|
+
TENANT_ID: TENANT,
|
|
13
|
+
LOCAL_AGENT_SIGNATURE_MODE: "enforce",
|
|
14
|
+
LOCAL_AGENT_SIGNING_PUBLIC_KEYS: `${srv.keyId}=${srv.publicKeyRaw}`,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
for (const flag of [undefined, "false", "off", "0", "bogus"]) {
|
|
18
|
+
test(`flag ${flag === undefined ? "unset" : JSON.stringify(flag)}: the v1 client, exactly: legacy register, no v2 frame, v1 command answered`, async () => {
|
|
19
|
+
const srv = await new ReferenceCpServer({}).start();
|
|
20
|
+
try {
|
|
21
|
+
await runAgent({
|
|
22
|
+
// everything v2 needs IS configured (UUID tenant, agent id, trusted key): only the flag keeps it off
|
|
23
|
+
srv, cfg: { authorizationId: AGENT, tenantId: TENANT }, env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: flag },
|
|
24
|
+
until: async () => {
|
|
25
|
+
if (srv.ws && srv.legacy.some((f) => f.type === "register") && !srv.sent) {
|
|
26
|
+
srv.sent = true;
|
|
27
|
+
srv.ws.send(srv.cp.v1Command("health", {}));
|
|
28
|
+
}
|
|
29
|
+
return srv.legacy.some((f) => f.command === "health");
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const reg = srv.legacy.find((f) => f.type === "register");
|
|
33
|
+
assert.deepEqual(Object.keys(reg).sort(), ["port", "tenantId", "type", "workspacePath"]);
|
|
34
|
+
assert.equal(reg.tenantId, TENANT);
|
|
35
|
+
const health = srv.legacy.find((f) => f.command === "health");
|
|
36
|
+
assert.equal(health.status, "ok");
|
|
37
|
+
assert.equal(srv.cp.rawIn.length, 0, "no v2 frame was ever sent");
|
|
38
|
+
assert.equal(srv.cp.registers.length, 0);
|
|
39
|
+
assert.equal(srv.textsFromLa.some((t) => t.includes("schemaVersion")), false);
|
|
40
|
+
} finally { await srv.stop(); }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("flag true: v2 handshake (register, hello, signed ready), no v1 register, and a v1 command is still answered on the same connection", async () => {
|
|
45
|
+
const srv = await new ReferenceCpServer({ requireEnforce: true }).start();
|
|
46
|
+
try {
|
|
47
|
+
await runAgent({
|
|
48
|
+
srv, cfg: { authorizationId: AGENT, tenantId: TENANT },
|
|
49
|
+
env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: "true" },
|
|
50
|
+
until: async () => {
|
|
51
|
+
if (srv.cp.state === "ready" && !srv.sent) { srv.sent = true; srv.ws.send(srv.cp.v1Command("health", {})); }
|
|
52
|
+
return srv.legacy.some((f) => f.command === "health");
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
assert.equal(srv.connections, 1);
|
|
56
|
+
const reg = srv.cp.registers[0];
|
|
57
|
+
assert.equal(reg.protocol, 2);
|
|
58
|
+
assert.equal(reg.signatureMode, "enforce");
|
|
59
|
+
assert.match(reg.laVersion, /^\d+\.\d+\.\d+$/);
|
|
60
|
+
assert.deepEqual(reg.capabilities, ["chunk.base64", "protocol.v2", "v1.commands"]);
|
|
61
|
+
assert.ok(reg.workspaces.every((w) => /^[0-9a-f]{64}$/.test(w.pathSha256)), "only hashes of paths");
|
|
62
|
+
assert.equal(JSON.stringify(srv.cp.rawIn).includes("/tmp") || JSON.stringify(srv.cp.rawIn).includes("v2e2e-ws"), false, "no workspace path on the wire");
|
|
63
|
+
assert.equal(srv.cp.hellos[0].signatureMode, "enforce");
|
|
64
|
+
assert.equal(srv.legacy.some((f) => f.type === "register"), false, "no v1 register: v2 owns the registration");
|
|
65
|
+
assert.deepEqual(srv.cp.errorsSent, []);
|
|
66
|
+
assert.equal(srv.legacy.find((f) => f.command === "health").status, "ok", "v1 command on a v2 connection");
|
|
67
|
+
} finally { await srv.stop(); }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("flag true, the CP sends a signed v2 job.cancel: acked `rejected protocol_unsupported` (no handler yet), connection stays up", async () => {
|
|
71
|
+
const srv = await new ReferenceCpServer({ requireEnforce: true }).start();
|
|
72
|
+
try {
|
|
73
|
+
await runAgent({
|
|
74
|
+
srv, cfg: { authorizationId: AGENT, tenantId: TENANT },
|
|
75
|
+
env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: "true" },
|
|
76
|
+
until: async () => {
|
|
77
|
+
if (srv.cp.state === "ready" && !srv.sent) { srv.sent = true; srv.cp.sendData("job.cancel", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", reason: "user" }); }
|
|
78
|
+
return srv.cp.acksFromLa.length > 0;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
assert.deepEqual(srv.cp.acksFromLa, [{ ackSeq: 1, status: "rejected", errorCode: "protocol_unsupported" }]);
|
|
82
|
+
assert.deepEqual(srv.cp.errorsSent, []);
|
|
83
|
+
} finally { await srv.stop(); }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("flag true against a gateway that does not speak v2: after the handshake timeout the v1 register goes out on the same connection", async () => {
|
|
87
|
+
const srv = await new ReferenceCpServer({ v2Disabled: true }).start();
|
|
88
|
+
try {
|
|
89
|
+
await runAgent({
|
|
90
|
+
srv, cfg: { authorizationId: AGENT, tenantId: TENANT },
|
|
91
|
+
env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: "true", LOCAL_AGENT_V2_HANDSHAKE_MS: "300" },
|
|
92
|
+
until: async () => srv.legacy.some((f) => f.type === "register"),
|
|
93
|
+
});
|
|
94
|
+
const reg = srv.legacy.find((f) => f.type === "register");
|
|
95
|
+
assert.deepEqual(Object.keys(reg).sort(), ["port", "tenantId", "type", "workspacePath"]);
|
|
96
|
+
assert.equal(srv.connections, 1, "same connection");
|
|
97
|
+
} finally { await srv.stop(); }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("flag true without an agent id (no authorizationId): v1 only, nothing of v2 on the wire", async () => {
|
|
101
|
+
const srv = await new ReferenceCpServer({}).start();
|
|
102
|
+
try {
|
|
103
|
+
await runAgent({
|
|
104
|
+
srv, env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: "true" },
|
|
105
|
+
until: async () => srv.legacy.some((f) => f.type === "register"),
|
|
106
|
+
});
|
|
107
|
+
assert.equal(srv.cp.rawIn.length, 0);
|
|
108
|
+
assert.ok(srv.legacy.some((f) => f.type === "register"));
|
|
109
|
+
} finally { await srv.stop(); }
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("flag true with a tenant id that is not a UUID: v1 only", async () => {
|
|
113
|
+
const srv = await new ReferenceCpServer({}).start();
|
|
114
|
+
try {
|
|
115
|
+
await runAgent({
|
|
116
|
+
srv, cfg: { authorizationId: AGENT },
|
|
117
|
+
env: { ...baseEnv(srv), TENANT_ID: "tenant-test", LOCAL_AGENT_SIGNATURE_MODE: "off", LOCAL_AGENT_PROTOCOL_V2: "true" },
|
|
118
|
+
until: async () => srv.legacy.some((f) => f.type === "register"),
|
|
119
|
+
});
|
|
120
|
+
assert.equal(srv.cp.rawIn.length, 0);
|
|
121
|
+
} finally { await srv.stop(); }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("sanity: waitFor is exported", () => assert.equal(typeof waitFor, "function"));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Graphify validation extras (T4-06, round 2, 2026-10-28)
|
|
2
|
+
|
|
3
|
+
Evidence scripts used for `10 Library/Plans/Fecho Dados 2026-10-28 T4.md`. They sit beside the harness and read its
|
|
4
|
+
output; nothing here is part of the agent runtime. Layout expected under `$GV_T4_ROOT`:
|
|
5
|
+
`harness/` (a `git archive` of this repo), `out/run1/` (a `run.sh --out` directory: `venv/`, `work/`, `results/`),
|
|
6
|
+
`bin/rg` (ripgrep wrapper), `g10/home/` (empty HOME), `pub/` (shallow clones of public Go/Terraform repos).
|
|
7
|
+
|
|
8
|
+
| Script | What it measures |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `g10.py` | cold run, network denied, determinism and per-language file coverage for extra repos (Python, TS, Go, Terraform, SQL) |
|
|
11
|
+
| `truth2.py` | `affected` recall against a code-only ground truth (comment, import and test lines removed), class level and class+methods |
|
|
12
|
+
| `bymethod.py` | `affected` by class versus by method, against `rg -w` as the harness does |
|
|
13
|
+
| `hybrid.py` | recall@8 of graph, lexical, reciprocal-rank fusion and lexical-first interleave |
|
|
14
|
+
| `g3aug.py` | G3 Spring fixture with a wrapper-level "call via interface reaches implementations" post-process |
|
|
15
|
+
| `docker/` | G9: image from `requirements-py311.lock` (build context `tools/graphify-validation`), `inner.sh` measures wall time and cgroup peak |
|
|
16
|
+
|
|
17
|
+
Docker run used for the numbers: `docker run --rm --read-only --network none --user 10001:10001 --cpus=2 --memory=4g
|
|
18
|
+
--tmpfs /tmp -e HOME=/data/home -v DATA:/data -v inner.sh:/inner.sh:ro --entrypoint /bin/sh IMAGE /inner.sh REPO RUN`.
|
|
19
|
+
Public repos are only parsed (tree-sitter), never executed. No customer repository is involved.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys, json, pathlib, subprocess, os
|
|
3
|
+
SP=pathlib.Path(os.environ['GV_T4_ROOT'])
|
|
4
|
+
sys.path.insert(0,str(SP/'harness/tools/graphify-validation/lib'))
|
|
5
|
+
os.environ['RG']=str(SP/'bin/rg')
|
|
6
|
+
import affected_vs_rg as A, gv_common as gc, graph_metrics as gm
|
|
7
|
+
S=json.load(open(SP/'harness/tools/graphify-validation/config/symbols.json'))
|
|
8
|
+
env=gc.clean_env(SP/'g10/home',str(SP/'out/run1/venv/bin')); G=str(SP/'out/run1/venv/bin/graphify')
|
|
9
|
+
out={}
|
|
10
|
+
for repo in ['vh-api-gateway','visionhammer-agents','deepdebug-local-agent']:
|
|
11
|
+
wd=SP/'out/run1/work'/repo; g=gm.load_graph(wd)
|
|
12
|
+
syms=S[repo] if isinstance(S,dict) else S
|
|
13
|
+
rows=[]
|
|
14
|
+
for sym in syms:
|
|
15
|
+
cls=[n for n in g['nodes'] if n.get('label')==sym and n.get('source_file') and not n['label']==n['source_file'].rsplit('/',1)[-1]]
|
|
16
|
+
if not cls: rows.append({'symbol':sym,'status':'nodef'}); continue
|
|
17
|
+
defs={n['source_file'] for n in cls}
|
|
18
|
+
ids=[n['id'] for n in cls]
|
|
19
|
+
for n in g['nodes']:
|
|
20
|
+
if n.get('source_file') in defs and n.get('label','').startswith('.') and n['label'].endswith('()'):
|
|
21
|
+
ids.append(n['id'])
|
|
22
|
+
files=set()
|
|
23
|
+
for i in ids:
|
|
24
|
+
r=subprocess.run([G,'affected',i,'--depth','1'],cwd=str(wd),env=env,capture_output=True,text=True,timeout=300)
|
|
25
|
+
files|=A.parse_affected(r.stdout)
|
|
26
|
+
files-=defs
|
|
27
|
+
rgf=gc.files_with_word(wd,sym,include_tests=True)-defs
|
|
28
|
+
m=lambda s:{f for f in s if not gc.is_test_path(f)}
|
|
29
|
+
gf,rf=m(files),m(rgf)
|
|
30
|
+
rows.append({'symbol':sym,'methods':len(ids)-len(cls),'graph':len(gf),'rg':len(rf),'recall':round(100*len(gf&rf)/len(rf),1) if rf else None,'precision':round(100*len(gf&rf)/len(gf),1) if gf else None})
|
|
31
|
+
rr=[r for r in rows if r.get('recall') is not None]
|
|
32
|
+
tot_rg=sum(r['rg'] for r in rr); tot_both=sum(round(r['recall']*r['rg']/100) for r in rr)
|
|
33
|
+
out[repo]={'rows':rows,'agg_recall_pct':round(100*tot_both/tot_rg,1) if tot_rg else None}
|
|
34
|
+
print(repo,out[repo]['agg_recall_pct'],[(r['symbol'],r.get('methods'),r.get('recall')) for r in rows])
|
|
35
|
+
json.dump(out,open(SP/'g10/bymethod.json','w'),indent=1)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# T4-06 G9 test image: mirrors the python slice of the LA Dockerfile.full (node:20-slim = Debian 12, python3 3.11) + packaging plan 2.2
|
|
2
|
+
FROM node:20-slim AS base
|
|
3
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git python3 python3-pip \
|
|
4
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
5
|
+
|
|
6
|
+
FROM base AS graphify
|
|
7
|
+
RUN apt-get update && apt-get install -y --no-install-recommends python3-venv ripgrep \
|
|
8
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
9
|
+
COPY requirements-py311.lock /tmp/graphify.lock
|
|
10
|
+
RUN python3 -m venv /opt/vh-graphify \
|
|
11
|
+
&& /opt/vh-graphify/bin/pip install --no-cache-dir --disable-pip-version-check \
|
|
12
|
+
--require-hashes --no-deps --only-binary=:all: -r /tmp/graphify.lock \
|
|
13
|
+
&& /opt/vh-graphify/bin/python -m compileall -q /opt/vh-graphify/lib \
|
|
14
|
+
&& rm -f /opt/vh-graphify/bin/graphify-mcp /tmp/graphify.lock
|
|
15
|
+
RUN useradd -u 10001 -m -d /nonexistent-home vhga 2>/dev/null || true
|
|
16
|
+
ENV PATH=/opt/vh-graphify/bin:$PATH PYTHONDONTWRITEBYTECODE=1 PYTHONHASHSEED=0
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# runs inside the container: $1 = repo dir under /data/ws
|
|
3
|
+
S=$(date +%s.%N)
|
|
4
|
+
graphify update /data/ws/$1 > /data/log-$1-$2.txt 2>&1; RC=$?
|
|
5
|
+
E=$(date +%s.%N)
|
|
6
|
+
PEAK=$(cat /sys/fs/cgroup/memory.peak 2>/dev/null)
|
|
7
|
+
echo "repo=$1 run=$2 rc=$RC wall=$(echo "$E $S" | awk '{printf "%.1f",$1-$2}')s cgroup_peak_mb=$(echo $PEAK | awk '{printf "%.0f",$1/1048576}')"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys, os, json, subprocess, shutil, pathlib, collections
|
|
3
|
+
LIB=os.path.join(os.environ['GV_T4_ROOT'],'harness/tools/graphify-validation/lib')
|
|
4
|
+
sys.path.insert(0, LIB)
|
|
5
|
+
import run_graphify as rgf, graph_metrics as gm, gv_common as gc
|
|
6
|
+
SP=pathlib.Path(os.environ['GV_T4_ROOT'])
|
|
7
|
+
VENV=SP/'out/run1/venv'; BIN=str(VENV/'bin'); G=str(VENV/'bin/graphify')
|
|
8
|
+
W=SP/'g10/work'; W.mkdir(exist_ok=True)
|
|
9
|
+
HOME=SP/'g10/home'; HOME.mkdir(exist_ok=True)
|
|
10
|
+
BASE='/Users/eduardooliveira/Documents/visionhammer projects/'
|
|
11
|
+
targets=[
|
|
12
|
+
('vh-litellm', BASE+'vh-litellm','origin/main'),
|
|
13
|
+
('visionhammer-pentest', BASE+'../x','') ,
|
|
14
|
+
('vision-heart-glow','%svision-heart-glow-3478cd8d'%BASE,'origin/main'),
|
|
15
|
+
('go-chi-chi', str(SP/'pub/chi'),'HEAD'),
|
|
16
|
+
('spf13-cobra', str(SP/'pub/cobra'),'HEAD'),
|
|
17
|
+
('tf-aws-vpc', str(SP/'pub/terraform-aws-vpc'),'HEAD'),
|
|
18
|
+
('tf-provider-random', str(SP/'pub/terraform-provider-random'),'HEAD'),
|
|
19
|
+
]
|
|
20
|
+
targets=[t for t in targets if t[2]]
|
|
21
|
+
targets.append(('visionhammer-pentest', str(pathlib.Path(os.environ.get('GV_PENTEST_CLONE','/nonexistent'))),'HEAD'))
|
|
22
|
+
wrap,mech=rgf.network_wrapper()
|
|
23
|
+
ok,why=rgf.verify_block(wrap,sys.executable)
|
|
24
|
+
print('network block',mech,ok,why)
|
|
25
|
+
EXTS={'.java':'java','.ts':'ts','.tsx':'ts','.js':'js','.jsx':'js','.mjs':'js','.py':'python','.go':'go','.tf':'terraform','.sql':'sql'}
|
|
26
|
+
res={}
|
|
27
|
+
for name,repo,ref in targets:
|
|
28
|
+
d=W/name
|
|
29
|
+
if d.exists(): shutil.rmtree(d)
|
|
30
|
+
d.mkdir(parents=True)
|
|
31
|
+
a=subprocess.Popen(['git','-C',repo,'archive',ref],stdout=subprocess.PIPE)
|
|
32
|
+
subprocess.run(['tar','-x','-C',str(d)],stdin=a.stdout,check=True); a.wait()
|
|
33
|
+
shutil.rmtree(d/'graphify-out',ignore_errors=True)
|
|
34
|
+
env=gc.clean_env(HOME,BIN)
|
|
35
|
+
audit=SP/('g10/audit-%s.tsv'%name)
|
|
36
|
+
r1=rgf.measure([G,'update','.'],d,env,wrapper=wrap,audit_file=audit)
|
|
37
|
+
g=gm.load_graph(d)
|
|
38
|
+
sig1=gm.signature(g)
|
|
39
|
+
# second cold
|
|
40
|
+
shutil.rmtree(d/'graphify-out')
|
|
41
|
+
r2=rgf.measure([G,'update','.'],d,env)
|
|
42
|
+
sig2=gm.signature(gm.load_graph(d))
|
|
43
|
+
# per-language coverage (own: includes sql), files with >=1 symbol
|
|
44
|
+
with_sym,_=gm.file_node_labels(g)
|
|
45
|
+
cnt=collections.defaultdict(lambda:[0,0])
|
|
46
|
+
for p in d.rglob('*'):
|
|
47
|
+
if p.is_file() and p.suffix in EXTS and 'node_modules' not in p.parts and 'graphify-out' not in p.parts and '.git' not in p.parts:
|
|
48
|
+
rel=str(p.relative_to(d)); lang=EXTS[p.suffix]
|
|
49
|
+
if gc.is_test_path(rel) and lang!='go': pass
|
|
50
|
+
cnt[lang][0]+=1
|
|
51
|
+
if rel in with_sym: cnt[lang][1]+=1
|
|
52
|
+
cov={k:{'files':v[0],'with_symbols':v[1],'pct':round(100*v[1]/v[0],1) if v[0] else None} for k,v in cnt.items()}
|
|
53
|
+
g2=gm.g2_edges(g)
|
|
54
|
+
rel=collections.Counter(e.get('relation') for e in gm.edges_of(g))
|
|
55
|
+
res[name]={'ref':ref,'cold':{k:r1[k] for k in ('exit_code','wall_s','peak_tree_rss_mb','max_child_rss_mb')},
|
|
56
|
+
'network_events':r1.get('audit'),'deterministic':sig1==sig2,'signature':sig1,'coverage_by_lang':cov,
|
|
57
|
+
'g1_harness':gm.g1_extraction(g,d),'inferred_pct':g2['inferred_pct'],'inferred_by_relation':g2['inferred_by_relation'],'relations_top':dict(rel.most_common(8))}
|
|
58
|
+
print(name, json.dumps({k:res[name][k] for k in ('cold','deterministic','coverage_by_lang','inferred_pct')}))
|
|
59
|
+
json.dump(res,open(SP/'g10/g10.json','w'),indent=1)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys, json, pathlib, os, copy, collections
|
|
3
|
+
SP=pathlib.Path(os.environ['GV_T4_ROOT'])
|
|
4
|
+
sys.path.insert(0,str(SP/'harness/tools/graphify-validation/lib'))
|
|
5
|
+
import spring_fixture as sf, graph_metrics as gm
|
|
6
|
+
wd=SP/'out/run1/work/spring-fixture'
|
|
7
|
+
g=gm.load_graph(wd)
|
|
8
|
+
exp=json.load(open(SP/'harness/tools/graphify-validation/fixtures/spring/expected.json'))
|
|
9
|
+
base=sf.check(g,exp)
|
|
10
|
+
print('baseline',{k:(v['found'],v['expected']) for k,v in base['groups'].items()})
|
|
11
|
+
def augment(g):
|
|
12
|
+
g=copy.deepcopy(g); es=gm.edges_of(g)
|
|
13
|
+
byid={n['id']:n for n in g['nodes']}
|
|
14
|
+
# class of a method node = node in same file with non-method label (first)
|
|
15
|
+
file_cls={}
|
|
16
|
+
for n in g['nodes']:
|
|
17
|
+
sfile=n.get('source_file'); lab=n.get('label','')
|
|
18
|
+
if sfile and not lab.startswith('.') and lab!=os.path.basename(sfile): file_cls.setdefault(sfile,n['id'])
|
|
19
|
+
impls=collections.defaultdict(list) # interface class id -> impl class ids
|
|
20
|
+
for e in es:
|
|
21
|
+
if e.get('relation') in ('implements','inherits','extends'): impls[e['target']].append(e['source'])
|
|
22
|
+
methods=collections.defaultdict(dict) # class id -> {label: id}
|
|
23
|
+
for n in g['nodes']:
|
|
24
|
+
lab=n.get('label','')
|
|
25
|
+
if lab.startswith('.') and n.get('source_file') in file_cls: methods[file_cls[n['source_file']]][lab]=n['id']
|
|
26
|
+
new=[]
|
|
27
|
+
for e in es:
|
|
28
|
+
if e.get('relation')!='calls': continue
|
|
29
|
+
t=byid.get(e['target']);
|
|
30
|
+
if not t or t.get('source_file') not in file_cls: continue
|
|
31
|
+
iface=file_cls[t['source_file']]
|
|
32
|
+
for c in impls.get(iface,[]):
|
|
33
|
+
m=methods[c].get(t['label'])
|
|
34
|
+
if m: new.append({**e,'target':m,'relation':'calls','confidence':'INFERRED','note':'via-interface (wrapper)'})
|
|
35
|
+
# field-type edges (references) + implements: class A references interface I => A reaches impl classes (depth)
|
|
36
|
+
for e in es:
|
|
37
|
+
if e.get('relation')=='references' and e['target'] in impls:
|
|
38
|
+
for c in impls[e['target']]: new.append({**e,'target':c,'relation':'references','confidence':'INFERRED'})
|
|
39
|
+
g['links']=es+new if 'links' in g else g.get('edges',[])+new
|
|
40
|
+
if 'edges' in g and 'links' not in g: g['edges']=es+new
|
|
41
|
+
return g,len(new)
|
|
42
|
+
g2,n=augment(g)
|
|
43
|
+
aug=sf.check(g2,exp)
|
|
44
|
+
print('augmented (+%d synthetic edges)'%n,{k:(v['found'],v['expected']) for k,v in aug['groups'].items()})
|
|
45
|
+
print('interface_impl pct',base['groups']['interface_impl']['pct'],'->',aug['groups']['interface_impl']['pct'])
|
|
46
|
+
for k,v in aug['groups'].items():
|
|
47
|
+
for it in v['items']:
|
|
48
|
+
if not it['found']: print(' still failing',k,it['id'],it['detail'])
|
|
49
|
+
json.dump({'baseline':{k:v['pct'] for k,v in base['groups'].items()},'augmented':{k:v['pct'] for k,v in aug['groups'].items()},'synthetic_edges':n},open(SP/'g10/g3aug.json','w'))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys, json, pathlib
|
|
3
|
+
SP=pathlib.Path(os.environ['GV_T4_ROOT'])
|
|
4
|
+
sys.path.insert(0,str(SP/'harness/tools/graphify-validation/lib'))
|
|
5
|
+
import recall as R, gv_common as gc
|
|
6
|
+
import os
|
|
7
|
+
os.environ['RG']=str(SP/'bin/rg')
|
|
8
|
+
Q=json.load(open(SP/'harness/tools/graphify-validation/config/questions.json'))
|
|
9
|
+
env=gc.clean_env(SP/'g10/home',str(SP/'out/run1/venv/bin'))
|
|
10
|
+
G=str(SP/'out/run1/venv/bin/graphify')
|
|
11
|
+
def rrf(*ranks,k=60):
|
|
12
|
+
s={}
|
|
13
|
+
for r in ranks:
|
|
14
|
+
for i,f in enumerate(R.clean(r)): s[f]=s.get(f,0)+1/(k+i+1)
|
|
15
|
+
return [f for f,_ in sorted(s.items(),key=lambda kv:-kv[1])]
|
|
16
|
+
def interleave(a,b):
|
|
17
|
+
out=[]
|
|
18
|
+
for x,y in zip(R.clean(a),R.clean(b)):
|
|
19
|
+
for z in (y,x):
|
|
20
|
+
if z not in out: out.append(z)
|
|
21
|
+
return out + [f for f in R.clean(a)+R.clean(b) if f not in out]
|
|
22
|
+
res={}
|
|
23
|
+
for repo,wd in [('vh-api-gateway',SP/'out/run1/work/vh-api-gateway'),('visionhammer-agents',SP/'out/run1/work/visionhammer-agents'),('deepdebug-local-agent',SP/'out/run1/work/deepdebug-local-agent')]:
|
|
24
|
+
rows=[]
|
|
25
|
+
for q in Q[repo]:
|
|
26
|
+
g,_=R.rank_graph(G,wd,q['question'],env)
|
|
27
|
+
l=R.rank_lexical(wd,R.auto_keywords(q['question']),whole_word=False,ignore_case=True)
|
|
28
|
+
rows.append({'kind':q['kind'],'graph':R.recall_at(g,q['truth'])[0],'lex':R.recall_at(l,q['truth'])[0],
|
|
29
|
+
'rrf':R.recall_at(rrf(g,l),q['truth'])[0],'inter':R.recall_at(interleave(l,g),q['truth'])[0]})
|
|
30
|
+
def avg(sel,k): return round(100*sum(r[k] for r in sel)/len(sel),1) if sel else None
|
|
31
|
+
res[repo]={kind:{k:avg([r for r in rows if kind=='all' or r['kind']==kind],k) for k in ('graph','lex','rrf','inter')}|{'n':len([r for r in rows if kind=='all' or r['kind']==kind])} for kind in ('all','symbolic','conceptual')}
|
|
32
|
+
print(repo,json.dumps(res[repo]))
|
|
33
|
+
tot=[];
|
|
34
|
+
json.dump(res,open(SP/'g10/hybrid.json','w'),indent=1)
|