Skip to content

Anti-AI-decompilation hardening — complete slate (cohort + runtime link + off-device secret + oracle/impurity/per-unit-salt) - #5

Open
owengregson wants to merge 13 commits into
mainfrom
anti-ai-hardening
Open

Anti-AI-decompilation hardening — complete slate (cohort + runtime link + off-device secret + oracle/impurity/per-unit-salt)#5
owengregson wants to merge 13 commits into
mainfrom
anti-ai-hardening

Conversation

@owengregson

Copy link
Copy Markdown
Owner

Anti-AI-decompilation hardening — complete slate (8 layers)

Attacks the root capability a generalized AI decompiler uses, not the specific steps any one transcript took. Derived from a frontier-model deobfuscation transcript of a max-preset artifact + a 47-agent adversarial swarm.

Root cause (validated)

Ruam ships the decryptor with the ciphertext. A complete, deterministic, secret-free machine that must self-decode on the attacker's hardware — so the attack reduces to four primitives: P1 recognize, P2 lift-and-run, P3 observe-at-a-tap, P4 oracle (P4a internal = killable; P4b terminal I/O = unkillable).

Honest ceiling

A single self-contained file — or a co-resident bundle — cannot be made impossible to reconstruct (client-executable code is client-observable; passive reads are undetectable in JS). On-device levers RAISE WORK FACTOR and DESTROY automatability/transferability; the only cryptographic lever is an off-device secret. This PR is framed accordingly — no over-claims.

Every layer (each tested across many random seeds; full suite 2381 pass / 0 fail; typecheck clean; lib builds)

Layer What Primitive Honest scope
W0 source-map gate Directory/bundle mode deletes copied *.map (= original source) + strips sourceMappingURL. Default-on; --keep-source-maps. total-bypass closes a real cleartext leak
W1 hole-tolerant dispatch _ht[PH] | 0: a wrong-key decode → handler 0 instead of an "undefined opcode" tell. | 0 identity on valid indices ⇒ legit execution unchanged. P3/P4a anti-automation
W2 per-unit key salt Every unit gets a distinct key (per-unit salt in the serialized header, folded via rcDeriveKey). Closes same-metadata key reuse (shared position keystream). P2 real key-distinctness fix
W3 decode impurity Chained keystream: can't decrypt instr K without 0..K-1 (anti random-access). Behind a MANDATORY build-time self-equality gate (loud fail, never silent). Cache-only; throws if combined with cache-disabling features. Opt-in. P2/P3 anti-automation
W4-L1 cohort tangle obfuscateBundle() — a per-bundle digest folded into each file's key anchor. bundle-scope work-factor (mandated cross-file feature)
W4-L2 runtime link crossFileLinking: a consumer cannot run without its declared provider present in the same realm (strict, no fallback). bundle-scope work-factor; the literal "can't run alone"
W5 externalKeyBinding Fold a secret read from a host accessor at runtime; secret value never embedded. P2 offline cryptographic — the one human-forcing lever. Breaks offline use. Opt-in.

Locked product decisions (honored)

  1. External-secret lever (W5) and cross-file link (W4-L2) = opt-in (default-OFF, no preset).
  2. Decode impurity (W3) ships only behind a mandatory build-time self-equality gate — a build/runtime divergence fails the build loudly rather than emitting a silent miscompile (honoring the project's all-seeds / never-silent-garbage principle).

On the three previously-deferred items (now built, per request)

  • W2: shipped as a per-unit key salt (the achievable real form). Per-unit code variation stays out of scope — it would bloat size / add per-dispatch branches.
  • W3: scoped to the decode cache's linear forward pass — the only build==runtime-symmetric form of trace-coupling (genuine data-dependent global coupling is mathematically incompatible with build-time encryption; the gate would reject it). Its "corrupt-under-observation" goal is honestly unachievable for passive reads — it ships as an anti-random-access / anti-automation lever.
  • W4-L2: shipped as an explicit user-declared strict link (the co-residence "proof" is the caller's declaration). Still work-factor vs a whole-bundle attacker, but delivers the literal cross-file dependency.

Invariants respected

All-seeds correctness (no flaky tests), deriveSeed-only, NameRegistry for all identifiers, no eval/CSP-safe, no size-bloat, build==runtime fold symmetry, watermark untouched, additive API (obfuscateCode/obfuscateFile signatures unchanged).

Docs

  • Design: docs/superpowers/specs/2026-06-30-anti-ai-decompilation-design.md
  • Plan: docs/superpowers/plans/2026-06-30-anti-ai-hardening.md

🤖 Generated with Claude Code

owengregson and others added 11 commits June 30, 2026 03:43
Root-cause (Ruam ships the decryptor with the ciphertext; four attacker
primitives P1-P4) + honest ceiling, derived from a frontier-model
deobfuscation transcript and a 47-agent adversarial swarm. Slate W0-W5:
source-map gate, internal-oracle annihilation, per-unit heterogeneity,
gated decode-path impurity, mandated cross-file cohort + opt-in runtime
link, opt-in off-device external secret.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Directory mode copies the whole input tree, so .js.map files (carrying
sourcesContent = original source) shipped verbatim next to obfuscated
output. New centralized source-map-gate deletes *.map and strips any
surviving sourceMappingURL annotation. On by default; --keep-source-maps
opts out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Directory/bundle obfuscation now forms a cohort: an order-independent
digest of every file's source, folded with a per-build cohort seed, is
XOR-folded into each file's key anchor via the same seam integrityHash
uses (build keyAnchor ^= term; runtime _ka ^= term). Mirrors exactly =>
build==runtime symmetric across all seeds (verified 50 random-seed
round-trips).

New: compiler/cohort.ts (CohortContext/createCohort), obfuscateBundle()
public API, optional cohort param threaded through obfuscateCode/transform.
runVmObfuscation + CLI directory mode + index batch now route through the
cohort and the source-map gate. Per-file hermeticity preserved otherwise.

Honest scope: work-factor (raises static-correlation unit to the whole
bundle; identical source diverges per bundle/build). Not cryptographic
against a whole-bundle attacker — that is W4-L2 (runtime link) + W5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The one lever that crosses from work-factor to cryptographic hardness:
fold fnv1a(secret) into the key anchor at build, recompute fnv1a over a
host-provided accessor at runtime and fold identically. The secret VALUE
is never embedded (only the dotted accessor path), so the artifact cannot
derive its key without the correct runtime secret => offline AI
reconstruction is denied. Opt-in only (no preset), auto-enables
rollingCipher; reuses the integrity-hash/_ka fold seam (build==runtime
symmetric). Accessor emitted as plain member access (no eval, CSP-safe),
read wrapped in try/catch so an absent secret yields garbage not a crash.

Verified across random seeds: correct secret => correct output (25/25),
wrong/absent secret => denied (25/25), secret never appears in output,
invalid-accessor build-time dev gate. CLI: --external-key-value/-accessor.

Trade-off (documented): breaks offline use of the protected asset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tell)

Internal-oracle annihilation (W1, safe slice). The transcript's step 15
oracle: a wrong rolling-cipher key yields out-of-range physical opcodes,
so _ht[PH] is undefined and dispatch throws / lands outside the handler
map => the attacker KNOWS the key is wrong and self-corrects.

Slow-path resolution is now _ht[PH] | 0: an out-of-range/hole index
(only reachable under a wrong key) coerces undefined -> handler 0 and
dispatches a plausible-but-wrong handler instead of revealing the error.
This matches the decode-cache path, where Int32Array storage already
coerces undefined -> 0.

Provably safe: | 0 is identity on every valid handler index, so
legitimate execution is byte-for-byte unchanged. Verified across 4
slow-path configs (max / opcodeMutation / observationResistance /
incrementalCipher) x 4 programs x 12 seeds, plus the full 2365-test suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The literal 'a file cannot run without the others' lever. A declared
provider file writes a per-cohort secret to a shared global at load; each
consumer folds fnv1a(that secret) into its key (reusing the externalKey
fold). STRICT — no fallback: a consumer cannot decrypt without its
provider present-and-earlier in the same realm (verified 20/20 runs with
provider, 20/20 denied without).

API: obfuscateBundle(files, options, link?) where link = {provider,
consumers}; paths matched by exact-or-basename. CLI: --link-provider /
--link-consumers. Provider write is char-code (CSP-safe, no plaintext,
no eval), prepended on every return path incl. shielded/no-unit files.
Caller owns the co-residence contract (prove-or-don't-link by explicit
declaration); mis-declaring bricks the consumer by design.

Honest scope: still work-factor vs an attacker holding the whole bundle
(co-acquired = co-possessed), but raises the unit and delivers the
literal cross-file dependency. Default-OFF, opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…key reuse)

