@bongos/core 1.19.201 → 1.19.203
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/.bongos-core.json +18 -13
- package/modules/security/artifact-scan/floor.js +57 -3
- package/modules/security/tests/floor.mjs +37 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/capture.js +15 -1
- package/src/module-api.js +5 -1
- package/tests/capture_error_format.mjs +40 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.203",
|
|
6
|
+
"core_contract": "1.19.203",
|
|
7
|
+
"source_commit": "3596d383ae4955438fc6ab7f9599e680ddafb570",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-17T19:
|
|
9
|
+
"built_at": "2026-08-17T19:20:16.116Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 310,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1295,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1626,
|
|
20
|
+
"tree_sha256": "d4a2fd8d4540f54177859f2312a5a48c6539b34b12e1e61757d0b242f92be615",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4072,7 +4072,7 @@
|
|
|
4072
4072
|
{
|
|
4073
4073
|
"path": "modules/security/artifact-scan/floor.js",
|
|
4074
4074
|
"mode": "0000644",
|
|
4075
|
-
"sha256": "
|
|
4075
|
+
"sha256": "27421162e08ed14ba9e26be02e11c936c566ad113b76a90d452474d7e867093b"
|
|
4076
4076
|
},
|
|
4077
4077
|
{
|
|
4078
4078
|
"path": "modules/security/artifact-scan/limits.js",
|
|
@@ -4157,7 +4157,7 @@
|
|
|
4157
4157
|
{
|
|
4158
4158
|
"path": "modules/security/tests/floor.mjs",
|
|
4159
4159
|
"mode": "0000644",
|
|
4160
|
-
"sha256": "
|
|
4160
|
+
"sha256": "7eaed4b59cce956509ca7ebcb92207d362f191ec9b54be90d3c47d069b8149e7"
|
|
4161
4161
|
},
|
|
4162
4162
|
{
|
|
4163
4163
|
"path": "modules/security/tests/mirror.mjs",
|
|
@@ -4272,12 +4272,12 @@
|
|
|
4272
4272
|
{
|
|
4273
4273
|
"path": "package-lock.json",
|
|
4274
4274
|
"mode": "0000644",
|
|
4275
|
-
"sha256": "
|
|
4275
|
+
"sha256": "30f113e605eea7d404a3393045402c6234a6822724656d0830a5a553a6215fac"
|
|
4276
4276
|
},
|
|
4277
4277
|
{
|
|
4278
4278
|
"path": "package.json",
|
|
4279
4279
|
"mode": "0000644",
|
|
4280
|
-
"sha256": "
|
|
4280
|
+
"sha256": "fa8341a0a90d43e710d3f9e04589ce7f055c8f180cf915f28fa074b0b9206977"
|
|
4281
4281
|
},
|
|
4282
4282
|
{
|
|
4283
4283
|
"path": "public-docs/index.html",
|
|
@@ -4727,7 +4727,7 @@
|
|
|
4727
4727
|
{
|
|
4728
4728
|
"path": "scripts/gds/capture.js",
|
|
4729
4729
|
"mode": "0000644",
|
|
4730
|
-
"sha256": "
|
|
4730
|
+
"sha256": "208bfd6c6a73f4234c06bb26e3319ec43ecf865927333bc810ca99efa9745e90"
|
|
4731
4731
|
},
|
|
4732
4732
|
{
|
|
4733
4733
|
"path": "scripts/gds/cf-tunnel.js",
|
|
@@ -5887,7 +5887,7 @@
|
|
|
5887
5887
|
{
|
|
5888
5888
|
"path": "src/module-api.js",
|
|
5889
5889
|
"mode": "0000644",
|
|
5890
|
-
"sha256": "
|
|
5890
|
+
"sha256": "f5b425d9be5b552f40d72f31cd96dbcb901b15478ed7a60a35221aaeb09b4fb7"
|
|
5891
5891
|
},
|
|
5892
5892
|
{
|
|
5893
5893
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6289,6 +6289,11 @@
|
|
|
6289
6289
|
"mode": "0000644",
|
|
6290
6290
|
"sha256": "5cd314c415b3e4e0b89a762cd622d9b0eb58e3659df76e78c816ca501368febf"
|
|
6291
6291
|
},
|
|
6292
|
+
{
|
|
6293
|
+
"path": "tests/capture_error_format.mjs",
|
|
6294
|
+
"mode": "0000644",
|
|
6295
|
+
"sha256": "bf8b5407d5e1234a74399f3a1e80acc44f9fabdfcfbccc3d64ca57d66f8e3085"
|
|
6296
|
+
},
|
|
6292
6297
|
{
|
|
6293
6298
|
"path": "tests/cascade-dispatch.mjs",
|
|
6294
6299
|
"mode": "0000644",
|
|
@@ -191,6 +191,54 @@ function coverageFindings(corpus) {
|
|
|
191
191
|
return findings;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// Cap the merged-path list so an artifact vendoring thousands of identical
|
|
195
|
+
// copies cannot balloon the persisted report through its own duplication.
|
|
196
|
+
const MAX_DUPLICATE_PATHS = 20;
|
|
197
|
+
|
|
198
|
+
// A repo that vendors the same tree N times produces N identical findings from
|
|
199
|
+
// one underlying file (same detector, same evidence, byte-identical content) —
|
|
200
|
+
// which triples counts and buries the real signal. Collapse findings that agree
|
|
201
|
+
// on (code, severity, message) AND whose files are byte-identical per the
|
|
202
|
+
// manifest sha256; the survivor lists the other copies. Keyed on the full
|
|
203
|
+
// message so per-path evidence (differing line numbers, labels) never merges
|
|
204
|
+
// wrongly, and findings without a manifest file entry ('(floor)', '(fetch)',
|
|
205
|
+
// directories, symlinks) pass through untouched. Verdict arithmetic is
|
|
206
|
+
// unaffected: max() over findings is idempotent under duplication.
|
|
207
|
+
function dedupeFindings(findings, entries) {
|
|
208
|
+
const shaByPath = new Map();
|
|
209
|
+
for (const e of entries || []) {
|
|
210
|
+
if (e && e.kind === 'file' && e.sha256) shaByPath.set(scrubUntrusted(String(e.path)), e.sha256);
|
|
211
|
+
}
|
|
212
|
+
const groups = new Map();
|
|
213
|
+
const slots = [];
|
|
214
|
+
for (const f of findings) {
|
|
215
|
+
const sha = f && typeof f.file === 'string' ? shaByPath.get(f.file) : null;
|
|
216
|
+
if (!sha) { slots.push({ finding: f }); continue; }
|
|
217
|
+
const key = JSON.stringify([f.code, f.severity, f.message, sha]);
|
|
218
|
+
const group = groups.get(key);
|
|
219
|
+
if (!group) {
|
|
220
|
+
groups.set(key, { first: f, dups: [] });
|
|
221
|
+
slots.push({ key });
|
|
222
|
+
} else {
|
|
223
|
+
group.dups.push(f.file);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return slots.map((slot) => {
|
|
227
|
+
if (slot.finding) return slot.finding;
|
|
228
|
+
const { first, dups } = groups.get(slot.key);
|
|
229
|
+
if (dups.length === 0) return first;
|
|
230
|
+
const baseDetail = first.detail && typeof first.detail === 'object' && !Array.isArray(first.detail) ? first.detail : {};
|
|
231
|
+
return {
|
|
232
|
+
...first,
|
|
233
|
+
detail: {
|
|
234
|
+
...baseDetail,
|
|
235
|
+
duplicate_files: dups.slice(0, MAX_DUPLICATE_PATHS),
|
|
236
|
+
duplicate_count: dups.length,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
194
242
|
// Run every detector crash-isolated: a detector that throws yields a finding,
|
|
195
243
|
// never a hole in the report. `detectors` is injectable so the crash-isolation
|
|
196
244
|
// contract is unit-testable with a deliberately-throwing detector.
|
|
@@ -273,15 +321,20 @@ async function runFloor({ pristineDir, quarantineRoot, entries, sha = null, cont
|
|
|
273
321
|
'The floor could not read the artifact tree at all — nothing here was reviewed.', { error: corpusError }));
|
|
274
322
|
}
|
|
275
323
|
|
|
324
|
+
// Collapse vendored-copy duplicates LAST, so every producing step above is
|
|
325
|
+
// covered; crash-isolated with the raw list as fallback (a broken dedupe must
|
|
326
|
+
// degrade to duplicate noise, never to lost findings).
|
|
327
|
+
const finalFindings = isolate('dedupe', () => dedupeFindings(findings, entries)) || findings;
|
|
328
|
+
|
|
276
329
|
const report = {
|
|
277
330
|
floor_version: FLOOR_VERSION,
|
|
278
331
|
detectors_run: DETECTORS.map(([name]) => name),
|
|
279
332
|
detector_errors: errors,
|
|
280
333
|
generated_at: new Date().toISOString(),
|
|
281
334
|
target: { sha, content_hash: contentHash },
|
|
282
|
-
findings,
|
|
283
|
-
counts: countBySeverity(
|
|
284
|
-
floor_verdict: computeFloorVerdict(
|
|
335
|
+
findings: finalFindings,
|
|
336
|
+
counts: countBySeverity(finalFindings),
|
|
337
|
+
floor_verdict: computeFloorVerdict(finalFindings),
|
|
285
338
|
// Printed, never scored — the reader's power budget for the artifact.
|
|
286
339
|
capabilities: capabilityInventory,
|
|
287
340
|
coverage: {
|
|
@@ -314,6 +367,7 @@ module.exports = {
|
|
|
314
367
|
countBySeverity,
|
|
315
368
|
coOccurrence,
|
|
316
369
|
coverageFindings,
|
|
370
|
+
dedupeFindings,
|
|
317
371
|
runDetectors,
|
|
318
372
|
runFloor,
|
|
319
373
|
};
|
|
@@ -58,6 +58,43 @@ function corpusOf(fileMap) {
|
|
|
58
58
|
const codesOf = (findings) => findings.map((f) => f.code);
|
|
59
59
|
const hasCode = (findings, code) => findings.some((f) => f.code === code);
|
|
60
60
|
|
|
61
|
+
// ---------- dedupe across byte-identical files ----------
|
|
62
|
+
|
|
63
|
+
await test('dedupe: identical findings on byte-identical files collapse to one, listing the copies', () => {
|
|
64
|
+
const entries = [
|
|
65
|
+
{ path: 'a/data.csv', kind: 'file', size: 10, sha256: 'aaa' },
|
|
66
|
+
{ path: 'b/data.csv', kind: 'file', size: 10, sha256: 'aaa' },
|
|
67
|
+
{ path: 'c/data.csv', kind: 'file', size: 10, sha256: 'aaa' },
|
|
68
|
+
{ path: 'd/other.csv', kind: 'file', size: 10, sha256: 'bbb' },
|
|
69
|
+
];
|
|
70
|
+
const mk = (file) => ({ code: 'x_hit', severity: 'critical', file, message: 'same claim', detail: { k: 1 } });
|
|
71
|
+
const input = [mk('a/data.csv'), mk('b/data.csv'), mk('c/data.csv'), mk('d/other.csv'),
|
|
72
|
+
{ code: 'detector_error', severity: 'high', file: '(floor)', message: 'crashed', detail: null }];
|
|
73
|
+
const out = floor.dedupeFindings(input, entries);
|
|
74
|
+
// vendored copies collapse; different bytes (d/) and non-file findings survive
|
|
75
|
+
assert.equal(out.length, 3);
|
|
76
|
+
const merged = out.find((f) => f.file === 'a/data.csv');
|
|
77
|
+
assert.deepEqual(merged.detail.duplicate_files, ['b/data.csv', 'c/data.csv']);
|
|
78
|
+
assert.equal(merged.detail.duplicate_count, 2);
|
|
79
|
+
assert.equal(merged.detail.k, 1, 'original detail keys survive the merge');
|
|
80
|
+
assert.ok(out.some((f) => f.file === 'd/other.csv'));
|
|
81
|
+
assert.ok(out.some((f) => f.file === '(floor)'));
|
|
82
|
+
// input findings were not mutated (the merged copy is a new object)
|
|
83
|
+
assert.equal(input[0].detail.duplicate_count, undefined);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await test('dedupe: same file bytes but different messages stay separate findings', () => {
|
|
87
|
+
const entries = [
|
|
88
|
+
{ path: 'a.js', kind: 'file', size: 5, sha256: 'ccc' },
|
|
89
|
+
{ path: 'b.js', kind: 'file', size: 5, sha256: 'ccc' },
|
|
90
|
+
];
|
|
91
|
+
const out = floor.dedupeFindings([
|
|
92
|
+
{ code: 'y', severity: 'medium', file: 'a.js', message: 'claim at line 3', detail: null },
|
|
93
|
+
{ code: 'y', severity: 'medium', file: 'b.js', message: 'claim at line 9', detail: null },
|
|
94
|
+
], entries);
|
|
95
|
+
assert.equal(out.length, 2);
|
|
96
|
+
});
|
|
97
|
+
|
|
61
98
|
// ---------- npm lifecycle ----------
|
|
62
99
|
|
|
63
100
|
await test('npm-lifecycle: quotes a postinstall verbatim as a high finding', () => {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.203",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.203",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.203",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/capture.js
CHANGED
|
@@ -47,6 +47,18 @@ async function resolveDefaultVersion() {
|
|
|
47
47
|
return building[0].id;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Full JSON, not console.error's depth-2 inspect — a validation 400's nested
|
|
51
|
+
// details otherwise print as "[ [Object] ]" and hide which field failed.
|
|
52
|
+
function formatCaptureFailure(status, data) {
|
|
53
|
+
let rendered;
|
|
54
|
+
try {
|
|
55
|
+
rendered = JSON.stringify(data, null, 2);
|
|
56
|
+
} catch {
|
|
57
|
+
rendered = String(data);
|
|
58
|
+
}
|
|
59
|
+
return `capture failed (${status}): ${rendered}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
async function main() {
|
|
51
63
|
await requireSession();
|
|
52
64
|
const args = process.argv.slice(2);
|
|
@@ -94,7 +106,7 @@ async function main() {
|
|
|
94
106
|
const api = await cliClient();
|
|
95
107
|
const r = await api.inbox.postInbox({ body: payload });
|
|
96
108
|
if (!r.ok) {
|
|
97
|
-
console.error(
|
|
109
|
+
console.error(formatCaptureFailure(r.status, r.data));
|
|
98
110
|
process.exit(1);
|
|
99
111
|
}
|
|
100
112
|
|
|
@@ -115,6 +127,8 @@ async function main() {
|
|
|
115
127
|
// referred to; promotion to a task re-runs the detection automatically.
|
|
116
128
|
}
|
|
117
129
|
|
|
130
|
+
module.exports = { resolveDefaultVersion, formatCaptureFailure };
|
|
131
|
+
|
|
118
132
|
if (require.main === module) {
|
|
119
133
|
main().catch((err) => {
|
|
120
134
|
console.error('fatal:', err);
|
package/src/module-api.js
CHANGED
|
@@ -829,8 +829,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
829
829
|
// landed since 1.19.199 with no explicit bump. run 32058296632. (task 1002620)
|
|
830
830
|
// 1.19.201 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
831
831
|
// landed since 1.19.200 with no explicit bump. run 32058666256. (task 1002620)
|
|
832
|
+
// 1.19.202 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
833
|
+
// landed since 1.19.201 with no explicit bump. run 32059276345. (task 1002620)
|
|
834
|
+
// 1.19.203 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
835
|
+
// landed since 1.19.202 with no explicit bump. run 32059705946. (task 1002620)
|
|
832
836
|
// ---------------------------------------------------------------------------
|
|
833
|
-
const CORE_VERSION = '1.19.
|
|
837
|
+
const CORE_VERSION = '1.19.203'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
834
838
|
|
|
835
839
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
836
840
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// capture.js error formatting (task 1003056, idea 1000780): a 400 from POST
|
|
2
|
+
// /inbox must print the nested validation details, not util.inspect's
|
|
3
|
+
// depth-clipped "[ [Object] ]" — the caller needs to see WHICH field failed.
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { formatCaptureFailure } = require('../scripts/gds/capture.js');
|
|
9
|
+
|
|
10
|
+
let passed = 0;
|
|
11
|
+
let failed = 0;
|
|
12
|
+
async function test(name, fn) {
|
|
13
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
14
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
await test('nested validation details are fully rendered, never [Object]', () => {
|
|
18
|
+
const out = formatCaptureFailure(400, {
|
|
19
|
+
error: {
|
|
20
|
+
code: 'validation_failed',
|
|
21
|
+
message: 'validation_failed',
|
|
22
|
+
details: [{ field: 'title', reason: 'exceeds max length 200' }],
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
assert.match(out, /capture failed \(400\)/);
|
|
26
|
+
assert.match(out, /exceeds max length 200/);
|
|
27
|
+
assert.match(out, /"field": "title"/);
|
|
28
|
+
assert.doesNotMatch(out, /\[Object\]/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await test('non-JSON-serializable data still produces a printable line', () => {
|
|
32
|
+
const circular = {};
|
|
33
|
+
circular.self = circular;
|
|
34
|
+
const out = formatCaptureFailure(500, circular);
|
|
35
|
+
assert.match(out, /capture failed \(500\)/);
|
|
36
|
+
assert.equal(typeof out, 'string');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
console.log(`\ncapture_error_format: ${passed} passed, ${failed} failed`);
|
|
40
|
+
if (failed > 0) process.exit(1);
|