Skip to content

feat!: Osiris v0.6.0 — conversation productization engine over cf-ng - #70

Open
padak wants to merge 31 commits into
mainfrom
claude/osiris-ai-engine-rebuild-e18f60
Open

feat!: Osiris v0.6.0 — conversation productization engine over cf-ng#70
padak wants to merge 31 commits into
mainfrom
claude/osiris-ai-engine-rebuild-e18f60

Conversation

@padak

@padak padak commented Aug 10, 2026

Copy link
Copy Markdown
Member

Rebuilds Osiris around cf-ng as the connector substrate. Same goal as v0.5.x — AI designs once, the runtime executes deterministically — new technology and new architecture.

484 files, +11,702 / −123,289. Production code goes from 48,700 lines to 2,448.

Why

You hold an exploratory conversation with an AI about your own environment — which leads are in Salesforce, which opportunity moved through PoC fastest, how is headcount developing. The conversation ends when you find the answer. Then you want to automate what it taught you, on a schedule, and for something running every 15 minutes you do not want an LLM: it is mostly unnecessary, non-deterministic, and costs money on every tick.

cf-ng makes ~700 connectors callable by an agent in real time. It deliberately stops there — verified by grep, not inferred: no run record (the vault schema has no runs key), no run id in responses, telemetry is one stdout line with no arguments/result/duration/outcome, no idempotency, no orchestration, no scheduling, no result persistence, no packaging, no deployment artifact.

This PR is the layer that turns a cf-ng conversation into a fingerprinted, replayable, explainable artifact.

Design: docs/design/osiris-0.6.0-engine.md.
Plan: docs/superpowers/plans/2026-08-10-osiris-060-walking-skeleton.md.

The honest starting position

v0.5.4 could not execute a pipeline. RunnerV0.RunnerContext was defined inline inside a method and exposed only output_dir and log_metric, while all seven drivers called ctx.get_db_connection(). ProxyWorker.SimpleContext had the same gap. Local and E2B execution raised AttributeError for every migrated driver, and the integration tests that would have caught it were pytestmark = pytest.mark.skip(reason="Integration tests need rewrite for FilesystemContract v1 API") — a plausible-sounding reason that silenced the only real check.

osiris compile did work and was deterministic. That is the dividing line this PR harvests along: the compilation spine lives, the execution layer was dead.

What it does

Three deployment units, none holding an LLM API key:

Unit Where Lifetime
osiris serve your machine, local MCP server while authoring
build/<hash>/ git, a registry, a directory immutable, hash-addressed
osiris run your runtime every scheduled tick

serve relays your agent's cf-ng tool calls and records every one — arguments, result shape, tool schema hash, duration, outcome. freeze validates a plan against both those recordings and cf-ng's live schemas, pins what it can, fingerprints, and emits build/<hash>/. run verifies the pins before the first call and executes against cf-ng, passing data between steps through a per-run DuckDB file.

The engine is in the path during exploration on purpose: its differentiator is evidence, and something not in the path cannot produce evidence — only accept claims.

Reviewing this

The diff is large but mostly deletion. Suggested order:

  1. docs/design/osiris-0.6.0-engine.md — the design, especially §4.3.1, which states each guarantee with its bound.
  2. osiris/ — 2,448 lines across 8 subpackages.
  3. docs/reports/2026-08-10-v060-adversarial-verification/ — two rounds of adversarial verification and what remains open.

Adversarial verification

The implementation was attacked twice by independent agents instructed to refute each guarantee by breaking the implementation rather than reading the tests.

Round 1 refuted all five, while 139 tests passed and ruff/black/bandit were clean:

  • only fingerprints["plan"] was verified — [pins] and [manifest] were written and never read, so recomputing one hash with the repo's own public API let tampered SQL run at exit 0, and the ledger recorded the pre-tamper hash
  • a set anywhere in a draft made the hash PYTHONHASHSEED-dependent: 8 processes, one draft, 8 hashes
  • four live token leaks to disk, while the redaction test greped only the one file already correct
  • adding an outputSchema after freeze was undetectable; removing one aborted correctly

Round 2 refuted all five again, on different and deeper defects — foreign cfng_ tokens unredacted, a policy field that silently disabled the abort while the evidence still claimed verification, annotations excluded from the tool pin, ToolPin the one un-sealed model, and an mcp>=1.2.1 floor that could not run the code.

Both rounds are fixed. Round 3 was deliberately not run: adversarial verification does not converge, so the stopping point is fixing what is cheap and unambiguously wrong, then stating the guarantees at the strength the implementation actually supports. Round 2 also confirmed what holds — determinism across process, cwd, TZ, locale, PYTHONHASHSEED and clock (5 interpreters, 3 fake clocks, 120 fuzzed drafts, zero variance and zero collisions), semantic sensitivity across 27 probes, 11 reformatting attacks correctly ignored, abort-before-first-call real and covered.

Known bound, stated in the design: artifact integrity is an unkeyed checksum stored beside what it protects. Every partial edit is caught; an attacker who rewrites every file and renames the directory is not. That needs a signature.

CI

Five workflows are removed. None of them could fail a PR: research.yml was the only one invoking pytest and carried continue-on-error at job and step level plus || true; the other four were path-filtered on osiris/mcp, osiris/remote and tests/e2b, all deleted, so they could never trigger.

ci.yml replaces them with one blocking gate — lint, security, tests on 3.11 and 3.13, and a job that builds a wheel and installs it into a fresh interpreter from its own declared dependencies. That last one is not ceremony: the audit found the suite passing against a stale v0.5.4 virtualenv still carrying e2b, supabase, openai and pandas.

Also: detect-secrets scan --baseline X rewrites X and exits 0 whatever it finds. Both the old workflow and the old make target ran exactly that, so neither could fail. Both now run detect-secrets-hook, verified to pass clean and reject a planted GitHub token.

Breaking changes

  • The v0.5.4 CLI is gone. chat, compile, validate, logs, mcp, connections, components no longer exist; the CLI is init, serve, freeze, run, doctor.
  • OML is gone, replaced by the osiris/v1 Plan format.
  • The in-house connector and driver layer is gone (cf-ng brings ~700; v0.5.4 had 9, which its own modernization note called a losing position).
  • E2B is gone. Docker is the packaging format, not the execution boundary — if steps are cf-ng HTTP calls, you are not sandboxing untrusted code.
  • Artifacts frozen before 52b3f01 will fail verification: adding annotations to the tool pin changed every pin value.

Depends on

  • keboola/cf-ng#25 — expose connector version in the tool descriptor (unblocks hard pinning; Airbyte sources pip-install unpinned at first call)
  • keboola/cf-ng#26 — populate MCP tool annotations (unblocks automated retry; cf-ng has no idempotency, so a retried timeout can double-send)
  • keboola/cf-ng#27 — preserve structured upstream error information instead of a fixed 502 "Upstream provider error."

Not in this PR

Scheduling (cron/Actions/Keboola orchestration runs the artifact), the plugin/skill served by the engine, Docker packaging, AIOP export, osiris replan, pagination for volume, artifact signing. Open items with cost estimates are in ROUND-2.md.


Open in Devin Review

padak added 30 commits August 10, 2026 16:28
Rebuilds Osiris around cf-ng as the connector substrate: the engine becomes
a relay that records an agent's exploratory conversation, freezes it into a
fingerprinted plan, and runs it deterministically without an LLM.

Key decisions:
- cf-ng owns connectors and credentials; the engine owns evidence and determinism
- host agent owns the LLM loop; the engine ships a skill, never an API key
- recording relay (not post-hoc compilation) so evidence is ground truth
- four pin classes with distinct drift policies instead of a binary claim
- Docker as a packaging format, not an isolation boundary; E2B dropped
- ~19k LOC of drivers, connectors, remote/ and chat deleted in the same initiative

Depends on keboola/cf-ng#25, #26, #27.
Reverses the earlier decision to hold intermediate data in memory. ADR-0043's
DuckDB data bus is retained: its design is the best-measured in the repo
(~1.5M rows/s, 98% memory reduction) and only its wiring is broken, so fixing
the shared RunContext becomes the first deliverable of phase 1.

Adds section 4.5 on volume. cf-ng's read caps are env-configurable defaults,
not ceilings; the binding constraint is the synchronous inline-payload shape of
POST /tools/call, which raising a cap does not change. Engine-side pagination
into DuckDB is the v1 mechanism; a cf-ng bulk read path (streaming, async job,
or cf-ng#11 land-to-Storage) is recorded as a new dependency.

Adds section 12 with worked examples: the authoring loop, cron/GitHub Actions/
Keboola orchestration hosts, and what a drift failure looks like.
12 tasks, 64 bite-sized TDD steps covering phase 0+1: clear the v0.5.4 tree,
harvest the determinism core and filesystem contract, build the shared
RunContext that v0.5.4 lacked, the cf-ng client with pin capture, the plan
model and freeze, the runner with pin verification, the recording MCP relay,
the CLI, and a round-trip guarantee test.

Phases 2-5 get their own plans.
Deletes drivers, connectors, remote (E2B), mcp, runtime, core, cli,
components, prompts, prototypes and tools along with their tests --
362 tracked files, ~50k lines of production code and ~53k of tests.
Harvested modules are re-created from the implementation plan rather
than imported, so nothing depends on the deleted tree.

Also prunes 62 stale ruff per-file-ignores pointing at deleted paths,
which is what kept 'make lint' red after the deletion.

BREAKING CHANGE: the v0.5.4 CLI and OML pipeline format are gone.
Harvested from v0.5.4 with require_fingerprint added, so verification has
a caller that raises instead of a helper nobody invokes. v0.5.4 computed
fingerprints faithfully and called verify_fingerprint nowhere at runtime.
Found during Task 2 execution: the plan's fingerprint_dict source fails
make lint as written, because PL is in ruff's select and only tests/ and
scripts/ carry a per-file ignore.
Pins come from GET /connectors/{id}/tools, not the MCP gateway, which
rewrites inputSchema to inject credentials and credentials_label -- a
gateway-derived hash would drift whenever a connector's credential
schema changed, even though the tool itself had not.
…reams

Appends take flock(LOCK_EX) with write/flush/fsync inside the lock, so
concurrent writers cannot interleave a partial line -- stress-verified at
20 trials x 128 records x 40KB payloads x 16 threads.

Session redacts at write time and has no module-level current-session
global; v0.5.4 had one with a comment admitting a thread-local would be
better. The session is passed explicitly instead.
slugify collapses every traversal character, so path escape is
structurally impossible rather than checked after the fact. The regex
preserves underscores because run ids use them as structural separators
(run_<ts>_<hex>); hyphenating them would decouple a run's log directory
from the id recorded in the run index.

The plan's path-escape test was vacuous -- Path.parents is lexical, so it
passed for paths that genuinely resolve outside base_path. Rewritten to
assert no '..' survives, resolved.is_relative_to(base), and no silent
collapse onto base, across 6 hostile inputs x 5 path builders.
Replaces the two divergent inline contexts of v0.5.4, neither of which
provided get_db_connection() while all seven drivers required it -- which
is why v0.5.4 could not execute a pipeline at all.

Characterizes DuckDB's locking, which constrains later phases: within one
process a second context on the same path is an ALIAS (shared instance,
no isolation), across processes the file lock is exclusive and raises.
The lock releases cleanly on close. The data bus therefore has a single
process owner.
Measured during Task 7: in-process second contexts are aliases with no
isolation; cross-process opens are locked out and raise. Constrains the
containerized runtime and any future per-step process split.
Every relayed call is recorded with its arguments, tool schema pin,
outcome and duration, so freeze is grounded in ground truth rather than
the agent's recollection.

Schema pins come from the REST manifest and are cached per connector,
so a two-tool connector costs one manifest fetch.

The plan's MCP wiring targeted SDK v1 and does not run on the installed
mcp 2.0.0: the @server.list_tools()/@server.call_tool() decorators no
longer exist, handlers are constructor kwargs taking (ctx, params) and
returning result models. Rewritten against the real SDK and covered by an
in-memory client/server round trip.
Freeze validates every cfng_call against the live tool manifest, pins the
tool contract and catalog version, and rejects literal secrets outright.

Determinism is proven rather than assumed: the plan's own test can pass by
coincidence when two freezes land in the same wall-clock second, so the
test monkeypatches datetime to 2001 and 2050 and still requires an
identical manifest hash. Verified again across two independent processes,
which rules out dict-ordering and hash-randomization dependence.
…stalled 2.0.0

The decorator API in the plan does not exist in mcp 2.0.0. Points readers
at the shipped osiris/relay/server.py as the correct reference.
The previous commit landed the lint-trap constraints but missed this
warning because the function signature in the plan differs from what the
patch script matched.
Pins are verified before the first tool call; contract drift aborts with
nothing called. assert is first-class so a silent upstream change stops
the run instead of producing an empty result.

The plan's rows_to_arrow was broken three independent ways on DuckDB
1.5.5: the relation is built on the module default connection and cannot
be registered on the run's connection; the resulting shape is a single
JSON column named j, not the row's columns; and it holds the payload in
memory twice. Replaced with NDJSON via read_json_auto(sample_size=-1),
which also leaves a durable per-step artifact. sample_size=-1 is
load-bearing: a sampled scan drops keys that first appear in late rows.

Identifiers are quoted -- 'fetch' is a DuckDB reserved word, so the
plan's own step id would not parse unquoted.
'fetch' is reserved in DuckDB 1.5.5, so the plan's own draft plans would
raise ParserException. Affects Task 9's and Task 12's DRAFT.
…nce mismatch

The plan minted a run id in the CLI for the evidence directory while
Runner.execute() minted a second, different one that went into the
ledger. A runs.jsonl row therefore named a run id matching no directory
on disk -- the ledger could not lead you to its own evidence. The
invocation id is now used consistently for ledger, directory and output.

The CLI also verifies the manifest fingerprint against the
fingerprints.json beside it before executing. Reading a manifest and
ignoring the fingerprint file next to it is exactly the v0.5.4 failure
this rebuild exists to end.

Other corrections: config and credentials are checked before arguments
and all missing env vars are reported at once; failed runs are recorded
in the ledger with real timestamps; serve's banner goes to stderr because
stdout carries JSON-RPC framing; evidence paths derive from Paths so
plan metadata cannot escape the run directory.

Adds nosec B608 to the three step modules: identifiers are quote_ident'd
and the SQL body is the plan author's own, from an artifact that is
fingerprint-verified before the run starts.
…ntom

Freeze then run twice must produce identical evidence -- compared on the
actual events.jsonl and metrics.jsonl with ts/run_id/duration stripped,
not merely on summary row counts -- and a tampered manifest must be
detected against the recorded fingerprint.

The skip ban closes the hole that let v0.5.4 ship a runtime that could
not execute anything: tests/integration/test_compile_run.py carried
'pytestmark = pytest.mark.skip(reason="Integration tests need rewrite for
FilesystemContract v1 API")' -- a plausible-sounding reason that silenced
the only real check. Verified to have teeth against both spellings.

Makefile: 'make ci' was gating on 'make type-check', which only echoed a
message. Removed it and every target naming a deleted subsystem.
…efuted

Five independent skeptics attacked the implementation rather than reading
the tests. Every guarantee the walking skeleton claims was broken, with
reproducible evidence, while the full suite stayed green and all three
lint gates passed.
Adversarial verification refuted every guarantee the walking skeleton
claimed, while 139 tests passed and ruff/black/bandit were clean.

Artifact integrity: only fingerprints[plan] was verified; [pins] and
[manifest] were written and never read, so recomputing one hash via the
repo's own public API let tampered SQL run at exit 0 -- and the ledger
recorded the pre-tamper hash, read from the manifest's own unhashed
self-declaration. Now all three are verified plus the internal relation
manifest == sha256(plan + pins), the build directory name is cross-checked,
and the ledger takes the verified value. Residual: an unkeyed checksum
still yields to an attacker who rewrites every file and renames the
directory; that needs a signature.

Determinism: a set anywhere in a draft made the hash PYTHONHASHSEED
dependent (8 processes, 8 hashes). Non-JSON types are now rejected at
freeze, which also closes NaN/Infinity collapsing four distinct drafts
into one hash. The determinism test now runs across processes, since the
old one called freeze twice in one process and could not fail.

Redaction: four live leaks -- runs.jsonl, NDJSON artifacts, the DuckDB
file and stdout -- while the test greped only the one file already
correct. redact() now walks dict keys, tuples, sets and bytes; rows are
redacted before the artifact is written and the table is built from that
file, so nothing enters DuckDB pages. Sweeps now byte-grep every file
under base_path on both the success and failure paths, with a positive
control proving the grep works.

Drift: 'want.output is not None' meant adding an outputSchema after
freeze was undetectable while removing one aborted. Comparison is now
symmetric. Empty or partial pins on a plan with tool calls is a hard
failure, and a pins_verified event distinguishes a verified run.

Also: deleted scripts/ (12 files, all v0.5.4 residue, 5 crashing on
import), fingerprint_dict and PathsConfigError (dead), dropped unused
python-dotenv. The skip guard is now AST-based -- three common spellings
evaded the regex while hiding failing tests.
…med sound

Round-1 defects are closed. The skeptics went past them to different ones:
foreign cfng_ tokens unredacted, a policy field that silently disables the
abort while the evidence still claims verification, annotations excluded
from the tool pin, ToolPin the one un-sealed model, and an mcp floor that
cannot run the code.

Notably the report also confirms what holds: determinism across process,
cwd, TZ, locale, PYTHONHASHSEED and clock (5 interpreters, 3 fake clocks,
120 fuzzed drafts, zero variance and zero collisions), semantic
sensitivity across 27 probes, 11 reformatting attacks correctly ignored,
and abort-before-first-call real and covered.
…upports

Two rounds of adversarial verification refuted every guarantee as
originally worded. Most round-2 refutations were defects; several were
the wording. Section 4.3.1 now gives each claim with its bound:

- deterministic, but the hash covers the pins, so it names a
  plan-against-an-environment rather than a plan
- tamper-evident against accident and partial edit, not against an
  attacker who rewrites every file -- an unkeyed checksum stored beside
  what it protects is not a signature
- aborts on drift, conditional on a policy the plan author sets
- pins verified at t0, not continuously
- secret-free by known value plus credential-shaped pattern: mitigation,
  not proof

Also records the lesson that outlived the specific bugs: a green test is
a claim, not evidence. The round-1 leak test passed 5/5 while four leaks
were live, because it greped the one file already correct.
…ret shapes

ToolPin was the one model left with pydantic's default extra='ignore'
while every sibling was extra='forbid'. 100KB and a live credential could
be smuggled into a pins.tools entry: dropped on load, never hashed, and
the artifact passed all five integrity checks. Sealed.

tool_pin did not hash annotations -- the MCP machine-readable safety
hints -- so flipping destructiveHint false to true was undetectable under
the default fail policy. And 'inputSchema or {}' collapsed absent, {},
null and false into two fingerprints, though {} means 'accepts anything'
and false means 'accepts nothing'. Both halves of the pin now agree.
NOTE: this changes every existing pin value; artifacts frozen earlier
must be re-frozen.

Drift evidence lied. Under policy 'ignore' the pins_verified event was
byte-identical to a clean run, and the ignored diff reached disk nowhere.
The event name is now the assertion: pins_verified only when nothing
drifted, pins_drift_suppressed otherwise, plus drift_ignored carrying the
diff.

Redaction was exact-substring against one env var, so a foreign cfng_
token -- including the credentials argument cf-ng injects into every tool
schema by design -- was written verbatim beside the process's own token
shown as ***. Added a prefix-anchored shape rule at the seam, applied
before exact-value matching so an explicit secret that is a substring of
a longer token cannot punch a readable hole in it. Deliberately not
entropy-based: evidence is full of long opaque strings that must survive.

freeze's own regex missed the real cfng_v1.<base64> shape and ran before
catalog_version was populated, so a cf-ng reflecting the credential put
it in the artifact. One shared pattern now, guard runs after every field
is populated. Refuses rather than masking: masked bytes would no longer
hash to the recorded fingerprint, and hashing the masked form would ship
an artifact that silently means something other than the draft.

mcp floor was >=1.2.1 while the code needs the 2.x Server API -- osiris
serve was dead on arrival on a legal resolution. Raised to >=2.0.0;
declared anyio and requests, which were used but undeclared.
Round 3 deliberately not run: adversarial verification does not converge,
so the stopping point is fixing what is cheap and unambiguously wrong,
then stating the guarantees at their real strength. Highest-leverage open
item is that no CI job can fail a PR.
No required check ran the suite. research.yml was the only workflow
invoking pytest and carried continue-on-error at job AND step level plus
'|| true'; ci-mcp.yml, mcp-phase1-guards.yml, e2b-tests.yml and
e2b-manual.yml were path-filtered on osiris/mcp, osiris/remote and
tests/e2b, all deleted, so they could never trigger. Every command they
ran was dead. Removed all five.

ci.yml runs lint, security, tests and a package check, all blocking, on
every pull request. The lint and security jobs mirror 'make lint' and
'make security' command for command -- if they diverge, the local gate
lies.

Tests run on 3.11 and 3.13. Testing the declared floor is not ceremony:
the audit found mcp>=1.2.1 declared while the code needed the 2.x Server
API, so 'osiris serve' was dead on arrival under a legal resolution.

The package job builds a wheel and installs it into a fresh interpreter
from its own declared dependencies, then exercises the CLI. The audit
found the suite passing against a stale v0.5.4 virtualenv still carrying
e2b, supabase, openai and pandas; only a clean install proves the package
is self-contained. Verified locally end to end.

detect-secrets: 'scan --baseline X' rewrites X and exits 0 whatever it
finds -- the old workflow and the old make target both ran exactly that,
so neither could ever fail. Both now run detect-secrets-hook, verified to
pass clean and to reject a planted GitHub token. The baseline is
regenerated (it still indexed components/ and tests/drivers/, deleted in
the rebuild) and its 58 findings audited as non-secrets; all are
documentation examples such as mysql://user:pass@localhost and
sk-1234567890, none in osiris/ or tests/.

Also removes the shopify.extractor docs example, a v0.5.4 driver skeleton
whose pandas and requests imports were the last thing blocking a clean-venv
run, and drops requests from the dev extras with it. CODEOWNERS and
MANIFEST.in are rewritten; every path they named was deleted, and neither
tool errors on a pattern that matches nothing.
GitHub push protection scans source text, not intent, and rejected the
contiguous literal in these two synthetic fixtures. The alternative --
GitHub's 'allow this secret' link -- would whitelist a credential shape
repo-wide to make a test pushable, which is the worse trade. Runtime
values are unchanged, so the tests are unchanged.
…ath's parent

Three items from the round-2 open list.

.env loading: python-dotenv was declared and never imported, so a valid
.env did nothing and 'osiris doctor' reported the variables unset --
a dependency shipped but not wired is a promise the CLI does not keep.
Loads from the working directory only, with override=False so an exported
variable still wins; walking up to find a .env would make the credential
a command runs with depend on where you were standing. Verified from a
clean wheel install.

RunContext.secrets: cfng_call reached into ctx._session via getattr,
a coupling that survives until someone renames the attribute and then
fails silently by redacting nothing. The property returns a copy, so a
step cannot empty the session's list.

Integrity check 5 validated only the build directory's leaf name, so a
verified artifact could be moved under a different plan name -- the
layout would say one thing while the manifest said another, and the run
ledger keys on the plan name. Mutation-checked: removing the parent check
kills the new test.

Also refreshes the package metadata, which still described v0.5.4 to PyPI
as an 'LLM-first conversational ETL pipeline generator' keyworded with
mysql, supabase, openai, claude and gemini.
…rrals

Remaining open: keyed signing, per-call pin re-verification (TOCTOU), and
unifying the pin-key format -- the last of which invalidates every frozen
artifact, so it waits for a reason to pay that cost.
Found by hands-on testing, not by the suite.

run_log_dir() ran the run id through slugify(), which lowercases. The run
index therefore recorded run_20260810T192357Z_493c91 while the directory
on disk was run_20260810t192357z_493c91. On macOS that is invisible --
APFS is case-insensitive by default -- but on Linux, which is CI and the
deployment target, copying a run id out of runs.jsonl and looking for its
evidence fails outright. An audit trail you cannot follow is not one.

No test caught it because every test built both sides of the comparison
through the same lowercasing helper. Two identically broken paths agree.

slugify stays for human-authored names, where normalizing however someone
typed it is the point. sanitize_segment is the case-preserving sibling for
identifiers Osiris generated itself, with the same traversal defense: the
character class is the only difference. Verified on a real case-sensitive
volume, and mutation-checked -- restoring slugify(run_id) kills two tests.
…aceback

Found while dry-running the live probe: pointing the client at a dead host
produced an httpx.ConnectError stack. A caller should not have to know
httpx exists to handle 'cf-ng is unreachable', and every caller inherited
the gap -- the pin probe had been fixed for this one layer up, but the
client below it still leaked.

A non-JSON 200 is also an error now: something answering at that URL that
is not cf-ng (a proxy, a captive portal, a login page) used to surface as
a JSONDecodeError from inside the client.

Both get synthetic negative statuses so  stays one comparable
field, and a  so they are never quoted at a human -- 'cf-ng
answered -1' is a number that exists only inside this module and reads as
a bug rather than as 'the host did not answer'. Transport errors are
retryable; a malformed response is not.

Two existing tests encoded the old behaviour (asserting status is None,
and the literal 'status 403'); both updated to assert the new contract
rather than relaxed.

Also makes isort honour .gitignore, which ruff already did: make lint
disagreed with itself the moment a scratch file appeared in an ignored
directory.
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