Two units with identical metadata (instr/reg/param/const counts) derived
the SAME implicit key => same position keystream => an attacker who
recovered one unit's keystream could decrypt another's same-position
instructions. Each unit now carries a per-unit salt (deriveSeed(seed,
'unitSalt:'+id)) in its serialized header, folded into the key anchor at
build and mirrored by rcDeriveKey's 'k ^= u.us' at runtime. Salts both the
instruction keystream AND the string-constant key (both derive from the
single rcDeriveKey(U) path), so symmetry is by construction.

New serialized u32 header field + _dsk temp + U.us. Verified: full suite
2370 pass; duplicate-metadata functions round-trip across rolling/medium/
max/incremental x seeds; per-unit salt distinctness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(W3)

The strongest on-device anti-agentic lever. Each instruction's decryption
keystream folds in an accumulator chained from every prior decrypted
instruction (linear decode order), removing the position cipher's
random-access property: an attacker cannot decrypt instruction K without
first decrypting 0..K-1, forcing full sequential per-unit simulation and
defeating cheap transferable automation.

Build==runtime symmetric because the decode cache materializes the stream
in a single linear forward pass at BOTH build and runtime (independent of
execution path), so the accumulator evolves identically. Gated to
cache-active builds; INCOMPATIBLE with incrementalCipher / opcodeMutation
/ observationResistance / vmShielding (each disables/bypasses the cache) —
combining throws at build time.

Every encoded unit passes a MANDATORY build-time self-equality gate
(assertChainedDecryptInverts) — a divergence is a loud build failure, never
a silent miscompile (honoring all-seeds / no-silent-garbage). Opt-in, no
preset, auto-enables rollingCipher. CLI: --decode-impurity.

Verified: 60 build==runtime round-trips across programs x seeds; gate fires
on tampered ciphertext; full suite 2381 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 30, 2026 12:01
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a multi-layer “anti-AI decompilation hardening” set of features for Ruam, centered around (a) preventing cleartext leaks in directory/bundle output, (b) increasing per-bundle/per-unit key diversity, and (c) adding optional runtime-bound keying mechanisms (cross-file linking + off-device secret binding), with test coverage for each layer.

Changes:

  • Add default-on source-map gating for directory outputs (delete *.map and strip sourceMappingURL comments), with --keep-source-maps / keepSourceMaps opt-out.
  • Add cross-file cohort support (createCohort, obfuscateBundle) and optional strict cross-file runtime linking (provider writes secret; consumers require it).
  • Add rolling-cipher hardening: per-unit key salt (wire format change), optional decode-impurity chained keystream with a mandatory build-time self-equality gate, plus optional externalKeyBinding folding a host-provided secret into the key anchor.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/ruam/test/security/source-map-gate.test.ts Tests source-map stripping and .map deletion across a directory tree.
packages/ruam/test/security/per-unit-salt.test.ts Tests per-unit salt behavior across duplicate-metadata units and seeds.
packages/ruam/test/security/oracle-annihilation.test.ts Tests hole-tolerant slow-path dispatch invariants across configs/seeds.
packages/ruam/test/security/external-key-binding.test.ts Tests externalKeyBinding secret non-embedding + runtime correctness/denial.
packages/ruam/test/security/decode-impurity.test.ts Tests chained rolling cipher + build-time self-equality gate + incompatibilities.
packages/ruam/test/security/cross-file-linking.test.ts Tests strict provider/consumer runtime link behavior for bundles.
packages/ruam/test/security/cohort.test.ts Tests cohort digest determinism + bundle build/runtime symmetry.
packages/ruam/test/helpers.ts Adds helpers for evaluating bundle output in isolated/shared contexts.
packages/ruam/src/types.ts Adds externalKeyBinding and decodeImpurity options + docs.
packages/ruam/src/transform.ts Threads cohort/link/external key terms into key anchor; adds obfuscateBundle; adds per-unit salt plumbing; supports decode impurity.
packages/ruam/src/source-map-gate.ts Implements .map deletion + sourceMappingURL stripping across output trees.
packages/ruam/src/ruamvm/builders/rolling-cipher.ts Runtime derive-key folds in per-unit salt (u.us).
packages/ruam/src/ruamvm/builders/interpreter.ts Implements decode-impurity chained keystream in cache materialization; adds _ht[PH] | 0 slow-path hole tolerance.
packages/ruam/src/ruamvm/builders/external-key.ts Emits runtime fold reading secret via accessor + FNV-1a into key anchor.
packages/ruam/src/ruamvm/builders/deserializer.ts Updates wire format to read/store per-unit salt field (us).
packages/ruam/src/ruamvm/assembler.ts Wires cohort term + external key accessor folds into runtime key anchor init; plumbs decodeImpurity option.
packages/ruam/src/presets.ts Excludes new options from presets typing; auto-enables rollingCipher for new features.
packages/ruam/src/naming/claims.ts Reserves _dsk temp key.
packages/ruam/src/index.ts Exposes obfuscateBundle/cohort APIs; routes directory obfuscation through bundling; runs source-map gate.
packages/ruam/src/compiler/rolling-cipher.ts Implements chained encryption + mandatory self-equality gate helpers.
packages/ruam/src/compiler/encode.ts Adds per-unit salt to unit header and folds into key derivation; supports chained rolling encryption option.
packages/ruam/src/compiler/cohort.ts Implements cohort digesting and resolved provider/consumer link material.
packages/ruam/src/cli.ts Adds CLI flags for keep-source-maps, decode impurity, external key binding, and runtime linking; changes directory mode to pre-read sources/cohort + gate maps.
docs/superpowers/specs/2026-06-30-anti-ai-decompilation-design.md Design spec describing threat model, layers, scope, and invariants.
docs/superpowers/plans/2026-06-30-anti-ai-hardening.md Implementation plan and wiring/testing strategy for W0–W5 layers.
CLAUDE.md Architecture notes documenting new hardening layers and their constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/ruam/src/ruamvm/builders/external-key.ts
Comment thread packages/ruam/src/cli.ts
Comment thread packages/ruam/src/cli.ts
Comment thread packages/ruam/src/cli.ts Outdated
owengregson and others added 2 commits June 30, 2026 17:20
…ider guard

