{
  "markdown": "# RedKraken\n\nA local-first bug-bounty hunting harness for web and API targets. The operator\nruns it on their own machine, against Programs they are authorised to test, and\nkeeps every artifact it produces.\n\nThis repository is being built one production increment at a time. Today it\ninstalls as one Python application and exposes two stable commands: `rk\n--version` and `rk doctor`.\n\n## Requirements\n\n- CPython `>=3.14,<3.15`. `pyproject.toml` and `rk doctor` declare the same\n  range, and starting outside it is a refusal rather than a warning.\n- No third-party Python production dependencies. The Python runtime is standard\n  library only; any package added later is declared as an exact pin and verified\n  at startup. Operations that invoke a system executable check it at the point\n  of use: TLS interception requires `openssl`, database dump and restore require\n  `pg_dump` and `pg_restore`, and Agent network isolation requires Docker.\n\n## Install\n\nFrom a clean checkout:\n\n```sh\npython3 -m venv .venv\n.venv/bin/pip install .\n.venv/bin/rk --version\n```\n\nWithout network access, build against the interpreter's own setuptools instead\nof a downloaded one:\n\n```sh\npython3 -m venv --system-site-packages .venv\n.venv/bin/pip install --no-build-isolation --check-build-dependencies .\n```\n\n`--check-build-dependencies` keeps the pinned build requirement enforced on\nthis path too; without it, skipping build isolation also skips the check.\n\n`rk` is then on the virtual environment's `PATH`. `python3 -m redkraken` runs\nthe identical command line from a source checkout, with `src` on `PYTHONPATH`.\n\n## Check the machine\n\n```sh\nrk version\n```\n\n`rk version` reports what is installed: the package version, the last migration\nin the corpus, how many there are, and a digest over all of them. Two machines\nrunning the same package version can be running different schemas, so the digest\nis the number that decides whether they agree. `rk --version` answers the same\nquestion as one line for a person; this answers it in the shape every other\ncommand answers in. It reaches no database and no network.\n\n```sh\nrk doctor\n```\n\n`rk doctor` reports whether this machine can be trusted to run a Program. It\nreads; it creates no state, sends no traffic and starts no run. Its result is\nJSON on stdout: the application version, the interpreter version and the\nsupported range, one entry per readiness assertion, and every violation it\nfound.\n\nGive it a Program configuration to validate that too:\n\n```sh\nrk doctor --config program.toml\n```\n\n## The Agent boundary\n\nEvery command that starts a container -- `rk run`, `rk tool run`, `rk browser\nrun` -- reads the same five variables and refuses if any is unset. Nothing is\ndefaulted: an image name guessed here would start a child in whatever the guess\nmatched, and a proxy URL guessed here would point a child's only route at\nwhatever answers on that port.\n\n| Variable | What it names |\n| -------- | ------------- |\n| `RK_AGENT_IMAGE` | The image children run in. Never pulled implicitly. |\n| `RK_AGENT_NETWORK` | A Docker network created `--internal`, whose only peer is the door. |\n| `RK_AGENT_PROXY_CONTAINER` | The door's container name on that network. |\n| `RK_AGENT_PROXY_URL` | `http://<container>:<port>`, uncredentialed. Children get it as `HTTP_PROXY`, and the door binds the port it names. |\n| `RK_PROXY_CA_FILE` | The certificate children verify every target against: `<authority>/ca.pem`. |\n\nThree more are optional and absent by default, because absent is the contained\nvalue: `RK_AGENT_APPLICATION`, `RK_AGENT_SDK` and `RK_AGENT_HOME` name the\napplication, the SDK and the home mounted inside a child. A container with no\nhome mounted has no credential at all rather than somebody else's. The home is a\ntemplate: each run is handed a copy of it, and the copy is removed when the run\nends, so what one child writes is never what the next child reads.\n\nClaude authentication is a setup-token file on the supervisor, at\n`~/.config/redkraken/claude-oauth-token` by default or the absolute path in\n`RK_AGENT_OAUTH_TOKEN_FILE`. Run `tools/setup-agent-oauth.sh` for the one human\n`claude setup-token` step and the Doctor/canary checks around it. The token is\nread only by the supervisor and sent in the private stdin envelope of the\nshort-lived child; it is never copied or mounted. An existing\n`.claude/.credentials.json` is ignored and left untouched.\n\n### Satisfying them\n\n`rk proxy door` puts the production fence on the Agent network as its only\npeer, and returns while it is still running. It takes no flags for the five\nvariables above -- a second place to say them is a day the boundary verifies\nagainst a door no child was pointed at -- and reads three more of its own:\n\n| Variable | What it names |\n| -------- | ------------- |\n| `RK_PROXY_DATABASE_URL` | The door's own connection, held as `rk2_proxy`. Not `RK_DATABASE_URL`: a fence running as the runtime is a fence with the privileges of the thing it fences. |\n| `RK_ARTIFACT_ROOT` | Where exchanges are filed. |\n| `RK_PROXY_AUTHORITY` | Where the door mints and keeps its signing material. |\n| `RK_ARTIFACT_KEY` | Optional. A **file**, not an `op://` reference: resolving one inside the container would mean a service-account token inside the container. Without it, sealed responses are refused. |\n\nThe door runs as `65534:65534` with every capability dropped and a read-only\nroot filesystem, so both directories it writes have to be writable by that\nuser. It creates neither -- a directory this command made would be owned by the\noperator, which is the state it refuses on the next line:\n\n```sh\nmkdir -p /var/lib/rk2/artifacts /var/lib/rk2/authority\nsudo chown 65534:65534 /var/lib/rk2/artifacts /var/lib/rk2/authority\n\ndocker network create --internal rk2-agent\ndocker network create rk2-egress\nexport RK_AGENT_IMAGE=rk2-agent:local\nexport RK_AGENT_NETWORK=rk2-agent\nexport RK_AGENT_PROXY_CONTAINER=rk2-door\nexport RK_AGENT_PROXY_URL=http://rk2-door:18080\nexport RK_PROXY_CA_FILE=/var/lib/rk2/authority/ca.pem\nexport RK_PROXY_DATABASE_URL=\"postgres://rk2_proxy:...@host.docker.internal:5432/rk2\"\nexport RK_ARTIFACT_ROOT=/var/lib/rk2/artifacts\nexport RK_PROXY_AUTHORITY=/var/lib/rk2/authority\n\nrk proxy door\n```\n\nThe door runs `python3 -m redkraken.door` inside `RK_AGENT_IMAGE`, from this\ncheckout mounted read-only, so that image needs a Python 3 and an `openssl` on\nits `PATH` -- the second is what mints the authority. The connection string is\nwritten from the container's point of view, which is why the example says\n`host.docker.internal`: the door is given that name explicitly, because a\nPostgres on this machine has no other spelling from inside a container.\n\nEverything decidable without starting anything is decided first: the boundary\nis described, both directories are writable by the user the door runs as, the\nkey is a file and not a reference, `RK_PROXY_CA_FILE` names the authority this\ndoor will sign with, no container already holds the door's name, and both\nnetworks are what they claim -- the Agent's internal and empty, the egress one\nroutable and empty. Then the door starts on its egress attachment alone -- so\nit has a database to reach before it is anywhere a child could see it -- and\njoins the Agent network only once it says it is serving.\n\nThe door is on two networks and the difference between them is the point. The\nAgent network carries no route anywhere: no database, no internet, no host. The\nsecond attachment carries both, and nothing but the door is on it, so a child\nreaches the internet only by asking the door to go -- which is the same thing\nas saying the fence sees every request. `--egress` names that second network\n(`rk2-egress` by default) and `--timeout` how long the door gets to bind and\nopen its fence before it is given up on and taken away.\n\nThat second network is a network of the operator's own rather than the engine's\ndefault `bridge`, and the door refuses to start on one that already has peers.\nThe door binds every interface it has, so a peer on the way out is a peer that\ncould reach the fence without a capability ever having been minted for it.\n\nThe door outlives the command that started it, and is not removed when it fails:\na door that vanished would take the only account of why with it. So `rk proxy\ndoor` refuses to start where one already exists, and taking it away is the\noperator's own `docker rm --force rk2-door`.\n\nThe command asserts the topology from out here, where an engine exists. A\nprocess inside a container cannot enumerate the peers of the networks it is on,\nand a door holding an engine socket would be a worse hole than the one this\ncloses -- so `python3 -m redkraken.door` run by hand somewhere else binds wide\non a network nobody vouched for. The door is only the door when `rk proxy door`\nis what put it there.\n\n`rk proxy serve` is the other door: the one an operator runs in a terminal, on\nloopback. It refuses a routable bind, because what arrives at a listening fence\nis bearer material anybody who can reach the port may spend. The contained door\nis the one exception, and only because the whole of what can reach its port is\nthe child the capability was minted for.\n\n## The Program configuration\n\nA configuration is versioned and declarative. The schema is closed: an\nunrecognised key is a refusal, not an ignored line. Absent permission is\ndenial, so a rule of engagement that is not set is off.\n\n```toml\nschema_version = 1\n\n[program]\nname = \"acme-web\"\nplatform = \"hackerone\"\n\n[rules_of_engagement]\nmutation = true\n\n[budgets]\nrequests = 5000\ntokens = 2000000\nrun_tokens = 40000\nrun_requests = 50\nlane_tokens = 500000\nlane_requests = 1000\nconcurrency = 2\nburst = 500\nwindow_seconds = 3600\n\n[[scope.include]]\nhost = \"app.example.com\"\nports = [443]\nprotocols = [\"https\"]\npaths = [\"/api/\"]\n\n[[scope.exclude]]\nhost = \"admin.example.com\"\nports = [443]\nprotocols = [\"https\"]\npaths = [\"/\"]\n\n[[identity]]\nname = \"member\"\nclass = \"user\"\nslot_ref = \"slot://identity/member\"\n\n[[required_header]]\nname = \"X-Bounty-Id\"\nvalue_ref = \"slot://header/bounty-id\"\n\n[[callback]]\nname = \"oob-dns\"\nkind = \"dns\"\nhost = \"oob.example.net\"\n```\n\nThe nine budget limits state three nested allowances, and the nesting is\nchecked: an Agent run is spent inside a Lane and a Lane inside the campaign, so\n`run_tokens` above `lane_tokens` or `tokens`, or `run_requests` above\n`lane_requests` or `requests`, is refused by name. Only the per-run ceiling is\ncompared upwards. A Lane allowed more than the campaign holds is slack rather\nthan a contradiction, because the campaign total binds first and the Lane never\ndoes.\n\nA scope entry names a hostname, an address, an address range in CIDR\nnotation such as `93.184.216.0/24`, or a wildcard such as\n`*.example.com`. A range must be globally routable, the same rule a bare\naddress is held to. An inclusion's wildcard must name at least two labels of\nits own, so `*.com` is refused, and an inclusion's range must be at least a\n`/16` in IPv4 or a `/32` in IPv6, so `1.0.0.0/8` and `2000::/3` are refused.\nBoth are floors rather than public-suffix rules or width judgements: `*.co.uk`\nand a `/16` both pass, a registry's block does not, and how wide an inclusion\nmay be under the floor remains the operator's judgement against the Program.\nAn exclusion has no floor, because breadth there withdraws\nauthority rather than claiming it. Hosts are compared in one spelling —\nlowercased, without a trailing root dot, addresses in canonical form — so two\nways of writing the same Program produce the same hash, and a repeated rule\ncounts once.\n\nA path names a prefix on the host that carries it. It begins with a single\nforward slash, so the protocol-relative `//elsewhere.example/admin` is refused;\nit holds no `..` segment and no unprintable character, so what is printed is\nwhat was matched.\n\nOpening a Program records one Application per inclusion and opens one `recon`\nTask against each, so a campaign has something to rank on its first pass. A\nprotocol, a host, a port and a path prefix are an address, and an address is\nwhat a Task can be sent to — which is why an inclusion naming two protocols is\ntwo Applications, and why an inclusion naming a wildcard records nothing:\n`*.example.com` names a set of hosts and no address. Both counts are reported\nas `first_tasks`, and rerunning an unchanged configuration records nothing and\nopens nothing, because the Task each subject already carries is the one being\nresumed.\n\nAn identity also declares its `class`, either `user` or `privileged`,\ndefaulting to `user`. Whether a credential is an administrator's is a\nprovisioning fact the operator knows and no response can reveal, so it is\nstated here rather than inferred.\n\nSecret material is never written into a configuration. An identity carries a\n`slot_ref` and a required header carries a `value_ref`. Both name a\nruntime-owned slot — the `slot://` scheme and nothing else, so a configuration\ncannot smuggle its own credential in a URL — which the runtime resolves and the\nproxy injects. A key that would hold a secret inline is refused by name.\n\nDiagnostic output follows the same rule. It reports names, counts, controls,\nversions and the two hashes that identify a configuration — `source_sha256`\nover the file as written and `canonical_sha256` over its normalised content, so\nreformatting does not change the policy's identity — and never a reference or a\nheader value.\n\n## The operator console\n\n```sh\nrk ui serve --config program.toml\n```\n\nThe console is the same operations this CLI already exposes, rendered as pages.\nEvery view is one of the reads — `rk ui read` for the Program's lifecycle and\nintegrity, its slates, runs, leases, budgets, findings, chains and the documents\non file; `rk state` for the model's own records; `rk decision list` for what is\nwaiting on a person; `rk report` for one rendering by label — and every button\nis one of the operator verbs, so a page and a command cannot come to mean\ndifferent things. It holds no query of its own.\n\nThree connection strings, because the console is three roles and not one. The\npanels read as the runtime, the record index reads as the agent because that is\nwhose isolation it is describing, and halt, resume, the two queue verbs, the\nreport and the gate clearance run as the operator. A console given one string in\nall three places renders every page and can lift nothing.\n\nIt listens on loopback, refuses a `Host` or `Origin` header that is not the\naddress it was given, and puts a token this process alone holds on every form,\nso a page in another tab cannot submit a verb to it. There is no login, because\nthere is no remote: the authority is the operator connection string, and anyone\nwho can reach the socket already has the machine.\n\n```sh\nrk ui read --config program.toml\n```\n\nThe same panels without a browser, in the shape every other command answers in.\nEach one is bounded and says how many rows it did not return.\n\n## Outcomes\n\n`rk doctor` aggregates: it reports every violation it found, not the first, and\nexits on the most fundamental one. A schema version this build cannot read is\nthe one exception, reported alone because it explains every other refusal such\na document would draw.\n\n| Exit | Meaning |\n| ---- | ------- |\n| `0` | Ready. |\n| `1` | A refusal this build cannot classify. |\n| `2` | The command line could not be understood. |\n| `3` | `invalid_configuration`: the configuration was refused. |\n| `4` | `unsupported_version`: an interpreter or schema version is out of range. |\n| `5` | `missing_dependency`: a required module or pinned distribution is absent. |\n\n## Tests\n\nThe suite is standard library only and runs offline:\n\n```sh\npython3 -m unittest discover -q\npython3 tools/check_baseline.py\npython3 -m tools.check_dispositions\npython3 -m tools.check_coverage\npython3 -m tools.check_intake\n```\n\nThe third is the migration ledger: for each of the 223 artifacts the census\nfroze, what became of it. A row either names something this checkout has and\ncites the file that proves it works, or names the open migration ticket\ncommitted to building it -- and fails the moment that ticket is marked resolved\nwith the thing still missing.\n\nThe fourth runs the third and then measures the shape of the answer, which no\nsingle row can show: the 49 in-scope Playbooks are all present, loadable by one\nrole and registered in the schema at the exact text this checkout ships; the 73\noperator references and 9 sink packs are each attached to one Skill or Playbook\nthat declares them and nothing sits loose in a `references/` directory; and the\n52 retirements split by kind under a scope whose reversal is on record.\n\nThe fifth is the technique intake: one row per public source read, carrying its\ndigest, the Property class it maps to and what it produced. Producing nothing is\nan outcome with a reason rather than a gap, a row cannot claim a fixture for a\nclass the schema records as not agent-makeable, and a fixture whose provenance\ncites the intake ticket and which no row produced fails the gate.\n\nThe composed suite is the same modules with a server and a container engine\nbehind them. It needs a PostgreSQL 18 superuser URL and, for the container\ncases, the two images:\n\n```sh\nRK_TEST_SUPERUSER_URL=postgres://postgres:...@127.0.0.1:5432/postgres \\\nRK_TEST_CONTAINERS=1 \\\npython3 -m unittest discover -q\n```\n\nWithout `RK_TEST_SUPERUSER_URL` the database cases skip; without\n`RK_TEST_CONTAINERS=1` the boundary cases skip. `RK_TEST_AGENT_IMAGE` and\n`RK_TEST_BROWSER_IMAGE` name the images those cases run in.\n\n## The release gate\n\nThe suites measure a checkout. The release gate measures the artifact:\n\n```sh\npython3 -m tools.release_gate --superuser-url postgres://postgres:...@127.0.0.1:5432/postgres\n```\n\nIt exports `HEAD` with `git archive` into a scratch directory, installs it\nthere through the documented offline path, and then does everything else with\nthat installation rather than with this checkout. Six stages, in order, each\nusing what the one before it built:\n\n* `export` — the commit and nothing lying next to it.\n* `install` — the offline install, then what it left behind: only `redkraken`\n  and `pip` in the environment, a distribution that declares no requirement of\n  its own, none of the checkout-only directories inside the installed package or\n  beside it (`docs`, `prototype`, `scratch`, `tmp`, `tests`, `tools`, `.git`,\n  `.venv`), and `rk --version`, `rk version` and `rk doctor` all answering from\n  a build the application recognises as its own.\n* `database` — provision, migrate, status, verify, migrate again, run, read,\n  dump, restore into a second database, verify that, and open the same Program\n  on the restored copy to prove it resumes rather than starts over. The second\n  migrate is the upgrade reading and it is judged on what it applied, not on\n  whether it succeeded: an installation that reapplied the corpus over a current\n  database reports the same `ok`.\n* `topology` — the internal Agent network, a routable egress network and the\n  door joined to both by `rk proxy door`, with the Agent network holding the\n  door and nothing else.\n* `privileges` — ticket 66's standing check on both databases, the migrated one\n  and the restored one.\n* `suites` — the offline suite and the composed suite, each twice, run from the\n  export against the installed application. Twice because the second run is what\n  proves the first left the server as it found it. A suite exits zero when it\n  skips everything, so the counts are read too: both runs of a suite must select\n  the same tests, and the composed run must skip fewer of them than the offline\n  run did.\n\n`--stage NAME` selects a stage (repeat it for several), `--keep` leaves the\nbuild directory and the databases behind, and the two together are how a single\nstage gets iterated on. A selection that names a stage whose input nothing built\nis refused at once, naming the stage that builds it: `--root` then has to point\nat a directory a kept run already left behind. `--keep` also leaves the\ngenerated role passwords in `roles.json` under that directory, readable only by\nthe user that ran the gate; a kept root is a credential and should be removed\nwith the databases it belongs to.\n\nNothing the gate runs inherits the calling environment; every child gets `PATH`,\n`HOME`, `TMPDIR` and `LANG` written from scratch, so neither a provider key nor\na database URL exported in the shell that starts it can reach the installation\nbeing measured. The `topology` stage needs a container engine and the Agent\nimage, and fails rather than skipping when either is missing.\n\nThe superuser URL must point at a server that can be dropped: the gate creates,\ndrops and recreates `rk2_release_gate`, `rk2_release_gate_restored` and\n`rk2_gate_suite`.\n\n## The release audit\n\nThe gate measures the artifact. The audit measures the Spec:\n\n```sh\npython3 -m tools.check_audit\npython3 -m tools.check_audit --run\n```\n\n`baseline/spec-verification.tsv` holds one row per requirement -- 230 user stories,\n19 Implementation Decisions, 24 Testing Decisions, 9 Out-of-Scope constraints, the\n6 release conditions under Further Notes and the 7 registered prototype\nregressions -- and each row names the tickets that built it and the tests or gates\nthat check it. The column is `verification` rather than `evidence`: Evidence is\nthe role an observation plays for a claim, which `CONTEXT.md` reserves, and the\nv1 ledger beside this one already spells the same idea the same way. The audit\nreads the Spec, the tracker and that table together and refuses:\n\n* a requirement with no row, a row for a requirement the Spec does not state, or\n  a requirement stated twice, since the weaker of two answers is the one nobody\n  reads;\n* a row whose digest no longer matches the requirement's own text, so a story\n  reworded after somebody mapped it stops matching;\n* a row naming a ticket that does not exist or is not resolved, or verification\n  that is neither a test this checkout can run nor a gate it ships -- a case\n  holding no test is not one, because `unittest` loads it to an empty suite -- there is no\n  third kind, which is how a citation to a document is refused rather than\n  counted, and this gate may not be cited as its own evidence;\n* a requirement whose evidence is *owed* -- `owed:64`, the open ticket that will\n  produce it -- where that ticket is finished, does not exist, or where the\n  release outcome has been resolved with the row still saying it. Two rows say it\n  today: the final review and final acceptance are ticket 64's and 65's, and a\n  map that cited something else for them would be citing something that does not\n  check them;\n* a Spec section nobody reads: the seven headings are frozen, so a requirement\n  arriving under a new one is release-blocking rather than invisible;\n* a ticket in 01 through 63 -- the ticket that wrote this gate included -- that\n  is unresolved, blocked by unfinished work, or\n  has no revision resolving it -- the commit that wrote its resolved status --\n  and any acceptance box it left unticked without naming an open ticket that\n  closes it;\n* a dependency graph with a cycle, a blocker nobody wrote, or a resolved ticket\n  with no path to the release outcome, which is what tickets raised beside the\n  plan look like until whichever ticket owns their outcome names them;\n* a named area of the release -- the runtime, the agents, the Skills, the 49\n  Playbooks, the operator surface, the v1 import, long-session recovery and the\n  first hunt -- holding no requirement at all, or holding requirements none of\n  which is checked by that area's anchor;\n* a registered prototype regression whose map does not name the tickets the\n  registry says it requires.\n\n`--run` then executes every cited test and every cited gate. A failure, an error\n*and a skip* are all refusals: most of the live arms stand down without a\ndatabase or a container, and a citation that stood down proves nothing about the\nrequirement citing it. So the run mode is a composed-suite command -- it wants\n`RK_TEST_SUPERUSER_URL`, `RK_TEST_CONTAINERS=1`, the two images and the Agent SDK\ninstalled in the interpreter, exactly like the composed suite. It wants the SDK\nat the one version the runtime is measured against, the pair `KNOWN_RUNTIME`\nnames; any other version is an unmeasured runtime, which every agent citation\nrefuses on purpose. The one skip it\naccepts is the inverse case, and the suite says so in its own words: a test that\nrequires the runtime to be *absent* cannot run where this mode requires it to be\npresent. The one citation it does not run is `gate:tools.release_gate`, which\nbuilds an install and provisions two databases and is reported as deferred.\n\nRun it on an idle machine. The cited set includes the surface benchmarks, and a\nbenchmark measures wall time in the process the audit is already running\neverything else in: on a loaded host a median drifts over its budget and the\naudit reports the drift as a failed citation, which is the honest reading of a\nmeasurement taken under load rather than a fault in the requirement.\n\nWhat it does not measure is whether the cited evidence is any good; a test that\nasserts nothing would satisfy it. That is the final code review's job. And it is\nnot one of the gates the release gate runs inside its export: it reads this\nrepository's history for the commit that resolved each ticket, and a tarball\ncommitted once as a checkout would answer that with one synthetic revision for\nevery ticket in the plan.\n",
  "bytes": 25455,
  "sha": "0d284fc3c4412e366ef0470cf0fad01192c7e08195b074ebc0f46f7af8f0d846",
  "repo_slug": "maajix/redclaude",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_maajix_redclaude_docs_okf_index_md_88ea6e95/readme"
}