kodelyth-ecc 2.24.7 → 2.24.8
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/CHANGELOG.md +82 -0
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/bin/kodelyth-ecc.js +9 -3
- package/package.json +1 -1
- package/scripts/dashboard/data.js +28 -2
- package/scripts/lib/tmux-worktree-orchestrator.js +18 -0
- package/scripts/supply-chain/verify.js +19 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Kodelyth ECC are documented here.
|
|
4
4
|
|
|
5
|
+
## v2.24.8 — adversarial pass: swarm, dashboard, supply-chain (October 2026)
|
|
6
|
+
|
|
7
|
+
Three defects found by attacking the remaining unprobed surfaces rather than
|
|
8
|
+
reading them.
|
|
9
|
+
|
|
10
|
+
### `baseRef` could reach git's argv as an option
|
|
11
|
+
|
|
12
|
+
Every spawn in the swarm orchestrator uses `spawnSync`'s array form with no shell,
|
|
13
|
+
so shell injection does not work — and that was tested, not assumed: every
|
|
14
|
+
generated `send-keys` string was executed in a real shell with a payload that
|
|
15
|
+
would touch a marker file, across sessionName, worker name, task, repoRoot and
|
|
16
|
+
baseRef. No marker was ever created.
|
|
17
|
+
|
|
18
|
+
Array form does not stop **argument** injection, though. git parses its own argv,
|
|
19
|
+
so in `git worktree add -b <branch> <path> <commit-ish>` a commit-ish beginning
|
|
20
|
+
with `-` is read as an option: `--git-dir=/elsewhere` points git at another
|
|
21
|
+
repository, `--force` changes the command's meaning. `baseRef` was the only value
|
|
22
|
+
reaching argv without being slugified or path-resolved first — sessionName goes
|
|
23
|
+
through `slugify`, branchName carries a literal prefix, worktreePath is absolute —
|
|
24
|
+
and it is user-settable through `--base-ref`. Swarm configs get shared, so this is
|
|
25
|
+
not purely self-inflicted.
|
|
26
|
+
|
|
27
|
+
Rejecting a leading `-` costs nothing: `git check-ref-format` forbids refs that
|
|
28
|
+
start with one, so `HEAD`, `main`, `v1.2.3` and `origin/main` all still work.
|
|
29
|
+
|
|
30
|
+
### An 800-character excerpt read the entire file
|
|
31
|
+
|
|
32
|
+
The dashboard's `safeReadExcerpt` called `readFileSync` on the whole file and then
|
|
33
|
+
sliced 800 characters off the front, so a fixed-size excerpt cost the full size of
|
|
34
|
+
the file. Measured on a 300MB `task.md`: **196ms and +628MB RSS** for 800
|
|
35
|
+
characters, scaling linearly — roughly twice the file, since the bytes land in a
|
|
36
|
+
Buffer and again as a UTF-16 string.
|
|
37
|
+
|
|
38
|
+
The dashboard is single threaded, so that blocks every other request, and it runs
|
|
39
|
+
three times per worker — task, handoff, status — across every session in
|
|
40
|
+
`.orchestration`. Those files are written by swarm workers, so their size is not
|
|
41
|
+
something the dashboard controls.
|
|
42
|
+
|
|
43
|
+
It now reads a bounded window: stat first, then `max * 4 + 4` bytes. Four because
|
|
44
|
+
UTF-8 encodes a character in at most four, so the window always holds at least
|
|
45
|
+
`max` characters. That detail is load bearing — the truncation decision counts
|
|
46
|
+
CHARACTERS, as it always did, so slicing a byte window would cut a multi-byte
|
|
47
|
+
character in half. 300MB now costs 0ms and +0MB. Output was diffed against the old
|
|
48
|
+
implementation across twelve cases including emoji on the boundary; all identical.
|
|
49
|
+
|
|
50
|
+
### `verify` passed with a planted file
|
|
51
|
+
|
|
52
|
+
An EXTRA file — present on disk, absent from the manifest — was advisory:
|
|
53
|
+
`ok: true`, exit code 0. Confirmed against the real CLI, where tampering and
|
|
54
|
+
deletion correctly exit 1 but planting `rules/common/zz-injected.md` exits 0.
|
|
55
|
+
|
|
56
|
+
The human output did say `⚠ extra: 1 (advisory)`. Automation did not hear it:
|
|
57
|
+
`kodelyth-ecc verify && deploy` passed with an unknown file in place — and in this
|
|
58
|
+
product an extra `.md` under `rules/`, `agents/` or `commands/` is not inert data.
|
|
59
|
+
Those directories are enumerated and loaded as instructions, so an injected rule
|
|
60
|
+
file becomes an always-on rule in every future session. That is the payload the
|
|
61
|
+
session-bundle traversal delivered in 2.24.5, reached a different way.
|
|
62
|
+
|
|
63
|
+
The default is deliberately unchanged. This is a toolkit people are meant to
|
|
64
|
+
extend, and failing verify on extras would fail it forever for anyone who added
|
|
65
|
+
their own agent — a check that always fails gets ignored. The choice is offered
|
|
66
|
+
instead:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
kodelyth-ecc verify # extras advisory, exit 0 (unchanged)
|
|
70
|
+
kodelyth-ecc verify --strict # extras fail the verdict, exit 1
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The report now carries `strict`, so a `--json` consumer need not infer which mode
|
|
74
|
+
produced the verdict.
|
|
75
|
+
|
|
76
|
+
### What held up
|
|
77
|
+
|
|
78
|
+
`scripts/evolve` was already hardened, and its comment records the same traversal
|
|
79
|
+
class found in replay along with how it was confirmed. Re-tested anyway: target
|
|
80
|
+
path traversal blocked, legitimate paths written, no prototype pollution. Swarm
|
|
81
|
+
worker names collapse through `slugify` rather than escaping the worktree root,
|
|
82
|
+
and `normalizeSeedPaths` rejects `..`, absolute paths and embedded traversal —
|
|
83
|
+
both now pinned by tests so a later change cannot quietly lose them.
|
|
84
|
+
|
|
85
|
+
**682 tests across 43 files.**
|
|
86
|
+
|
|
5
87
|
## v2.24.7 — the brand render scripts could not run (October 2026)
|
|
6
88
|
|
|
7
89
|
Developer tooling only. Nothing here ships in the npm package — `brand/` is not in
|
package/CLAUDE.md
CHANGED
|
@@ -26,7 +26,7 @@ scripts/ → Node.js utilities: MCP server, dashboard, swarm, replay, router
|
|
|
26
26
|
bundles/ → 3 power bundles (indie-hacker, red-team, enterprise)
|
|
27
27
|
actions/ → GitHub Action (CI/CD integration for PR review)
|
|
28
28
|
docs/ → Feature docs (arena.md, mcp.md, dashboard.md, swarm.md, replay.md, evolve.md, supply-chain.md)
|
|
29
|
-
tests/ →
|
|
29
|
+
tests/ → 682 passing tests across 43 test files
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Running Tests
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ Most "AI agent kits" are folders of markdown files you have to remember the name
|
|
|
118
118
|
| Quality hooks | ✓ 44 | Some | ✗ | ✗ |
|
|
119
119
|
| IDE platforms | **11** (Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, Roo Code, Aider, Kimi, Gemini CLI) | 1-2 | 1 | Varies |
|
|
120
120
|
| Telemetry | **None** | Varies | None | Varies |
|
|
121
|
-
| Test coverage | ✓
|
|
121
|
+
| Test coverage | ✓ 682 tests | ✗ | ✗ | ✗ |
|
|
122
122
|
| Distributed via | `npx`, curl, clone | Manual | Manual | Manual |
|
|
123
123
|
|
|
124
124
|
---
|
package/bin/kodelyth-ecc.js
CHANGED
|
@@ -1288,7 +1288,7 @@ if (args[0] === 'session-export' || args[0] === 'session-import' || args[0] ===
|
|
|
1288
1288
|
// Usage:
|
|
1289
1289
|
// kodelyth-ecc sbom [--root DIR] [--out FILE] [--json]
|
|
1290
1290
|
// kodelyth-ecc manifest [--root DIR] [--out FILE] [--json]
|
|
1291
|
-
// kodelyth-ecc verify [--root DIR] [--manifest FILE] [--json]
|
|
1291
|
+
// kodelyth-ecc verify [--root DIR] [--manifest FILE] [--json] [--strict]
|
|
1292
1292
|
if (args[0] === 'sbom' || args[0] === 'manifest' || args[0] === 'verify') {
|
|
1293
1293
|
(async () => {
|
|
1294
1294
|
const fs = require('fs');
|
|
@@ -1355,7 +1355,12 @@ if (args[0] === 'sbom' || args[0] === 'manifest' || args[0] === 'verify') {
|
|
|
1355
1355
|
throw new Error(`verify: manifest not found at ${manifestPath} (use --manifest FILE)`);
|
|
1356
1356
|
}
|
|
1357
1357
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
1358
|
-
|
|
1358
|
+
// --strict folds EXTRA files into the verdict. Off by default, because a
|
|
1359
|
+
// toolkit people extend would otherwise fail verify for everyone who added
|
|
1360
|
+
// their own agent; on for automation, where an unknown file under rules/ or
|
|
1361
|
+
// agents/ is loaded as instructions and should stop a deploy.
|
|
1362
|
+
const strict = has('--strict');
|
|
1363
|
+
const report = verifyAgainstManifest({ rootDir, manifest, strict });
|
|
1359
1364
|
|
|
1360
1365
|
if (has('--json')) {
|
|
1361
1366
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -1370,7 +1375,7 @@ if (args[0] === 'sbom' || args[0] === 'manifest' || args[0] === 'verify') {
|
|
|
1370
1375
|
console.log(` ✓ ok: ${s.ok}`);
|
|
1371
1376
|
console.log(` ✗ modified: ${s.modified}`);
|
|
1372
1377
|
console.log(` ✗ missing: ${s.missing}`);
|
|
1373
|
-
console.log(` ⚠ extra: ${s.extra} (advisory)`);
|
|
1378
|
+
console.log(` ⚠ extra: ${s.extra} ${strict ? '(fails verify: --strict)' : '(advisory — use --strict to fail on these)'}`);
|
|
1374
1379
|
|
|
1375
1380
|
if (report.details.modified.length) {
|
|
1376
1381
|
console.log(`\nModified files:`);
|
|
@@ -1724,6 +1729,7 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
1724
1729
|
npx kodelyth-ecc sbom [--out file.json] Emit a CycloneDX 1.5 SBOM for the package
|
|
1725
1730
|
npx kodelyth-ecc manifest [--out file.json] Emit a sha256 content manifest of all assets
|
|
1726
1731
|
npx kodelyth-ecc verify [--manifest file] Verify installed copy against a content manifest
|
|
1732
|
+
npx kodelyth-ecc verify --strict ...and fail on unexpected extra files too
|
|
1727
1733
|
npx kodelyth-ecc evolve analyze Generate skill / routing proposals from memory + miss signals
|
|
1728
1734
|
npx kodelyth-ecc dashboard Boot localhost-only observability dashboard (zero telemetry)
|
|
1729
1735
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodelyth-ecc",
|
|
3
|
-
"version": "2.24.
|
|
3
|
+
"version": "2.24.8",
|
|
4
4
|
"description": "Production-grade AI coding toolkit — 70 agents (incl. devil-mode adversarial crew), 196 skills, 103 commands, parallel multi-agent commands, semantic intent routing, self-learning memory, and a built-in MCP server (16 tools / 6 prompts / 377 resources) that bridges to Claude Desktop, LangGraph, AutoGen, CrewAI, and OpenAI Agents SDK. Works on macOS, Linux and Windows with Claude Code, Windsurf, Cursor, Codex, Antigravity, OpenCode, Cline, RooCode, Aider, Kimi, and Gemini CLI.",
|
|
5
5
|
"author": "Kodelyth <github.com/sifxprime>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -335,10 +335,36 @@ function sessionDetail({ session, coordRoot = defaultCoordRoot() } = {}) {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
function safeReadExcerpt(p, max = 800) {
|
|
338
|
+
// Reads only the window it needs. This used to readFileSync the WHOLE file and
|
|
339
|
+
// then slice 800 characters off the front, which meant a large file in a
|
|
340
|
+
// coordination directory cost its full size in time and memory to produce a
|
|
341
|
+
// fixed-size excerpt. Measured: a 300MB task.md took 693ms and +733MB RSS to
|
|
342
|
+
// return 800 characters, scaling linearly — and the dashboard is single
|
|
343
|
+
// threaded, so that blocks every other request. It is called three times per
|
|
344
|
+
// worker (task, handoff, status) across every session, so the cost multiplies.
|
|
345
|
+
//
|
|
346
|
+
// The window is max*4 bytes because UTF-8 encodes a character in at most four,
|
|
347
|
+
// so it always contains at least `max` characters when the file has them. That
|
|
348
|
+
// matters: the truncation decision below counts CHARACTERS, as it always did,
|
|
349
|
+
// so slicing a byte window would otherwise cut a multi-byte character in half
|
|
350
|
+
// and change the output for any non-ASCII file.
|
|
351
|
+
let fd;
|
|
338
352
|
try {
|
|
339
|
-
const
|
|
353
|
+
const st = fs.statSync(p);
|
|
354
|
+
if (!st.isFile()) return null;
|
|
355
|
+
const window = Math.min(st.size, max * 4 + 4);
|
|
356
|
+
const buf = Buffer.alloc(window);
|
|
357
|
+
fd = fs.openSync(p, 'r');
|
|
358
|
+
const bytesRead = fs.readSync(fd, buf, 0, window, 0);
|
|
359
|
+
const content = buf.subarray(0, bytesRead).toString('utf8');
|
|
340
360
|
return content.length > max ? content.slice(0, max) + '\n…(truncated)' : content;
|
|
341
|
-
} catch {
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
} finally {
|
|
364
|
+
if (fd !== undefined) {
|
|
365
|
+
try { fs.closeSync(fd); } catch { /* already gone */ }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
342
368
|
}
|
|
343
369
|
|
|
344
370
|
// ── IDE sessions (Claude Code / Windsurf / Antigravity) ──────────────────────
|
|
@@ -183,7 +183,25 @@ function buildOrchestrationPlan(config = {}) {
|
|
|
183
183
|
config.coordinationRoot || path.join(repoRoot, '.orchestration')
|
|
184
184
|
);
|
|
185
185
|
const coordinationDir = path.join(coordinationRoot, sessionName);
|
|
186
|
+
// baseRef is the only value in this file that reaches a command's argv without
|
|
187
|
+
// being slugified or path-resolved first. Every spawn here uses the array form
|
|
188
|
+
// with no shell, which stops shell injection — but it does NOT stop ARGUMENT
|
|
189
|
+
// injection: git parses argv itself, so a ref beginning with "-" is read as an
|
|
190
|
+
// option rather than a commit-ish. In
|
|
191
|
+
// git worktree add -b <branch> <path> <commit-ish>
|
|
192
|
+
// a baseRef of "--git-dir=/elsewhere" silently redirects git at another
|
|
193
|
+
// repository, and "--force" changes the command's meaning. It is user-settable
|
|
194
|
+
// through --base-ref, and swarm configs get shared, so it is not only
|
|
195
|
+
// self-inflicted.
|
|
196
|
+
//
|
|
197
|
+
// Rejecting a leading "-" is enough: git refs cannot begin with one anyway
|
|
198
|
+
// (git check-ref-format forbids it), so nothing legitimate is lost.
|
|
186
199
|
const baseRef = config.baseRef || 'HEAD';
|
|
200
|
+
if (typeof baseRef !== 'string' || baseRef.startsWith('-')) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`baseRef must be a ref name, not an option: ${JSON.stringify(String(baseRef))}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
187
205
|
const defaultLauncher = config.launcherCommand || '';
|
|
188
206
|
|
|
189
207
|
if (workers.length === 0) {
|
|
@@ -23,7 +23,7 @@ const { hashFile, walkFiles, MANIFEST_SCHEMA } = require('./manifest.js');
|
|
|
23
23
|
|
|
24
24
|
const VERIFY_RESULT_KEYS = ['ok', 'modified', 'missing', 'extra'];
|
|
25
25
|
|
|
26
|
-
function verifyAgainstManifest({ rootDir, manifest } = {}) {
|
|
26
|
+
function verifyAgainstManifest({ rootDir, manifest, strict = false } = {}) {
|
|
27
27
|
if (!rootDir) throw new Error('verifyAgainstManifest: rootDir is required');
|
|
28
28
|
if (!manifest) throw new Error('verifyAgainstManifest: manifest is required');
|
|
29
29
|
if (manifest.schema !== MANIFEST_SCHEMA) {
|
|
@@ -81,11 +81,27 @@ function verifyAgainstManifest({ rootDir, manifest } = {}) {
|
|
|
81
81
|
extra: extraFiles.length,
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
//
|
|
85
|
-
|
|
84
|
+
// By default an EXTRA file is advisory: it does not fail the verdict. That is
|
|
85
|
+
// deliberate and correct for a toolkit people are meant to extend — anyone who
|
|
86
|
+
// has added their own agent or skill would otherwise see verify fail forever.
|
|
87
|
+
//
|
|
88
|
+
// It is the wrong default for automation, though. A planted file returns ok
|
|
89
|
+
// true and exit code 0, so `verify && deploy` passes with an unknown file in
|
|
90
|
+
// place — and in this product an extra .md under rules/, agents/ or commands/
|
|
91
|
+
// is not inert data, it is loaded as instructions, so an injected
|
|
92
|
+
// rules/common/zz.md becomes an always-on rule. Confirmed: a clean tree reports
|
|
93
|
+
// ok true, planting that file still reports ok true with extra 1.
|
|
94
|
+
//
|
|
95
|
+
// So the choice is offered rather than made. strict folds extras into the
|
|
96
|
+
// verdict for callers that want a gate; the default is unchanged.
|
|
97
|
+
const ok =
|
|
98
|
+
summary.missing === 0 &&
|
|
99
|
+
summary.modified === 0 &&
|
|
100
|
+
(!strict || summary.extra === 0);
|
|
86
101
|
|
|
87
102
|
return {
|
|
88
103
|
ok,
|
|
104
|
+
strict,
|
|
89
105
|
summary,
|
|
90
106
|
details: {
|
|
91
107
|
ok: okFiles,
|