Copilot review (PR #5):
- external-key.ts: generate the try/catch binding via nameGen() instead of the
  hard-coded "_eke" (NameRegistry invariant). The catch body is empty so there is
  no behavior change — this only removes the parallel-naming outlier.
- cli.ts directory mode: track per-file read success. A pre-read failure is now
  surfaced as an error and the file is SKIPPED in the write loop, so the copied
  original is never overwritten with output generated from empty input (the
  data-loss path Copilot flagged).
- cli.ts: surface a source-map gate failure as a visible warning instead of
  silently swallowing it — that gate is what prevents shipping original source via
  .map files, so a swallowed throw could leak source unnoticed.

Adversarial self-review (confirmed real defect in the touched path):
- The read-skip above interacts with the strict cross-file link. If the declared
  --link-provider is missing from the obfuscated set or fails to read, its runtime
  secret write is never emitted while consumers still bake in the strict (no-
  fallback) fold — so the consumers ship NON-RUNNABLE yet are reported as
  successes. Now fail loudly: abort before writing any consumer when the provider
  is missing or unreadable, and name the blast radius when the provider reads OK
  but fails to obfuscate. Normal (no --link) runs are unaffected (guard is gated
  on args.link).

All 2381 tests pass; typecheck clean; lib builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-contained prompt for a fresh agent to run the full
ideation -> adversarial critique -> spec -> plan -> TDD -> PR loop
for a superior anti-AI-decompilation layer. Centers invention on
AI-agent-specific weaknesses (lift-and-run, oracle-seeking,
pattern-matching, confident hallucination, fan-out, adversarial-input),
keeps gen-1's hard constraints (all-seeds build==runtime symmetry,
CSP-safe, NameRegistry, no size-bloat), and adds a hard product
boundary: server-free / no off-device secret. Reframes the gen-1
"honest ceiling" as a floor to beat while keeping honesty strict on
claims and tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 1, 2026 05:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.

Comment on lines +472 to 487
export function obfuscateBundle(
files: BundleFile[],
options: VmObfuscationOptions = {},
link?: CohortLink
): BundleFile[] {
// A cohort is created when there are 2+ files OR a runtime link is declared
// (a link needs a shared cohort even if the provider/consumer set is small).
const cohort: CohortContext | undefined =
files.length >= 2 || link
? createCohort(files as CohortFile[], generateCryptoSeed(), link)
: undefined;
return files.map((f) => ({
path: f.path,
code: obfuscateCode(f.code, options, cohort, f.path),
}));
}
Comment on lines +66 to +73
const { globby } = await import("globby");
const all = await globby(["**/*"], {
cwd: outputDir,
ignore: ["**/node_modules/**"],
absolute: true,
dot: true,
onlyFiles: true,
});
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.

2 participants