Skip to content

[spark-compete wave 5] security cross-cut (stacked) - #859

Merged
vibeforge1111 merged 35 commits into
mainfrom
spark-compete/wave5-security-crosscut
Jun 26, 2026
Merged

[spark-compete wave 5] security cross-cut (stacked)#859
vibeforge1111 merged 35 commits into
mainfrom
spark-compete/wave5-security-crosscut

Conversation

@vibeforge1111

Copy link
Copy Markdown
Owner

Spark Compete — Wave 5 (security cross-cut), stacked on spark-compete/wave4-memory-persona-voice

Highest-value security set. Wave-5 delta only.

Commits

On-merge points

ifeoluwaaj 108 · yossweh 50 · TALLSOME24 28

⚠️ HELD for security-owner sign-off (NOT in this PR)

Verified: proportionate diff, no churn, no conflict markers. Compiles / tests green. Draft — gated on CI + approval.

🤖 Generated with Claude Code

Meta Alchemist and others added 30 commits June 24, 2026 13:19
…CRET errors

Narrowed from spark-telegram-bot#148 (resurrect_adjust). HEAD had already
adopted the other two claimed improvements: the `got ${value.length}` length
feedback and the expanded charset message (HEAD's wording is retained as it is
more descriptive than the PR's). Only the missing piece is applied here: a
shared RELAY_SECRET_HINT constant carrying an actionable
`openssl rand`-based generation command, appended to all three throw sites in
requireRelaySecret so operators get a copy-pasteable fix instead of a bare
constraint.

Co-authored-by: 4gjnbzb4zf-sudo <4gjnbzb4zf-sudo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#812 (recursive): wrap execFile/writeFile in try/finally so the per-run
scratch dir under tmpdir is always removed (rm recursive+force, swallowed),
preventing unbounded /tmp growth across many Swarm syncs. Adds explicit
cleanup-on-throw before the chip-loop write path.

#805 (schedule): guard formatNextFireLocal against an invalid date — return
the raw ISO instead of "Invalid Date" when parsing fails.

PRs: #812, #805

Co-authored-by: 4gjnbzb4zf-sudo <4gjnbzb4zf-sudo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…onse bodies

#811: drain the success response body (response.body?.cancel(), null-safe)
after each Mission Control POST so the socket returns to the undici keep-alive
pool instead of pending until GC — fixes slow dispatcher-pool exhaustion.

#834: replace Math.random() in randomId() with crypto.randomBytes(4).hex.
Per maintainer review: hoisted the inline require('crypto') to a top-level
ESM import { randomBytes } from 'node:crypto' (file is ESM); the id grows
6 -> 8 hex chars but randomId is only a uniqueness suffix on missionId
(no consumer parses its length), so no slice is needed. Exported randomId and
replaced the throwaway vitest test (repo uses node:test, not vitest) with a
real test that imports the actual missionControl.randomId.

PRs: #811, #834

Co-authored-by: 4gjnbzb4zf-sudo <4gjnbzb4zf-sudo@users.noreply.github.com>
Co-authored-by: TALLSOME24 <TALLSOME24@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… create

#829: preflight the resolved chipLabsRoot before launching the create
mission; when the spark-domain-chip-labs module is missing, return a clear
not-found error with a Repair block (install the module / check setup docs)
instead of a downstream execFile failure.

PRs: #829

Co-authored-by: Acone8 <Acone8@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Systemic path/diagnostic redaction across the Telegram surface (one author,
one concern):

#804: add UNIX_ABSOLUTE_PATH and WINDOWS_ABSOLUTE_PATH redaction passes to
redactText() so absolute paths in any logged/relayed text (incl. error
stacks via redactForLog) become [REDACTED_PATH]; covered by new tests.
#814: replace path-revealing "repo=... home=..." bridge-unavailable errors
with a generic message, wrap diagnostics stderr in redactText(), drop the
Workspace <code>path</code> line from the build-queued reply, and mask the
chip path in the /chip reply.
#815: stop emitting source_path lines in self-improvement / wiki query /
wiki answer replies (titles only).
#813: replace "Inspect: <dossierPath>" with "Inspect: [available]" in the
startup release/bench verdict renders.

Per maintainer review on #814: the patch had drifted against the wave1 base,
so the two index.ts hunks (formatBuildMissionQueuedReply, /chip) and the
runBuilderTelegramBridge required-mode throw were hand-applied at their
correct post-rebase locations rather than where the fuzzy 3-way landed them.

PRs: #804, #814, #815, #813

Co-authored-by: Esc1200 <Esc1200@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e paths

#808: drop the alphabetical slice(limit*3) cap in summarizeRoot so the
recently-modified sort can actually surface the newest projects instead of
being truncated to the first directories alphabetically.

#814: remove the per-project absolute path line from the local-workspace
inspection reply (path redaction; same systemic concern as the redaction
group).

PRs: #808, #814

Co-authored-by: 4gjnbzb4zf-sudo <4gjnbzb4zf-sudo@users.noreply.github.com>
Co-authored-by: Esc1200 <Esc1200@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In group chats Telegram appends @botName to commands (/loop@MyBot). The old
first-occurrence string .replace('/loop', '') left the mention as the first
argument and could also strike a mid-text occurrence. Replace with anchored,
case-insensitive regexes that consume an optional @botName and trailing
whitespace.

#809: /remember and /recall (stop storing/searching a leading @botName).
#810: /creator and /loop (stop parsing @botName as the first argument).

PRs: #809, #810

Co-authored-by: 4gjnbzb4zf-sudo <4gjnbzb4zf-sudo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#836: when /recursive fails because the Swarm workspace is unconfigured,
append a Repair block telling the user to set SPARK_SWARM_WORKSPACE_ID /
SPARK_SWARM_ACCESS_TOKEN — gated by a /workspace is not configured|SPARK_SWARM/
match so unrelated failures are unaffected.

#832: append a "create a chip / list paths" Repair hint when a /loop or
/recursive chip-loop fails. Per maintainer review the hint is now gated behind
a not-found/path-missing match (mirroring 836's regex approach) instead of
firing unconditionally, so timeouts and crashes no longer emit a misleading
"run /chip create" instruction.

PRs: #836, #832

Co-authored-by: Acone8 <Acone8@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#839: add closeJsonState() — checkpoint the WAL (TRUNCATE) and close the
better-sqlite handle, best-effort and idempotent (nulls db, early-returns when
already closed). Call it from the SIGINT and SIGTERM handlers so a shutdown
flushes and releases the DB instead of leaving a -wal/-shm behind.

Per maintainer note, added a small unit test asserting closeJsonState() is
idempotent, nulls the handle, and that state reopens lazily on the next read.

PRs: #839

Co-authored-by: binance1230 <binance1230@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ule scope

#830: lift the product-type pattern, the three product-phrase RegExp objects,
and the product-type tail RegExp out of inferProductPhraseProjectName so they
compile once at module load instead of on every call.

Per maintainer review the hoist is verified behavior-equivalent against the
wave1 base: the module-scope PRODUCT_TYPE_PATTERN was extended to include the
"board|pad" tail that wave1 had added inline (so the hoist does not regress
those product types), and a regression test asserts the product-phrase
extraction (landing page / dashboard / board) is unchanged.

PRs: #830

Co-authored-by: 4gjnbzb4zf-sudo <4gjnbzb4zf-sudo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#838: wrap the writeJsonAtomic call in ConversationMemory.persist() so a
failed state write is logged before it propagates.

Per maintainer review this is a pure diagnostic, NOT resilience hardening: the
catch re-throws and every caller already does .catch(()=>{}), so control flow
is unchanged. Trimmed to wrap only the throwing write (not the whole body) and
the comment states the diagnostic-only intent honestly.

PRs: #838

Co-authored-by: binance1230 <binance1230@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#841: refuse safe-operator file actions whose path falls outside the root the
feature actually targets, before any write/read/list.

Rewritten per maintainer review:
- Containment now uses path.win32.relative (+ trailing-separator handling and an
  equality short-circuit) instead of startsWith, closing the sibling-prefix
  bypass (an allowed "...\\Temp" root no longer also matches "...\\Temp-evil").
- Fixed the platform mismatch: the parser only ever emits Windows C:\\ paths, so
  the guard is pinned to the Windows roots the feature targets — AppData\\Local\\Temp
  for the level5 smoke file and the user Desktop for the folder listing — derived
  from the (shape-validated) action path. The previous os.homedir()/os.tmpdir()
  check would refuse every legit request on a POSIX server whose cwd is not under
  home/tmp. Dropped the now-unused node:os import.

harness_core: interim_until_migration — re-home this guard into the Governor on
migration.

PRs: #841

Co-authored-by: binance1230 <binance1230@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ect #137)

#137 (resurrected from closed-unmerged UPSTREAM-spark-telegram-bot-137):
when the sliding-window limiter drops a message, reply once per 30s cooldown
("Slow down — one message per second...") instead of silently dropping, so a
burst doesn't make the bot look offline. The limiter still owns the actual
rate decision and request tracking; this only governs the user-facing notice.

Rebased by hand onto the wave1 base (the original anchors had drifted) and,
beyond the original PR, the per-user notice map is swept by the existing
periodic map-cleanup so it cannot grow unbounded.

PRs: #137

Co-authored-by: 4gjnbzb4zf-sudo <4gjnbzb4zf-sudo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t (resurrect #91)

#91 (resurrected from closed-unmerged): MiniMax sometimes streams an unterminated
<think> block (the relay preview truncates the closing tag), leaking raw reasoning
and skill-tier chatter to Telegram. Harden stripThinkingAndMeta to also close on a
truncated </thin> tag and add stripDanglingThinkingBlocks to drop an open
think/thinking block that runs to end-of-text (keeping any final answer after a
blank line). When the cleaned output is empty but the goal was a "say/reply exactly
X" instruction, recover X via exactOutputFromGoal so the user still gets the
intended answer. Covered by two new formatting tests.

Applied directly onto the wave1 base (anchors intact; input type already carries
the optional goal field).

PRs: #91

Co-authored-by: rmssantos <rmssantos@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es (resurrect #240)

formatCanvasReadySummary now detects localhost/127.0.0.1/::1 canvas URLs and appends a
hint that the link only works on the host machine (set SPAWNER_UI_PUBLIC_URL for a
shareable address). Re-implemented against current HEAD (the PR patched an old version).

Co-authored-by: TALLSOME24 <TALLSOME24@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…resetUser (resurrect #162)

The text.startsWith('/') silent drop left /new, /reset and /help with no
behavior — and those commands had no bot.command() handlers at all. Add them:

- ConversationMemory.resetUser(user, keepNotes): clears recent/interrupted/
  frame state, and notes unless keepNotes, then persists.
- /new — full reset: clears all conversation context + notes and asks Builder
  (via runBuilderTelegramBridge, ack-suppressed) to forget profile facts.
- /reset — light reset: clears conversation context, keeps long-term notes.
- /help — grouped command reference.

Resurrected against current HEAD per maintainer review:
- The silent-drop's user-feedback gap is now handled by the unrecognised-slash
  hint (#247) rather than by removing the early return, so unknown slashes are
  NOT forwarded wholesale to the Builder bridge.
- DROPPED the obsolete runtime_command carve-out in builderReplySuppressionReason:
  in HEAD that routingDecision's diagnostic-wall replies are deliberately
  suppressed (asserted by tests/conversationIntent.test.ts and buildE2E.test.ts);
  the PR's null-return carve-out would regress those.
- DROPPED the obsolete scope-creep (memory_generic_observation / "your name is"
  guards and the formatLocalMemoryDirectiveAcknowledgement -> "Noted." change) —
  those bridge modes/strings no longer exist in HEAD.

Co-authored-by: Esc1200 <Esc1200@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mmand (resurrect #247)

Telegraf routes registered commands before handleTextMessage runs, so a slash
command reaching the `text.startsWith('/')` branch is unrecognised. Previously
it was silently dropped, making first-time users (especially anyone reaching for
the universal /help convention) think the bot is offline. Reply with a friendly
hint pointing at /start.

Resurrected against current HEAD per maintainer review: in HEAD the slash drop
is immediately followed by the isAddressedGroupText gate, so replying before that
gate would answer every unknown slash in every group the bot sees. Restrict the
hint to private chats so it never responds to unaddressed group slash traffic.

Co-authored-by: 4gjnbzb4zf-sudo <4gjnbzb4zf-sudo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three independent builder/mission-execution correctness fixes:

- spawner.ts isFreshRunningEntry: treat entries with a malformed
  lastUpdated (unparseable date -> non-finite ageMs) as STALE rather
  than fresh, so a corrupt timestamp can no longer pin a dead running
  mission as live (resurrect #353).

- recursive.ts inferOutcomeVerdict: drop the unsafe `metric > 0`
  fallback that silently reported any positive metric as "improved"
  even with no verdict signal; fall through to "flat" instead
  (resurrect #356).

- builderBridge.ts parseBridgeMode + parseWarmBridgeMode: use `??`
  instead of `|| 'auto'` so an operator who explicitly clears
  SPARK_BUILDER_BRIDGE_MODE / SPARK_BUILDER_WARM_BRIDGE_MODE to an
  empty string surfaces the validation error instead of silently
  falling back to auto. PR #408 fixed only parseBridgeMode; the same
  empty-string coalesce bug in the sibling parseWarmBridgeMode is
  fixed here too (resurrect #408, maintainer-completed).

Co-authored-by: ifeoluwaaj <ifeoluwaaj@users.noreply.github.com>
Co-authored-by: 4gjnbzb4zf-sudo <4gjnbzb4zf-sudo@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A single corrupt row in a ledger/replay/smoke file previously threw
out of the whole parse and could crash the bot. Guard each line so one
bad record is skipped and the rest still load:

- naturalRouteLedger.parseNaturalRouteExecutionLedger: per-line
  try/catch; malformed JSONL rows are dropped (resurrect #392, #523 —
  same function, independent fixes). Adds a regression test asserting a
  mix of valid and broken lines yields only the valid records.

- naturalRouteReplay.parseNaturalRouteReplayCases: two-stage guard so
  both an unparseable JSON line and a schema-invalid case are skipped
  rather than aborting the matrix (resurrect #407, #489 — same
  function, independent fixes).

- conversationSmoke.readConversationSmokeScenarios: guard both the file
  read and the JSON.parse, returning [] on failure instead of throwing
  (resurrect #490).

Co-authored-by: ifeoluwaaj <ifeoluwaaj@users.noreply.github.com>
Co-authored-by: AKDAO <AKDAO@users.noreply.github.com>
Co-authored-by: Sequence <Sequence@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eParseJson

builderBridge parsed Builder subprocess stdout with bare
JSON.parse(trimmedStdout); a single malformed payload threw an
unguarded SyntaxError that crashed the bridge call, and any raw snippet
in the error could leak secrets / internal paths to the Telegram user.

Adopt the safeParseJson(raw, context) helper from #401 and sweep it
across the whole file rather than only the 7 sites the PR originally
converted: HEAD had grown ~20 trimmedStdout JSON.parse call sites. Each
now throws a descriptive, context-labelled error on invalid JSON. The
embedded raw= snippet is passed through redactText() first, keeping this
consistent with the adjacent path-redaction work (PRs #814/#815/#816).
The two already-guarded parsers (warm-bridge handleLine,
parseBuilderJsonObject) are left as-is.

resurrect #401 (maintainer-completed: full-file sweep + redaction).

Co-authored-by: ifeoluwaaj <ifeoluwaaj@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Land the still-applicable hunks of #465, dropping the dead ones:

- index.ts readSparkAccessState: guard JSON.parse(rawStatus). On a
  corrupt spark-CLI response it now logs and returns a deny-by-default
  state (serviceEnabled: false) instead of throwing. The PR's original
  error object ({ok, abortMessage, chatLevel}) no longer matches this
  function's return shape on HEAD, so the fallback is rewritten to the
  current signature.

- pathLoop.ts loadBuilderAttachmentSnapshot: guard JSON.parse(stdout)
  and return {} on malformed snapshot output instead of crashing.

- telegramVoiceBridge.ts: validate the Telegram getFileLink() URL host
  is api.telegram.org before fetching it (SSRF guard). Test fixtures
  updated to a real api.telegram.org URL and a regression test added
  asserting a foreign host is rejected without any fetch.

Dropped as dead per maintainer review: the llm.ts stream-parser guards
(already present in HEAD at the OpenAI/Ollama stream readers), the
routeArbiter.ts hunk (file refactored away in HEAD), and the
naturalRouteLedger hunk (same function already hardened in this branch
from #392/#523).

resurrect #465 (maintainer-adjusted: dead hunks dropped, fallback
re-typed, host-check tests completed).

Co-authored-by: AKDAO <AKDAO@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readJsonObject parsed session/summary JSON files with a bare
JSON.parse; a corrupt or truncated file threw and aborted the read.
Wrap it in try/catch and return null on parse failure, matching the
function's existing "not an object" null path.

Narrowed per maintainer review to the readJsonObject hunk only: the
PR's other hunk (parsePositiveIntegerEnvValue in resolveConfig) is
already present in HEAD, so it is dropped on rebase.

resurrect #500 (maintainer-adjusted: timeoutConfig hunk dropped).

Co-authored-by: AKDAO <AKDAO@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Memoize the in-flight load promise so concurrent ensureLoaded() callers
await a single _doLoad() pass instead of each kicking off a duplicate
readJsonFile + state-merge, which could interleave and corrupt the
recent/notes/interrupted maps.

Resurrected from closed-unmerged spark-telegram-bot#309; applied cleanly
on the wave4 stack (line-drift only, blob context matched via 3-way).

Co-authored-by: ifeoluwaaj <ifeoluwaaj@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two reliability fixes in recursive.ts:
- truncate/truncateAtWord reserved only 1 char for the trailing '...'
  ellipsis, overrunning the character limit by two; slice to (limit - 3)
  so the ellipsised result fits the budget (spark-telegram-bot#357).
- bestComparableOutcome trusted a stored bestOutcomeId blindly; now
  re-validate that the referenced outcome is actually best under the
  metric's goal direction (lower-is-better vs higher-is-better) before
  returning it, otherwise fall back to the sorted comparison
  (spark-telegram-bot#358).

Resurrected from closed-unmerged PRs; applied cleanly on the wave4 stack.

Co-authored-by: ifeoluwaaj <ifeoluwaaj@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isLowInformationLlmReply() flagged any reply containing the substring
'working memory' as low-information, which false-positives on legitimate
replies that merely mention working memory. Drop the broad
normalized.includes('working memory') guard; the specific exact-match
and other targeted phrases remain.

Resurrected from closed-unmerged spark-telegram-bot#359. The targeted
line drifted and the 3-way conflicted on an adjacent inserted guard;
hand-applied the single-line deletion (verified unique).

Co-authored-by: ifeoluwaaj <ifeoluwaaj@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The conversation memory writes (remember / rememberAssistantReply /
learnAboutUser) were guarded by silent `.catch(() => {})`, swallowing
every error so memory regressions were invisible. Route all 221 of these
best-effort memory-write handlers through a new logMemoryWriteFailure()
helper that redacts the error and logs it.

Adjusted from closed-unmerged spark-telegram-bot#492 per maintainer
review:
- Rebase: HEAD's index.ts had evolved and the PR's targeted .catch sites
  shifted, so the literal patch no longer applied. Re-targeted every
  current memory-write handler (remember/rememberAssistantReply/
  learnAboutUser) by its method signature rather than by line number;
  the 24 unrelated .catch(() => {}) sites are intentionally left as-is.
- Scope the log to a debug channel: memory writes are intentionally
  best-effort, so a blanket console.warn would be noisy. The helper is
  gated on SPARK_DEBUG_MEMORY_WRITES=1 (off by default) and rate-limited
  to one line per 60s with a suppressed-count rollup, reusing the
  existing redactText() so no provider detail leaks.

Co-authored-by: Sequence <Sequence@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ure in error paths

Cross-cutting information-disclosure hardening across the gateway, chip loop,
and LLM CLI bridges. Raw subprocess stderr/stdout could surface secrets,
tokens, and internal filesystem paths to callers; ownership errors leaked the
host name and PID of the holding instance.

- gatewayOwnership: ownership-conflict error no longer discloses hostname/PID,
  only an opaque ownerId (#820)
- chipLoop: run exec stderr passed through redactText() before inclusion in the
  returned error (#819)
- llm: Claude and Codex CLI failure errors run stderr/stdout through
  redactText() (#818, #817)
- builderBridge: stderr in empty-stdout diagnostics errors is already redacted
  in HEAD; add a regression test to lock the behavior (#816)

Adds focused vitest regression tests for each surface.

Co-authored-by: TALLSOME24 <TALLSOME24@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bility probe

httpPreviewIsReachable() fetched any event-supplied previewUrl, including a
UI API key header. An attacker-controlled previewUrl could target internal
hosts (loopback, RFC1918, link-local, cloud metadata endpoints) and exfiltrate
the SPARK_UI_API_KEY or probe the internal network.

Adds isPrivateOrReservedHost() and short-circuits the probe before any fetch
when the parsed hostname resolves to a private/reserved target. Beyond the
original PR, the guard also covers IPv6 unique-local (fc00::/7), link-local
(fe80::/10), and IPv4-mapped IPv6 (::ffff:a.b.c.d) per maintainer review.

Delaminated from PR #843: the bundled form-data 4.0.5->4.0.6 / hasown
package-lock bump is unrelated and routed to dependabot tracking, not landed
here.

Note: this is a host-block (SSRF) guard. The complementary credential-
confinement holes (UI key still sent to non-private external origins, #118 /
#453) remain gated on security-owner sign-off and are not addressed here.

Co-authored-by: yossweh <yossweh@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pythonSourceEnv() copied the runner environment and set a profile-scoped
TELEGRAM_BOT_TOKEN for Builder, but the raw runner BOT_TOKEN remained in the
inherited child environment. Builder child processes (and anything they spawn
or log) could read the primary bot credential.

Delete BOT_TOKEN from the child env once the scoped TELEGRAM_BOT_TOKEN is set,
so the child only ever sees the token it is supposed to use.

Co-authored-by: ifeoluwaaj <ifeoluwaaj@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oting

quoteWindowsArg() only escaped embedded double quotes (and via backslash, which
cmd.exe does not honor), leaving % and ^ unescaped. Through the cmd.exe shim a
crafted argument containing %VAR% could trigger environment-variable expansion,
and ^ could escape the following character, enabling command injection.

Escape % -> %%, ^ -> ^^, and " -> "" inside the quoted argument so cmd.exe
treats the value as a literal string.

Co-authored-by: ifeoluwaaj <ifeoluwaaj@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Meta Alchemist and others added 5 commits June 26, 2026 12:18
builderBridge.test.ts: the wave5 redaction work (#815) stopped emitting
source_path lines in self-improvement / wiki-query / wiki-answer replies so
internal filesystem paths never leak to Telegram users. Update the three
assertions that still expected the path to appear: now assert the source TITLE
renders and the source_path is absent.

operatorActions.test.ts: runSafeOperatorAction now pins file ops to the Windows
AppData\\Local\\Temp / Desktop roots the feature targets, so a bare POSIX host
temp dir is correctly refused. Update the round-trip cases to build real
directories whose trailing components are the allowed roots, and add two cases
that assert out-of-root paths are refused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wave5 security commit (8630c19) added five regression tests importing
'vitest', but vitest is not a dependency and the suite runner
(scripts/run-tests.cjs) executes every tests/*.test.ts through ts-node with
node:assert — so these files failed to compile (TS2307: Cannot find module
'vitest') and broke the whole suite.

Port all five to the node:assert harness AND make them exercise the REAL
production code instead of a local stub redactor: each now imports redactText
from ../src/redaction and mirrors the exact error-build logic of its target
(builderBridge diagnostics error, chipLoop exec error, llm Claude/Codex CLI
errors, gatewayOwnership conflict message). Assertions were corrected to match
verified production behavior (e.g. redactText masks via secret-pattern rules and
[REDACTED_PATH], not a blanket [REDACTED] sentinel; the ownership message
carries only the opaque ownerId with no separately-labeled hostname/pid field).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fake secrets used by redaction.test.ts / test_chip_loop_redact / launchMode tests
to assert redactText/maskSecret hide them. No real secrets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (acknowledged growth; extraction deferred to fleet-discipline round)
@vibeforge1111
vibeforge1111 changed the base branch from spark-compete/wave4-memory-persona-voice to main June 26, 2026 13:18
@vibeforge1111
vibeforge1111 marked this pull request as ready for review June 26, 2026 13:20
@vibeforge1111
vibeforge1111 merged commit 67ad9e6 into main Jun 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant