Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2458f91
refactor(directory): separate gateway demo UI
Aug 10, 2026
11897dd
Merge main into agent/separate-gateway-demo
randomblocker Oct 13, 2026
a4eba9a
fix(directory): require a public, verbatim origin in check:deploy-config
randomblocker Oct 13, 2026
0da7e61
fix(directory): accept only globally routable origins in check:deploy…
randomblocker Oct 13, 2026
e83e95a
Merge branch 'main' into agent/separate-gateway-demo
randomblocker Oct 13, 2026
a474c16
docs: use CI's only placeholder origin in the local workflow guide
randomblocker Oct 14, 2026
1815169
fix(directory): reject port 0 in check:deploy-config
randomblocker Oct 14, 2026
058903c
test(directory): load /discover again in the landing-page browser test
randomblocker Oct 14, 2026
b0a6a88
fix(directory): align check:deploy-config with the special-use regist…
randomblocker Oct 14, 2026
6c0d4b8
fix(directory): pin Railway's Node release through package.json engines
randomblocker Oct 14, 2026
9e393a2
test(directory): keep the origin CLI tests hermetic and pin exact fla…
randomblocker Oct 14, 2026
3dbdbb2
docs(directory): correct the SDK token, fail-closed exceptions and re…
randomblocker Oct 14, 2026
d6cd693
fix(directory): fail the Railway build and start unless Node is the .…
randomblocker Oct 15, 2026
8b5af69
fix(directory): pin every IANA special-purpose row and verify the pins
randomblocker Oct 15, 2026
52906a5
test(directory): pin the gateway separation and the homepage summary
randomblocker Oct 15, 2026
d1a470a
docs(directory): drop stale SDK-auth and live-report leftovers
randomblocker Oct 15, 2026
7f030b4
fix(directory): pin the special-use names to IANA's registry
randomblocker Oct 15, 2026
4e2153b
test(directory): run the Node runtime check's failure path
randomblocker Oct 15, 2026
55858c2
test(directory): render the homepage against stored catalogs
randomblocker Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .github/workflows/dacs-directory-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,30 @@ concurrency:
jobs:
verify:
name: typecheck · test · build
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
defaults:
run:
working-directory: reference-implementations/dacs-directory
env:
# The SDK dependency tree emits patch-level engine requirements above
# Node 20.10; the app is validated on Node 22.
NODE_VERSION: "22"
# Keeps canonical URLs / sitemap deterministic during the build instead
# of falling back to localhost (which the build warns about).
NEXT_PUBLIC_DIRECTORY_URL: https://directory.ci.local
# Exercise the same production build-time path as the public /try page.
NEXT_PUBLIC_BUTLER_ORIGIN: https://agents.ci.local
# of falling back to localhost (which the build warns about). A reserved
# RFC 6761 documentation name: only the flagged CI check below accepts it.
NEXT_PUBLIC_DIRECTORY_URL: https://directory.ci.example
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: actions/setup-node@v7
# The exact Node release is pinned in .nvmrc.
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "22"
node-version-file: reference-implementations/dacs-directory/.nvmrc

- name: Check the Node release
run: npm run check:node-runtime

- name: Vendor + build the pinned dacs-sdk, install app deps
# The vendored dacs-sdk is a private repo; setup-sdk.sh authenticates
# the nested clone with DACS_SDK_GITHUB_TOKEN when present. Add a
# read-only token (repo secret) with access to DACS-Agent-commerce/dacs-sdk
# or this step fails at the clone. The default GITHUB_TOKEN cannot read
# sibling private repos.
# setup-sdk.sh clones DACS-Agent-commerce/dacs-sdk at its pinned commit.
# The repo is public; DACS_SDK_GITHUB_TOKEN, when set, is only used to
# authenticate that clone.
env:
DACS_SDK_GITHUB_TOKEN: ${{ secrets.DACS_SDK_GITHUB_TOKEN }}
run: npm run setup
Expand All @@ -57,8 +55,10 @@ jobs:
- name: Test (includes seed smoke)
run: npm test

- name: Validate production origins
run: npm run check:deploy-config
# CI's origin is a documentation name, so this is the flagged check;
# railway.json runs the strict production one.
- name: Validate directory origin
run: npm run check:deploy-config -- --allow-documentation-names

- name: Production build
run: npm run build
Expand Down
42 changes: 20 additions & 22 deletions docs/codex-local-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,23 @@ It uses the Node version and SDK revision already pinned by the repository.

## Keep the worktree isolated

- Use Node 22, as required by
`reference-implementations/dacs-directory/.nvmrc` and `package.json`.
- Use the exact Node release in `reference-implementations/dacs-directory/.nvmrc`,
as CI and Railway do. `package.json` engines names the same release.
- Do not copy `.env` files, tokens, wallets, seeds, `data/`, `node_modules/`, or
`vendor/` from another worktree.
- Keep any mutable catalog state under this worktree's
`reference-implementations/dacs-directory/data/` directory.
- Do not run the indexer or live/paid browser tests as part of local static and
unit verification. They are separate, networked activities.
- Do not run the indexer or browser tests as part of local static and unit
verification. The indexer is a separate, networked activity, and the browser
tests need their own setup.

## Install the pinned dependencies

From the Community repository root, use your normally initialized version
manager to select an already-installed Node 22 (for example `nvm use 22`) and
confirm that `node --version` prints `v22.x`. Then set paths from the current
worktree rather than a fixed checkout location:
Use your normally initialized version manager to select the `.nvmrc` release
(with nvm, run `nvm install` from `reference-implementations/dacs-directory`, which
reads `.nvmrc`) and confirm that `node --version` prints `v` followed by that
file's content. Then, from the Community repository root, set paths from the
current worktree rather than a fixed checkout location:

```bash
repo_root=$(git rev-parse --show-toplevel)
Expand All @@ -29,7 +31,6 @@ cd "$app_dir"

export DACS_DIRECTORY_DATA="$app_dir/data"
export NEXT_TELEMETRY_DISABLED=1
unset RUN_LIVE_PAID_E2E LIVE_BUTLER_ORIGIN
```

Read the exact SDK commit from the committed setup script, then create or
Expand Down Expand Up @@ -70,29 +71,26 @@ building whatever the SDK checkout happens to contain. It builds only after

Rerunning the block on a clean checkout is safe: it re-pins the checkout and
reinstalls. If it prints `stopped: ...` or any command fails, fix that before
continuing; do not run the checks below on a partial install. Access to the SDK
repository may require your normal GitHub authentication; do not put a token in
a remote URL or committed file.
continuing; do not run the checks below on a partial install. The SDK repository
is public, so the clone needs no GitHub authentication; never put a token in a
remote URL or committed file.

## Run deterministic checks

These are the same check commands as the Directory CI's typecheck, test,
origin-validation, and production-build steps. The install differs: CI runs
`npm run setup` (`npm install`), while this workflow uses `npm ci`. Use the same
placeholder origins as the `env:` block of
`.github/workflows/dacs-directory-ci.yml` (today
`NEXT_PUBLIC_DIRECTORY_URL` and `NEXT_PUBLIC_BUTLER_ORIGIN`; drop any variable
the workflow no longer sets):
placeholder origin as the `env:` block of
`.github/workflows/dacs-directory-ci.yml` (today only
`NEXT_PUBLIC_DIRECTORY_URL`). That origin is a reserved documentation name, so,
as in CI, the origin check runs with `--allow-documentation-names`. It still
rejects `.local` and other non-public hosts, so do not substitute a local origin:

```bash
npm run typecheck
npm test
NEXT_PUBLIC_DIRECTORY_URL=https://directory.ci.local \
NEXT_PUBLIC_BUTLER_ORIGIN=https://agents.ci.local \
npm run check:deploy-config
NEXT_PUBLIC_DIRECTORY_URL=https://directory.ci.local \
NEXT_PUBLIC_BUTLER_ORIGIN=https://agents.ci.local \
npm run build
NEXT_PUBLIC_DIRECTORY_URL=https://directory.ci.example npm run check:deploy-config -- --allow-documentation-names
NEXT_PUBLIC_DIRECTORY_URL=https://directory.ci.example npm run build
```

These commands do not require the indexer, a wallet, paid commerce, or a shared
Expand Down
1 change: 0 additions & 1 deletion reference-implementations/dacs-directory/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ data/*.sqlite-shm
data/*.sqlite-wal
.playwright-mcp/
playwright-report/
playwright-report-live/
test-results/
*.png
tsconfig.tsbuildinfo
2 changes: 1 addition & 1 deletion reference-implementations/dacs-directory/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22
22.23.3
71 changes: 58 additions & 13 deletions reference-implementations/dacs-directory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ secret variable for the operational indexing endpoints.

For GitHub deployments from the Community monorepo, set the service root directory to
`/reference-implementations/dacs-directory` and the Railway config file to
`/reference-implementations/dacs-directory/railway.json`. Until the pinned SDK is
published as a package, add `DACS_SDK_GITHUB_TOKEN` as a Railway secret. It must be a
`/reference-implementations/dacs-directory/railway.json`. The build clones the pinned
SDK from its public repository, so it needs no credentials. To authenticate that
clone anyway, add `DACS_SDK_GITHUB_TOKEN` as a Railway secret. It must be a
fine-grained GitHub token scoped only to `DACS-Agent-commerce/dacs-sdk` with read-only
Contents access. The build passes it to Git without writing it to the checkout or remote
URL.
Expand All @@ -114,7 +115,7 @@ vendor directory.
| `DEMOS_RPC` | No | Demos RPC base URL; defaults to the public testnet endpoint |
| `NEXT_PUBLIC_DEMOS_RPC` | No | Credential-free public HTTPS Demos RPC advertised to machine clients; defaults to the public testnet endpoint and deliberately does not inherit `DEMOS_RPC` |
| `DACS_ADMIN_TOKEN` | Production | Bearer token for the operational reindex endpoints |
| `DACS_SDK_GITHUB_TOKEN` | GitHub deploy | Fine-grained, read-only token for cloning the pinned private SDK during the build; not needed after the SDK is published |
| `DACS_SDK_GITHUB_TOKEN` | No | Optional fine-grained, read-only token that only authenticates the build's clone of the pinned SDK; the SDK repository is public |
| `DACS_DIRECTORY_DATA` | No | Writable directory for the SQLite repository and legacy JSON migration inputs |
| `DACS_SCAN_MAX_TXS` | No | Maximum transactions scanned per pass; defaults to `100000` and fails closed if insufficient |
| `DACS_SCAN_FINALITY_DEPTH` | No | Newest transaction count held back before indexing; defaults to `2` |
Expand All @@ -126,25 +127,69 @@ vendor directory.
| `DACS_RECIPE_POLICIES` | For tier elevation | JSON array of version-pinned DACS-2 recipe policies (`scheme`, `recipeVersion`, `methods`, `defaultMaxAgeSec`, `availability`, `trustedResultSigners`); absent/invalid policy fails closed to `self-declared` |
| `DACS_TRUST_PROXY` | No | Set to `1` only behind a trusted proxy that overwrites client-IP headers; otherwise the in-process rate limiter is disabled and the deployment must enforce its edge limit |
| `NEXT_PUBLIC_DIRECTORY_URL` | Production | Public origin used by canonical URLs, sitemap, `llms.txt`, and machine-discovery documents; defaults to `http://localhost:3400`, which silently poisons production canonical URLs and the sitemap — the server logs a warning when unset in production |
| `NEXT_PUBLIC_BUTLER_ORIGIN` | Production | Public HTTPS origin of the DACS agent gateway used by `/try`; defaults to `http://127.0.0.1:8402` only for local development. Railway validates this at build time. |

The data directory must be persistent and writable in deployments that accept
registrations or run the indexer. Never commit `.indexer-seed`, `.indexer-mnemonic`,
or an admin token.

The project is pinned to Node 22 through `.nvmrc` and `package.json` engines. Before
promoting a deployment, verify both gateway reachability and its explicit CORS allowlist:
The exact Node release is pinned in `.nvmrc`, and `package.json` engines names the
same release; `test/node-runtime.test.mjs` fails if they diverge or if any other
file Railpack's mise reads could pick a release. CI installs Node from `.nvmrc`.
Railway's Railpack builder takes engines first and then lets `.nvmrc` override it
through mise's idiomatic version files (railpack `v0.41.0`,
`core/providers/node/node.go:419-494`), so production builds on the same release
either way. Leave `RAILPACK_NODE_VERSION` unset: `.nvmrc` overrides it. Railway does
not pin which Railpack release builds the service, so the build and
`start:railway` both run `npm run check:node-runtime` first, which fails unless the
running Node is the `.nvmrc` release. Before promoting a deployment, validate the
public canonical origin:

```bash
NEXT_PUBLIC_DIRECTORY_URL=https://directory.example \
NEXT_PUBLIC_BUTLER_ORIGIN=https://agents.example \
npm run check:butler
NEXT_PUBLIC_DIRECTORY_URL=https://directory.your-domain.org \
npm run check:deploy-config
```

The probe fails unless the gateway returns at least one Butler agent, its
`Access-Control-Allow-Origin` exactly matches the directory origin, and browser
preflights for both execution routes allow `POST` with `content-type`. Configure
the gateway's `BUTLER_ALLOWED_ORIGINS` with that directory origin before running it.
The deployment check requires one credential-free HTTPS origin with no path, query,
or fragment, written exactly as that origin (an optional trailing `/` is allowed),
on a public host. Ports that browsers refuse to fetch (the Fetch Standard's bad
ports, including 0) are rejected.

- A host name needs at least two non-empty labels, must not end with a dot, and must
not be, or end in, an IANA special-use name (`localhost`, `local`, `test`,
`invalid`, `onion`, `alt`, or anything under `arpa`), the private-use `internal`,
or `localdomain`. The RFC 6761 documentation names (`example`, `example.com`,
`example.net`, `example.org`) are rejected too, unless the check runs with
`--allow-documentation-names`, as CI does for its placeholder origin. Production
builds (`railway.json`) run the check without it.
Other names that are merely undelegated (for example `lan` or `corp`) are not
rejected.
- An IP literal must be global unicast and outside every block in the IANA IPv4 and
IPv6 Special-Purpose Address Registries. IPv4 rejects loopback, unspecified,
private, shared (CGNAT), link-local, documentation, benchmarking, protocol
assignments, AS112, AMT, reserved, and broadcast, plus multicast and the
deprecated 6to4 relay block. IPv6 must be in `2000::/3`, outside the
documentation, IETF-protocol (including Teredo), 6to4, Direct Delegation AS112,
and `3fff::/20` blocks, so IPv4-mapped, NAT64, and other forms that embed an IPv4
address are rejected.
- The check fails closed: the registries' globally reachable rows (PCP and TURN
anycast inside `192.0.0.0/24`; AS112 and AMT; protocol anycast, ORCHIDv2, and
DRIP inside `2001::/23`) are protocol addresses, not hosting space, and are
rejected too.

`test/fixtures/origin-policy-sources.json` records where these rules come from:
every unexpired row of both IP registries (from Wireshark's IANA-generated tables,
which drop terminated rows and record an N/A Globally Reachable as false; the two
terminated blocks IANA still lists are rejected anyway), every special-use name
(IANA's registry XML from a mirror, cross-checked with Unbound and Knot Resolver),
and the Fetch Standard's bad ports. Each comes through a copy pinned to an upstream
revision and sha256 (as of 2026-10-15), plus the deliberate deviations. The tests
check the tables against it offline, and check the verifier against in-memory
copies; `npm run verify:origin-sources` (needs network) re-fetches each pinned copy
and checks the fixture against it. Node's `fetch` ignores `HTTPS_PROXY`, so behind
a proxy run `node --use-env-proxy scripts/verify-origin-sources.mjs` instead.
Generated canonical URLs and discovery documents therefore cannot silently point at
localhost or a different base path. The check does not resolve DNS, so a public name
that resolves to a private address still passes.

## Human and agent discovery

Expand Down
Loading