Skip to content

Releases: ianclemence/lantern

lantern 0.1.6

Choose a tag to compare

@ianclemence ianclemence released this 02 Oct 05:38
d5f6c11

lantern 0.1.6

Every item on the 0.1.5 roadmap is now built, plus two security-hardening
passes over the agent's own attack surface, bounded multi-agent delegation,
full session CRUD, and a real daemon mode.

Roadmap, closed

  • AD / Impacket-style tooling (--offensive-gated): GetUserSPNs.py
    (Kerberoasting), GetNPUsers.py (AS-REP roasting), crackmapexec
    (SMB/WinRM/LDAP enumeration - a closed allowlist of read-only modules,
    no execution method ever accepted), bloodhound-python (AD relationship
    collection).
  • API-schema-aware testing: api_schema_scan walks an OpenAPI/Swagger
    document and resolves the real auth requirement per endpoint, honoring
    operation-level security: [] overriding a document default.
    graphql_introspect reports whether introspection is enabled and, if so,
    the schema's types and root names. Both passive - they read a schema the
    target already serves and call nothing it describes.
  • Cloud posture / container exposure: cloud_bucket_check (anonymous
    S3/Azure Blob/GCS listing, credential-free), container_expose_check
    (unauthenticated Docker daemon API or anonymous Kubelet API, one read-only
    request), host_kubehunter (active, --offensive-gated).
  • Daemon mode: lantern queue add/list/remove plus lantern daemon,
    a polling loop against Lantern's own SQLite file (no socket, no HTTP
    server) that claims and runs jobs one at a time and stops cleanly on
    SIGINT/SIGTERM.
  • Bounded multi-agent delegation: researcher/coder/pentester can call
    delegate_task to spin off one sub-investigation. Four properties hold by
    construction, not just by docstring: a delegated task's tools are a
    subset of the calling role's own list, it shares the parent's scope/
    --offensive grant exactly, it cannot delegate again, and it's capped at
    5 steps per call / 10 total per role turn. Now rendered in chat as a
    nested block (↳ researcher delegates: "...") and logged to the audit
    trail as its own event.
  • Full session CRUD: lantern delete <flow-id> --yes; chat's /new
    and /delete <flow-id> yes, exercised live under a pty.

Five more native tools, and role-scoped tool schemas

waf_fingerprint (WAF/CDN signature match from one ordinary response - no
payloads - so a zero-finding scan can tell "clean" apart from "a WAF ate
every probe"), secret_scan (regex match for cloud/SaaS keys and JWTs on
text another tool already captured, every hit masked), host_testssl,
host_gobuster, and host_amass (active, --offensive-gated - distinct
from the passive subdomain_enum below).

Registry::defs_for(focus) now scopes the tool schemas sent to the model
to each role's own focus list instead of sending all 26 tools' schemas to
every role - pentester's per-call overhead drops to ~2,656 tokens,
researcher ~1,898, coder ~953, reflector ~444, down from a flat worst case
that was already over 3,400 before this release added anything.

Passive subdomain discovery, and prompts that earn their own budget

subdomain_enum (certificate-transparency lookup via crt.sh - touches no
target infrastructure). A role whose prompt repeats enough of its own
vocabulary (named injection classes, GraphQL/IAM/Kerberoasting/JWT/
container-escape, ...) now earns up to 4 extra steps over its static
default. Engagement-profile prompts (WebApp/Api/InternalAd/Cloud/
NetworkInfra) add a short block naming the right taxonomy (OWASP, MITRE
ATT&CK, CIS) and what the toolset genuinely cannot check for that profile,
so a role says "no tool for that" instead of narrating a check it never ran.

Security hardening

  • Scope bypass, closed: a bare CIDR target (e.g. msfconsole's RHOSTS)
    was checked against only its network's base address, so a scope of
    10.0.0.0/24 waved through a target of 10.0.0.0/8. A CIDR target is
    now checked as the whole range it names.
  • DNS drift pinned to the audit trail: host-tool targets are resolved
    immediately before execution; a hostname that now resolves outside every
    declared scope range is refused, and the resolved IPs land in the command
    audit row (new resolved_ips column) and in lantern report either way.
  • Runtime limits now actually device-derived: concurrency and child
    memory limits were computed from the live device profile and then
    discarded in favor of hardcoded defaults. They now derive from the host's
    real cores/RAM unless LANTERN_CONCURRENCY/LANTERN_CHILD_MEM_MB is set.
  • Unbounded reads, closed on both sides: every in-process HTTP fetch
    facing a target (http_probe, waf_fingerprint, dir_bruteforce,
    subdomain/search lookups) buffered a full response before truncating -
    a hostile or oversized response could grow this process's own heap
    without limit. New capped readers stop at the limit instead of reading
    past it first. The same pattern is fixed on the model-provider side
    (Anthropic/OpenAI-compatible/Ollama embedding clients, the setup wizard's
    model-list fetch).
  • Target-facing User-Agent is now configurable (LANTERN_USER_AGENT),
    self-identifying by default.
  • Cross-process flow-id collision, fixed: flow IDs mixed only a
    millisecond timestamp and a per-process counter that resets on every
    start - two lantern run processes launched in the same millisecond
    produced the same flow ID and shared one row. The OS pid is now mixed in;
    verified against 20 real concurrent processes, 20 distinct IDs.
  • chat TUI: the approval card's default highlight flipped from
    "allow once" to "deny" (matching the module's own "deny-by-inaction"
    doc comment - a reflexive Enter no longer grants active testing);
    str::floor_char_boundary reimplemented locally rather than relying on
    a newer Rust than the declared 1.85 minimum; a Mutex guard held across
    an .await in the steering-queue drain is fixed (clippy
    await_holding_lock).

README

Rewritten twice for structure and to close out every section the roadmap
items touch - "Roles and delegation," "Session lifecycle," "Concurrency,
sessions, and the daemon," and a corrected "Detectability, traces, and the
agent's own attack surface" - plus GETTING_STARTED.md kept in sync with
the current tool and test counts.

Verified

  • 317 tests pass, 0 failed, 1 ignored (the live NVD query) across the
    workspace: lantern-core 58, lantern-llm 33, lantern-tools 109,
    lantern-agent 65, lantern-cli 52.
  • cargo check --workspace --all-targets --release: zero warnings.
  • lantern doctor on the reference device (4-core aarch64, 8.5 GiB RAM):
    reports the device-derived runtime (concurrency 4, child limit 503 MB,
    token budget 6000) and exits in ~91 ms.
  • Built and tested on this device at 2.8 GB / 29 GB free (under its own 20%
    floor) - doctor correctly reports UNDER THE FLOOR, confirming the
    0.1.5 behavior (the floor guards setup, not run/ask/chat) still
    holds.
  • Release binary: 9,464,360 bytes (aarch64).

Upgrade

git pull
cargo build --release          # ~/.cargo/bin/cargo if it is not on PATH
./target/release/lantern doctor

Schema migrates automatically to v3 (the resolved_ips column from 0.1.5's
work and the new queue table) - no manual step, and flows recorded on
earlier versions still render.

Attached: lantern-v0.1.6-aarch64-linux-gnu (9,464,360 bytes), built on
Debian 13 / aarch64, kernel 6.18. For anything else, build from source as
above.

lantern 0.1.5

Choose a tag to compare

@ianclemence ianclemence released this 29 Sep 06:45

lantern 0.1.5

A conversational terminal over the assessment flow — and assessments are no
longer blocked by the disk floor.

lantern chat

The same pipeline as run and ask, behind a screen. The transcript stays in
the terminal's own scrollback (no alternate buffer, so the terminal's native
scrolling reaches every previous message) while a ten-row viewport at the
bottom holds the status line, the live activity trail and the input:

 example.com |example.com recon|deepseek-flash |idle
 › Check TLS and headers, defensive only
   Enter send · Esc abort · / commands · ^C quit
  • / palette — /target /scope /roles /offensive /dry-run /steps /flows /clear /quit, filtered as you type, Tab completes, an exactly typed name
    wins over the highlighted row, and commands that need arguments stay
    composed instead of erroring on an empty one.
  • Live progress — the flow reports plan, role starts, each tool call and
    role finishes as they happen; the status line carries the running role, the
    step count and elapsed time.
  • Inline approval — active testing requested while the session does not
    allow it raises a card instead of a wall of text: 1 allows it for this
    flow only, 2 (or Esc, which is deny-by-inaction) keeps reconnaissance.
    The session grant is recorded on the per-flow config, so the runtime's own
    check agrees with the card.
  • Steering — a line typed while a flow runs joins the working set and
    cross-role memory at the next step boundary, so the roles still to run act
    on it.
  • Abort — Esc or Ctrl-C sets a flag the runtime checks per model step and
    per tool call. The flow reports aborted, keeps what it finished, and the
    stored report says aborted too rather than completed.

Nothing here is trusted either: without a terminal chat refuses and names
ask; there is still no prompt that can hang; roles cannot stop for questions
in chat (the UI owns stdin), so flows run non-interactively by design.

The disk floor guards provisioning, not work

The 20% free-space floor refused run, ask and chat on machines that were
already configured. Past setup, commands are bounded by the data-root and log
caps, so the floor was blocking work over unrelated disk usage.

It now guards the one step that can actually consume gigabytes —
lantern setup, which downloads packages and builds john from source. The
refusal names setup for the same reason. Reports stay informational:
lantern doctor still prints UNDER THE FLOOR, every startup line still
prints the numbers, and lantern gc still relieves the pressure.

Dependencies

The terminal is ratatui + crossterm (+ unicode-width), which is the Rust
counterpart of the framework this screen was modelled on. No new build tooling
enters the aarch64 build: no cmake, no ring, no bundled libclang.

Verified

  • 234 tests pass, 1 ignored (the live NVD query), 0 warnings — up from 227
    at 0.1.4. Seven new: composer editing on char boundaries, history, palette
    matching and picking, approval keys, line wrapping, plus a runtime test for
    progress events and one for abort semantics.
  • Driven through a real pty that answers cursor-position queries like an
    emulator: intent card, live trail, approval allow-once, Esc mid-flow
    ending in flow flw_… aborted with an aborted row and report in SQLite,
    and a clean session end. A real bug surfaced this way — the session grant
    was not reaching the runtime's process-level check, so a correctly allowed
    flow still warned about --offensive; fixed before release.
  • Floor behaviour verified at 5.21 GB free against the 6.16 GB floor:
    setup refuses naming itself, while run, ask and chat start, complete
    and write their reports.
  • Binary: 8,939,992 bytes (the screen added ~262 KB over the 0.1.4 asset's
    8,612,232). lantern doctor exits in 36–41 ms.
  • run --dry-run measured 0.29–0.94 s idle and up to 15 s while this host was
    loaded (load average 4.8 on four cores) — the spread is the host, not the
    binary, and it is stated here rather than averaged away.
  • No warnings across cargo check --workspace --all-targets, and no line
    added in this window exceeds the project's 100-column style.

Upgrade

git pull
cargo build --release          # ~/.cargo/bin/cargo if it is not on PATH
./target/release/lantern doctor

No database schema change: chat adds a flow status (aborted) and reads the
options already stored, so flows recorded on 0.1.0–0.1.4 still render. No new
required variables, and no change to run or ask flags.

Attached: lantern-v0.1.5-aarch64-linux-gnu (8,939,992 bytes), built on Debian
13 / glibc 2.41 / aarch64. For anything else, build from source as above.

lantern 0.1.4

Choose a tag to compare

@ianclemence ianclemence released this 29 Sep 05:53

lantern 0.1.4

The assessment flow now takes an instruction in words, not just flags.
lantern ask runs the same pipeline as lantern run, but the operator's
directive — a sentence or a whole framework — travels with the flow instead
of a fixed mission string.

lantern ask

lantern ask --target shop.example.com --scope "shop.example.com" --file framework.md
lantern ask --target example.com --scope "example.com" --prompt "Check TLS and headers, defensive only"
echo "Check DNS and TLS on example.com" | lantern ask --target example.com --scope "example.com"

The full text is stored verbatim as a retention-tracked directive.md
artifact with the flow; each role objective and the plan phase see its
3,000-character head, so a 35-section framework fits the 6,000-token working
set. A framework excerpt measured here reports 7,257 in / 16 out tokens (scripted - estimate) with peak 369/6,000 — headroom confirmed, not assumed.

Intent is read, authority is not moved

The prompt is reduced to intent deterministically — no model call, no tokens,
covered by tests:

  • action verbs and gated tool names (exploit, hydra, sqlmap, active
    testing) mean active testing; coverage nouns alone do not (a framework
    listing "privilege escalation" as a checklist item is a topic, not an
    instruction to escalate);
  • an explicit restraint (defensive only, do not exploit, read-only)
    always wins — even over --offensive;
  • contract fields (SYSTEM_NAME through SPECIAL_INSTRUCTIONS) parse out,
    and --target stays the authority: if it disagrees with the contract's
    TARGETS line, the run warns naming both and the flag governs.

The rule, stated once: the prompt can restrain --offensive, never grant
it. Ask for exploitation without the flag and you get reconnaissance plus the
flag named; pass the flag with a defensive-only prompt and the prompt wins.
Both cases say why, on screen, before anything runs:

instruction : Exploit the login form with hydra and run sqlmap against search
intent      : active testing requested
contract    : none declared (TARGETS etc. all empty)
note        : prompt asks for active testing but --offensive was not passed: running reconnaissance only (re-run with --offensive to allow it)

run and ask report a finished flow through one shared outcome block, so
the two lines at the end — spend and context — read identically either way.

Verified

  • 227 tests pass, 1 ignored (the live NVD query), 0 warnings — up from 213
    at 0.1.3. Fourteen new: ten for intent (including restraint-beats-verbs and
    nouns-are-not-instructions), three for the ask gate, one proving the
    directive artifact lands and the flow options record its length.
  • All four gate paths run live against --dry-run: recon restraint,
    exploit-ask downgrade, flag-vs-restraint, contract parse with eight fields,
    --target mismatch warning, and piped-stdin instruction.
  • run --dry-run: 0.34–0.99 s for the full pipeline; ask --dry-run:
    0.36–0.66 s. lantern doctor exits 0 in 36–39 ms.
  • No warnings across cargo check --workspace --all-targets, and no line
    added here passes the 100-column style.
  • The 20% disk floor did its job on the author again: at 5.82 GB free against
    a 6.16 GB floor, run refused with both numbers in the message — and the
    first "benchmark" pass of this release measured those 5 ms refusals, not
    runs. The timings above are re-measured after clearing regenerable build
    weight.

Upgrade

git pull
cargo build --release          # ~/.cargo/bin/cargo if it is not on PATH
./target/release/lantern doctor

No database schema change: the directive length joins the flow-options blob,
so flows recorded on 0.1.0–0.1.3 still render. No new required variables and
no new required flags — ask reuses --target, --scope, --roles,
--steps, --interactive and --offensive with their existing meanings.

Attached: lantern-v0.1.4-aarch64-linux-gnu (8,612,232 bytes), built on Debian
13 / glibc 2.41 / aarch64. For anything else, build from source as above.

lantern 0.1.3

Choose a tag to compare

@ianclemence ianclemence released this 29 Sep 05:23

lantern 0.1.3

A run now ends with what it cost and whether the token budget got in the way, a
scripted setup saves what the environment already knows, and the report
describes a flow that has actually finished.

The cost of a run, measured

findings: 3
report  : /home/ianclemence/.local/share/lantern/reports/flw_1a0eb8865620000.md
model   : 2,468 in / 112 out tokens - $0.0008
context : peak 91 / 6,000 tokens, 0 summarization(s), 0 budget stop(s)

Every model call now goes through one counted path, so the tokens are the
endpoint's own usage block rather than a number reconstructed afterwards.
Tokens print on every run; money prints only when you say what a token costs
where you buy it, because a rate recalled from memory would be a fiction with
a dollar sign on it:

export LANTERN_PRICE_INPUT_PER_MTOK=0.27    # USD per million input tokens
export LANTERN_PRICE_OUTPUT_PER_MTOK=1.10   # ...and per million output tokens

The context line is the instrument the defaults were missing: the peak working
set, how often it was compressed, and how often LANTERN_TOKEN_BUDGET ended a
role before it said it was done. A budget stop was previously invisible — the
role simply stopped.

A --dry-run prints the same line labelled (scripted - estimate, nothing billed), and that estimate now counts the tool schemas too, so it is a real
pre-flight size check instead of a floor.

The report stopped describing a flow that had ended

The report was rendered before the flow's own counters landed, so every
stored report claimed Status: running
and quoted whatever the rows
happened to offer at that moment — the 16-step, 38-tool-call example.com run
recorded 5 model steps and 0 tool invocations in its own report.

Status, counters and spend are now written to the row before it is read:

- Status: completed

## Summary

0 finding(s), 0 tool invocation(s), 2 model step(s), 2,468 in / 112 out tokens.

Context peaked at 91 of 6,000 tokens: 0 summarization(s), 0 budget stop(s).

A flow from before this release has no measured spend, so its summary omits the
token clause rather than claiming it spent nothing.

Setup, with and without a keyboard

With a terminal the three questions are unchanged. Without one there was
nobody to ask, so setup printed a generic "export the key" line and left the
machine unconfigured — the two questions it existed to answer were simply
dropped.

It now takes provider, model, endpoint and key from the environment (the same
environment-beats-file-beats-preset merge the run itself uses), checks them
with one real call, and stores them: config.json plus credentials at
0600. A second run reports already configured and calls nothing. When the
environment cannot finish the job, nothing is written and the message names
the one variable that would:

[skip] AI provider: no terminal here - export DEEPSEEK_API_KEY; run `lantern setup` in a terminal to be walked through provider, model and key

Also new since 0.1.2: lantern setup links the binary into ~/.local/bin so
the next terminal simply types lantern. It never edits your startup files —
if that directory is not on PATH yet, the export line is printed — and it
refuses to clobber a real file already called lantern.

GETTING_STARTED.md

The walkthrough is now in the repository rather than in a chat log: clone,
build, setup (both paths), doctor, the free dry run, the real read-only
assessment, reading the report, and what the two lines at the end of a run are
telling you to change — with a table saying where each default came from.

Verified

  • 213 tests pass, 1 ignored (the live NVD query), 0 warnings — up from 198
    at 0.1.2. README and walkthrough quote the same number.
  • Measured on this host: a call starts at 3,034–3,290 tokens before the
    conversation has said anything
    — 2,770 tokens of schema for the 20 tool
    definitions sent on every request, plus 264–520 for the system prompt. A
    test now fails if that overhead grows past 4,000.
  • Cost line proven against a local stand-in endpoint (no key, no spend):
    2,468 in / 112 out at 0.27/1.10 renders as $0.0008, and the stored flow
    options carry the same numbers.
  • Both scripted-setup paths run live: complete environment → connected in 1 ms → config.json + credentials at 0600; no key → the skip line above
    and nothing written.
  • run --dry-run: 0.3–0.5 s for the full pipeline (one cold run 1.6 s), report
    written, nothing billed. lantern doctor exits 0 in 36–38 ms.
  • The 20% disk floor earned its keep while this was being built: at 5.98 GB
    free against a 6.16 GB floor, run refused with both numbers in the message
    rather than starting and failing halfway.
  • No warnings across cargo check --workspace --all-targets, and no line this
    window added exceeds the project's 100-column style.

