Skip to content

docs(harbor-dev): sender-owner routing — enableEvmProxy: false + seiload shardRouting from autobahn.json (PLT-1283) - #456

Merged
bdchatham merged 1 commit into
mainfrom
devin/1789542925-harbor-dev-shard-routing
Sep 16, 2026
Merged

bdchatham merged 1 commit into
mainfrom
devin/1789542925-harbor-dev-shard-routing

Conversation

@bdchatham

Copy link
Copy Markdown
Collaborator

Summary

Teaches /harbor-dev the one thing PLT-1283 left unwired on the Harbor side: benching an Autobahn chain with spec.consensus.autobahn.enableEvmProxy: false (sei-k8s-controller #560) needs the seiload profile to carry shardRouting (sei-load #117) so seiload sends each transaction straight to the validator that owns the sender. Without it seiload's addr mod len(endpoints) spread lands most sends on a non-owner and the run measures rejections.

No controller, S3, IAM or init-container work: every input already exists on the cluster once the network is Ready. The new Sender-owner routing section in references/autobahn-giga.md reads autobahn.json off a validator pod (the file the existing verification loop already checks for) and shapes it:

kubectl exec … -c seid -- cat /home/nonroot/.sei/config/autobahn.json \
  | jq '{validators: [.validators[] | {consensusPubKey: .validator_key, power: 1, endpoint: .evmrpc}]}'
# count-gate against seinetwork .status.replicas, then
PROFILE=$(| jq --argjson sr "$SHARD_ROUTING" '. + {shardRouting: $sr}')   # re-run seiload validate after

Rules carried with consequences: power: 1 is exact only while every gentx stakes the same stakingAmount (the presets do); endpoints stays on followers (Tier 2) or the same validators (Tier 3) for the trackers; never pass --nodes with shardRouting (seiload refuses); image must carry sei-load #117 (older strict decoder rejects the key); enableEvmProxy is create-only with the rest of spec.consensus.

Also: enableEvmProxy added to the Tier 2/3 YAML example and seinetwork-crd.md; shardRouting added to the envelope list in sei-load-bench.md; two new halt conditions (proxy off without shardRouting, enableEvmProxy on a CRD that prunes it); intent-table triggers "proxy off" / "route to the owning validator" in SKILL.md.

Checks: skill-package-checks.sh --skill-dir .claude/skills/harbor-dev unchanged (only the pre-existing T2/A1 findings); vale reports no errors on the touched files.

Link to Devin session: https://app.devin.ai/sessions/60cb09e09f2f45e08c4e1dba208e1074
Open in Devin Desktop: https://app.devin.ai/desktop/session/60cb09e09f2f45e08c4e1dba208e1074?variant=devin
Requested by: @bdchatham

… + seiload shardRouting from autobahn.json (PLT-1283)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@cursor

cursor Bot commented Sep 16, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Documentation-only updates to the harbor-dev agent skill; no runtime, controller, or infrastructure code changes.

Overview
Documents how to bench Autobahn chains with spec.consensus.autobahn.enableEvmProxy: false (controller #560): without EVM proxy forwarding, seiload must use profile shardRouting (sei-load #117) so sends hit the validator that owns each sender instead of spreading via endpoints.

The harbor-dev skill gains intent phrases ("proxy off", "route to the owning validator") and a new Sender-owner routing section in autobahn-giga.md with a kubectl exec + jq recipe to build shardRouting from validator autobahn.json, merge/validate rules, and halt conditions (no shardRouting with proxy off, seiload before #117, CRD missing enableEvmProxy). sei-load-bench.md and seinetwork-crd.md now describe the shardRouting envelope field and enableEvmProxy on the SeiNetwork spec.

Reviewed by Cursor Bugbot for commit d0e4527. Bugbot is set up for automated code reviews on this repo. Configure here.

| "Run a load test against chain X" / "bench it" / "stress test chain X" | **PR-based bench** (see Procedure: spin up a load test). Live-fetch chain rpc per-pod URLs, substitute into the profile JSON, render Job + ConfigMap from the templates in `references/sei-load-bench.md`, open a PR against `harbor-engineering-workspace` at `engineers/<alias>/bench-<RUN_ID>/`. Merge → Flux applies → seiload runs → the uploader sidecar pushes the results to S3. When the host lists the `sei-load` MCP tools, call them for the scenario menu and the profile verdict. Fall back to the docker path when it does not, and never block on them (`references/sei-load-bench.md`, *`seiload mcp`*). |
| "Compare PR 3399 to main on sei-chain" / "bench A against B" / "diff the perf of these two commits" | **PR-based comparative bench** (see Procedure: comparative bench). Renders two ephemeral chains (each running its own seid image) + two sei-load Jobs (identical profile + duration) into a single PR. After merge, watches both chains in parallel and polls both Jobs to terminal. Then fetches both reports from S3 and surfaces a side-by-side metrics table (TPS / latency / success rate / errors). Lives at `engineers/<alias>/compare-<COMPARE_RUN_ID>/`. |
| "Spin up a giga chain" / "enable the giga executor" / "autobahn network" / "evm-only chain" / "reproduce the autobahn README on harbor" | **Tier check first** (see `references/autobahn-giga.md`). Giga executor/storage = `spec.configValues` on a normal chain. Autobahn consensus and EVM-only = `spec.consensus: {engine: Autobahn, evmOnly}` on the SeiNetwork (`seictl network apply --consensus-engine Autobahn [--evm-only]`) — **gate on the CRD carrying `spec.consensus`** (sei-k8s-controller#553) before rendering; on an older cell offer the Giga subset and ask for a controller pin advance (file it as a PLT-1259 follow-up). Never set `evm-only`, `autobahn-config-file` or the RPC/API/gRPC listener keys through `configValues`: the controller owns them and refuses the entry. |
| "Spin up a giga chain" / "enable the giga executor" / "autobahn network" / "evm-only chain" / "proxy off" / "route to the owning validator" / "reproduce the autobahn README on harbor" | **Tier check first** (see `references/autobahn-giga.md`). Giga executor/storage = `spec.configValues` on a normal chain. Autobahn consensus and EVM-only = `spec.consensus: {engine: Autobahn, evmOnly}` on the SeiNetwork (`seictl network apply --consensus-engine Autobahn [--evm-only]`) — **gate on the CRD carrying `spec.consensus`** (sei-k8s-controller#553) before rendering; on an older cell offer the Giga subset and ask for a controller pin advance (file it as a PLT-1259 follow-up). Never set `evm-only`, `autobahn-config-file` or the RPC/API/gRPC listener keys through `configValues`: the controller owns them and refuses the entry. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] <AgenticWriting.STE-SentenceLength-Description> reported by reviewdog 🐶
Descriptive sentence has 27 words. ASD-STE100 allows 25. Split it.

| "Run a load test against chain X" / "bench it" / "stress test chain X" | **PR-based bench** (see Procedure: spin up a load test). Live-fetch chain rpc per-pod URLs, substitute into the profile JSON, render Job + ConfigMap from the templates in `references/sei-load-bench.md`, open a PR against `harbor-engineering-workspace` at `engineers/<alias>/bench-<RUN_ID>/`. Merge → Flux applies → seiload runs → the uploader sidecar pushes the results to S3. When the host lists the `sei-load` MCP tools, call them for the scenario menu and the profile verdict. Fall back to the docker path when it does not, and never block on them (`references/sei-load-bench.md`, *`seiload mcp`*). |
| "Compare PR 3399 to main on sei-chain" / "bench A against B" / "diff the perf of these two commits" | **PR-based comparative bench** (see Procedure: comparative bench). Renders two ephemeral chains (each running its own seid image) + two sei-load Jobs (identical profile + duration) into a single PR. After merge, watches both chains in parallel and polls both Jobs to terminal. Then fetches both reports from S3 and surfaces a side-by-side metrics table (TPS / latency / success rate / errors). Lives at `engineers/<alias>/compare-<COMPARE_RUN_ID>/`. |
| "Spin up a giga chain" / "enable the giga executor" / "autobahn network" / "evm-only chain" / "reproduce the autobahn README on harbor" | **Tier check first** (see `references/autobahn-giga.md`). Giga executor/storage = `spec.configValues` on a normal chain. Autobahn consensus and EVM-only = `spec.consensus: {engine: Autobahn, evmOnly}` on the SeiNetwork (`seictl network apply --consensus-engine Autobahn [--evm-only]`) — **gate on the CRD carrying `spec.consensus`** (sei-k8s-controller#553) before rendering; on an older cell offer the Giga subset and ask for a controller pin advance (file it as a PLT-1259 follow-up). Never set `evm-only`, `autobahn-config-file` or the RPC/API/gRPC listener keys through `configValues`: the controller owns them and refuses the entry. |
| "Spin up a giga chain" / "enable the giga executor" / "autobahn network" / "evm-only chain" / "proxy off" / "route to the owning validator" / "reproduce the autobahn README on harbor" | **Tier check first** (see `references/autobahn-giga.md`). Giga executor/storage = `spec.configValues` on a normal chain. Autobahn consensus and EVM-only = `spec.consensus: {engine: Autobahn, evmOnly}` on the SeiNetwork (`seictl network apply --consensus-engine Autobahn [--evm-only]`) — **gate on the CRD carrying `spec.consensus`** (sei-k8s-controller#553) before rendering; on an older cell offer the Giga subset and ask for a controller pin advance (file it as a PLT-1259 follow-up). Never set `evm-only`, `autobahn-config-file` or the RPC/API/gRPC listener keys through `configValues`: the controller owns them and refuses the entry. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] <AgenticWriting.STE-SentenceLength-Description> reported by reviewdog 🐶
Descriptive sentence has 42 words. ASD-STE100 allows 25. Split it.

| `image-resolution.md` | **Canonical image-resolution recipes** for sei-chain (ECR) and sei-load (GHCR). PR/commit/branch input → full SHA → expected tag → registry probe → trigger + watch the build workflow if missing |
| `state-sync-bootstrap.md` | **Read this if the engineer wants a node that bootstraps via state sync from their own chain.** The `rpcServers` witness recipe: witnesses-vs-snapshot-providers model, preconditions, spec shape, `StateSyncReady` gate semantics, failure table (incl. the wrong-chain witness footgun) |
| `autobahn-giga.md` | **Read this if the engineer says giga, autobahn, evm-only, flatkv, or cites `sei-chain/integration_test/autobahn/README.md`.** Three tiers (Giga via `configValues`; Autobahn and EVM-only via `spec.consensus`, gated on the controller carrying #553), the `app.toml` key set for executor + storage + receipts with the `sc-write-mode` enum, the README-to-CRD mapping with the controller-owned keys `configValues` must not touch, `spec.genesis.consensusParams` for `max_gas`, the follower/seed/freeze rules, and every assumption elsewhere in this skill that EVM-only breaks (two-method RPC, validators serve EVM, height 0 idle, no funding) |
| `autobahn-giga.md` | **Read this if the engineer says giga, autobahn, evm-only, flatkv, or cites `sei-chain/integration_test/autobahn/README.md`.** Three tiers (Giga via `configValues`; Autobahn and EVM-only via `spec.consensus`, gated on the controller carrying #553), the `app.toml` key set for executor + storage + receipts with the `sc-write-mode` enum, the README-to-CRD mapping with the controller-owned keys `configValues` must not touch, `spec.genesis.consensusParams` for `max_gas`, the follower/seed/freeze rules, sender-owner routing (`spec.consensus.autobahn.enableEvmProxy: false` paired with a seiload `shardRouting` block built from the validators' `autobahn.json`), and every assumption elsewhere in this skill that EVM-only breaks (two-method RPC, validators serve EVM, height 0 idle, no funding) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] <AgenticWriting.STE-SentenceLength-Description> reported by reviewdog 🐶
Descriptive sentence has 78 words. ASD-STE100 allows 25. Split it.


## Sender-owner routing — `enableEvmProxy: false` + seiload `shardRouting`

Autobahn shards the EVM mempool by sender: `sha256(address) mod total_power` over the validators sorted by consensus key picks the one validator that sequences that sender (`sei-tendermint` `Committee.EvmShard`). With `enable_evm_proxy` on (the default) a validator forwards a foreign sender's transaction to its owner — a synchronous hop the bench then measures. `spec.consensus.autobahn.enableEvmProxy: false` (controller #560, `kubectl explain seinetwork.spec.consensus.autobahn.enableEvmProxy` is the gate) removes the hop, and then a transaction that reaches the wrong validator is not forwarded. seiload's default `endpoints` spread — sender address modulo endpoint count — is a different rule, so with proxying off most of its transactions land on a non-owner and the run measures rejections. The profile's `shardRouting` block (sei-load #117) makes seiload compute the same owner and dial it directly:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] <AgenticWriting.STE-SentenceLength-Description> reported by reviewdog 🐶
Descriptive sentence has 31 words. ASD-STE100 allows 25. Split it.


## Sender-owner routing — `enableEvmProxy: false` + seiload `shardRouting`

Autobahn shards the EVM mempool by sender: `sha256(address) mod total_power` over the validators sorted by consensus key picks the one validator that sequences that sender (`sei-tendermint` `Committee.EvmShard`). With `enable_evm_proxy` on (the default) a validator forwards a foreign sender's transaction to its owner — a synchronous hop the bench then measures. `spec.consensus.autobahn.enableEvmProxy: false` (controller #560, `kubectl explain seinetwork.spec.consensus.autobahn.enableEvmProxy` is the gate) removes the hop, and then a transaction that reaches the wrong validator is not forwarded. seiload's default `endpoints` spread — sender address modulo endpoint count — is a different rule, so with proxying off most of its transactions land on a non-owner and the run measures rejections. The profile's `shardRouting` block (sei-load #117) makes seiload compute the same owner and dial it directly:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 [vale] <AgenticWriting.STE-NounCluster> reported by reviewdog 🐶
Noun cluster of more than three nouns: 'sender address modulo endpoint'. Break it with 'of' or 'for'.

- Engineer asks for Autobahn or EVM-only on a cell whose CRD gate (above) fails → offer Tier 1 (Giga on CometBFT) as the deployable subset, ask the platform team to advance the controller pin. Never render `evm-only = true` or `autobahn-config-file` through `configValues` — on a new controller it is refused at plan build, on an old one it is a pod that never reads Ready.
- Engineer asks for a non-default `autobahn.json` field → `spec.consensus.autobahn.{blockInterval, allowEmptyBlocks, maxTxsPerBlock}` in the manifest (controller #555; `kubectl explain seinetwork.spec.consensus.autobahn` is the gate, and there is no seictl flag). Create-only: a change is a new chain. Any other `autobahn.json` field is not expressible; say so rather than editing the file in the pod. `maxTxsPerBlock` above 2000 is refused by the CRD; the protocol clamps there anyway.
- Follower requested on an EVM-only chain → ask what it is for; the validators serve `:8545` and a follower serves nothing the bench reads.
- `enableEvmProxy: false` requested with a profile that has no `shardRouting` block, or `shardRouting` requested on a seiload image predating sei-load #117 → refuse to render the bench; the first measures rejections, the second fails at the strict decoder. Both fixes are in *Sender-owner routing* above.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] <AgenticWriting.STE-SentenceLength-Description> reported by reviewdog 🐶
Descriptive sentence has 34 words. ASD-STE100 allows 25. Split it.

- Engineer asks for a non-default `autobahn.json` field → `spec.consensus.autobahn.{blockInterval, allowEmptyBlocks, maxTxsPerBlock}` in the manifest (controller #555; `kubectl explain seinetwork.spec.consensus.autobahn` is the gate, and there is no seictl flag). Create-only: a change is a new chain. Any other `autobahn.json` field is not expressible; say so rather than editing the file in the pod. `maxTxsPerBlock` above 2000 is refused by the CRD; the protocol clamps there anyway.
- Follower requested on an EVM-only chain → ask what it is for; the validators serve `:8545` and a follower serves nothing the bench reads.
- `enableEvmProxy: false` requested with a profile that has no `shardRouting` block, or `shardRouting` requested on a seiload image predating sei-load #117 → refuse to render the bench; the first measures rejections, the second fails at the strict decoder. Both fixes are in *Sender-owner routing* above.
- `enableEvmProxy` requested on a cell where `kubectl explain seinetwork.spec.consensus.autobahn.enableEvmProxy` fails → the CRD prunes it silently and the chain boots with the proxy on; ask the platform team to advance the controller pin past #560 rather than rendering.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] <AgenticWriting.STE-SentenceLength-Description> reported by reviewdog 🐶
Descriptive sentence has 33 words. ASD-STE100 allows 25. Split it.

### Profile shape

- **Envelope** (`config.LoadConfig`): `chainId`, `seiChainID` (the Go tag is `seiChainID`; `encoding/json` matches keys case-insensitively, so `seiChainId` in an older profile is accepted too — write `seiChainID`, as the nightly profiles do), `endpoints` (required; sending shards across them), `receiptEndpoint`, `accounts` (`config.AccountConfig`: `count`, `newAccountRate`), `scenarios[]`, `mockDeploy`, `settings`, `funding`, `reportPath`, `seed`. `gasFeeCapWei` is *not* a field — the fee cap resolves from the chain at startup.
- **Envelope** (`config.LoadConfig`): `chainId`, `seiChainID` (the Go tag is `seiChainID`; `encoding/json` matches keys case-insensitively, so `seiChainId` in an older profile is accepted too — write `seiChainID`, as the nightly profiles do), `endpoints` (required; sending shards across them), `receiptEndpoint`, `accounts` (`config.AccountConfig`: `count`, `newAccountRate`), `scenarios[]`, `mockDeploy`, `settings`, `funding`, `reportPath`, `seed`, `shardRouting` (sei-load #117: `{validators: [{consensusPubKey, power, endpoint}]}` — present, sends dial the Autobahn validator that owns each sender instead of spreading over `endpoints`; required on an `enableEvmProxy: false` chain, built from the validators' `autobahn.json` per `autobahn-giga.md` *Sender-owner routing*; `generate_profile` does not emit it, merge it with `jq` after generation and validate again). `gasFeeCapWei` is *not* a field — the fee cap resolves from the chain at startup.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] <AgenticWriting.STE-SentenceLength-Description> reported by reviewdog 🐶
Descriptive sentence has 66 words. ASD-STE100 allows 25. Split it.

### Profile shape

- **Envelope** (`config.LoadConfig`): `chainId`, `seiChainID` (the Go tag is `seiChainID`; `encoding/json` matches keys case-insensitively, so `seiChainId` in an older profile is accepted too — write `seiChainID`, as the nightly profiles do), `endpoints` (required; sending shards across them), `receiptEndpoint`, `accounts` (`config.AccountConfig`: `count`, `newAccountRate`), `scenarios[]`, `mockDeploy`, `settings`, `funding`, `reportPath`, `seed`. `gasFeeCapWei` is *not* a field — the fee cap resolves from the chain at startup.
- **Envelope** (`config.LoadConfig`): `chainId`, `seiChainID` (the Go tag is `seiChainID`; `encoding/json` matches keys case-insensitively, so `seiChainId` in an older profile is accepted too — write `seiChainID`, as the nightly profiles do), `endpoints` (required; sending shards across them), `receiptEndpoint`, `accounts` (`config.AccountConfig`: `count`, `newAccountRate`), `scenarios[]`, `mockDeploy`, `settings`, `funding`, `reportPath`, `seed`, `shardRouting` (sei-load #117: `{validators: [{consensusPubKey, power, endpoint}]}` — present, sends dial the Autobahn validator that owns each sender instead of spreading over `endpoints`; required on an `enableEvmProxy: false` chain, built from the validators' `autobahn.json` per `autobahn-giga.md` *Sender-owner routing*; `generate_profile` does not emit it, merge it with `jq` after generation and validate again). `gasFeeCapWei` is *not* a field — the fee cap resolves from the chain at startup.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] <AgenticWriting.STE-Passive> reported by reviewdog 🐶
Passive voice: 'is accepted'. ASD-STE100 asks for the active voice.

- `spec.dataVolume.storage.volumeAttributesClassName` — the storage performance selection for each pool validator, a sibling of the size path above. Names a platform-managed VolumeAttributesClass, which carries the gp3 IOPS and throughput. `network apply` resolves the name from the `--iops` and `--throughput` pair you supply. Unset means the standard tier. **Create-only.**
- `spec.consensus` — `{engine: Tendermint|Autobahn, evmOnly: bool}` (`--consensus-engine`, `--evm-only`), copied into every validator child. Omitted resolves to Tendermint. `evmOnly: true` requires `engine: Autobahn` (CEL). **Create-only on its effective value** — changing the engine or `evmOnly` is a new chain. Under Autobahn the ceremony publishes `autobahn.json` beside `genesis.json` and the controller owns `config.toml` `autobahn-config-file`/`evm-only` and the listener toggles; a `configValues` entry on those keys fails plan build. See `autobahn-giga.md`.
- `spec.consensus.autobahn` — `{blockInterval, allowEmptyBlocks, maxTxsPerBlock}`, the operator-settable slice of the ceremony's `autobahn.json`; requires `engine: Autobahn` (CEL). Omitted fields keep `gen-autobahn-config` defaults (`400ms`, `false`, `2000`). `blockInterval` is a positive Go duration; `maxTxsPerBlock` is `1..2000` — 2000 is the protocol ceiling, so only lowering it changes anything. **Create-only.** No seictl flag yet; set it in the manifest. See `autobahn-giga.md`.
- `spec.consensus.autobahn` — `{blockInterval, allowEmptyBlocks, maxTxsPerBlock, enableEvmProxy}` (the last from #560: `false` stops validators forwarding foreign senders' EVM transactions, so the load generator must route to the owner itself — `autobahn-giga.md` *Sender-owner routing*), the operator-settable slice of the ceremony's `autobahn.json`; requires `engine: Autobahn` (CEL). Omitted fields keep `gen-autobahn-config` defaults (`400ms`, `false`, `2000`). `blockInterval` is a positive Go duration; `maxTxsPerBlock` is `1..2000` — 2000 is the protocol ceiling, so only lowering it changes anything. **Create-only.** No seictl flag yet; set it in the manifest. See `autobahn-giga.md`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [vale] <AgenticWriting.STE-SentenceLength-Description> reported by reviewdog 🐶
Descriptive sentence has 31 words. ASD-STE100 allows 25. Split it.

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review found nothing blocking.

@seidroid

seidroid Bot commented Sep 16, 2026

Copy link
Copy Markdown

Settled by the scouts (codex) without a review turn: every one of them read the diff, found nothing, and reported it inert -- comments, documentation or whitespace only, with no executable surface changed.

seidroid review · decision approve · settled by the scouts codex · no review turn ran

Findings: 0 blocking | 0 non-blocking | 0 posted inline

@bdchatham
bdchatham merged commit 52045da into main Sep 16, 2026
15 checks passed
@bdchatham
bdchatham deleted the devin/1789542925-harbor-dev-shard-routing branch September 16, 2026 07:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant