Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4a193e9
fix(dacs-directory): accept only canonical signature values on the co…
randomblocker Oct 14, 2026
74ebc0c
fix(dacs-directory): validate listings against the full DACS-1 and PS…
randomblocker Oct 14, 2026
7ddc0b4
fix(dacs-directory): never publish a listing as active while its revo…
randomblocker Oct 14, 2026
8ea4d4d
fix(dacs-directory): re-examine a rejected revocation marker whose st…
randomblocker Oct 14, 2026
b5105ef
fix(dacs-directory): attribute discovered deals through the bundle's …
randomblocker Oct 14, 2026
41c616c
fix(dacs-directory): reconcile a seller's verified deals per jobId (§…
randomblocker Oct 14, 2026
cc1e3dc
fix(dacs-directory): check recognised listing members against their D…
randomblocker Oct 14, 2026
3936c74
fix(dacs-directory): hold agreements and vet records in the evidence …
randomblocker Oct 14, 2026
d0aa844
test(dacs-directory): pin the browser deal verdict and the artifact p…
randomblocker Oct 14, 2026
190e2d5
fix(dacs-directory): break SB-2 settlement ties on the lower evidence…
randomblocker Oct 14, 2026
bf6d935
fix(dacs-directory): treat the node's 400 INVALID_REQUEST storage ans…
randomblocker Oct 14, 2026
f08877c
fix(dacs-directory): show only the seller's own delivery attestation …
randomblocker Oct 14, 2026
c89dc6c
docs(dacs-directory): record the pinned upstreams and correct stale n…
randomblocker Oct 14, 2026
a40203b
test(dacs-directory): pin the listing gate and RB-4 rules the mutatio…
randomblocker Oct 15, 2026
759a638
test(dacs-directory): pin the evidence graph and BundleBinding rules
randomblocker Oct 15, 2026
a0d82d0
test(dacs-directory): give the legacy deal path a positive end-to-end…
randomblocker Oct 15, 2026
aca78b8
test(dacs-directory): pin the identity tier, reputation derivation an…
randomblocker Oct 15, 2026
fd851fe
fix(dacs-directory): hold evidence, settlement, reputation, catalog a…
randomblocker Oct 15, 2026
b8b8381
test(dacs-directory): vendor the pinned Standard's vector sets and ru…
randomblocker Oct 15, 2026
79101db
docs(dacs-directory): record the node deployment contract and enforce…
randomblocker Oct 15, 2026
8110b41
fix(dacs-directory): align evidence and catalog derivation with pinne…
randomblocker Oct 16, 2026
945ce64
fix(dacs-directory): reconcile catalog saves and validate pipeline ev…
randomblocker Oct 16, 2026
fd4da2f
fix(dacs-directory): enforce signed session and payment evidence cont…
randomblocker Oct 16, 2026
6e4f28a
fix(dacs-directory): bind procurement vet proof to counterparty
randomblocker Oct 16, 2026
853d564
fix(dacs-directory): bind vetting to presented identity evidence
randomblocker Oct 17, 2026
e958a82
fix(dacs-directory): apply recipe policies to session verification
randomblocker Oct 17, 2026
e8ac9d4
fix(dacs-directory): verify storage delivery and neutral verification…
randomblocker Oct 17, 2026
8f9d3d7
fix(dacs-directory): bind complete payloads and optional vet policies
randomblocker Oct 17, 2026
121d041
test(dacs-directory): pin nested signer, x402 orphan chainId and grap…
randomblocker Oct 17, 2026
0a7d064
fix(dacs-directory): bind settlement verification and neutral fault s…
randomblocker Oct 17, 2026
27e6c79
fix(dacs-directory): gate settlement profiles and unify discovery exp…
randomblocker Oct 17, 2026
127d951
fix(directory): enforce pinned session evidence admission
randomblocker Oct 17, 2026
24efd89
fix(directory): bind current evidence profiles and migrate revocation…
randomblocker Oct 17, 2026
c9fae5a
fix(directory): bind Vet challenges to complete attempts
randomblocker Oct 17, 2026
0119e3e
fix(directory): enforce pinned verification and withdrawal contracts
randomblocker Oct 18, 2026
ff86556
fix(directory): authenticate withdrawal discovery and retain incomple…
randomblocker Oct 18, 2026
fcf4fd3
fix(directory): verify withdrawal publishers and expose signing guidance
randomblocker Oct 18, 2026
c59af8f
fix(directory): retain authenticated withdrawals and bound discovery …
randomblocker Oct 18, 2026
8f40621
fix(directory): retain source withdrawal checks across discovery upgr…
randomblocker Oct 18, 2026
1ba1cb2
fix(directory): scope upgrade replay holds and retain source index or…
randomblocker Oct 18, 2026
8994dfb
fix(directory): hash complete listing input specifications
randomblocker Oct 18, 2026
7f2da25
fix(directory): withhold creates after incomplete fallback searches
randomblocker Oct 18, 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
12 changes: 5 additions & 7 deletions .github/workflows/dacs-directory-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,16 @@ jobs:
# Exercise the same production build-time path as the public /try page.
NEXT_PUBLIC_BUTLER_ORIGIN: https://agents.ci.local
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- uses: actions/setup-node@v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
node-version: "22"

- 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.
# DACS-Agent-commerce/dacs-sdk is public, so the clone needs no token.
# setup-sdk.sh still authenticates the nested clone with
# DACS_SDK_GITHUB_TOKEN when present, for builds from a private mirror.
env:
DACS_SDK_GITHUB_TOKEN: ${{ secrets.DACS_SDK_GITHUB_TOKEN }}
run: npm run setup
Expand Down
195 changes: 176 additions & 19 deletions reference-implementations/dacs-directory/README.md

Large diffs are not rendered by default.

117 changes: 117 additions & 0 deletions reference-implementations/dacs-directory/SPEC_PIN
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# DACS Standard revision this Directory implements and tests against
# github.com/DACS-Agent-commerce/DACS-Standard, v0.4 (annotated tag 0be9e80b342d0f3e269ae9ab45c2bbc372f161d4)
4bb9e48a1095ab32c06c25b7c0b52018d3ce4091
# Profile notes: primary claims use the did:demos:agent ClaimReference profile
# (src/catalog/claimRef.ts), which the pinned revision does not yet define. It comes from
# DACS-Standard #296, merged into `next` at 725fa78bd124a7adb65df27adee376d5d44a4bd7; its fixture
# is vendored below. Its keys are Ed25519, so artifacts signed with ecdsa-secp256k1 or
# sr1-aggregate cannot be verified under this profile and are not published or counted.
# Recorded deviations (vectors: test/standard-vectors.test.ts; local guards: test/evidence-graph-rules.test.ts,
# test/indexer-binding-rules.test.ts, test/legacy-deal-rules.test.ts and test/native-payment.test.ts):
# §8.5.2 checks 5-6 need the CommitmentRecord's anchored committedAt, which a bundle does not
# reference: a session whose listing sets deadlineSecAfterCommit or validity.notAfter is
# indeterminate and never counted.
# CD-1: a non-canonical amount (e.g. "95.00") is rejected rather than canonicalised (fail-closed).
# agreement-listing-v0.1 `fractional-pct-out` expects reject; the §8.5.2 text (round to the CD-1
# digits of bandCenter) accepts it, and the Directory follows the text.
# SB-1: pinned evm/x402 refs omit logIndex; Solana refs omit instructionIndex. These refs are
# error and never counted. AP2, htlc-* and liquidity-tank refs have no SB-1 key format and are
# likewise uncounted (HTLC-5 session binding is not an SB-1 key). Every legacy SDK evm-erc20
# or x402 deal is uncounted because its TxRef lacks the event index; only legacy pay-dem keys.
# Authoritative bundle absence: the Demos mapping declares no absence-evidence policy. Both
# absence sets below run; their policy-qualified one-sided pass/include cases deliberately
# remain indeterminate/excluded. No Directory endpoint authenticates those projected proofs.
# Vet admission resolves historical IdentityBundle bytes and checks the presentation/hash. This
# profile can read a signed BundleParty extension `identityBundle` (inline) or
# `identityBundleRef` (storage-program locator and IdentityBundle hash), or use
# EvidenceGraphDeps.resolveIdentityBundle. Publication self-bindings cannot supply session identity.
# IdentityBundle hashes omit presentation per DACS-1. Session Vet additionally requires a trusted
# resolveSessionChallenge adapter authenticating verifier issuance, job/party/nonce, challenge
# lifetime, authenticated consumedAt and consumedByPresentationHash: sha256Hex(canonicalize(identity))
# of the complete first attempted IdentityBundle including presentation/signatures, even on failure (CORE SN-1..4).
# A signed-scope bundle hash is insufficient; historical adapters must supply the complete digest.
# IdentityBundle.presentedAt is schema-checked but informational.
# No production historical-challenge resolver is installed: those sessions remain indeterminate.
# Listing publication continues to accept a nonce-free signed self-binding.
# Required credential matching uses the signed composite generatedAt (unix ms) as the historical
# Vet instant, bounded by finalisedAt; it does not expire old sessions against the wall clock.
# Session recipeRegistryVersion selects an operator-authenticated DACS_RECIPE_SNAPSHOTS roster.
# Unknown snapshots and results outside that roster remain indeterminate. Unversioned requirements
# select the latest recipe in the roster. validUntil governs; without it the roster supplies defaultMaxAgeSec for the exact
# scheme/recipeVersion. Every required result, including one with validUntil, also needs an
# live exact-version recipe policy, an allowed method and a trusted authenticated signer.
# RAV-2: the catalog does not disclose per-result availability yet. Non-live vet results
# remain indeterminate; non-live recipes cannot elevate identity tiers either. This applies
# to every included VerifyResult, optional ones too: each needs a valid live recipe policy.
# Wrapper expiry and listing maxAge only narrow that authority window.
# No scheme-specific requirement-parameter interpreter is configured: non-empty parameters
# are unmatched, as DACS-1 requires for unrecognised parameters, rather than inferred from data.
# DV-1..3: storage delivery resolves object cleartext and node ACL metadata. The DV-1 hash covers
# the complete NFC/JCS payload, including any signature/signatures fields (the pinned SDK's
# contentHash strips them; the Standard's lifecycle walkthrough does not). Unreadable payloads,
# encrypt-to-buyer payloads (no buyer decryption capability), and external pointer payloads
# (no external cleartext resolver) remain indeterminate and never count as completion/volume.
# Rail admission uses explicitly provisioned DACS_RAIL_SNAPSHOTS (PA-1 operator trust).
# Snapshots pin registryVersion and railVersion, handler, asset, network, parameters and availability.
# Operators must authenticate the registry index/definitions before provisioning; no snapshot is
# bundled or inferred from a listing or RPC. Unknown versions remain indeterminate. Only a live
# demos-native:DEM definition with native-dem/DEM/9 and network demos can use the native adapter.
# Native pay-dem: success additionally requires blockNumber and getTxByHash/getBlockByNumber
# reads establishing confirmed inclusion, native-send endpoints and the exact DEM price in OS.
# Stored RPC amounts are numeric/string OS; the original send payload must carry canonical string OS.
# Numeric legacy DEM payloads need historical fork semantics and remain indeterminate. Unavailable reads never count. The node source calls terminal inclusion "confirmed".
# Attested-payload and entitlement success remain indeterminate until authoritative attestation
# and grant adapters can establish their agreement-bound proofs; signatures alone do not count.
# All non-native current rails and paid SDK legacy sessions remain indeterminate without authoritative
# adapters; they never contribute completion or volume. AP2 and x402 success is indeterminate: this profile has no authenticated provider-receipt
# resolver. A signed receipt pointer/hash alone cannot establish provider or HTTP settlement.
# Current publisher rail IDs are demos-native:DEM and x402:default. Historical signed phase-name
# aliases remain readable for evidence, but public discovery excludes those current offers;
# their publisher must issue a new signed version. Legacy SDK rail names retain that profile.
# Anonymous restricted-program reads on node ac8f45f are denied: buyer-only is indeterminate
# on that deployment. ACL-success tests model a hypothetical readable metadata contract.
# Revocation candidates: discovered markers returning NOT_FOUND, PERMISSION_DENIED or a transport
# failure stay pending/indeterminate. Persistent 1–60 second exponential backoff bounds retries.
# Only successfully read invalid content can reject a candidate; verified revocation is retained.
# Per-claim PresentationSignature.signature retains the SDK auto-detected legacy encoding;
# strict SIG-6 value-envelope decoding is enforced separately.
# Skipped vector sets at this pin:
# private-deliverables-v0.1: projected cci/40-byte buyer addresses and encryption/entitlement
# gates do not map directly to this agent profile; signed local graph cases pin DV-1..3.
# verifyresult-acceptance-v0.1: recipe-registry steward signatures and cci claims are outside
# the deployment-policy recipe adapter; local vet/recipe tests pin the supported profile.
# fab-bundle-extended-pointer-v0.3: no external extended-pointer resolver.
# settlement-finalization-propagation-v0.3 and receipt-rederivation-v0.3: handler-side
# finalization/receipt emission, not this read-only catalog consumer.
# commitment-anchor-authority-v0.3: no CommitmentRecord locator is exposed by current bundles.
# bundle-binding-v0.1 and outsider-binding-flooding-v0.3 use did:demos:buyer/seller signed claims
# outside the supported did:demos:agent profile. Changing claims would alter signed bytes;
# local bundle-binding tests cover BB-4..8 without rewriting these upstream fixtures.
# Previously skipped unresolved-vs-absent-v0.3 and bundle-absence-evidence-v0.3 are now vendored
# and exercised, with the two one-sided pass cases explicitly recorded above.
# Node compatibility: ac8f45f pins source/API behavior, not the revision of a remote deployment.
# No remote revision attestation is available; runtime probe output is dated evidence only.
# Vendored conformance vectors (byte-identical copies; sha256 checked by test/standard-vectors.test.ts):
# conformance/vectors/security/signature-value-encoding-v0.1.json bf47cd0a42b0e0e30f2ff032327395d5a1b820758e6188c51212eb9129ba8312
# conformance/vectors/security/revocation-binding-v0.3.json 3e83dfe5cb603e3dce98a4280e2456bb30bcae9979f4a0db1ae322982bdaf1ce
# conformance/vectors/security/sb2-settlement-uniqueness-v0.1.json 31c9455dc0147f27cf7a7d47cb17ffc019016bf63aa366b9dbadd720f8b16b81
# conformance/vectors/security/agreement-listing-v0.1.json 857ebf0ea4b885b92b0d6ad509a37cf5076218881c510ef2f2d05d734d94503b
# conformance/vectors/security/mixed-version-reconciliation-v0.3.json 41c1bc7eee95ac3bf625eaea4db88929e2eb6cb20c6bade9c3192dc3337a9e0b
# conformance/vectors/security/fault-bundle-perspective-pair-v0.3.json f56586a27afa851202a121576814eb0a68bddc4bdc9ce675740ba085870ed285
# conformance/vectors/security/payee-destination-binding-v0.1.json cd83d95982d6529ac3f776d48df8d92c86b80806a88e96a62d80674563aaa779
# conformance/vectors/security/listing-preserve-unknown-v0.1.json 7dbe60aa89d44321a3bb04661b1d71fc15bc84501b76f23b486a05cb905f91bd
# conformance/fixtures/identity/demos-agent-claim-reference.json (at 725fa78) demos-agent-claim-reference.json 0e70d2c12d9d4dd14d676519e1841b12767051153044659adcadbf8a3c994933
# conformance/vectors/security/unresolved-vs-absent-v0.3.json 8a543319798cb60073631a9a2431f45e866f89db8b136a76b679780718a15b91
# conformance/vectors/security/bundle-absence-evidence-v0.3.json 304c07d0f39f92db52dc3f5d2feac9149cf253298abe514668501451e9ee7b8e
# conformance/vectors/security/metered-pricing-v0.3.json 36ec8725d058062ab6dfefe79a8b0c7c97b33be3c614b6073f392647c05d4009
# conformance/vectors/security/phase-kind-divergence-v0.3.json 0c2c97ca1d4dece0b1f16011834aa21c446b3226311ef0a23417228565ed151e

# Additional consumer/publisher profile:
# Canonical hashing rejects lone UTF-16 surrogates in values and keys (server and browser).
# The pinned SDK serializer accepts only integer JSON numbers. Listing publication rejects
# fractional negotiation percentages with HTTP 400; fractional-percentage publication is unsupported.
# Authority evidence needs a method-specific authenticated proof/hash/signer/validator-epoch adapter.
# No production authority adapter is installed; identity tiers and session evidence fail closed.
# Listing discovery retains revoked tombstones with the normative `revocation` binding.
# Generated A2A cards require an explicit service endpoint and transport; otherwise only the
# DACS discovery descriptor is emitted, without claiming an A2A service at the hosting origin.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* nonce plus the live empty salt, and readback re-binds owner/name/content.
*/
import { NextRequest, NextResponse } from "next/server";
import { contentHash, listingAddress } from "@kynesyslabs/dacs/canonical";
import { assertValidUnicode, contentHash, listingAddress } from "../../../../src/catalog/canonical.js";
import { ed25519Verify, publicKeyFromRaw } from "@kynesyslabs/dacs/crypto";
import {
deriveStorageAddress,
Expand Down Expand Up @@ -76,9 +76,10 @@ export async function POST(req: NextRequest) {
if (body.description.length > 2000) {
return NextResponse.json({ error: "description exceeds the 2000-char spec cap" }, { status: 400 });
}
try { assertValidUnicode(body); } catch { return NextResponse.json({ error: "invalid Unicode" }, { status: 400 }); }
if (
!Array.isArray(body.rails) || body.rails.length > 16 ||
!body.rails.every((v) => typeof v === "string" && /^[a-z0-9:-]{1,128}$/.test(v)) ||
!body.rails.every((v) => typeof v === "string" && /^[A-Za-z0-9:-]{1,128}$/.test(v)) ||
!Array.isArray(body.delivery) || body.delivery.length > 16 ||
!body.delivery.every((v) => typeof v === "string" && /^[a-z0-9:-]{1,128}$/.test(v))
) {
Expand Down Expand Up @@ -130,9 +131,9 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "pricing needs a positive canonical amount and currency" }, { status: 400 });
}
if (pricingKind === "negotiable" && (
!Number.isFinite(Number(body.pricing?.minPct)) || !Number.isFinite(Number(body.pricing?.maxPct)) ||
!Number.isSafeInteger(body.pricing?.minPct) || !Number.isSafeInteger(body.pricing?.maxPct) ||
Number(body.pricing?.minPct) < 0 || Number(body.pricing?.minPct) >= 100 || Number(body.pricing?.maxPct) < 0
)) return NextResponse.json({ error: "negotiation percentages are invalid" }, { status: 400 });
)) return NextResponse.json({ error: "negotiation percentages must be nonnegative integers in this publisher profile" }, { status: 400 });
if (pricingKind === "auction" && body.pricing?.selectionRule && !["lowest-price", "highest-price", "first-acceptable"].includes(body.pricing.selectionRule)) {
return NextResponse.json({ error: "unsupported auction selection rule" }, { status: 400 });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { verifyBundleBinding } from "@/src/catalog/bundleBinding";
import { parsePagination } from "@/src/catalog/pagination";
import { loadScanState } from "@/src/catalog/store";

/**
* DACS-1 §6.3.6: an unauthenticated read that serves every BB-4-valid BundleBinding this catalog
* holds for the jobId, whoever signed it, in pages. The BB-6 per-signer budget applies only when
* a consumer resolves a side, never to what the catalog serves.
*/
export async function GET(
_request: Request,
request: NextRequest,
{ params }: { params: Promise<{ jobId: string }> },
) {
const { jobId } = await params;
if (jobId.length < 1 || jobId.length > 160) {
return NextResponse.json({ error: "jobId must be 1-160 characters" }, { status: 400 });
}
const candidates = loadScanState().bundleBindings?.[jobId] ?? [];
const bindings = (await Promise.all(candidates.map((binding) => verifyBundleBinding(binding))))
const page = parsePagination(request.nextUrl.searchParams.get("limit"), request.nextUrl.searchParams.get("cursor"));
if (!page.ok) return NextResponse.json({ error: page.error }, { status: 400 });
const stored = loadScanState().bundleBindings;
const candidates = stored && Object.prototype.hasOwnProperty.call(stored, jobId) ? stored[jobId] ?? [] : [];
const slice = candidates.slice(page.cursor, page.cursor + page.limit);
const bindings = (await Promise.all(slice.map((binding) => verifyBundleBinding(binding))))
.filter((binding) => binding !== null);
return NextResponse.json({ bindings });
const next = page.cursor + page.limit;
return NextResponse.json({ bindings, total: candidates.length, cursor: next < candidates.length ? String(next) : undefined });
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Normative filters plus q/profile directory extensions.
*/
import { NextRequest, NextResponse } from "next/server";
import { activeCatalogListings, primaryClaimMatches } from "@/src/catalog/discovery";
import { discoveryListing, primaryClaimMatches } from "@/src/catalog/discovery";
import { loadCatalog } from "@/src/catalog/store";
import { parsePagination } from "@/src/catalog/pagination";
import { catalogJson } from "@/src/catalog/http";
Expand Down Expand Up @@ -48,9 +48,8 @@ export async function GET(req: NextRequest) {
const { limit, cursor } = pagination;

const catalog = loadCatalog();
// Discovery only advertises offers that remain active. Revoked listings stay
// available on the seller history/detail surfaces with an explicit status.
const all = activeCatalogListings(catalog);
// Machine discovery retains authenticated withdrawal tombstones.
const all = catalog.sellers.flatMap((seller) => seller.listings).filter(discoveryListing);
const tierByClaim = new Map(catalog.sellers.map((seller) => [seller.primaryClaim, seller.identityTier ?? "self-declared"]));
const filtered = all.filter((l) => {
if (category && l.offering.category !== category && !l.offering.category.startsWith(category + ".")) return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import { NextRequest, NextResponse } from "next/server";
import { isListing } from "@kynesyslabs/dacs/artifacts";
import { stripSignature } from "@kynesyslabs/dacs/canonical";
import { stripSignature } from "../../../../src/catalog/canonical.js";
import { parseCciRecord } from "@kynesyslabs/dacs/identity";
import { readAnchor } from "@/src/catalog/chain";
import { gcrGetIdentities } from "@/src/catalog/gcr";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* derived only from chain-verified bundles; re-derivable client-side).
*/
import { NextRequest, NextResponse } from "next/server";
import { discoveryListing } from "@/src/catalog/discovery";
import { loadCatalog } from "@/src/catalog/store";
import { catalogJson } from "@/src/catalog/http";
import { requestBaseUrl } from "@/src/catalog/publicUrl";
Expand All @@ -22,7 +23,7 @@ export async function GET(
if (!seller) return NextResponse.json({ error: "seller not found" }, { status: 404 });
const origin = requestBaseUrl(req);
return catalogJson(req, {
listings: seller.listings,
listings: seller.listings.filter(discoveryListing),
identity: seller.identityBundle ?? {
profile: "legacy-sdk-v0.1",
primaryClaim: seller.primaryClaim,
Expand Down
Loading