diff --git a/.github/workflows/dacs-directory-ci.yml b/.github/workflows/dacs-directory-ci.yml index 844cbff..00e7b90 100644 --- a/.github/workflows/dacs-directory-ci.yml +++ b/.github/workflows/dacs-directory-ci.yml @@ -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 diff --git a/reference-implementations/dacs-directory/README.md b/reference-implementations/dacs-directory/README.md index 454124b..966611a 100644 --- a/reference-implementations/dacs-directory/README.md +++ b/reference-implementations/dacs-directory/README.md @@ -114,7 +114,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 | Fine-grained, read-only token for cloning the pinned SDK from a private mirror during the build; the upstream 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` | @@ -123,7 +123,9 @@ vendor directory. | `DACS_CURSOR_STALL_SECONDS` | No | Cursor-stall alert threshold; defaults to twice the valid index interval (minimum `300`, default `1800`, maximum `86400`) | | `DACS_REACHABILITY_MAX_PROBES` | No | Maximum due listing surfaces probed per reindex; defaults to `20` (bounded to `1..100`) | | `DACS_REACHABILITY_CONCURRENCY` | No | Concurrent pinned HTTPS reachability probes; defaults to `5` (bounded to `1..10`) | -| `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_RAIL_SNAPSHOTS` | For positive payment verdicts | Operator-authenticated PA-1 registry snapshots (`registryVersion`, `rails` with exact `railId`, `railVersion`, `phaseHandler`, `railType`, `asset`, `network`, `parameters`, `availability`); absent/unknown versions remain indeterminate. Provision only after authenticating the registry index and definitions. Only live native DEM definitions have a settlement adapter. | +| `DACS_RECIPE_POLICIES` | For identity tier policy | JSON array of version-pinned DACS-2 recipe policies (`scheme`, `recipeVersion`, `methods`, `defaultMaxAgeSec`, `availability`, `trustedResultSigners`); absent/invalid policy leaves claims unverified for tiers and included vet results indeterminate for session admission | +| `DACS_RECIPE_SNAPSHOTS` | For session vetting | Operator-authenticated JSON array of historical `{registryVersion, recipes}` snapshots; `recipes` uses the same policy shape. Session pins select exact snapshots; unversioned requirements select the latest recipe in that snapshot. Missing snapshots remain indeterminate. | | `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. | @@ -171,9 +173,13 @@ DACS artifact; it does not attribute a publishing failure to an agent. Raw excep payloads, internal URLs and stack traces are never returned. Storage-read diagnostics distinguish `STORAGE_NOT_FOUND`, `STORAGE_NOT_PUBLIC`, -`STORAGE_RPC_UNAVAILABLE`, and `STORAGE_INVALID_RESPONSE`. Missing and non-public +`STORAGE_RPC_UNAVAILABLE`, `STORAGE_INVALID_RESPONSE`, and `ARTIFACT_REJECTED`. Missing and non-public locators are terminal until a later chain replay observes them again; transient node -and response failures retain bounded retries. `STORAGE_NOT_FOUND` is operational +and response failures retain bounded retries. `ARTIFACT_REJECTED` means the content was +read but fails the artifact policy (oversized, a DACS-named program without object data, +or no canonical JSON form). It has its own retry class: it is never dead-lettered, it is +retried with backoff while at most 1,000 rejected locators are active, and the +longest-waiting one takes the next free slot. `STORAGE_NOT_FOUND` is operational diagnostic evidence only: under the current Demos mapping it is never authoritative DACS-5 absence evidence and cannot satisfy BB-8. @@ -185,27 +191,43 @@ DACS-5 absence evidence and cannot satisfy BB-8. registration. 2. **Chain scanning** (passive): the reindex pass walks the node's transaction history (`nodeCall getTransactions`, plain fetch), spots storage-program writes, classifies - anchored DACS artifacts by their self-describing program names, and attributes deals - to sellers via the buyer-anchored agreement. Agents nobody registered appear as + current-profile DACS artifacts by their signed content, and attributes a deal to its + seller through the agreement its buyer bundle references by locator and content hash. + Program names are never resolution keys, except on the legacy SDK compatibility path + (`dacs5:bundle:`, `dacs3:agreement:`). Agents nobody registered appear as "discovered on-chain". Depth: `DACS_SCAN_MAX_TXS` (default 100000); a pass that hits the cap fails rather than advancing the cursor and silently skipping history. - Revocation discovery retains at most 16 candidates per listing hash, except for - locators that already passed RB-4 verification; truncation is recorded in reindex logs. - New scan observations precede prior unverified state in that window; within each - group, the first distinct locators in scan iteration order survive. A valid marker - outside the retained window is not evaluated. Its publisher can anchor a fresh - marker to re-enter discovery, but continued overflow can exclude that marker again. - After one marker verifies, later pruning cannot make it disappear. - BB-4-valid BundleBindings are also classified and accumulated under a deterministic - per-job/role total-work ceiling; any overflow is sticky and makes that side - `indeterminate`, never absent. + Revocation markers are recognised by their signed content, never by program name. + Every candidate locator is queued once per listing hash; each pass fully verifies + at most 16 per listing — markers that already passed RB-4 first, then those + anchored by the listing owner, then arrival order. The queue itself has no cap. + While any candidate is unread or not yet examined the RB-4 check is incomplete + (`indeterminate`, RB-5), so the listing is withheld from the catalog rather than + published `active`. Once a marker verifies, the listing stays `revoked` with its + binding even if the marker later cannot be read (RB-3). + BB-4-valid BundleBindings are also classified and kept, all of them: DACS-1 §6.3.6 + requires a carrying catalog to serve every such record whoever signed it, and + `GET /api/dacs/bundles/{jobId}` pages through the full set (`limit`, `cursor`). The + Directory's own BB-6 resolution works through at most 32 per signer of each job and + role; another signer's bindings never use that budget. Overflow is sticky and makes + that signer's side `indeterminate`, never absent. There is no total across signers, + so stored bindings, and the work to re-verify them each pass, grow with the number of + bindings published for one job. 3. **Evidence graph and federation**: current bundle copies are reached only after deriving the role-specific logical address and resolving a signed BundleBinding. The optional DACS-1 well-known bundle-binding index is hash-bound and SSRF-bounded. Resolved bundles recursively validate listings, agreements, settlement evidence and amendment chains, composite/VerifyResult vet - records, and ratings. Legacy SDK artifacts remain on an explicitly-labelled - compatibility path. + records, and ratings. The signed listing fixes every phase kind and index; completed + sessions include every listed settle phase, with one evidence record per invocation. + A successful payment states its amount, rail-specific finality and observation time, + and transactions. Payment phase indices are recovered independently from the SR-2 + program name and checked against the evidence hash and the listing rail. Settlements + are keyed per SB-1 (`demos:{txHash}`, `evm:{chainId}:{txHash}:{logIndex}`, + `solana:{cluster}:{signature}:{instructionIndex}`); a reference that cannot be keyed + leaves the deal listed but uncounted. One catalog-wide SB-2 ledger admits claims before + computing any seller or category totals. Legacy SDK artifacts remain on an + explicitly-labelled compatibility path. ## Architecture note: the web app is chain-fetch-only @@ -234,12 +256,46 @@ client (browser: @noble-shimmed `node:crypto`, base64url-patched Buffer). unusually large interval exceeds the configured cap. - **DACS-2 recipe governance is deployment policy.** `verifiedBy` evidence cannot elevate a tier unless its exact recipe version/method/availability/max-age policy is - present in `DACS_RECIPE_POLICIES`; missing policy fails closed. + present in `DACS_RECIPE_POLICIES`; missing policy fails closed. Required session vetting + selects the exact `DACS_RECIPE_SNAPSHOTS` registry pin: the scheme/version must be live, permit the result method, + and trust its authenticated signer, including results with an explicit expiry. Every + included result, even an optional one, needs a valid live policy. Missing, invalid and + non-live recipes remain indeterminate until the catalog can disclose each result's availability. +- **Authority proofs need a method-specific deployment verifier.** `EvidenceGraphDeps.verifyAttestation` + and the identity-tier verification argument must authenticate the original format's hash scope, + required signer, validator roster/epoch and proof. The result signer's signature does not replace + this check. No production authority adapter is installed, so these proofs remain indeterminate + for sessions and do not elevate identity tiers. +- **Canonical input uses Unicode scalar values.** Server and browser reject lone UTF-16 surrogates + in string values and property names before canonical hashing. This publisher's pinned serializer + supports integer negotiation percentages; fractional percentages receive HTTP 400. This is a + narrower publication profile than the Standard's fractional-percentage inputs. +- **Discovery keeps withdrawals.** Machine catalog and well-known listing indexes retain revoked + versions with `revocation`; active skills and sitemap links use executable offers. Crawled + withdrawals enter the marker verification queue; unreadable proofs withhold active admission. + Cached `revocationBinding` summaries migrate to the normative wire field on read. +- **A2A cards require an actual service endpoint.** The well-known generator accepts `a2aEndpoint` + and `a2aTransport` (`JSONRPC`, `GRPC`, or `HTTP+JSON`) together. The endpoint may be hosted + separately from the discovery domain. Without them it emits a DACS discovery descriptor at + `agent.json`, with no A2A transport declaration or `agent-card.json` file. Endpoint configuration + is the publisher's declaration; generation does not probe its protocol implementation. +- **Storage delivery is verified from readable cleartext and node access metadata.** + Successful `deliver-storage-program` evidence must name a payload anchor and cleartext hash. + The hash covers the complete canonical payload, including any `signature` or `signatures` + fields. The payload must match that hash and the job's program name; `buyer-only` requires a + restricted ACL naming the agreement-bound buyer. The pinned node denies anonymous reads of + restricted programs, so buyer-only delivery is indeterminate for this anonymous catalog. The + readable restricted-ACL tests describe a hypothetical node metadata contract. Unreadable payloads, encrypted payloads + and external pointers remain indeterminate and contribute no completion or volume. - **BundleBinding key resolution currently implements the directory's canonical Demos agent profile.** BB-4 accepts self-describing `did:demos:agent:<64hex>` claims. A binding signed through another ClaimReference/key-resolution method is not carried or used until that resolver is configured; it fails closed to `indeterminate` rather than being relabelled as verified. +- **DACS-3 §8.5.2 checks 5 and 6 need the commitment's anchored `committedAt`**, and a + bundle does not reference the CommitmentRecord. A session whose listing sets + `terms.deadlineSecAfterCommit` or `validity.notAfter` is therefore `indeterminate` and + never counted; `SPEC_PIN` lists this and the other recorded deviations. - **Listing versions are allocated from observed catalog state**, without a mutable in-process lock. Publishers must serialize writes for one `seller + listingId` until the substrate or SDK provides an atomic version allocator; concurrent publishers can @@ -255,7 +311,108 @@ unresolved recipe/evidence/freshness, and DACS-5 derivation includes ratings, vo settlement uniqueness, anchor-time windowing, and deterministic receipts. Catalog computations remain advisory and independently reproducible from their refs. +Every catalog save applies one settlement ledger across all sellers, including scheduled +indexing and immediate refreshes. A deal's `reputationEligible` records its verified evidence +eligibility; `settlementRejected` records its current catalog admission separately. Rejected +deals contribute no metrics or listing hints, and later saves reconsider them if the winning +claim disappears or changes. Run a full reindex when upgrading an existing catalog so its +cached evidence eligibility is refreshed. + +**Upstream source contracts.** The Directory records these source revisions: + +- **DACS Standard**: `SPEC_PIN` records the revision this Directory implements (v0.4, + `4bb9e48`), the conformance vector files vendored under `test/fixtures/dacs-standard/` + with their hashes, and the recorded deviations. `test/standard-vectors.test.ts` checks + the hashes and runs each set through an adapter onto the Directory's own decisions. The + `did:demos:agent` primary-claim profile is the one deliberate addition; it comes from + DACS-Standard #296 on `next` at `725fa78`, whose fixture is vendored too. +- **dacs-sdk**: `SDK_REV` in `scripts/setup-sdk.sh`. It supplies canonical hashing, + Ed25519, CCI parsing and the verifier for the labelled legacy SDK path. +- **Demos node**: the read contract is pinned to `kynesyslabs/node` `ac8f45f`. + `GET /storage-program/
` answers `{success: false, errorCode}` with 404 + `NOT_FOUND`, 403 `PERMISSION_DENIED` or 400 `INVALID_REQUEST` (an address without the + `stor-` prefix), or `{success, owner, programName, data, ...}`; a program holds at most + 1 MiB (`STORAGE_PROGRAM_MAX_SIZE_BYTES`). Only the node's 404 `NOT_FOUND` is a + definitive absence; a 403 `PERMISSION_DENIED` or any other 4xx, such as a proxy's + or a wrong route's, leaves a revocation check incomplete and a scan read retryable. + `test/chain-storage.test.ts` pins each classification. This source pin does not + attest the revision running at a remote RPC. The configured live service remains + unpinned unless its operator supplies a verifiable deployment revision. + +**Node deployment contract.** At `ac8f45f` the exact-name program search used by the +producer resume lookup exists only as the read-authenticated `gcr_routine +searchStoragePrograms`; a deployed node may also answer it as a `nodeCall` (node2 did on +2026-10-15). The Directory asks the `nodeCall` first and falls back to the `gcr_routine` +only on a recognized "Unknown message" answer. The fallback can recover a listing +after a fresh read confirms its owner. Its pages are ACL-filtered after pagination +and cannot establish absence, so an unresolved fallback lookup returns HTTP 503 +and withholds new publication until a complete search contract is available. +`npm run check:node -- ` is a read-only probe (no +transactions) that reports the storage not-found answer and both search paths, and fails +when they disagree; its output is dated runtime evidence, not a pin. Run it when the +`DEMOS_RPC` deployment changes. + ## License MIT. This in-tree submission is covered by the Community repository's root [`LICENSE`](../../LICENSE). + +Current publishers use the registry IDs `demos-native:DEM` and `x402:default`, paired with +`pay-dem` and `pay-x402` phase kinds. Historical current-profile listings signed with phase +names as rail IDs remain readable as evidence, but are excluded from public discovery; publish +a new signed listing version to make those offers executable. + +Native DEM success requires public transaction and block reads confirming inclusion, the payer +and payee, and the exact agreed amount (1 DEM = 10^9 OS). The stored RPC amount is OS, +carried as a safe integer or an exact string; the send payload must contain a canonical OS +string. Historical numeric DEM payloads remain indeterminate pending a historical fork adapter. +All other current payment rails and paid SDK legacy sessions remain indeterminate until their +authoritative settlement adapters are available. They contribute no completion or volume. +Historical listings remain available on explicitly archival seller and service pages; the +well-known index, skills, seller API and sitemap advertise executable offers only. + +Historical Vet admission also requires an authenticated challenge resolver. The graph checks the +verifier-issued nonce against its job, party, issuance window, authenticated first consumption +time (`consumedAt`) and `consumedByPresentationHash`, `sha256Hex(canonicalize(identity))` of the complete +first attempted IdentityBundle including `presentation` and its signatures, even when that attempt +failed. The signed bundle scope hash omits `presentation` and cannot identify an attempt. The signed +`presentedAt` timestamp is informational. This deployment does not install that resolver, so sessions +requiring Vet remain indeterminate for completion and volume. Listing publication uses its signed self-binding. +Attested-payload and entitlement deliveries likewise remain indeterminate until authoritative +attestation and grant adapters are installed. Storage delivery verifies payload bytes and ACLs. + +Executable offers discovered through a hosted well-known index require source provenance. +Use a seller publisher signature, or configure `DACS_WITHDRAWAL_SOURCE_PINS` as a +server-side JSON object mapping normalized HTTPS origins to canonical seller DIDs +(for example, `{"https://agent.example":"did:demos:agent:<64 lowercase hex>"}`). +Verify that seller/domain relationship independently before adding a pin. Public +domain submissions and listing endpoints do not create pins. An unsigned, unpinned +index can carry independently verified tombstones for known listings, but cannot +introduce executable offers. A pinned unsigned index retains incomplete withdrawal +checks until a completed check from that same source or a verified revocation. + +For publisher signatures, +The well-known file export returns `withdrawalSigning.message`: sign those exact UTF-8 +bytes with the seller's ed25519 key and place the unpadded base64url signature in +`dacs.listings.publisherSignature` in `agent.json` (and the identical A2A alias, when +present). Keep `listings.json` byte-exact. The signing message binds the normalized +HTTPS domain and SHA-256 hash of that index; generate a new signature whenever either +changes. The registration page displays this message and the completion instructions. +Before queuing a carried marker, the crawler independently verifies the listing's +publisher, chain owner, content hash, ID and version. Unsigned Standard indexes can +discover independently verified seller-signed tombstones. A listing's `seller.publicEndpoint` +is a self-claim and does not authorize unresolved hints. Discovery deduplicates anchors +and bindings and spends at most 32 node reads per domain per pass, with a persisted +cursor for continuation. Authenticated bindings remain seller-scoped until the indexer +verifies the listing's publisher, owner and tuple using its own listing read. An authenticated +index with an incomplete withdrawal entry withholds that offer until a later +complete authenticated check or a verified revocation resolves it. Failed later +crawls retain incomplete checks; verified tombstones remain revoked. Upgrades retain +affected seller/hash holds, including checks with no queued marker. Legacy sources +without recorded provenance require a signature or operator pin before reconciliation; +chain replay alone cannot release those holds. Ambiguous legacy candidates hold only +their listing hashes until chain replay completes; unrelated source failures do not +delay replay completion. Authenticated recrawls must retain each source's index +timestamp and hash ordering, including after migration. A stored timestamp that is +incorrectly ahead of the publisher's current index requires explicit operator +revalidation of that source; an older index cannot silently replace it. diff --git a/reference-implementations/dacs-directory/SPEC_PIN b/reference-implementations/dacs-directory/SPEC_PIN new file mode 100644 index 0000000..72b549e --- /dev/null +++ b/reference-implementations/dacs-directory/SPEC_PIN @@ -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. diff --git a/reference-implementations/dacs-directory/app/api/dacs/build-listing/route.ts b/reference-implementations/dacs-directory/app/api/dacs/build-listing/route.ts index e94052f..221a8a8 100644 --- a/reference-implementations/dacs-directory/app/api/dacs/build-listing/route.ts +++ b/reference-implementations/dacs-directory/app/api/dacs/build-listing/route.ts @@ -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, @@ -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)) ) { @@ -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 }); } diff --git a/reference-implementations/dacs-directory/app/api/dacs/bundles/[jobId]/route.ts b/reference-implementations/dacs-directory/app/api/dacs/bundles/[jobId]/route.ts index d1a7497..98e0ea3 100644 --- a/reference-implementations/dacs-directory/app/api/dacs/bundles/[jobId]/route.ts +++ b/reference-implementations/dacs-directory/app/api/dacs/bundles/[jobId]/route.ts @@ -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 }); } diff --git a/reference-implementations/dacs-directory/app/api/dacs/listings/route.ts b/reference-implementations/dacs-directory/app/api/dacs/listings/route.ts index cc13d66..35b78b6 100644 --- a/reference-implementations/dacs-directory/app/api/dacs/listings/route.ts +++ b/reference-implementations/dacs-directory/app/api/dacs/listings/route.ts @@ -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"; @@ -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; diff --git a/reference-implementations/dacs-directory/app/api/dacs/lookup/route.ts b/reference-implementations/dacs-directory/app/api/dacs/lookup/route.ts index 6cc6832..a67f674 100644 --- a/reference-implementations/dacs-directory/app/api/dacs/lookup/route.ts +++ b/reference-implementations/dacs-directory/app/api/dacs/lookup/route.ts @@ -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"; diff --git a/reference-implementations/dacs-directory/app/api/dacs/sellers/[primaryClaimRef]/route.ts b/reference-implementations/dacs-directory/app/api/dacs/sellers/[primaryClaimRef]/route.ts index 5b6b5f4..ffea361 100644 --- a/reference-implementations/dacs-directory/app/api/dacs/sellers/[primaryClaimRef]/route.ts +++ b/reference-implementations/dacs-directory/app/api/dacs/sellers/[primaryClaimRef]/route.ts @@ -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"; @@ -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, diff --git a/reference-implementations/dacs-directory/app/api/dacs/wellknown-files/route.ts b/reference-implementations/dacs-directory/app/api/dacs/wellknown-files/route.ts index 65acf41..481761b 100644 --- a/reference-implementations/dacs-directory/app/api/dacs/wellknown-files/route.ts +++ b/reference-implementations/dacs-directory/app/api/dacs/wellknown-files/route.ts @@ -7,11 +7,13 @@ * construction — host them byte-exact, then register the domain here. */ import { NextRequest, NextResponse } from "next/server"; -import { contentHash, sha256Hex, stripSignature } from "@kynesyslabs/dacs/canonical"; +import { sha256Hex } from "../../../../src/catalog/canonical.js"; import { parseCciRecord } from "@kynesyslabs/dacs/identity"; import { readAnchor } from "@/src/catalog/chain"; import { gcrGetIdentities } from "@/src/catalog/gcr"; +import { activeOffer, discoveryListing } from "@/src/catalog/discovery"; import { loadCatalog } from "@/src/catalog/store"; +import { wellKnownWithdrawalMessage } from "@/src/catalog/wellknown"; export async function GET(req: NextRequest) { const q = req.nextUrl.searchParams; @@ -19,6 +21,15 @@ export async function GET(req: NextRequest) { const domain = (q.get("domain")?.trim() || "YOUR-DOMAIN.example") .replace(/^https?:\/\//, "") .replace(/\/$/, ""); + const endpoint = q.get("a2aEndpoint"); + const transport = q.get("a2aTransport"); + if (endpoint || transport) { + try { + const url = new URL(endpoint ?? ""); + if (url.protocol !== "https:" || url.username || url.password || url.hash || + !["JSONRPC", "GRPC", "HTTP+JSON"].includes(transport ?? "")) throw new Error(); + } catch { return NextResponse.json({ error: "provide an HTTPS a2aEndpoint and its a2aTransport" }, { status: 400 }); } + } const hex = claim?.match(/([0-9a-fA-F]{64})$/)?.[1]; if (!claim || !hex) return NextResponse.json({ error: "need ?claim=" }, { status: 400 }); @@ -30,14 +41,16 @@ export async function GET(req: NextRequest) { ); } + const listings = seller.listings.filter(activeOffer); + const published = seller.listings.filter(discoveryListing); const entries = []; - for (const l of seller.listings) { + for (const l of published) { const raw = await readAnchor(l.anchor.locator); - if (!raw) continue; + if (!raw && l.status !== "revoked") continue; entries.push({ listingId: l.listingId, version: l.version, - contentHash: contentHash(stripSignature(raw)), + contentHash: l.contentHash, anchor: { kind: "storage-program", locator: l.anchor.locator }, summary: { title: l.offering.title, @@ -46,6 +59,7 @@ export async function GET(req: NextRequest) { ...(l.pricing.priceHint ? { priceHint: l.pricing.priceHint } : {}), }, status: l.status, + ...(l.status === "revoked" ? { revocation: l.revocation } : {}), }); } @@ -66,15 +80,12 @@ export async function GET(req: NextRequest) { const agentJson = JSON.stringify( { name: seller.displayName, - description: seller.listings[0]?.offering.description ?? "", - url: `https://${domain}`, - version: "1.0.0", - protocolVersion: "0.3.0", - preferredTransport: "HTTP+JSON", + description: listings[0]?.offering.description ?? "", + ...(endpoint ? { url: endpoint, version: "1.0.0", protocolVersion: "0.3.0", preferredTransport: transport } : {}), capabilities: { streaming: false, pushNotifications: false, stateTransitionHistory: false }, defaultInputModes: ["application/json", "text/plain"], defaultOutputModes: ["application/json"], - skills: seller.listings.filter((listing) => listing.status === "active").map((listing) => ({ + skills: listings.map((listing) => ({ id: listing.listingId, name: listing.offering.title, description: listing.offering.description ?? "DACS service", @@ -95,9 +106,16 @@ export async function GET(req: NextRequest) { return NextResponse.json({ files: { + ...(endpoint ? { "/.well-known/agent-card.json": agentJson } : {}), "/.well-known/agent.json": agentJson, "/.well-known/dacs/listings.json": listingsJson, }, - note: "Host both files byte-exact (the indexHash binds listings.json), then register the domain on this page.", + withdrawalSigning: { + algorithm: "ed25519", signer: claim, + message: wellKnownWithdrawalMessage(`https://${domain}`, sha256Hex(listingsJson)), + signatureField: "dacs.listings.publisherSignature", + encoding: "base64url", + }, + note: "Host these files byte-exact (the indexHash binds listings.json), then register the domain on this page.", }); } diff --git a/reference-implementations/dacs-directory/app/deal/[ref]/page.tsx b/reference-implementations/dacs-directory/app/deal/[ref]/page.tsx index ce6f7e3..d56fce1 100644 --- a/reference-implementations/dacs-directory/app/deal/[ref]/page.tsx +++ b/reference-implementations/dacs-directory/app/deal/[ref]/page.tsx @@ -4,8 +4,8 @@ import type { Metadata } from "next"; import CopyText from "@/src/components/CopyText"; import VerifyAttestation from "@/src/components/VerifyAttestation"; import VerifyDeal from "@/src/components/VerifyDeal"; -import { readAnchor } from "@/src/catalog/chain"; -import { resolveProgramAddress } from "@/src/catalog/store"; +import { readAnchor, readAnchorRecord } from "@/src/catalog/chain"; +import { canonicalProgramOwner, resolveProgramAddress } from "@/src/catalog/store"; const EXPLORER = "https://explorer.demos.sh"; @@ -47,7 +47,11 @@ export default async function Deal({ const attestationAddress = sellerClaim && jobId ? resolveProgramAddress(sellerClaim, deliveryName) : null; - const attestation = attestationAddress ? await readAnchor(attestationAddress) : null; + // The node does not check that an address matches its derivation, so only a record the + // expected seller owns is shown; its signature is still unverified until the check runs. + const attestationRecord = attestationAddress ? await readAnchorRecord(attestationAddress) : null; + const attestation = attestationRecord?.owner && sellerClaim && + canonicalProgramOwner(attestationRecord.owner) === canonicalProgramOwner(sellerClaim) ? attestationRecord.data : null; const att = attestation as { repo?: string; pullNumber?: number; reviewId?: number; ghAuthor?: string; deliveredAt?: string; ghStateHash?: string; @@ -101,9 +105,10 @@ export default async function Deal({ )}

- The seller signed this delivery attestation (DACS-X) over the GitHub state at + The seller's storage holds this delivery attestation (DACS-X) over the GitHub state at delivery time — state hash {att.ghStateHash?.slice(0, 16)}…. - The attestation rides beside the bundle until the SDK can reference it in-bundle. + Check its signature to confirm the seller signed it. The attestation rides beside the + bundle until the SDK can reference it in-bundle.

)} diff --git a/reference-implementations/dacs-directory/app/register/page.tsx b/reference-implementations/dacs-directory/app/register/page.tsx index 477e9d3..b39f29b 100644 --- a/reference-implementations/dacs-directory/app/register/page.tsx +++ b/reference-implementations/dacs-directory/app/register/page.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { useDemosWallet } from "@/src/components/useDemosWallet"; +import { WithdrawalSigningInstructions, type WithdrawalSigning } from "@/src/components/withdrawal-signing"; import { clearPendingListingPublication, readPendingListingPublication, @@ -10,6 +11,7 @@ import { type PendingListingPublication, } from "@/src/components/listing-publication-recovery"; import { safePublicEndpoint } from "@/src/catalog/publicEndpoint"; +import { importLegacySignatureValue } from "@/src/catalog/signatureValue"; import { negotiationPhaseForPricing, publishableRail, @@ -52,7 +54,7 @@ export default function Register() { const [serviceId, setServiceId] = useState(""); const [name, setName] = useState(""); const [description, setDescription] = useState(""); - const [rails, setRails] = useState(["pay-dem"]); + const [rails, setRails] = useState(["demos-native:DEM"]); const [category, setCategory] = useState("services.other"); const [tags, setTags] = useState(""); const [delivery, setDelivery] = useState(DELIVERY_OPTIONS[0].id); @@ -284,9 +286,12 @@ export default function Register() { setStatus("Now approve the complete structured listing."); const signature = await wallet.sign(publication.message); if (!signature) throw new Error(wallet.error ?? "The listing signature was declined."); + // The wallet returns hex; a ListingSignature value is unpadded Base64URL (CORE SIG-6). + const value = importLegacySignatureValue(signature.replace(/^0x/i, "").toLowerCase(), "lowercase-hex"); + if (!value) throw new Error("The wallet returned a listing signature that is not a 64-byte Ed25519 signature."); signedListing = { ...publication.listing, - signature: { algorithm: "ed25519", signer: claim, value: signature.replace(/^(0x)+/i, "") }, + signature: { algorithm: "ed25519", signer: claim, value }, }; transaction = structuredClone(transaction); const content = transaction.content as Record | undefined; @@ -506,14 +511,15 @@ function Progress({ label, state }: { label: string; state: ProgressState }) { function WellKnownFiles({ claim }: { claim: string | null }) { const [domain, setDomain] = useState(""); const [files, setFiles] = useState | null>(null); + const [signing, setSigning] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const generate = async () => { if (!claim) return; - setBusy(true); setError(null); setFiles(null); + setBusy(true); setError(null); setFiles(null); setSigning(null); const response = await fetch(`/api/dacs/wellknown-files?claim=${encodeURIComponent(claim)}&domain=${encodeURIComponent(domain.trim())}`); const body = await response.json(); - if (!response.ok) setError(body.error); else setFiles(body.files); + if (!response.ok) setError(body.error); else { setFiles(body.files); setSigning(body.withdrawalSigning); } setBusy(false); }; return ( @@ -523,6 +529,7 @@ function WellKnownFiles({ claim }: { claim: string | null }) {
setDomain(event.target.value)} />
{error &&

{error}

} + {signing && } {files && Object.entries(files).map(([path, content]) =>
{path}
{content}
)} ); diff --git a/reference-implementations/dacs-directory/app/seller/[claim]/page.tsx b/reference-implementations/dacs-directory/app/seller/[claim]/page.tsx index c093ca3..7338e74 100644 --- a/reference-implementations/dacs-directory/app/seller/[claim]/page.tsx +++ b/reference-implementations/dacs-directory/app/seller/[claim]/page.tsx @@ -5,6 +5,7 @@ import { notFound } from "next/navigation"; import CopyText from "@/src/components/CopyText"; import { ChipGroup, CciChip } from "@/src/components/Badge"; import { railLabel, deliveryLabel, negotiationLabel, tierMeta } from "@/src/components/labels"; +import { activeOffer } from "@/src/catalog/discovery"; import { loadCatalog } from "@/src/catalog/store"; export const dynamic = "force-dynamic"; @@ -19,7 +20,7 @@ export async function generateMetadata({ params }: { params: Promise<{ claim: st if (!seller) return { title: "Agent not found" }; return { title: seller.displayName, - description: `${seller.displayName} offers ${seller.listings.length} verifiable DACS service${seller.listings.length === 1 ? "" : "s"}.`, + description: `${seller.displayName} offers ${seller.listings.filter(activeOffer).length} active DACS service${seller.listings.filter(activeOffer).length === 1 ? "" : "s"}.`, alternates: { canonical: `/seller/${encodeURIComponent(seller.primaryClaim)}`, types: { "application/json": `/api/dacs/sellers/${encodeURIComponent(seller.primaryClaim)}` }, @@ -31,7 +32,7 @@ export default async function Seller({ params }: { params: Promise<{ claim: stri const { claim } = await params; const seller = findSeller(claim); if (!seller) notFound(); - const activeListingCount = seller.listings.filter((listing) => listing.status === "active").length; + const activeListingCount = seller.listings.filter(activeOffer).length; const hasFixtureListings = seller.listings.some((listing) => listing.anchor.kind === "fixture"); const t = tierMeta(seller.identityTier ?? "self-declared"); @@ -105,6 +106,7 @@ export default async function Seller({ params }: { params: Promise<{ claim: stri

{l.offering.title}{" "} + {!activeOffer(l) && archival listing} {l.status === "revoked" && revoked} {l.artifactProfile === "dacs-v0.1" ? "current DACS" : "legacy SDK"}

diff --git a/reference-implementations/dacs-directory/app/service/[seller]/[listingId]/[version]/page.tsx b/reference-implementations/dacs-directory/app/service/[seller]/[listingId]/[version]/page.tsx index 7ff1d04..607614c 100644 --- a/reference-implementations/dacs-directory/app/service/[seller]/[listingId]/[version]/page.tsx +++ b/reference-implementations/dacs-directory/app/service/[seller]/[listingId]/[version]/page.tsx @@ -6,6 +6,7 @@ import CopyText from "@/src/components/CopyText"; import { deliveryLabel, pricingModelLabel, publishedPriceLabel, railLabel, tierMeta } from "@/src/components/labels"; import { inspectServicePath } from "@/src/catalog/inspection"; import { isCounterpartyEvidenceDemoListing } from "@/src/catalog/counterpartyEvidence"; +import { activeOffer } from "@/src/catalog/discovery"; import { loadCatalog } from "@/src/catalog/store"; import { safeJsonLd } from "@/src/components/structuredData"; import { directoryEvidenceState } from "@/src/components/directory-evidence-state"; @@ -49,7 +50,8 @@ export default async function ServicePage({ params }: { params: Promise const identity = tierMeta(evidence.identityTier); const apiHref = `/api/dacs/listings/${encodeURIComponent(listing.listingId)}/${listing.version}?seller=${encodeURIComponent(seller.primaryClaim)}`; const inspectHref = inspectServicePath(listing); - const engagementEndpoint = evidence.endpoint.url; + const executable = activeOffer(listing); + const engagementEndpoint = executable ? evidence.endpoint.url : undefined; const isCounterpartyEvidenceDemo = isCounterpartyEvidenceDemoListing(seller.primaryClaim, listing); const isFixtureListing = listing.anchor.kind === "fixture"; const technicalArtifactProfile = isFixtureListing ? "fixture-listing" : (listing.artifactProfile ?? "legacy-sdk-v0.1"); @@ -64,7 +66,7 @@ export default async function ServicePage({ params }: { params: Promise description: listing.offering.description, identifier: `${listing.listingId}@${listing.version}`, provider: { "@type": "Organization", name: seller.displayName }, - offers: listing.pricing.priceHint ? { + offers: executable && listing.pricing.priceHint ? { "@type": "Offer", price: listing.pricing.priceHint, priceCurrency: listing.pricing.currency, @@ -75,6 +77,7 @@ export default async function ServicePage({ params }: { params: Promise <>