Skip to content

[Bug]: renderPublicScenarioSummary throws for any repo whose name contains a forbidden term (hotkey/coldkey/wallet/…), failing the public scenario summary #624

Description

@galuis116

Summary

assertPublicSummaryClean is the final safety guard for the public-safe scenario
summary. It serializes the entire summary object and rejects it if it matches
FORBIDDEN_PUBLIC_LANGUAGE. But summary.repoFullName is set from the input
un-sanitized (it is the repo identifier, not rendered content). So when a repo
name contains a forbidden substring — and hotkey, coldkey, and wallet are
literal Bittensor/Gittensor protocol terms — the guard matches and
renderPublicScenarioSummary throws, so no scenario summary is produced for
that repo at all.

Evidence

// src/scenarios/scenario-summary.ts:92
const FORBIDDEN_PUBLIC_LANGUAGE =
  /wallet|hotkey|coldkey|mnemonic|seed phrase|payout|reward[-\s]?estimate|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i;

// :180  the guard scans the WHOLE serialized summary, including repoFullName
function assertPublicSummaryClean(summary: PublicScenarioSummary): void {
  const serialized = JSON.stringify(summary);
  /* v8 ignore start -- All text fields are sanitized before this guard; defensive check for future fields. */
  if (FORBIDDEN_PUBLIC_LANGUAGE.test(serialized)) {
    throw new Error("Public scenario summary still contains forbidden language.");
  }
  /* v8 ignore end */
}

// :196  repoFullName is passed through verbatim -- never sanitized
export function renderPublicScenarioSummary(input: ScenarioSummaryInput): PublicScenarioSummary {
  const summary: PublicScenarioSummary = {
    repoFullName: input.repoFullName,   // <-- un-sanitized; flows into the JSON.stringify guard above
    ...
  };
  assertPublicSummaryClean(summary);
  return summary;
}

Every rendered text field (headline, options[].*, *Notes, dataClassification)
is run through sanitizePublicComment, so the guard's /* v8 ignore */ rationale
("all text fields are sanitized … defensive check") holds for those — but
repoFullName is a structural identifier that is not sanitized and is fully
controlled by the repo name. The author's stated invariant (the throw is
unreachable) is therefore false.

Concrete trace

renderPublicScenarioSummary({
  repoFullName: "octo/hotkey-manager",   // a Bittensor-themed repo name
  generatedAt: "2026-06-12T00:00:00.000Z",
  // ...any valid scenario inputs...
});
  • summary.repoFullName = "octo/hotkey-manager".
  • JSON.stringify(summary) contains "octo/hotkey-manager".
  • FORBIDDEN_PUBLIC_LANGUAGE.test(serialized) matches hotkeythrows
    "Public scenario summary still contains forbidden language.".

The same happens for owner/wallet, metamask/wallet-adapter,
org/coldkey-vault, team/farming-bot, etc. The public scenario summary
(MCP/API/control-panel surface) cannot be produced for these repos.

Why it's wrong

The guard's purpose is to catch forbidden language that slipped into rendered
content
, not to validate the repo identifier the summary is about. A repo is
legitimately allowed to be named hotkey-manager; refusing to render its advisory
scenario summary is a denial of functionality for exactly the ecosystem
Gittensory serves (Bittensor, where hotkey/coldkey/wallet are core terms).

Reachability

renderPublicScenarioSummary is the public-safe renderer for MCP/API clients and
control-panel UIs. repoFullName is the analyzed repo, so real repo names flow in
directly. Bittensor/crypto repos frequently contain hotkey, coldkey, wallet,
or farming in their names, making this readily reachable.

Test status

Not locked in. Every case in test/unit/scenario-summary.test.ts uses
repoFullName: "octo/demo"; no test exercises a repo name containing a forbidden
term, and none asserts renderPublicScenarioSummary either throws or succeeds for
such a name.

Suggested fix

Exclude structural identifiers (repoFullName, generatedAt) from the
forbidden-language guard so it only scans rendered free-text fields:

function assertPublicSummaryClean(summary: PublicScenarioSummary): void {
  const { repoFullName: _repo, generatedAt: _at, ...rest } = summary;
  if (FORBIDDEN_PUBLIC_LANGUAGE.test(JSON.stringify(rest))) {
    throw new Error("Public scenario summary still contains forbidden language.");
  }
}

Add a regression test: renderPublicScenarioSummary({ repoFullName: "octo/hotkey-wallet", … })
returns a summary whose repoFullName is preserved and does not throw.

Distinct from prior reports

Same class as the merged #457 (miner-command sanitizer breaking legitimate
repo/login names like wallet-adapter), but a different surface and mechanism:
here a public-output guard (not a redactor) hard-throws on the un-sanitized
repoFullName. The feature PRs #416/#345/#290 introduce the renderer; no existing
issue covers the repo-name throw.

Metadata

Metadata

Assignees

No one assigned

    Labels

    slopAI slop and/or attempts to game additional points via manipulation or alt profiles.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions