Summary
normalizeStringList in src/signals/focus-manifest.ts normalizes the manifest
string lists (wantedPaths, blockedPaths, preferredLabels,
testExpectations). For an entry longer than MAX_ITEM_LENGTH (300), it
truncates, pushes, and continues — skipping both the de-duplication check
and the MAX_LIST_ITEMS (200) cap that the normal path enforces. Manifests are
fetched from untrusted, arbitrary repositories (.gittensory.yml /
.gittensory.json via loadRepoFocusManifest), so a crafted manifest defeats
both safety limits.
Evidence
// src/signals/focus-manifest.ts:95-114
const result: string[] = [];
for (const entry of value) {
if (typeof entry !== "string") { /* warn + continue */ }
const trimmed = entry.trim();
if (!trimmed) continue;
if (trimmed.length > MAX_ITEM_LENGTH) {
warnings.push(`Manifest field "${field}" truncated an over-long entry.`);
result.push(trimmed.slice(0, MAX_ITEM_LENGTH));
continue; // <-- skips dedup AND the cap below
}
if (!result.includes(trimmed)) result.push(trimmed); // dedup (skipped above)
if (result.length >= MAX_LIST_ITEMS) { /* warn */ break; } // cap (skipped above)
}
Constants: MAX_LIST_ITEMS = 200, MAX_ITEM_LENGTH = 300,
MAX_FOCUS_MANIFEST_BYTES = 64 * 1024.
Concrete traces
(1) De-duplication bypass. Two distinct over-long entries that share a
300-char prefix:
wantedPaths:
- "<300 'a' chars>X" # length 301
- "<300 'a' chars>Y" # length 301
Both are truncated to the same "a"×300 and pushed without the !result.includes
check → wantedPaths = ["a…300", "a…300"] (a duplicate). The normal path
(de-duplicates repeated entries within the list cap, focus-manifest.test.ts:80)
would have collapsed them.
(2) MAX_LIST_ITEMS cap bypass (reachable under the byte gate). Because the
over-long branch continues before the cap check, a list composed entirely of
over-long entries never triggers the break. 201 entries of 301 chars fit under
the 64 KB pre-parse gate:
- JSON:
201 × ("<301 chars>" + quotes + comma) ≈ 201 × 304 ≈ 61.1 KB < 65,536
- YAML:
201 × (" - <301 chars>\n") ≈ 201 × 306 ≈ 61.5 KB < 65,536
So parseFocusManifestContent accepts it, and normalizeStringList returns a
201-entry list (up to ~214 fit), exceeding the 200-item cap that exists to
bound the list. The oversized list then flows into compileFocusManifestPolicy,
the onboarding-pack / entry-guidance output, and the
GET /v1/repos/:owner/:repo/focus-manifest response.
Why it's wrong
The truncation branch should still de-duplicate and respect the size cap — those
limits exist precisely to keep untrusted manifest input bounded and clean. As
written, the one input class the limits most need to handle (pathological,
over-long entries) is the one class that bypasses them.
Test status
Not locked in. focus-manifest.test.ts:73 (caps over-long lists and de-duplicates entries) uses short path-${index} entries, so it exercises
the normal dedup/cap path, never the over-long branch. No test asserts dedup or
the 200-cap for entries longer than MAX_ITEM_LENGTH.
Suggested fix
Truncate in place, then flow through the same de-dup and cap logic instead of
continue-ing past them:
let normalized = trimmed;
if (normalized.length > MAX_ITEM_LENGTH) {
warnings.push(`Manifest field "${field}" truncated an over-long entry.`);
normalized = normalized.slice(0, MAX_ITEM_LENGTH);
}
if (!result.includes(normalized)) result.push(normalized);
if (result.length >= MAX_LIST_ITEMS) {
warnings.push(`Manifest field "${field}" exceeded ${MAX_LIST_ITEMS} entries; extra entries ignored.`);
break;
}
Add tests: (a) two over-long entries sharing a 300-char prefix de-duplicate to a
single entry; (b) >200 over-long entries cap at 200 with the exceeded 200 entries warning.
Distinct from prior reports
Same family as the merged #464 (reward-risk next-actions deduped after capping),
but a different site and a different defect (a continue that bypasses dedup
and the list cap for over-long entries), in the newly-added focus-manifest
parser (#389 / #118). No existing issue covers focus-manifest list normalization.
Summary
normalizeStringListinsrc/signals/focus-manifest.tsnormalizes the manifeststring lists (
wantedPaths,blockedPaths,preferredLabels,testExpectations). For an entry longer thanMAX_ITEM_LENGTH(300), ittruncates, pushes, and
continues — skipping both the de-duplication checkand the
MAX_LIST_ITEMS(200) cap that the normal path enforces. Manifests arefetched from untrusted, arbitrary repositories (
.gittensory.yml/.gittensory.jsonvialoadRepoFocusManifest), so a crafted manifest defeatsboth safety limits.
Evidence
Constants:
MAX_LIST_ITEMS = 200,MAX_ITEM_LENGTH = 300,MAX_FOCUS_MANIFEST_BYTES = 64 * 1024.Concrete traces
(1) De-duplication bypass. Two distinct over-long entries that share a
300-char prefix:
Both are truncated to the same
"a"×300and pushed without the!result.includescheck →
wantedPaths = ["a…300", "a…300"](a duplicate). The normal path(
de-duplicates repeated entries within the list cap,focus-manifest.test.ts:80)would have collapsed them.
(2)
MAX_LIST_ITEMScap bypass (reachable under the byte gate). Because theover-long branch
continues before the cap check, a list composed entirely ofover-long entries never triggers the
break. 201 entries of 301 chars fit underthe 64 KB pre-parse gate:
201 × ("<301 chars>" + quotes + comma) ≈ 201 × 304 ≈ 61.1 KB < 65,536201 × (" - <301 chars>\n") ≈ 201 × 306 ≈ 61.5 KB < 65,536So
parseFocusManifestContentaccepts it, andnormalizeStringListreturns a201-entry list (up to ~214 fit), exceeding the 200-item cap that exists to
bound the list. The oversized list then flows into
compileFocusManifestPolicy,the onboarding-pack / entry-guidance output, and the
GET /v1/repos/:owner/:repo/focus-manifestresponse.Why it's wrong
The truncation branch should still de-duplicate and respect the size cap — those
limits exist precisely to keep untrusted manifest input bounded and clean. As
written, the one input class the limits most need to handle (pathological,
over-long entries) is the one class that bypasses them.
Test status
Not locked in.
focus-manifest.test.ts:73(caps over-long lists and de-duplicates entries) uses shortpath-${index}entries, so it exercisesthe normal dedup/cap path, never the over-long branch. No test asserts dedup or
the 200-cap for entries longer than
MAX_ITEM_LENGTH.Suggested fix
Truncate in place, then flow through the same de-dup and cap logic instead of
continue-ing past them:Add tests: (a) two over-long entries sharing a 300-char prefix de-duplicate to a
single entry; (b) >200 over-long entries cap at 200 with the
exceeded 200 entrieswarning.Distinct from prior reports
Same family as the merged #464 (reward-risk next-actions deduped after capping),
but a different site and a different defect (a
continuethat bypasses dedupand the list cap for over-long entries), in the newly-added focus-manifest
parser (#389 / #118). No existing issue covers focus-manifest list normalization.