Repository navigation
Namespace→Space/Bucket/Seal mapping + Harbor service-key management in Worker secrets #7
Description
Context
Namespaces are app-layer strings (namespaceForTarget, packages/core/src/utils.ts) tracked in the contextmem_namespaces D1 table (migrations 0002_hosted_namespaces.sql / 0003_namespace_platform.sql). To store private blobs in Harbor we must map each namespace to a Harbor Space/Bucket + Seal policy and persist those ids for recall, and the Worker needs the Harbor API key + SUI service key as secrets (mirroring resolveMemwalCreds, worker.ts:3341).
Goal / user story
As ContextMEM, I want a deterministic namespace → Space/Bucket/Seal-policy mapping persisted in D1, plus the Harbor/Sui credentials wired into Worker secrets — so any private remember/recall can resolve its on-chain storage targets.
Acceptance criteria
- New migration
apps/api/migrations/0007_*.sqladdsspace_id,bucket_id,seal_policy_id,seal_identity_salt, andstorage_providercolumns tocontextmem_namespaces. -
CloudflareNamespaceStore(worker.ts:401) reads/writes the new columns; a helper resolves-or-creates the Bucket + policy for a private namespace lazily on first private write and persists the ids. -
WorkerEnv(worker.ts:32) gainsHARBOR_API_KEY,HARBOR_SERVICE_PRIVATE_KEY(suiprivkey1...), andHARBOR_SPACE_ID;.env.exampleandapps/api/cloudflare/wrangler.example.jsoncdocument them; aresolveHarborCreds(request, env)mirrorsresolveMemwalCreds' header-or-secret precedence. - One Space per environment (prod/staging); deterministic Bucket name per namespace string (documented mapping, e.g. sha256 of the namespace).
- DECISION recorded: shared service key (simple, decrypts everything) vs per-owner key (true isolation) — reflected in the credential helper.
- Secrets are never logged; handling is consistent with existing key redaction (
sanitizeMemwalKeypattern).
Implementation notes
- Set secrets via
wrangler secret put HARBOR_API_KEY --config apps/api/cloudflare/wrangler.jsonc(and the staging--env stagingvariant); usedecodeSuiPrivateKeyfor the service key. - Bucket creation goes through
HarborClient.createBucket. Persisting the ids letsGET /api/memwal/artifactresolve refs without re-deriving. - Keep the namespace→bucket mapping deterministic so the Node CLI/MCP path and the Worker agree on the same bucket for a given namespace.
Sui Overflow angle
Establishes the per-namespace on-chain object graph (Bucket + Seal-policy objects on Sui testnet) and the signing key that creates them — the reproducible backbone that makes the Sui-native private-memory demo work identically across prod and staging.
Dependencies
The HarborClient.createBucket issue. Feeds the StorageProvider + SEAL encrypted remember/recall issue.
Part of the ContextMEM roadmap (#4) • Sui Overflow build.
Activity
harrymove-ctrl commentedon Jun 19, 2026
Implementation Plan
Approach
Add a
0007D1 migration that bolts Harbor/SEAL target columns ontocontextmem_namespaces, teachCloudflareNamespaceStoreto read/write them, and add a lazyensureHarborTargets(namespace)helper that — on the first private write — derives a deterministic bucket name, callsHarborClient.createBucket(reserve→sign→finalize with the service key), and persistsspace_id/bucket_id/seal_policy_id/seal_identity_salt/storage_provider. WireHARBOR_API_KEY/HARBOR_SERVICE_PRIVATE_KEY/HARBOR_SPACE_IDintoWorkerEnvwith aresolveHarborCreds(request, env)that mirrorsresolveMemwalCreds' header-or-secret precedence and redaction. Key design decision: one shared service key + one Space per environment, with a per-namespaceseal_identity_saltproviding in-policy isolation, and the bucket-name derivation (not the on-chain id) is the deterministic contract that lets the Node CLI/MCP and the Worker converge on the same bucket — so the derivation function lives in a shared package, computed fromsha256(namespace)independently in each runtime.Prerequisites & dependencies
HarborClientissue (HarborClient.createBucketinpackages/walrus/src/harbor.ts).ensureHarborTargetscalls it. Build Namespace→Space/Bucket/Seal mapping + Harbor service-key management in Worker secrets #7's pure parts (migration, columns,resolveHarborCreds, deterministic naming, read paths) against an injected/typedHarborClientinterface so they merge even before the client lands; gate only the create call on it.@mysten/sealWorker spike (Spike: run @mysten/seal encrypt+decrypt inside a Cloudflare Worker #5) only if SEAL policy creation happens at bucket-create time and pulls@mysten/sealinto the Worker. Bucket create + Sui signing needs only@mysten/sui(already a dep —apps/api/package.jsonhas@mysten/sui ^2.9.1, anddecodeSuiPrivateKey/Ed25519Keypair.fromSecretKey("suiprivkey1…")is already used inpackages/core/src/snapshot.ts:135), which is Worker-safe undernodejs_compat. Ifseal_policy_idprovisioning forces@mysten/sealand that busts Worker bundle/CPU limits,ensureHarborTargetsmust run in the queue consumer or the Node CLI/MCP path instead of the request path.hbr_…API key, asuiprivkey1…service key, and a pre-created HarborHARBOR_SPACE_IDper env (prod + staging). No new Cloudflare binding needed (reusesCONTEXTMEM_DB).Step-by-step
apps/api/migrations/0007_harbor_storage.sql. Mirror theADD COLUMN … CHECKstyle proven in0006_namespace_build_sources.sql:storage_provideris NOT NULL; the rest are nullable, filled lazily.)apps/api/src/worker.ts: extendNamespaceRow(worker.ts:103) withstorage_provider,space_id,bucket_id,seal_policy_id,seal_identity_salt; add the same five SELECT columns to the four queries that hydrateNamespaceRow→getNamespace(worker.ts:415),listNamespacesowner + all (worker.ts:928,:937),listDirectoryNamespaces(worker.ts:1033); map them innamespaceFromRow(worker.ts:4531). AddstorageProvider?,spaceId?,bucketId?,sealPolicyId?,sealIdentitySalt?toHostedNamespaceSummaryinpackages/mcp/src/hosted.ts:13.harborBucketName(namespaceSha256Hex: string): string(e.g.`cmem-${hex.slice(0, 40)}`, lowercase) topackages/walrus/src/harbor.ts(the HarborClient file) exported via a leaf path with nonode:deps. It takes the already-computed sha256 hex so each runtime hashes with its own primitive (Worker = its async WebCryptosha256Hex; Node CLI/MCP =sha256Hexinpackages/core/src/utils.ts:14) yet produces an identical name — satisfying the Node↔Worker determinism requirement without bundlingnode:cryptointo the Worker.worker.tsnearresolveMemwalCreds(worker.ts:3341), addsanitizeHarborKey()(stripBearer/whitespace, mirrorsanitizeMemwalKeyworker.ts:3331) andresolveHarborCreds(request, env)returning{ apiUrl, apiKey, serviceKey, spaceId }with header-first then env precedence (x-harbor-api-key/x-harbor-service-key/x-harbor-space-id, validateapiKeystartshbr_andserviceKeystartssuiprivkey1). Decode the service key withdecodeSuiPrivateKey→Ed25519Keypair. Record the DECISION in a comment block: shared service key (singleHARBOR_SERVICE_PRIVATE_KEY, can decrypt everything — simplest for the grant demo) over per-owner keys; the per-namespaceseal_identity_saltis the lever that gives namespace-level separation under the shared key. The header override path is the BYO-Harbor power-user escape hatch (same pattern as the MemWal delegate headers), keeping the key out of any persisted/logged surface.ensureHarborTargets(namespace)as a method onCloudflareNamespaceStore(worker.ts:401) plus a read-onlygetHarborTargets(namespace):storage_provider='harbor'andbucket_id+seal_policy_idpresent → return them (no tx);resolveHarborCreds; if unconfigured → returnnullso callers fall back to Tatum for non-private (private write should error clearly);harborBucketName(await sha256Hex(namespace)); generate a randomseal_identity_salt(e.g.crypto.getRandomValues→ hex);HarborClient.createBucket({ spaceId, name })(reserve→sign→finalize as one tight sequence; tolerate the ~3smirror_missing_grant403 retry inside the client) →{ bucketId, sealPolicyId };UPDATE contextmem_namespaces SET storage_provider='harbor', space_id=?, bucket_id=?, seal_policy_id=?, seal_identity_salt=?, updated_at=? WHERE namespace=? AND bucket_id IS NULL, then re-read (idempotent against concurrent first-writes; deterministic name means a duplicate create resolves to the same bucket).This is the lazy "on first private write" hook the StorageProvider/SEAL remember-recall issue will call.
WorkerEnv(worker.ts:32): addHARBOR_API_KEY?,HARBOR_SERVICE_PRIVATE_KEY?,HARBOR_SPACE_ID?,HARBOR_API_URL?.apps/api/cloudflare/wrangler.example.jsonc: addHARBOR_API_KEY,HARBOR_SERVICE_PRIVATE_KEYtosecrets.required; documentHARBOR_SPACE_ID(an identifier, not a secret) as avarsentry.apps/api/cloudflare/wrangler.jsonc: addHARBOR_SPACE_IDto top-levelvarsand to theenv.staging.varsblock (distinct space per env, AC4)..env.example: add a "Harbor (private/encrypted storage)" section:HARBOR_API_KEY=,HARBOR_SERVICE_PRIVATE_KEY=(suiprivkey1…),HARBOR_SPACE_ID=,HARBOR_API_URL=,HARBOR_NETWORK=testnet.wrangler.jsonc:12):echo "<hbr_…>" | npx wrangler secret put HARBOR_API_KEY --config apps/api/cloudflare/wrangler.jsoncand the--env stagingvariant.New / changed files
apps/api/migrations/0007_harbor_storage.sqlapps/api/src/worker.ts(WorkerEnv,NamespaceRow, 4 SELECTs,namespaceFromRow,sanitizeHarborKey,resolveHarborCreds,CloudflareNamespaceStore.ensureHarborTargets/getHarborTargets)packages/mcp/src/hosted.ts(HostedNamespaceSummarynew optional fields)packages/walrus/src/harbor.ts— pureharborBucketName(sha256Hex)formatter (leaf, nonode:deps)apps/api/cloudflare/wrangler.example.jsonc,apps/api/cloudflare/wrangler.jsonc,.env.exampleapps/api/src/worker.harbor.test.ts(or extend an existing harness alongsidepackages/mcp/src/hosted.test.ts)Test plan
harborBucketNameis deterministic + stable (same namespace → same name across runs; verify Nodesha256Hexand a WebCrypto sha256 produce the identical name for one fixture);resolveHarborCredsprecedence (valid header overrides env, env fallback, malformedhbr_/suiprivkey1rejected, missing →undefined);sanitizeHarborKeystripsBearer/0x/whitespace; assert no code path passes the raw service key toconsole.*.hosted.test.ts): firstensureHarborTargetscall invokescreateBucketonce and writes all five columns; second call returns persisted ids with zerocreateBucketcalls; concurrent first-writes create at most one bucket (conditionalWHERE bucket_id IS NULL);namespaceFromRowround-trips the new columns.wrangler d1 migrations apply contextmem --localthenPRAGMA table_info(contextmem_namespaces)shows the five new columns + index (add to CI's migration step).bucket_id/seal_policy_id/storage_provider='harbor'); reload and confirm no second tx is signed.Risks & gotchas
mirror_missing_grant~3s 403 after finalize — tolerate/retry (owned by HarborClient;ensureHarborTargetsmust not treat the first 403 as fatal).@mysten/suiis fine in-Worker; ifseal_policy_idprovisioning needs@mysten/sealand busts bundle/CPU caps, moveensureHarborTargetsto the queue consumer / Node path.storage_providerrecords the actual provider so recall never mixes networks.storage_provider); the rest stay nullable.sanitizeHarborKey/redaction consistent with thesanitizeMemwalKeypattern; never echoserviceKeyin error envelopes (handleWorkerRequestatworker.ts:562surfaces messages to clients).node:-free leaf so CLI/MCP and Worker agree.Acceptance-criteria mapping
0007_*.sqladds the 5 columns → Step 1.CloudflareNamespaceStorereads/writes new columns + lazy resolve-or-create helper → Steps 2 & 5.WorkerEnvgains the 3 vars;.env.example+wrangler.example.jsoncdocument them;resolveHarborCredsmirrorsresolveMemwalCreds→ Steps 4 & 6.HARBOR_SPACE_IDvars +harborBucketName).sanitizeMemwalKeyredaction → Step 4 + Risks.Effort
M — ~2-3 person-days for #7's own scope (migration, columns, helpers, config, tests). Excludes the blocking
HarborClient.createBucketwork; if that and the SEAL-policy path land in the Worker, add the #5 spike outcome as a dependency.Implementation plan generated for execution (planning phase). Part of the roadmap (#4).
feat(api): wire Harbor/Seal private storage into the Worker (#7)
Merge pull request #29 from harrymove-ctrl/feat/harbor-seal-storage