Upgrade

git pull
cargo build --release          # ~/.cargo/bin/cargo if it is not on PATH
./target/release/lantern doctor

No database schema change, so flows recorded on 0.1.0–0.1.2 still render —
their reports keep the text they were written with. No new CLI flags, and no
new required variables: the two price variables are optional, and DEEPSEEK_API_KEY
(or the wizard's files) still works exactly as it did.

Attached: lantern-v0.1.3-aarch64-linux-gnu (8,546,696 bytes), built on Debian
13 / glibc 2.41 / aarch64. For anything else, build from source as above.

lantern 0.1.2

Choose a tag to compare

@ianclemence ianclemence released this 29 Sep 03:54

lantern 0.1.2

Ten model providers behind one preset table, and a setup wizard that turns
"which endpoint, which model, which key" into three questions asked once.

Providers

id endpoint key variable default model
deepseek https://api.deepseek.com DEEPSEEK_API_KEY deepseek-flash
openai https://api.openai.com/v1 OPENAI_API_KEY gpt-4o-mini
anthropic https://api.anthropic.com ANTHROPIC_API_KEY claude-sonnet-4-5
gemini https://generativelanguage.googleapis.com/v1beta/openai GEMINI_API_KEY gemini-2.5-flash
openrouter https://openrouter.ai/api/v1 OPENROUTER_API_KEY deepseek/deepseek-chat
groq https://api.groq.com/openai/v1 GROQ_API_KEY llama-3.3-70b-versatile
mistral https://api.mistral.ai/v1 MISTRAL_API_KEY mistral-small-latest
xai https://api.x.ai/v1 XAI_API_KEY grok-4
ollama http://127.0.0.1:11434/v1 none (local) first model it lists
custom your own URL LANTERN_LLM_API_KEY (optional) you name it

A preset carries the endpoint, the key variable and a model shortlist, so
switching providers means naming an id instead of memorising a URL. Order of
precedence is environment, then the wizard's files, then the preset — a preset
only supplies what the operator did not, and a key saved for one provider is
never sent to another.

Nine presets speak chat completions; anthropic speaks the Messages API, which
needed a second client: system turns move to a top-level field, tool results
become tool_result blocks inside one user turn (consecutive turns merge,
because the endpoint requires strict alternation), and tool_use input arrives
as an object rather than a string.

Setup asks three questions

lantern setup now opens with the model half, because that is the part only
you can answer:

  1. provider — all ten listed with their endpoints, current one marked;
  2. model — the endpoint's own model list is fetched live, so the menu shows
    what that account can actually call; the preset's shortlist is the fallback
    when it will not answer, and m lets you type any other name;
  3. key — read with the input hidden, then checked with one real 16-token
    completion. A key that cannot reach its endpoint is not worth storing, and
    the check costs one call instead of a failed flow later.

The key lands in ~/.config/lantern/credentials at mode 0600 — pinned again
after every write, because an existing file keeps whatever mode it was created
with. Provider, model and endpoint go to config.json beside it, which holds
no secret. Both files sit outside the data root, so neither is ever in the
database, a log, a trace or an artifact, and LANTERN_CONFIG_DIR relocates
them.

With no terminal attached the wizard prints one line and stands down, so a
scripted lantern setup still provisions and never blocks. lantern doctor
now reports provider / model @ endpoint [where the key came from] without
printing the key.

Also in this window

  • The development-only judgement client is gone from the product: config, env
    vars, doctor row and README rows deleted; the reflector does a plain review
    and reports reviewed N finding(s) (or no findings to review).
  • The device profile no longer has a SoC vendor in its CPU whitelist — such a
    host would have been reported as if the SoC were the CPU. It gets the neutral
    core-and-architecture line instead.
  • lantern --version reports the real number: 0.1.1 shipped a binary that
    called itself lantern 0.1.0.

Verified

  • 198 tests pass, 1 ignored (the live NVD query), 0 warnings — up from 166 at
    0.1.1.
  • The whole wizard was driven through a real pty: provider menu, model list,
    masked key input, a live 401 from the endpoint with the key redacted in the
    message, save anyway, then files on disk — credentials at 0600,
    config.json free of secrets.
  • Two mutation checks: the cross-provider key test and the Anthropic turn
    merging test both fail with their fix removed and pass with it.
  • Precedence spot-checked on the built binary: LANTERN_LLM_PROVIDER=groq
    overrides the saved file and pulls Groq's preset defaults; doctor labels
    credentials file vs GROQ_API_KEY (environment).
  • lantern setup < /dev/null prints the skip line and provisions anyway.
  • run --dry-run: 0.635 s, report written. lantern doctor exits 0 in 35–37 ms
    (5 runs) with 18.8 MB peak RSS and 12 native tools.
  • The intermediate commit builds on its own: clean-worktree cargo check,
    exit 0.

Upgrade

git pull
cargo build --release          # ~/.cargo/bin/cargo if it is not on PATH
./target/release/lantern doctor

Nothing for existing setups to migrate: DEEPSEEK_API_KEY in the environment
still works exactly as before, since deepseek remains the default preset, and a
fresh clone still boots with no configuration at all. The database schema is
untouched, so flows recorded on 0.1.0 and 0.1.1 still render. No new CLI flags —
lantern setup still takes none.

Attached: lantern-v0.1.2-aarch64-linux-gnu (8,546,696 bytes), built on Debian
13 / glibc 2.41 / aarch64. For anything else, build from source as above.

lantern 0.1.1

Choose a tag to compare

@ianclemence ianclemence released this 29 Sep 02:28

lantern 0.1.1

Fixes from the first live run. The pipeline was pointed at a public
documentation host, timed and priced end to end, and it produced one finding,
two warnings and a report whose numbers were invented. Each defect was
reproduced at the root before it was fixed.

Same target, before and after

first run after the fixes
wall time 211.4 s 152.4 s
model steps 11 16
findings 1 3
warnings 2 0
plan fell back to the default written by the model
researcher failed on call 9 6 steps, 1 finding
report counters 4 invocations / 5 steps 38 invocations / 16 steps
cost $0.01 ≤ $0.01

The four defects

1. The planner answered with nothing. Asked for 500 completion tokens, the
model returned finish_reason: length with 500 reasoning tokens and an empty
string — a reasoning model spends the budget thinking before it answers, and
the empty plan fell back to the default. complete() now asks with headroom
(4×, floor 2 048), retries a blank reply once at double the budget, and
reports an error instead of handing an empty string to the caller.

2. The researcher died on its ninth call. Token-based context trimming
could cut an assistant message and keep its tool reply, which the API rejects
outright ("Messages with role 'tool' must be a response to a preceding message
with 'tool_calls'"). Trimming and summarization now drop replies whose request
was cut away, so every window slice keeps call and reply paired.

3. nikto never scanned. host_nikto passed -Format txt with no output
file, and nikto 2.1.5 aborts before scanning in that case
(Unable to open '' for write, exit 2, 0.4 s). The format flag is gone —
screen output is what the executor captures — and -maxtime now caps the
scan below the adapter's timeout so it reports what it found instead of being
killed mid-run.

4. The report counted rows, not work. "5 model step(s)" was the number of
database rows and "4 tool invocation(s)" counted child processes only, so
in-process tool calls were invisible. The runtime now records real counters on
the flow and the report quotes them, falling back to row counts for flows
recorded before this release.

Verified

  • 166 tests pass, 1 ignored (the live NVD query), 0 warnings — up from 158.
  • The two context-window tests are mutation-checked: both fail with the fix
    removed and pass with it.
  • The corrected nikto argv was run against a real host: flags parse, the scan
    caps itself, findings still print, exit 0.
  • Binary 8,481,128 bytes (8.09 MB); lantern doctor exits 0 in 53 ms with
    2 MB peak RSS and 12 native tools.

Upgrade

git pull
cargo build --release          # ~/.cargo/bin/cargo if it is not on PATH
./target/release/lantern doctor

Nothing in the CLI, the configuration or the database schema changed: flows
recorded on 0.1.0 still render, using the row-count fallback for their
summary line. For what this project is and how it is installed, see the
0.1.0 notes.

lantern 0.1.0

Choose a tag to compare

@ianclemence ianclemence released this 29 Sep 01:47

lantern 0.1.0

First tagged release. lantern is a native, autonomous security assessment
agent for small Linux hosts: one binary, one SQLite file, no services to run.
The pipeline — plan → recon → check → verify → reflect — executes as host
processes on the machine you point it at, through a CLI.

What landed

Real exploitation capability

  • Host tools behind the scope gate: host_nmap, host_nikto,
    host_tcpdump, host_sqlmap*, host_hydra*, host_nuclei*,
    host_msfconsole*, host_john* — those marked * only under --offensive.
  • lantern setup provisions the machine in one command: distribution
    packages, john built from source (328 formats instead of the handful a
    package ships), nuclei with its template set, and the sandbox launcher.
    Metasploit is an opt-in repo. With no passwordless sudo it prints exactly
    what is missing rather than failing halfway through.
  • lantern doctor reports device, budget, integrations and every tool path
    without touching the network.

A model that remembers, asks, and re-plans

  • Two memory namespaces: per-engagement scope keyed exactly like the runtime's
    scope, and a global namespace for reusable guides — so a guide written once
    reaches every later engagement. Semantic recall through local embeddings,
    with an FTS5-only fallback that warns instead of pretending.
  • ask_operator under --interactive: the model can ask the person running
    the assessment mid-flow, wait up to 300 s, then carry on unanswered. A stale
    environment variable can never make a silent run answer itself.
  • The plan is shared state. Every role reads the current version at prompt
    time, the planner's ordered rewrite is adopted when it comes back as a real
    list, and plan_patch adds or drops a step once reconnaissance proves the
    plan wrong. The amended plan is what gets reported.

Search and compute

  • web_search with mode: vulnerability: keyless NVD 2.0 keyword search
    merged ahead of the web results, relevance-filtered to the query tokens,
    sorted by CVSS, half the result budget.
  • code_run for the coder: one Python script, standard library only, behind
    bwrap — empty network namespace, read-only filesystem, only the flow's own
    directory writable. Computation only; there is no way for it to reach a
    target. The script text is stored as an artifact; the binary and its entire
    argv are fixed in code.

Safety properties

  • Never a shell: Command::new().args everywhere, argv arrays only, a binary
    allowlist, rlimits (CPU, AS, FSIZE, NOFILE, NPROC), a per-flow working
    directory, a timeout, an output cap, and every child audited to SQLite and
    JSONL.
  • --offensive and the scope gate are checked before any active tool body
    runs; gating is derived from the registry, not a hand-kept list.
  • API keys live in the environment and are never written to disk.
  • Data root capped at 1.28 GB, logs at 200 MB, artifacts expire after 7 days,
    findings retained. The disk floor is never touched.

Verified on this build

  • 158 tests pass, 1 ignored (the live NVD query), 0 warnings.
  • Binary: 8,481,128 bytes (8.09 MB) — thin LTO, a single codegen unit, panic
    on abort, stripped.
  • lantern doctor: exit 0 in 53 ms, 2 MB peak RSS, 12 native tools.
  • The sandbox is asserted by the suite itself: no connection out, no write
    outside the flow directory, and rejected input never spawns anything.

Build and run

git clone https://github.com/ianclemence/lantern && cd lantern
cargo build --release              # ~/.cargo/bin/cargo if it is not on PATH
./target/release/lantern setup     # provision the host tools, once
./target/release/lantern doctor
export DEEPSEEK_API_KEY=...        # environment only, never on disk
./target/release/lantern run --target example.com --scope "example.com"

--dry-run walks the entire pipeline without a key or a single network call,
which is the fastest way to see what the report looks like.