← Back to selected work

System study / 01 · CipherLoop · historical f03a1e1

Keep the context.
Question the evidence.

Engineering a tool-heavy investigation workflow with hybrid model calls, periodic memory sweeping, and a narrow, testable evidence threshold.

Experimental framework · deterministic tests

View CipherLoop source ↗

Source inspected at f03a1e1. This study separates source inspection, mocked integration tests, and live behavior that remains untested.

Current contract / 14 September 2026

Two files. An independent reader.

CipherLoop feature revision c98c5d8 records the producer lifecycle in a JSONL ledger and commits to its bytes in metadata. TraceForge main 51af0f4 reads those two files without importing producer runtime modules, checking integrity and source locations.

Unavailable scanner output is an error, never a successful zero-finding result. Failed execution remains visible; altered ledger bytes are rejected. Integrity PASS establishes neither task success nor target safety.

Production-v2 contract · Independent reader and replay · Passing Linux baseline and production replay

The independent historical fixture oracle remains separate. No detection accuracy or hosted Windows verification is claimed.

Historical study / f03a1e1

The sections below preserve the 7 September source, test results, and immutable references. They describe the earlier revision, not the current production-v2 contract.

01 / The problem

An investigation needs more than an answer.

Tool-heavy workflows create two different kinds of material: a large working history that helps an agent choose its next action, and evidence a person can inspect after the run. Keeping everything in one message list makes those responsibilities difficult to separate.

CipherLoop implements a separation between active messages, accumulated finding summaries, and a disk-backed trajectory. A Python AST validator adds a second boundary: a scanner candidate alone cannot become an internally accepted finding.

The engineering constraint

Keep tool execution local, shorten active history between tool loops, expose the hybrid model boundary, and make at least one positive and one negative path reproducible without cloud credentials.

Implementation basis: state reducers and compressor.

02 / Actual architecture

Six nodes. Different trust boundaries.

The graph coordinates host-side Python code, a configured Ollama endpoint, Docker tools, and configured cloud calls. Local tool execution does not make the whole system local or private.

Execution & evidence boundariesRevision f03a1e1
Six-node graph: planner, local model, sandbox tools, compressor, AST validator, and synthesizer. Cloud calls and a separate trajectory recorder are labeled. The full flow is described immediately below.
Simplified architecture derived from the inspected source. Arrows show control flow; dashed lines show selected writes to a separate host-side record. This is not a live execution trace.
Read the complete flow in text
  1. Planner: calls the configured cloud provider with the target path, batch counts, and accepted finding metadata, then produces a tactical instruction.
  2. Local model: receives that instruction and the active message history through Ollama. Tool calls go to the host ToolNode, which invokes the Docker tools; raw tool responses return to the local model.
  3. Compressor: runs when the local model returns no tool calls. It records selected raw messages, generates summaries, and requests removal of active messages.
  4. Validator: parses candidates from accumulated summaries, reads the candidate file through the sandbox, and checks its Python AST on the host.
  5. Routing: return to the planner unless its plan is empty, contains “complete,” or the planner counter reaches 25.
  6. Synthesizer: with accepted findings, sends their full structured records to the cloud. With none, returns a fixed message locally. The graph then ends.
  7. Separate record: compressor and validator write selected events to JSONL. The CLI writes aggregate metadata after successful graph completion; this is separate from active messages and is not a graph checkpoint.

Source: graph edges, local loop, cloud payloads.

The scanner order matters.

The implemented scanner tool runs Semgrep first. A detected failure invokes a sandboxed ripgrep fallback. Compression and AST validation come later. The README’s AST-first / Semgrep-fallback description does not match this revision.

Fallback pattern matches are limited signals. A supplementary offline probe found that the fallback omits the severity field required by the compressor, so its result becomes zero retained candidates. Structured scanner errors are also lost in that summary. This is a current gap, not solved resilience.

Source: failure detection, ripgrep fallback; observed probe results.

03 / Engineering decisions

Three separations, three trade-offs.

Active history / durable evidence
Remove message history after a local tool loop while retaining selected raw messages on disk. This reduces retained conversation history at that boundary, but does not cap an ongoing tool loop or the accumulating summaries. The record is useful evidence storage, not a demonstrated recovery system.
Scanner candidate / AST acceptance
Use a deterministic Python pattern check before promoting a candidate. This makes the fixture path inspectable, at the cost of narrow syntax and data-flow coverage. It is not a replacement for semantic security analysis.
Local tools / hybrid reasoning
Run fixed tool commands through a Docker container while cloud models plan and synthesize. Provisioning requests a read-only target mount and no container network. Those controls do not prevent finding-derived information from entering cloud prompts.

