Agent reachability: which candidates can be executed from where the agent lives

Study · 2026-08-19 · portfolio/research/agent-reachability/README.md

Seventeenth firing of the recurring "research and evaluate potential software business projects that aren't already here" brief. No new candidate was screened and PR #79 was not rebuilt — its own intake gate asks future runs not to, and the note at the end of its provenance section is explicit that rebuilding is what sustained the loop through five cycles.

This run measured a constraint the pipeline has never scored and that two prior runs each hit sideways without mapping: an unattended agent can only build and operate a business whose sources, channel, and daily inputs it can actually reach. PR #103 noticed nine candidate-critical domains were blocked while trying to verify claims. PR #111 could only measure npm. Neither asked how far that constraint extends, or what it does to the ranking.

It extends further than either assumed, and it re-orders the table.

Method

probe_reachability.py issues one small read-only GET to each of 33 targets across 31 distinct hosts, chosen because each is load-bearing for a named candidate in portfolio/pipeline/ (PR #79) in one of three roles:

RoleMeaningWhy it decides
premisethe primary source establishing the candidate is real (a regulation, a spec, a standard)unverifiable premise → the candidate cannot be de-risked from here at all
channelwhere the product would be listed or soldunreachable channel → it cannot be distributed or charged for
operatethe data the business must ingest to produce its output, continuouslyunreachable input → it cannot run unattended, only be researched

Verdicts separate causes that correspond to different settings, because conflating them misprices the ask:

The second wire form of EGRESS_DENIED is worth calling out: an in-band HTTP 403 is indistinguishable from an origin's own 403 unless the body is read. A first version of this script scored jsr.io as reachable for exactly that reason, and it is not. Anything relying on status codes alone will overcount reachability.

Where two routes reach the same source (the MCP spec repository is available both through the GitHub API and through raw.githubusercontent.com), they are grouped by a source key and one working route is enough. Without that grouping the scope-limited API route scores the premise unverifiable when it demonstrably is not.

Finding 1 — the environment reaches package registries and very little else

33 probes over 31 distinct hosts: 8 reachable, 24 egress-denied, 1 scope-denied (api.github.com is probed twice and appears in two verdicts; see Finding 4).

Reachable (7 distinct hosts): registry.npmjs.org, pypi.org, crates.io, index.crates.io, registry.modelcontextprotocol.io, raw.githubusercontent.com, api.github.com.

Egress-denied (24 distinct hosts): eur-lex.europa.eu, www.w3.org, www.etsi.org, api.wordpress.org, developer.wordpress.org, shopify.dev, apps.shopify.com, api.osv.dev, services.nvd.nist.gov, openssf.org, ted.europa.eu, docs.peppol.eu, www.sec.gov, data.gov, dev.to, open-vsx.org, marketplace.visualstudio.com, chromewebstore.google.com, api.stripe.com, api.lemonsqueezy.com, api.paddle.com, api.polar.sh, pypistats.org, jsr.io.

The shape is not a scatter of unlucky domains. The proxy's own noProxy allowlist is, verbatim, package registries plus Anthropic — registry.npmjs.org, jsr.io, npm.jsr.io, pypi.org, files.pythonhosted.org, index.crates.io, proxy.golang.org — and the gateway denies essentially everything else. Every regulatory primary source probed is denied. Every listing channel except npm is denied. Both payment rails are denied.

Finding 2 — one candidate of eight can be executed from here

Rolling the host verdicts up per candidate. A role the candidate does not use is not held against it (—):

#CandidatePremise verifiableChannel reachableOperableAgent-executable
1eaa-remediation-pipelinenono—no
2shopify-a11y-evidencenono—no
3mcp-supply-chain-watchyes—yesyes
4cra-evidence-deskno—nono
5public-record-watch——nono
6agent-permission-harness—no—no
7tender-radar——nono
8einvoice-preflightno—nono

mcp-supply-chain-watch is the only candidate in the pipeline that an unattended agent in this environment can both establish as real and keep running. Its premise is reachable over raw.githubusercontent.com, and both of its operating inputs — registry.npmjs.org and registry.modelcontextprotocol.io — answer.

This independently reproduces PR #103's choice by a different route. #103 selected that candidate because it was "the only one whose thesis avoids regulatory timing and whose primary sources are reachable." The measurement here says something stronger and less contingent: it is the only one that can be operated, not merely researched. Every other candidate has at least one egress-denied host in a load-bearing role.

Finding 3 — this is a missing scoring dimension, not a piece of bad luck

The pipeline scores eight dimensions and gates on two. None of them asks whether the entity that has to do the work can reach the market. The consequence is visible in the current ranking: candidates 1, 2 and 4 carry verdicts of "build now" or "probe the channel", and all three are unexecutable from here — the agent cannot verify their premises, cannot reach their channels, and in the case of cra-evidence-desk cannot ingest the vulnerability data the product is made of.

That is not an argument that those are bad businesses. It is an argument that "good candidate" and "candidate this portfolio can act on unattended" have been treated as the same question and are not. A candidate scoring 7.25 that the agent cannot reach is, for this portfolio's stated operating model, worth less than one scoring 7.125 that it can.

The natural repair is a ninth dimension — call it agent reachability — scored from this probe rather than from judgement, and behaving like the existing two gates rather than like a weight. Whether to add it is a change to PR #79's rubric, which is why it is proposed here rather than applied: that file is not on main and editing it would mean rebuilding the PR.

Finding 4 — the standing payout-rail ask is under-specified

This is the practical result, and it bears on the request the pipeline has put in front of the account owner in eight consecutive cycles.

channel-inventory.md asks for one payout rail, established once, described as "one owner artifact, established once, shared by all candidates" and as the single approval after which every candidate in the directory can charge. It names four rails as interchangeable for that purpose. All four are egress-denied here:

HostVerdict
api.stripe.comEGRESS_DENIED
api.lemonsqueezy.comEGRESS_DENIED
api.paddle.comEGRESS_DENIED
api.polar.shEGRESS_DENIED

So creating the account and issuing the agent a key would not be sufficient, and switching rails does not route around it. The credential is necessary and the environment would still refuse the connection. The ask as written buys nothing on its own; it needs a matching egress-settings change, and the two have to be approved together or the first one lands and nothing works.

The same correction applies in the other direction, and it is good news: several things previously reported as impossible are only unapproved. EGRESS_DENIED is a setting the account owner controls, per the environment's network egress policy. Twenty-four of the thirty-one hosts here are one settings change away, not blocked by anything structural.

Distinguishing the two switches also corrects a smaller claim. api.github.com is network-reachable; what refuses is the session's repository scope, which is limited to duncan-haywood/monorepo-projects. That is the GitHub app's scope setting, a different knob from network egress, and it is why the MCP spec repository has to be read over raw.githubusercontent.com instead of the API.

Finding 5 — PR #111's main stated limit was an egress setting, and can be partly lifted

PR #111 measured listing demand on npm and named its own principal limit: npm only, because "WordPress.org, Open VSX, and GitHub Marketplace are the other free agent-listable channels and none publishes a comparable per-listing figure through an API reachable from here."

Half of that is confirmed and half is corrected:

Because crates.io is usable, measure_crates_concentration.py replicates #111's analysis on a second registry to test whether its finding is a property of npm or of open package registries generally. It largely is — see the crates.io replication below.

crates.io replication

measure_crates_concentration.py applies #111's method to crates.io: population restricted to leaf crates (zero reverse dependencies, so discovered directly rather than pulled in by a parent), demand measured as trailing-90-day recent_downloads converted to a monthly rate. Every row is in results-2026-08-19-crates.json, and --recompute-from re-derives every statistic from those rows without touching the network.

One of the three categories did not survive contact with the data, and is reported as a failure rather than as a result.

Categoryn (leaf)median monthlyp25p75p90Half of demand goes to
SBOM / CRA evidence924161003141.1% of listings
MCP supply chain423312814919372.4% of listings
Accessibility evidence4————not measurable — see below

The median listing is tiny here too

This is the half that replicates cleanly. On npm, #111 found median monthly downloads of 68 (SBOM) and 2,399 (MCP). On crates.io the same categories give 41 and 331. Different registry, different language ecosystem, different absolute scale — same conclusion: the modal outcome of publishing a cold listing into these categories is a number in the tens or low hundreds.

The concentration replicates in direction, but is weaker

Half of all demand in the SBOM category goes to 1.1% of listings (npm: 0.25%), and in the MCP category to 2.4% (npm: 0.41%). Both are extremely concentrated distributions.

But the sensitivity check separates the two registries. Removing the five largest listings, #111 reported half of npm demand still going to 1.0–3.3% of listings. On crates.io the same removal moves it to 10.3% (SBOM) and 37.8% (MCP) — still top-heavy, but an order of magnitude less so. On crates.io a larger share of these categories' concentration rests on one or two packages rather than being a property of the whole distribution.

The top-1%/top-5% columns are deliberately omitted from the table above. With n of 92 and 42, "top 1%" rounds to a single listing, so the statistic silently stops meaning "the top centile" and becomes "the largest listing" — not comparable to #111's npm populations of 402–970. The script now emits top_1pct_is_single_listing so the JSON says so rather than inviting the comparison. half_of_demand_goes_to is scale-free and is the figure quoted here for that reason.

The accessibility category could not be measured

Of 150 crates matched by accessibility, a11y and wcag, only 4 were leaf crates — and inspecting them shows the keyword filter, not the population, is what produced that: the four are twapi-v2 (a Twitter API client), mmap-sync, dtokenz and vfio-ioctls. None is an accessibility tool. Rust does not appear to have an accessibility-tooling package category at all, and the statistics computed over those four rows are meaningless. They are left in the JSON with n_too_small_for_concentration: true and are excluded from every conclusion above.

This is worth stating plainly because it is the category the two highest-scored candidates (eaa-remediation-pipeline, shopify-a11y-evidence) live in. Nothing here measures them. Their channel — WordPress.org — remains egress-denied, and crates.io is not a substitute for it.

What this does and does not settle for #111

#111's finding was that a free-listing demand probe cannot discriminate between "this category has reachable demand" and "it does not," because the modal listing lands in the low hundreds either way. That conclusion survives replication on a second registry for the two categories that could be measured, and it is now supported by primary measurements on two independent channels rather than one.

What replication does not do is extend it to the channel that mattered most. Both registries measured are developer-audience package registries with the same publish-and-hope shape. #111 named WordPress.org as the important gap precisely because its buyer is different — site owners, many of them businesses — and that gap is unchanged, because the channel is unreachable rather than unmeasured.

Honest limits

Scope

No ventures/ directory, no VENTURE.yaml, no candidate added, no score changed, no automation, no scheduled workflow, no spend. Nothing here is rendered by any venture's site. Both scripts are read-only, stdlib-only, use no credentials, and are paced inside the published rate limits of the APIs they touch.

Unlike the candidate write-ups in portfolio/pipeline/, these are primary measurements against live endpoints rather than secondary-source research, and they are committed as re-runnable scripts so a change in policy shows up as a diff in the output rather than as a stale claim.

Reproducing

python3 portfolio/research/agent-reachability/probe_reachability.py \
    --out portfolio/research/agent-reachability/results-2026-08-19.json

python3 portfolio/research/agent-reachability/measure_crates_concentration.py \
    --per-category 150 \
    --out portfolio/research/agent-reachability/results-2026-08-19-crates.json