Repository navigation
Crawl Walrus Sites via on-chain resource enumeration (Site object + blobIds) #22
Description
Context
The production Worker treats *.wal.app targets as plain HTTP through the portal — it sets mode: 'walrus' and scrapes x-walrus-* response headers (apps/api/src/worker.ts ~2462/2554) but never reads the on-chain Site object. The real, verifiable enumeration already exists in packages/walrus/src/resources.ts listWalrusResources(site), which reads each resource's blob_id from the Sui Site object (~line 103) using a WalrusSiteContext from resolve.ts — but it is Node-only (CLI materializeWalrusSite) and not used in the hosted crawl. So Walrus-native sites are crawled by HTML link-guessing instead of their authoritative on-chain manifest.
Goal / user story
As a user pointing ContextMEM at a Walrus Site (a .wal.app URL, SuiNS name, or 0x site object id), I want it to enumerate the site's resources and blobIds directly from the on-chain Site object, so extraction is complete and provenance is verifiable rather than guessed from anchors.
Acceptance criteria
- For a Walrus-Site target, the run resolves a
WalrusSiteContextand enumerates resource paths + blobIds from the on-chain Site object (not HTML link discovery). - Each resource is fetched via the Walrus aggregator (
quilt.tsblobAggregatorEndpoint) and mapped into manifest pages/resources carryingsource.blobId/resourcePath. -
buildSiteStructure(packages/core/src/site-structure.ts) populates its existing Walrus Provenance group frominput.walrus.resourcesfor these runs. - Extracted chunks/facts carry blobId-backed
FactSourceRefprovenance so the "why" links resolve to on-chain blobs. - If the on-chain read fails (RPC/grpc error), the crawl falls back to today's HTTP portal crawl and the run still completes.
Implementation notes
- Reuse
packages/walrus/src/resolve.ts(resolveWalrusTarget) +resources.ts(listWalrusResources) from the Worker walrus branch, or port a Worker-safe variant. - SPIKE first: confirm
@mysten/sui/grpcSuiGrpcClient.getObjectruns in the Worker undernodejs_compat(the roadmap notesproof.ts/history.ts/resolve.tsalready use it for reads, but they run in Node today).resources.tsusesnode:buffer— verify undernodejs_compat. - Touch
apps/api/src/worker.ts(walrus branch dispatch), wireWalrusResourceRecord[]into the existingSiteStructureInput.walrus.resourcesseam, and reuseaggregatorUrlfrom the resolved context. - Gotchas: large sites need a resource cap/concurrency limit; aggregator fetch sizes must respect the Worker's existing 1.5MB guard; non-HTML resources (assets) should be enumerated but not chunked.
Sui Overflow angle
This is the uniquely Sui-native crawl: reading content straight from on-chain Walrus Site objects and Walrus blobs gives verifiable, tamper-evident provenance no traditional crawler can match. Paired with the chunkGraphDigest-keyed extract receipt, it tells a complete on-chain provenance story — the most differentiated thing to demo at a Sui hackathon.
Dependencies
Spike: @mysten/sui/grpc (SuiGrpcClient) in the Cloudflare Worker (shared with the on-chain receipt work). Coordinates with, but is not blocked by, the on-chain attribution-receipt issue.
Part of the ContextMEM roadmap (#4) • Sui Overflow build.
Activity
harrymove-ctrl commentedon Jun 19, 2026
Implementation Plan
Approach
Wire the already-real on-chain enumeration (
packages/walrusresolveWalrusTarget+listWalrusResources) into the production Worker's per-source extractor (apps/api/src/worker.tsextractTargetContext, ~L2452). For.wal.app/ SuiNS /0xtargets we resolve aWalrusSiteContext, enumerate resource paths + blobIds from the Sui Site object, fetch HTML/markdown bodies through the Walrus aggregator (verifyingblobHash), and emit manifest pages whosesourcecarriesblobId/resourcePath. The key design decision is to avoid the@contextmem/walrusbarrel (it re-exportsstorage.ts'schild_process/tarandresources.ts'snode:http.createServerpreview server, which break the Worker bundle) by adding a Worker-safe subpath@contextmem/walrus/enumerate, and to thread Walrus provenance all the way into chunks/facts soFactSourceRef.blobIdresolves. On any RPC/grpc failure the existing HTTP-portal crawl runs unchanged.Prerequisites & dependencies
@mysten/sui/grpcSuiGrpcClient(getObject,listDynamicFields,nameService.lookupName) runs in the Worker undernodejs_compat— bundle size, CPU, and whether the Connect/gRPC-over-fetch transport works on Workers. This is the sameSuiGrpcClient-in-Worker spike the roadmap calls a shared dependency with the on-chain receipt work (roadmap §"Dependencies"). If it busts limits, the enumeration still runs insideprocessExtractionJob, which already executes in the queue consumer (worker.ts:553) with a larger CPU budget than a request handler — note this as the fallback host, not the Node CLI.https://fullnode.mainnet.sui.io) + the public aggregator (https://aggregator.walrus-mainnet.walrus.space). NoHARBOR_*, no Vectorize, no wallet/service key. (This zero-secret, verifiable read is the Sui-native demo angle.)@mysten/sui ^2.9.1is already a dep ofapps/api(apps/api/package.json:20) and@contextmem/walrusis already a workspace dep (line 19).p-limitis a pure-JS walrus dep and bundles fine.resolveWalrusTargetforcesnetwork:"mainnet"for.wal.app(resolve.ts:82-90) and defaults mainnet for0xids (resolve.ts:16). No mainnet/testnet split to reconcile here.Step-by-step
SPIKE:
SuiGrpcClientin the Worker. Add a temporary route or a tiny test Worker that callsresolveWalrusTarget("https://seal-docs.wal.app/")(the configuredCONTEXTMEM_DEMO_SAMPLE_TARGET, wrangler.jsonc:8) thenlistWalrusResources(site)and logs resource count + firstblobId. Verify it returns non-empty, the bundle builds withwrangler deploy --dry-run, and CPU stays in budget. Decide Worker vs queue-consumer host. Delete the spike route after.Create the Worker-safe enumeration module. New
packages/walrus/src/enumerate.tsexportingresolveWalrusTarget(re-export from./resolve.js) plus Worker-safelistWalrusResourcesandfetchWalrusResourceextracted fromresources.ts— same Sui/BCS logic (SuiGrpcClient.listDynamicFields+getObject+DynamicFieldStruct(ResourcePathStruct, ResourceStruct), bcs.ts),blobAggregatorEndpoint/quiltAggregatorEndpoint(quilt.ts),sha256Base64verify — but with nonode:fscache, nonode:http, nomaterializeWalrusSite/startWalrusPreview. Refactorresources.tsto import the shared core fromenumerate.ts(single source of truth; CLI keeps working via the barrel). Add topackages/walrus/package.jsonexports:"./enumerate": "./src/enumerate.ts". (Note:resolve.ts's top-levelimport fs from "node:fs/promises"is only used in the.jsonbranch, never hit for.wal.app/0x, and resolves harmlessly undernodejs_compat.)Add the Walrus enumeration branch in
extractTargetContext(worker.ts:2452-2607). Detection already exists viatarget.hostname.endsWith(".wal.app")(used at 2462/2475/2554/2568). For Walrus targets, before the current header-scrape path:import { resolveWalrusTarget, listWalrusResources, fetchWalrusResource } from "@contextmem/walrus/enumerate";const site = await resolveWalrusTarget(target.toString());const records = await listWalrusResources(site, { concurrency: 5 });/html/i.test(contentType)or.html?$) + markdown (.mdx?$) resources to fetch as pages; enumerate (but do not fetch bodies for) non-HTML assets — they still get blobId provenance in the structure.await fetchWalrusResource(site, record)(omitcacheDir), respecting a per-resource byte cap aligned with the existing 1.5MB guard (worker.ts:2813/2816): skip/flag any resource > ~1.5MB instead of loading it. Convert HTML→markdown with the Worker's existinghtmlToText/markdown path; verifysha256Base64(body) === record.blobHash(on mismatch, skip the page, don't fail the run).try/catch(Step 7).Build manifest pages + walrus.resources with real provenance. Replace the fake
walrusResources(worker.ts:2479-2485, blobId"") with the realWalrusResourceRecord[]. Each manifest page built from a Walrus resource must setsource: { kind: "walrus", resourcePath: record.path, blobId: record.blobId, blobHash: record.blobHash, quiltPatchId: record.quiltPatchId }(matchesPageArtifact.source, types.ts:335). Setmanifest.walrus = { site: { network: site.network, siteObjectId: site.siteObjectId, aggregatorUrl: site.aggregatorUrl }, resources: records }(replacing the headers-derived block at 2568-2576). Keepmode: "walrus"(2554).Populate the Walrus Provenance group via core
buildSiteStructure. For Walrus runs, callbuildSiteStructure({ target, pages, walrus: { resources: records } })(site-structure.ts:16; already exported from core, index.ts:10) instead of the hand-rolledsiteStructureobject (worker.ts:2581-2607) — itswalrus-provenancegroup (site-structure.ts:148) is built directly frominput.walrus.resources. Because the final namespace manifest is assembled bybuildCombinedNamespaceBundle(worker.ts:2125), also: (a) makesourceFromSingleExtraction(worker.ts:2069) carry the realWalrusResourceRecord[]for walrus sources so they flow throughExtractedNamespaceSource.resourcesintobuildCombinedWalrusSummary(worker.ts:2244); and (b) inbuildCombinedNamespaceBundle, when any source is walrus, setmanifest.siteStructure = buildSiteStructure({ target: job.target, pages, walrus: { resources: combinedWalrusRecords } })(replacingbuildCombinedSiteStructureat 2166 for walrus runs; keep it for pure-web).Thread blobId provenance into chunks → facts (AC4).
ContextChunk(types.ts:726) with optionalblobId?,resourcePath?,quiltPatchId?.buildChunks(chunks.ts:13-43) copypage.source?.blobId/resourcePath/quiltPatchIdonto each emitted chunk.chunkGraphDigest(chunks.ts:59) only hasheschunkId:contentHash, so adding these fields does NOT change the digest —planMemoryWritediffing stays stable (call this out in the PR).sourceFromChunk(facts.ts) addblobId: chunk.blobId, resourcePath: chunk.resourcePathto the returnedFactSourceRef(the fields already exist onFactSourceRef, facts.ts:25-32 — just unpopulated today).factsPages(worker.ts:2660-2671) preserves each page'ssource(currently dropped) sobuildSiteFacts/buildChunksemit blobId-backed sources. VerifysourcesurvivessourceFromSingleExtraction(...pagespread, worker.ts:2080) and the combinedbuildChunks(pages…)(worker.ts:2186) so/context/chunks.ndjsonand/context/facts.jsonboth carry blobIds.Fallback (AC5). Wrap Steps 3-4 in
try/catch. On any error (SuiNS lookup failure, RPC/grpc error, empty enumeration),console.warnand fall through to the existing HTTP-portal crawl (the currentextractWalrusHeaders+ link-discovery path, worker.ts:2475-2606) so the run still completes withmode:"walrus".New / changed files
packages/walrus/src/enumerate.ts— NEW Worker-saferesolveWalrusTargetre-export +listWalrusResources/fetchWalrusResource(no fs/http/preview/cache).packages/walrus/package.json— add"./enumerate"toexports.packages/walrus/src/resources.ts— import shared enumeration core fromenumerate.ts(de-dupe; keep Node caching/materialize wrappers here).packages/core/src/types.ts—ContextChunk+=blobId?/resourcePath?/quiltPatchId?(L726).packages/core/src/chunks.ts—buildChunkscopiespage.sourceprovenance (L13-43).packages/core/src/facts.ts—sourceFromChunkemitsblobId/resourcePath.apps/api/src/worker.ts— Walrus enumeration branch + provenance inextractTargetContext(~L2452-2607); real walrus records throughsourceFromSingleExtraction(L2069) +buildCombinedWalrusSummary(L2233);buildSiteStructurefor walrus runs (replace L2166/L2581-2607); preservesourceinfactsPages(L2660).Test plan
packages/core):buildChunkson aPageArtifactwithsource.blobIdemits chunks carrying thatblobId;sourceFromChunkround-trips it intoFactSourceRef.blobId; assertchunkGraphDigestis byte-identical before/after the field addition (proves diff stability).packages/walrus, network): againstseal-docs.wal.app,resolveWalrusTargetreturns a realsiteObjectId;listWalrusResourcesreturns >0 records each with a non-emptyblobId;fetchWalrusResourcebody passessha256Base64 === blobHash(the round-trip provenance assert). This is the verifiable, tamper-evident proof a normal crawler can't produce.https://seal-docs.wal.app/, then inspect the run's/context/manifest.json(walrus.resources[].blobIdpopulated, not""),/context/site-structure.json(non-empty Walrus Provenance group),/context/facts.json+/context/chunks.ndjson(source.blobId/chunkblobIdpresent). Record the resolvedsiteObjectIdand oneblobIdin the issue as evidence..wal.apphost while forcing an RPC error (badrpcUrlin the spike, or block the fullnode) and confirm the run still completes via the HTTP portal path.Risks & gotchas
@mysten/sui/grpc— the gating spike. If over budget, keep enumeration in the queue consumer (already the host forprocessExtractionJob); do not regress to Node-only.@contextmem/walrusbarrel in the Worker — it pullsstorage.ts(node:child_process/spawn,node:os) andresources.ts(node:http.createServer), which fail undernodejs_compat. The./enumeratesubpath is mandatory..wal.app→nameService.lookupName, resolve.ts:109) — both are covered by the Step 7 try/catch fallback.buildCombinedNamespaceBundle; ifsource/walrus records aren't threaded throughsourceFromSingleExtraction, AC3/AC4 will pass per-source but vanish from the run output. Test the combined output, not just per-source.Acceptance-criteria mapping
WalrusSiteContext+ enumerate paths/blobIds from the on-chain Site object (not HTML discovery) → Steps 2, 3.quilt.tsblobAggregatorEndpoint) and mapped into manifest pages/resources carryingsource.blobId/resourcePath→ Steps 3, 4.buildSiteStructurepopulates its existing Walrus Provenance group frominput.walrus.resources→ Step 5.FactSourceRefprovenance → Step 6.Effort
L — ~4-6 person-days. ~1 day gating spike; ~2-3 days Worker wiring + walrus subpath + combined-manifest plumbing; ~1-1.5 days core chunks/facts provenance + tests + staging verification.
Implementation plan generated for execution (planning phase). Part of the roadmap (#4).