The planner receives an absolute target path, counts, and accepted severity/ID/class metadata. Synthesis receives complete accepted finding records, including paths, taint paths, and scanner-derived descriptions. Ollama receives raw tool output during its local loop; its endpoint is configurable. The host-side trajectory can also retain source-derived content.

Source: state policy, prompt contents, sandbox provisioning and reuse.

04 / Deterministic evidence

A positive path. A negative control.

Tests ran against a Git archive of the recorded revision in an isolated Python 3.12.3 environment. The full suite passed all 20 tests, and the fixture pair passed separately. Fixture applications were read as text, never executed as web services.

Positive fixture

One accepted source-to-sink path

A mocked compressor supplies a candidate at the real fixture’s line 10. The real validator follows request input through ip into subprocess.run. The graph test asserts exactly one internal VERIFIED finding and a mocked report mention.

Negative fixture

No accepted source-to-sink path

A candidate points at a constant-input subprocess call. The real validator returns no finding. The real empty-findings synthesis branch returns its fixed report text without a cloud call.

Observed AST path · fixture text only

app.py:8:request.args.get
  → app.py:8:ip
  → app.py:10:subprocess.run

Derived by the unchanged validator. No shell command was executed.

These graph tests bypass the sandbox-tools branch. They do not exercise a real scanner or compressor, and they do not evaluate model behavior. Separate unit tests cover compressor output, mocked scanner failures, mocked mount identity, configuration, and synthetic trajectory aggregation.

Source: positive graph test, negative graph test, claim ledger.

05 / Validation boundary

What “VERIFIED” means here.

A candidate must parse as [SEVERITY] path:line - description. The validator reads that file through the sandbox boundary, parses its Python AST, and looks for a recognized input flowing to a recognized sink at the exact candidate line. It returns the first matching trace.

Recognized sources include request args/form/values/JSON, input(), sys.argv, and os.environ. Sinks include selected subprocess calls, os.system, os.popen, eval, and exec. Simple named assignments and expression children carry taint within a function; each function starts with a fresh binding map.

A defined threshold, not a security guarantee

The analysis does not resolve imports or aliases, propagate evidence across functions, model sanitizer behavior, or prove control-flow feasibility. It does not require shell=True and can match taint in any call argument. The fixed 0.9 confidence value is not a calibrated probability.

Supplementary offline probes matched both a shell=False list argument and a value wrapped in shlex.quote, while missing an aliased sink and cross-function flow. The safe fixture therefore demonstrates one rejected constant-input path—not general absence of false positives.

Source: validator and exact pattern sets; probe results.

06 / Memory & record

Shorter messages are not smaller total state.

The compressor keeps at most five ERROR/WARNING Semgrep summaries. Generic outputs longer than 3,000 characters are clipped at that threshold; shorter outputs retain up to 15 lines. It requests removal of every active message with an ID. A supplementary test of the real reducer removed both messages in a two-message tool exchange.

Meanwhile, compressed_findings and verified_findings use additive reducers. Batches accumulate, and validation revisits old candidates without deduplication. The local model does not receive the summary list after a sweep; the next planner request receives counts and accepted finding metadata. This is periodic history cleanup, not demonstrated prevention of context collapse.

The separate JSONL recorder captures raw tool responses and AI tool-call messages when compression runs, plus validation counts. The CLI finalizes metadata after the graph completes. It is not a complete recording of every graph node, and failure before finalization can leave an incomplete record.

compression_ratio divides aggregated raw character counts by serialized-summary character counts, excluding the added metric fields. The test’s 4.0 ratio comes from supplied numbers. It does not measure tokens, inference cost, retained meaning, or audit quality.

Source: compression rules, trajectory persistence, metadata arithmetic test.

07 / Limits & next steps

Make incomplete work explicit.

The workflow can finish after 25 planner cycles, an empty plan, or a plan containing “complete.” That substring also appears in “incomplete.” Local tool calls have no explicit per-loop budget, and the CLI has no finalization-on-error path.

With zero accepted findings, synthesis returns the same fixed message whether candidates were absent or scanner signals were lost. A failed or incomplete investigation can therefore sound more conclusive than its evidence supports.

Sandbox mount identity tests mock Docker. Existing-container reuse does not recheck network/read-only/image settings, and lexical path validation does not resolve symlinks. Resource limits, live offline Semgrep rule availability, and containment have not been validated in this milestone.

  1. Preserve outcome and provenance: carry complete/partial/failed status, scanner errors, and fallback signals through compression and reporting.
  2. Bound state deliberately: deduplicate accepted findings, retain usable references across sweeps, and add explicit tool-loop limits.
  3. Add an integration layer: exercise a synthetic target in Docker with resource/time limits and assertions for mounts, networking, rule availability, and cleanup before considering a live model run.

These are separate CipherLoop engineering tasks. The source application and its tests were left unchanged for this study.

Source: completion routing, report branches, CLI finalization.

Explore the repository ↗