kodelyth-ecc 2.24.1 → 2.24.3
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 +112 -0
- package/CLAUDE.md +1 -1
- package/README.md +11 -11
- package/agents/chaos-engineer.md +7 -7
- package/commands/devil-mode.md +3 -3
- package/package.json +1 -1
- package/scripts/dashboard/server.js +12 -1
- package/scripts/mcp/catalog.js +22 -1
- package/scripts/memory/store.js +33 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,118 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Kodelyth ECC are documented here.
|
|
4
4
|
|
|
5
|
+
## v2.24.3 — remove decorative emoji from docs and catalog (October 2026)
|
|
6
|
+
|
|
7
|
+
Documentation only. No code, no behaviour change.
|
|
8
|
+
|
|
9
|
+
`rules/common/coding-style.md` bans decorative emoji and names the permitted
|
|
10
|
+
status indicators: `PASS`/`FAIL` or the text characters `✓` (U+2713) and `✗`
|
|
11
|
+
(U+2717). Three shipped files were not following it.
|
|
12
|
+
|
|
13
|
+
### README comparison table
|
|
14
|
+
|
|
15
|
+
39 emoji across 11 rows replaced with `✓` / `✗`.
|
|
16
|
+
|
|
17
|
+
This also fixed a polarity bug the emoji were hiding. In every row `❌` meant
|
|
18
|
+
"lacks this", where lacking it is the bad outcome. In the telemetry row it meant
|
|
19
|
+
"none", where none is the *good* outcome — so one glyph carried opposite meanings
|
|
20
|
+
in the same column, and ECC's `❌` sat beside a competitor's `❌` looking like
|
|
21
|
+
agreement while reading as praise and criticism. A mechanical swap would have
|
|
22
|
+
preserved that, so the row now states the value instead of a verdict:
|
|
23
|
+
|
|
24
|
+
| Telemetry | **None** | Varies | None | Varies |
|
|
25
|
+
|
|
26
|
+
### Agent and command bodies
|
|
27
|
+
|
|
28
|
+
10 decorative `❌` prefixes removed from bullet lists in
|
|
29
|
+
`agents/chaos-engineer.md` and `commands/devil-mode.md`. Dropped rather than
|
|
30
|
+
swapped for `✗`: both lists sit under headings that already state the negation —
|
|
31
|
+
"What You DON'T Do" and "When NOT to Use" — so every bullet was marked negative
|
|
32
|
+
twice.
|
|
33
|
+
|
|
34
|
+
These two files were also the upstream source of the emoji appearing on the
|
|
35
|
+
website. `ecc-web` syncs agent bodies into `src/lib/agents.json`, so the site
|
|
36
|
+
inherited them and could not be fixed there; a site-side edit would be
|
|
37
|
+
overwritten by the next sync.
|
|
38
|
+
|
|
39
|
+
### Deliberately left alone
|
|
40
|
+
|
|
41
|
+
`rules/common/coding-style.md` still contains `✅ 🔍 🎯 🚀 ❌`. Every one sits in
|
|
42
|
+
the **Wrong** column of the table teaching which emoji not to use. They are
|
|
43
|
+
quoted specimens, not decoration — stripping them would empty the column and
|
|
44
|
+
leave the rule unable to demonstrate its own point. Future emoji sweeps should
|
|
45
|
+
skip that file.
|
|
46
|
+
|
|
47
|
+
The `← ↑ ↓ ⚙` in README.md are also kept. They sit inside terminal-output code
|
|
48
|
+
blocks and reproduce what the CLI actually prints — the gear comes from
|
|
49
|
+
`scripts/cli/menu.js:243`. Changing them would make the README stop matching the
|
|
50
|
+
tool.
|
|
51
|
+
|
|
52
|
+
Catalog parsing verified unchanged: 70 agents, 103 commands, frontmatter intact.
|
|
53
|
+
**665 tests across 43 files.**
|
|
54
|
+
|
|
55
|
+
## v2.24.2 — adversarial pass: path traversal in get_rule, unrecallable memories (October 2026)
|
|
56
|
+
|
|
57
|
+
**Security fix.** Three defects found by attacking the memory store, MCP server
|
|
58
|
+
and dashboard rather than reading them.
|
|
59
|
+
|
|
60
|
+
### `get_rule` returned any markdown file on the machine
|
|
61
|
+
|
|
62
|
+
`loadRule` was the only catalog loader that built a filesystem path out of its
|
|
63
|
+
argument. Every other loader — agents, skills, commands, bundles — enumerates a
|
|
64
|
+
directory and matches by name, so a caller can only ever select something already
|
|
65
|
+
listed. This one did `path.join(PATHS.rules, name + '.md')`, so:
|
|
66
|
+
|
|
67
|
+
get_rule("../../README") -> 51,297 bytes of ROOT/README.md
|
|
68
|
+
get_rule("testing/../../../README") -> same, via nested traversal
|
|
69
|
+
|
|
70
|
+
The `.md` suffix was the only thing bounding the reach, which left every readable
|
|
71
|
+
markdown file on the machine in scope.
|
|
72
|
+
|
|
73
|
+
This matters because the caller is not always the user. An MCP tool argument can
|
|
74
|
+
originate in text the agent merely read — a dependency README, an issue comment,
|
|
75
|
+
a fetched page — which made this an arbitrary-markdown-read primitive reachable
|
|
76
|
+
by prompt injection. That is exactly the class ECC's own `prompt-injection-hunter`
|
|
77
|
+
agent exists to catch.
|
|
78
|
+
|
|
79
|
+
Fixed with two guards that fail differently: a name pattern that rejects anything
|
|
80
|
+
that is a path rather than a name before touching disk, and the existing
|
|
81
|
+
`resolveContained()` from `scripts/lib/safe-fs.js`, which proves the target sits
|
|
82
|
+
under `rules/common` once symlinks resolve. The pattern alone is not enough —
|
|
83
|
+
`escape-probe` is a valid rule name, and a symlink under that name reads 51KB.
|
|
84
|
+
|
|
85
|
+
`loadRule` had no test coverage at all, which is why this survived. It now has
|
|
86
|
+
four tests, including the symlink case.
|
|
87
|
+
|
|
88
|
+
### One memory was mathematically unrecallable
|
|
89
|
+
|
|
90
|
+
`search()` defaulted to an absolute `minScore` of 0.5 against unnormalised BM25
|
|
91
|
+
scores. BM25 IDF is `log(1 + (N - df + 0.5) / (df + 0.5))`, so a term appearing in
|
|
92
|
+
every memory drives IDF toward zero and can never clear a fixed floor at any
|
|
93
|
+
corpus size. The floor is now relative to the top hit, so it scales with the
|
|
94
|
+
corpus instead of fighting it. An explicitly passed `minScore` is still honoured
|
|
95
|
+
exactly. `recall_memory` passes none, so it was using the broken default.
|
|
96
|
+
|
|
97
|
+
### Malformed request URIs returned 500 and wrote to stderr
|
|
98
|
+
|
|
99
|
+
`decodeURIComponent` throws on `/%`, `/%zz` or a truncated `/%e0%a4`. The dashboard
|
|
100
|
+
did not guard it, so a malformed *client* URI became a 500 "internal server error"
|
|
101
|
+
plus one stderr line per request — meaning any local process could spam the
|
|
102
|
+
terminal the dashboard runs in. A path that cannot be decoded names no file, so
|
|
103
|
+
it is now a 404 like any other miss.
|
|
104
|
+
|
|
105
|
+
### What resisted
|
|
106
|
+
|
|
107
|
+
Worth recording, since the absence of findings was tested rather than assumed.
|
|
108
|
+
Dashboard traversal (plain, encoded, double-encoded, backslash, absolute,
|
|
109
|
+
null-byte) all returned 404. Host validation rejects absent, empty and foreign
|
|
110
|
+
`Host` headers. Non-GET is 405. Stored XSS was tested in a real browser: a memory
|
|
111
|
+
captured through the real store with breakout payloads in problem, approach, tags,
|
|
112
|
+
language and project rendered as visible text, setting no global, creating no
|
|
113
|
+
element and no attribute, with the `</pre>` breakout still inside its `<pre>`.
|
|
114
|
+
|
|
115
|
+
**665 tests across 43 files.**
|
|
116
|
+
|
|
5
117
|
## v2.24.1 — verify Windows downloads before running them (October 2026)
|
|
6
118
|
|
|
7
119
|
**Security fix.** The Windows installer added in 2.23.0 downloaded a release zip
|
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/ → 665 passing tests across 43 test files
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Running Tests
|
package/README.md
CHANGED
|
@@ -107,18 +107,18 @@ Most "AI agent kits" are folders of markdown files you have to remember the name
|
|
|
107
107
|
| Feature | **Kodelyth ECC** | `agency-agents` | `awesome-claude-agents` | Generic prompt libs |
|
|
108
108
|
|---|---|---|---|---|
|
|
109
109
|
| Specialist agents | **70** | ~30 | ~20 | Varies |
|
|
110
|
-
| Skills as separate layer |
|
|
111
|
-
| Slash commands |
|
|
112
|
-
| **Parallel multi-agent commands** |
|
|
113
|
-
| **Intent routing (plain language → agent)** |
|
|
114
|
-
| **Local BM25 self-learning memory** |
|
|
115
|
-
| **Compound learning from corrections** |
|
|
116
|
-
| **Adversarial / red-team agents** |
|
|
117
|
-
| **Adversarial build/attack loop** |
|
|
118
|
-
| Quality hooks |
|
|
110
|
+
| Skills as separate layer | ✓ 196 | ✗ | ✗ | ✗ |
|
|
111
|
+
| Slash commands | ✓ 103 | Some | Some | ✗ |
|
|
112
|
+
| **Parallel multi-agent commands** | ✓ 8 (incl. `/devil-mode`) | ✗ | ✗ | ✗ |
|
|
113
|
+
| **Intent routing (plain language → agent)** | ✓ 10-tier rule | ✗ | ✗ | ✗ |
|
|
114
|
+
| **Local BM25 self-learning memory** | ✓ | ✗ | ✗ | ✗ |
|
|
115
|
+
| **Compound learning from corrections** | ✓ `tasks/lessons.md` | ✗ | ✗ | ✗ |
|
|
116
|
+
| **Adversarial / red-team agents** | ✓ 8 (devil-mode) | ✗ | ✗ | ✗ |
|
|
117
|
+
| **Adversarial build/attack loop** | ✓ `/arena` — scored, verified, converges | ✗ | ✗ | ✗ |
|
|
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
|
-
| Telemetry |
|
|
121
|
-
| Test coverage |
|
|
120
|
+
| Telemetry | **None** | Varies | None | Varies |
|
|
121
|
+
| Test coverage | ✓ 665 tests | ✗ | ✗ | ✗ |
|
|
122
122
|
| Distributed via | `npx`, curl, clone | Manual | Manual | Manual |
|
|
123
123
|
|
|
124
124
|
---
|
package/agents/chaos-engineer.md
CHANGED
|
@@ -173,13 +173,13 @@ curl -X POST localhost:3000/admin/flags/new-feature --data '{"enabled":true}'
|
|
|
173
173
|
|
|
174
174
|
## What You DON'T Do
|
|
175
175
|
|
|
176
|
-
-
|
|
177
|
-
-
|
|
178
|
-
-
|
|
179
|
-
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
-
|
|
176
|
+
- Run experiments in production without an SRE on call and explicit sign-off
|
|
177
|
+
- Touch production data without an explicit backup and tested restore
|
|
178
|
+
- Cause unbounded blast radius (kill all services, all regions, all replicas)
|
|
179
|
+
- Run during high-traffic events (peak hours, launches, marketing campaigns)
|
|
180
|
+
- Run without monitoring (chaos without observability is just sabotage)
|
|
181
|
+
- Continue past abort criteria — if abort fires, abort, no exceptions
|
|
182
|
+
- Inject faults into systems you don't own without coordination
|
|
183
183
|
|
|
184
184
|
## Output Format
|
|
185
185
|
|
package/commands/devil-mode.md
CHANGED
|
@@ -83,9 +83,9 @@ PRIORITIZED ACTIONS:
|
|
|
83
83
|
|
|
84
84
|
## When NOT to Use
|
|
85
85
|
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
86
|
+
- On code you don't have permission to audit (run only on your own repos / authorized targets)
|
|
87
|
+
- On production systems for `chaos-engineer` flag — chaos requires `--staging` or signed-off blast radius
|
|
88
|
+
- As a substitute for actual security review — this finds patterns; humans verify exploitability
|
|
89
89
|
|
|
90
90
|
## Execute Now
|
|
91
91
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodelyth-ecc",
|
|
3
|
-
"version": "2.24.
|
|
3
|
+
"version": "2.24.3",
|
|
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",
|
|
@@ -151,7 +151,18 @@ function serveStaticFile(res, filePath) {
|
|
|
151
151
|
// baseDir is injectable so the containment logic can be tested against a real
|
|
152
152
|
// symlink without planting one in the shipped static/ directory.
|
|
153
153
|
function resolveStatic(reqPath, baseDir = STATIC_DIR) {
|
|
154
|
-
|
|
154
|
+
// decodeURIComponent throws URIError on malformed percent-encoding — "/%",
|
|
155
|
+
// "/%zz", a truncated "/%e0%a4". That threw out to the route handler, which
|
|
156
|
+
// turned a client's malformed URI into a 500 "internal server error" and wrote
|
|
157
|
+
// a line to stderr, so any local process could both mislabel its own bad
|
|
158
|
+
// request as a server fault and spam the terminal the dashboard runs in.
|
|
159
|
+
// A path that cannot be decoded names no file, so it is simply not found.
|
|
160
|
+
let decoded;
|
|
161
|
+
try {
|
|
162
|
+
decoded = decodeURIComponent(reqPath.replace(/^\/+/, ''));
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
155
166
|
if (decoded.includes('..')) return null;
|
|
156
167
|
if (decoded === '' || decoded === '/') return path.join(baseDir, 'index.html');
|
|
157
168
|
// Containment, including symlink resolution — see scripts/lib/safe-fs.js.
|
package/scripts/mcp/catalog.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
+
const { resolveContained } = require('../lib/safe-fs');
|
|
11
12
|
|
|
12
13
|
const ROOT = path.join(__dirname, '..', '..');
|
|
13
14
|
|
|
@@ -157,8 +158,28 @@ function loadCommands() {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
// ── Rules ────────────────────────────────────────────────────────────────────
|
|
161
|
+
// A rule name is a flat basename: rules/common has no subdirectories, and every
|
|
162
|
+
// other loader here resolves a name against a directory LISTING, so a caller can
|
|
163
|
+
// only ever select something already enumerated. This one built a path out of the
|
|
164
|
+
// argument instead, which made `../../README` read ROOT/README.md — any .md file
|
|
165
|
+
// the process could reach, through a tool whose contract is "return a rule".
|
|
166
|
+
//
|
|
167
|
+
// That matters because the caller is not always the user. An MCP tool argument
|
|
168
|
+
// can originate in text the agent merely read — a dependency's README, an issue
|
|
169
|
+
// comment, a fetched page — so this was an arbitrary-markdown-read primitive
|
|
170
|
+
// reachable by prompt injection, which is exactly what ECC's own
|
|
171
|
+
// prompt-injection-hunter agent exists to catch.
|
|
172
|
+
//
|
|
173
|
+
// Two guards, because they fail differently. The pattern rejects anything that is
|
|
174
|
+
// a path rather than a name, before touching disk. resolveContained then proves
|
|
175
|
+
// the target really sits under rules/common once symlinks are resolved, which a
|
|
176
|
+
// pattern cannot know.
|
|
177
|
+
const RULE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
178
|
+
|
|
160
179
|
function loadRule(name) {
|
|
161
|
-
|
|
180
|
+
if (typeof name !== 'string' || !RULE_NAME.test(name)) return null;
|
|
181
|
+
const file = resolveContained(path.join(PATHS.rules, `${name}.md`), PATHS.rules);
|
|
182
|
+
if (file === null) return null;
|
|
162
183
|
const raw = safeReadFile(file);
|
|
163
184
|
if (raw === null) return null;
|
|
164
185
|
return { name, file, relpath: path.relative(ROOT, file), body: raw };
|
package/scripts/memory/store.js
CHANGED
|
@@ -185,8 +185,33 @@ function indexMemory(index, memory) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
// ── BM25 retrieval (k1=1.5, b=0.75) ──────────────────────────────────────────
|
|
188
|
+
/**
|
|
189
|
+
* Fraction of the best hit's score a result must reach to be kept, when the
|
|
190
|
+
* caller does not impose an absolute floor of its own.
|
|
191
|
+
*
|
|
192
|
+
* Relative, not absolute, and that is the whole point. BM25 scores are not
|
|
193
|
+
* normalised — they scale with corpus size and term rarity — so any fixed
|
|
194
|
+
* threshold is really a statement about how many memories you have. The old
|
|
195
|
+
* default of 0.5 made a single stored memory mathematically unrecallable: with
|
|
196
|
+
* N=1 and df=1 the IDF term caps the score at ~0.288, so recall() returned
|
|
197
|
+
* nothing no matter what you searched for.
|
|
198
|
+
*
|
|
199
|
+
* It was worse for common words. A token appearing in every memory scores
|
|
200
|
+
* 0.288 at N=1, 0.087 at N=5 and 0.010 at N=50 — correct BM25 behaviour, since
|
|
201
|
+
* a term that is everywhere carries no signal, but under a fixed floor it meant
|
|
202
|
+
* those words never matched at any corpus size.
|
|
203
|
+
*
|
|
204
|
+
* Three call sites had already worked around this by passing minScore: 0.1
|
|
205
|
+
* (doctor and two tests, one of which names the ~0.29 figure outright). The
|
|
206
|
+
* user-facing paths had not: the MCP recall_memory tool, the memory CLI and
|
|
207
|
+
* SessionStart injection all used the default and silently returned nothing.
|
|
208
|
+
*/
|
|
209
|
+
const RELATIVE_SCORE_FLOOR = 0.25;
|
|
210
|
+
|
|
188
211
|
function search(query, options = {}) {
|
|
189
|
-
|
|
212
|
+
// minScore stays honoured when passed, so existing callers keep their exact
|
|
213
|
+
// behaviour; only the default changes from an absolute floor to a relative one.
|
|
214
|
+
const { limit = 5, minScore = null, projectFilter = null } = options;
|
|
190
215
|
const index = loadIndex();
|
|
191
216
|
const tokens = tokenise(query);
|
|
192
217
|
if (tokens.length === 0 || index.docCount === 0) return [];
|
|
@@ -208,9 +233,13 @@ function search(query, options = {}) {
|
|
|
208
233
|
}
|
|
209
234
|
}
|
|
210
235
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
236
|
+
const sorted = Object.entries(scores).sort(([, a], [, b]) => b - a);
|
|
237
|
+
const topScore = sorted.length ? sorted[0][1] : 0;
|
|
238
|
+
const floor = minScore !== null && minScore !== undefined
|
|
239
|
+
? minScore
|
|
240
|
+
: topScore * RELATIVE_SCORE_FLOOR;
|
|
241
|
+
const ranked = sorted
|
|
242
|
+
.filter(([, score]) => score >= floor)
|
|
214
243
|
.slice(0, limit * 3);
|
|
215
244
|
|
|
216
245
|
if (ranked.length === 0) return [];
|