\\(oo)// bridgeline
// \\
A crawling agent harness in which page content structurally cannot become an instruction — and a claim you can falsify on your own machine in five seconds:
uvx bridgeline verify
That runs sixteen prompt-injection attacks through an undefended agent and through bridgeline, offline, with no API key. The undefended agent is hijacked by 14 of them. bridgeline leaks on 0. The exit code is the verdict.
An agent that reads the open web is reading attacker-controlled text. bridgeline's answer is not to detect that, but to make it not matter: a trusted planner commits the complete plan before any byte is fetched, and nothing read afterwards can add, remove, reorder or redirect a tool call.
That image is a real run against demo/serve.py, exported from the terminal
rather than mocked. The fixture page carries an injected instruction to fetch
attacker.test. The plan named its destinations before the page was read, so
the link is refused — and the refusal is the only thing on screen that glows.
A bridge line is the first silk thread an orb-weaver casts across a gap, before any web exists. Everything else is anchored to it, and nothing can exceed it.
Use it
uv tool install 'bridgeline[anthropic]' # or [gemini], or both; pip works too
bridgeline "Summarize the release notes at https://example.com/docs/notes and write the summary to notes/summary.md"
You get the committed plan first, and approve it, edit it in $EDITOR, or quit.
Every destination in it — the seed URL, every field of scope, every
write_file path — is a literal; every {slot} sits in a content position.
That is the whole invariant, on one screen, before anything is fetched.
--dry-run prints the plan as JSON and exits; --yes skips the gate.
Chrome goes to stderr and the answer to stdout, so bridgeline … > out.md gives
you a clean file, and a redirected stream gets no colour and no mascot.
With no API key it still runs, on deterministic stubs: the crawl is real and the plan is sensible, but the "answer" is an echo of page text rather than a summary. That configuration exists for the security measurement, not for use.
Two layers
| Mechanism | Question | |
|---|---|---|
| Layer 1 | plan-then-execute + capability gate | Is this call plan-derived at all? |
| Layer 2 | bouncer-core's ContractEngine, unmodified |
Is this argument value permissible? |
Layer 1 is the proof — the attack suite passes with Layer 2 switched off
(--no-guard, and the layer-1-only column of bridgeline verify). Layer 2 is
an independent contract witness over the frontier URLs Layer 1 structurally
cannot see.
A crawl cannot enumerate its URLs at plan time, which is what usually makes plan-then-execute useless for one. bridgeline's plan commits a destination space instead of a list: a discovered link is followed only if a pure function of the URL string and plan-fixed literals admits it. Untrusted pages choose which member of that space is visited. They can never choose a non-member.
Both layers are mine
That weakens the independence claim, so here is what to check instead of taking it on trust:
- Layer 2 is never load-bearing. The full acceptance suite passes with the
contract engine replaced by a
NullGuard(SPEC criterion S4). Whoever wrote Layer 2, the security result does not rest on it. - No private access, enforced rather than asserted. A test walks the AST of
every module in
src/and fails on any reach into another object's private attributes (S5). - The integration notes are complaints. FINDINGS.md records four things the engine made awkward — an undocumented by-reference retention, a permissive fallback that had to be refused, an unsynchronized engine, and a dependency split it needed. That is what a constrained consumer writes, not an author shaping both sides to fit.
What the claim was measured in
The security result is measured only with no API key: a deterministic
planner and a deliberately hijacked extractor that does whatever a page says.
bridgeline verify is that configuration. Classifiers, LLM judges and
instruction-hierarchy prompting are not used anywhere — not as a fallback, not
as defense in depth.
Running with a key is a configuration the security claim was not measured in.
Structurally it should be safer — a live extractor is less hostile than one
built to be hijacked. The genuinely new surface is the planner: with a key,
it is an LLM, and it is the thing that commits the scope. The plan gate is where
a scope broader than you meant gets caught. --yes skips it.
A live planner does target the plan schema: 13 of 15 blind-authored benign tasks produced a valid plan against Gemini 2.5 Flash. Extraction quality with a live model is untested.
What it does not claim
- Confidentiality of destinations, not integrity of routing. A malicious in-scope page decides which in-scope pages get visited and in what order, and can spend the page budget. It cannot send content anywhere the plan did not fix.
- An extracted URL is never fetched under the default policy, even a legitimate one. That is the price, not a bug.
- DNS rebinding between resolution and connection is a documented, open window.
- Sixteen attacks by one blind author is not AgentDojo.
The full accounting is in FINDINGS.md.
Reproduce everything
git clone https://github.com/Ezed9/bridgeline && cd bridgeline
uv sync --extra dev
uv run bridgeline verify # the attack suite
uv run python -m demo.utility_demo # 15 benign tasks, defended vs undefended
uv run pytest -q
- SPEC.md — the invariant and the success and kill criteria, committed before any implementation existed, and corrected once in the open
- FINDINGS.md — results, the narrow claim, and what it costs
- docs/mascot-variants.md — how the spider was chosen
bridgeline was called crawlgate until 2026-09-12. SPEC.md still uses that
name throughout, deliberately: it is a pre-registration. It has been amended
once, on 2026-09-06, to narrow an invariant that overclaimed — git log -p SPEC.md shows exactly what changed, and the commit message says why.
Python 3.12+. MIT licensed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bridgeline-0.1.0.tar.gz.
File metadata
- Download URL: bridgeline-0.1.0.tar.gz
- Upload date:
- Size: 179.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bdfb79c1dbfc124ae76c17f54012ef5bc719b22145c3f9063c7ec27495d714d
|
|
| MD5 |
88e08119b55e23abc3bf4cfd688a3b6b
|
|
| BLAKE2b-256 |
72a319514a818e7881452bd1c7d97d91c271615365762f72ffe614f7568e88e6
|
Provenance
The following attestation bundles were made for bridgeline-0.1.0.tar.gz:
Publisher:
release.yml on Ezed9/bridgeline
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bridgeline-0.1.0.tar.gz -
Subject digest:
9bdfb79c1dbfc124ae76c17f54012ef5bc719b22145c3f9063c7ec27495d714d - Sigstore transparency entry: 2814515296
- Sigstore integration time:
-
Permalink:
Ezed9/bridgeline@5fec156e82515585ad505c63204f1a3eba63e5e4 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Ezed9
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5fec156e82515585ad505c63204f1a3eba63e5e4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file bridgeline-0.1.0-py3-none-any.whl.
File metadata
- Download URL: bridgeline-0.1.0-py3-none-any.whl
- Upload date:
- Size: 55.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
914b734529030a100d49170261e445e9f97640765d4e88a90b26405ed3a6485f
|
|
| MD5 |
bafc252b3ca8f09bcca5d1936d58a7e8
|
|
| BLAKE2b-256 |
4cc8db2bb986d8c6e652b30b5fe9f61190569cc0cf33bd8f08b9ebec3a043f9c
|
Provenance
The following attestation bundles were made for bridgeline-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Ezed9/bridgeline
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bridgeline-0.1.0-py3-none-any.whl -
Subject digest:
914b734529030a100d49170261e445e9f97640765d4e88a90b26405ed3a6485f - Sigstore transparency entry: 2814515329
- Sigstore integration time:
-
Permalink:
Ezed9/bridgeline@5fec156e82515585ad505c63204f1a3eba63e5e4 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Ezed9
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5fec156e82515585ad505c63204f1a3eba63e5e4 -
Trigger Event:
push
-
Statement type: