Skip to content

fix(exit-certificate): AET-12 :step E prevent uint64 overflow panic in decodeABIString#1684

Merged
joanestebanr merged 2 commits into
feature/exit-certificate-toolfrom
fix/step-e-decode-abi-string-overflow
Jun 30, 2026
Merged

fix(exit-certificate): AET-12 :step E prevent uint64 overflow panic in decodeABIString#1684
joanestebanr merged 2 commits into
feature/exit-certificate-toolfrom
fix/step-e-decode-abi-string-overflow

Conversation

@joanestebanr

Copy link
Copy Markdown
Collaborator

🔄 Changes Summary

  • Fix a uint64 integer overflow in step_e.decodeABIString() (SP002). A maliciously crafted L1 ERC-20 whose name()/symbol() returns an oversized ABI length field could make 64+strLen wrap past MaxUint64 to a small value, bypassing the bounds guard and triggering a slice out-of-range panic that aborts the exit certificate tool for any chain with an unclaimed L1→L2 deposit of such a token.
  • Reject length fields that don't fit in uint64 (big.Int.IsUint64()) and compare strLen against the remaining byte count without the overflowing addition (strLen > uint64(len(data))-twoABIWords, safe since len(data) >= 64).

⚠️ Breaking Changes

  • None.

📋 Config Updates

  • None.

✅ Testing

  • 🤖 Automatic: Added TestDecodeABIString with regression cases — MaxUint64 length field (the overflow case) and a length field larger than uint64 — asserting no panic and "" return, plus valid/empty/too-short/length-exceeds-data cases. Full go test ./tools/exit_certificate/... passes; go vet and gofmt clean.
  • 🖱️ Manual: n/a (deterministic decode logic covered by unit test).

🐞 Issues

📝 Notes

  • The guard now also rejects length fields exceeding uint64 range, which big.Int.Uint64() would have silently truncated.

🤖 Generated with Claude Code

@joanestebanr joanestebanr changed the title fix(exit-certificate): prevent uint64 overflow panic in decodeABIString fix(exit-certificate): step E prevent uint64 overflow panic in decodeABIString Jun 29, 2026
@claude

claude Bot commented Jun 29, 2026

Copy link
Copy Markdown

Claude finished @joanestebanr's task in 1m 24s —— View job


PR Review: uint64 Overflow Fix in decodeABIString


🚀 What's New

This PR fixes a critical uint64 integer overflow vulnerability in decodeABIString() that could cause a panic and abort the exit certificate tool. The fix prevents maliciously crafted ERC-20 tokens with oversized ABI length fields from causing DoS via panic when processing L1→L2 deposits.

🐛 Bug Fixes

  • Security Fix (SP002): Prevents uint64 overflow in step_e.decodeABIString() that could bypass bounds checks and cause a slice out-of-range panic
  • The overflow occurred when computing 64 + strLen where strLen ≈ MaxUint64, causing the sum to wrap to a small value and bypass the > uint64(len(data)) guard
  • Now validates that the length field fits in uint64 using big.Int.IsUint64() and performs safe comparison without overflow-prone addition

📋 Config Updates

None.

⚠️ Breaking Changes

None.


Review Feedback

🟢 Excellent Security Fix

The fix correctly addresses the integer overflow vulnerability. The approach is sound:

  1. Validates uint64 range first (!lenField.IsUint64()) - prevents truncation that big.Int.Uint64() would silently perform
  2. Safe comparison without addition - strLen > uint64(len(data))-twoABIWords avoids the overflow-prone twoABIWords+strLen
  3. Safe subtraction - The comment correctly notes that len(data) >= 64 due to the earlier guard, so the subtraction is safe

Key strength: The subtraction uint64(len(data))-twoABIWords is safe because we know len(data) >= twoABIWords from the guard on line 348:

if len(data) < twoABIWords {
    return ""
}

🟢 Comprehensive Test Coverage

The test suite at tools/exit_certificate/step_e_test.go:234-292 is excellent:

  • Regression cases: MaxUint64 length (the overflow trigger) and >uint64 length
  • Edge cases: empty data, too-short data, valid strings, empty strings, length exceeds available data
  • Panic protection: All tests wrapped in require.NotPanics() to catch any remaining panic paths
  • Helper functions: Clean abiString() and header() helpers for test data construction

🟢 Code Quality

  • ✅ Clear, detailed comments explaining the vulnerability and fix rationale
  • ✅ Consistent use of constants (abiWordBytes, twoABIWords)
  • ✅ Proper error handling (returns empty string on invalid input)
  • ✅ Follows Go best practices

🟡 Minor Suggestions (Optional)

  1. Consider adding a fuzz test for additional confidence in edge cases (tools/exit_certificate/step_e_test.go:234):

    func FuzzDecodeABIString(f *testing.F) {
        f.Add([]byte{}) // seed corpus
        f.Fuzz(func(t *testing.T, data []byte) {
            require.NotPanics(t, func() { decodeABIString(data) })
        })
    }
  2. Constants documentation - The constants in hex.go are well-defined, but a brief reference to EIP/ABI spec would be helpful for future maintainers.

🟢 Impact Assessment

Severity: 🔴 High - This is a DoS vulnerability that could halt certificate processing for an entire chain

Attack Vector: An attacker could deploy a malicious ERC-20 contract on L1 with a name() or symbol() function that returns an oversized length field, then bridge tokens to trigger the panic whenever Step E processes unclaimed deposits.

Mitigation: The fix fully addresses the vulnerability by rejecting invalid length fields before they can cause overflow.


Summary

This is a well-executed security fix with excellent test coverage. The code is correct, safe, and properly documented. The vulnerability was serious but is now fully mitigated.

Recommendation: ✅ Approve and merge (pending CI checks)

Minor suggestions above are optional enhancements, not blockers.


@joanestebanr joanestebanr self-assigned this Jun 29, 2026
@joanestebanr joanestebanr added bug Something isn't working exit_certificate_tool Tool to create a final exit certificate labels Jun 29, 2026
@joanestebanr joanestebanr force-pushed the fix/step-e-decode-abi-string-overflow branch from c9abfbb to f9f0c35 Compare June 30, 2026 07:45
joanestebanr and others added 2 commits June 30, 2026 10:03
A maliciously crafted L1 ERC-20 whose name()/symbol() returns an oversized
ABI length field could overflow the uint64 bounds check in
step_e.decodeABIString(): computing 64+strLen wrapped past MaxUint64 to a
small value, bypassing the guard and triggering a slice out-of-range panic
that aborts the exit certificate tool.

Reject length fields that don't fit in uint64 and compare strLen against the
remaining byte count without the overflowing addition. Add a regression test
covering the overflow and oversized-length cases.

Fixes #1680

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allocate the ABI-string buffer at full size and copy into it instead of
appending to a non-zero-length slice. Also drop the redundant loopvar copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr joanestebanr force-pushed the fix/step-e-decode-abi-string-overflow branch from f9f0c35 to a5a1f5e Compare June 30, 2026 08:16
@sonarqubecloud

Copy link
Copy Markdown

@joanestebanr joanestebanr merged commit 7882c20 into feature/exit-certificate-tool Jun 30, 2026
22 of 23 checks passed
@joanestebanr joanestebanr deleted the fix/step-e-decode-abi-string-overflow branch June 30, 2026 08:56
joanestebanr added a commit that referenced this pull request Jun 30, 2026
…deABIString (#1684)

## 🔄 Changes Summary
- Fix a uint64 integer overflow in `step_e.decodeABIString()` (SP002). A
maliciously crafted L1 ERC-20 whose `name()`/`symbol()` returns an
oversized ABI length field could make `64+strLen` wrap past `MaxUint64`
to a small value, bypassing the bounds guard and triggering a slice
out-of-range **panic** that aborts the exit certificate tool for any
chain with an unclaimed L1→L2 deposit of such a token.
- Reject length fields that don't fit in `uint64` (`big.Int.IsUint64()`)
and compare `strLen` against the remaining byte count without the
overflowing addition (`strLen > uint64(len(data))-twoABIWords`, safe
since `len(data) >= 64`).

## ⚠️ Breaking Changes
- None.

## 📋 Config Updates
- None.

## ✅ Testing
- 🤖 **Automatic**: Added `TestDecodeABIString` with regression cases —
`MaxUint64` length field (the overflow case) and a length field larger
than `uint64` — asserting no panic and `""` return, plus
valid/empty/too-short/length-exceeds-data cases. Full `go test
./tools/exit_certificate/...` passes; `go vet` and `gofmt` clean.
- 🖱️ **Manual**: n/a (deterministic decode logic covered by unit test).

## 🐞 Issues
- Closes #1680

## 📝 Notes
- The guard now also rejects length fields exceeding `uint64` range,
which `big.Int.Uint64()` would have silently truncated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@joanestebanr

joanestebanr commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

AET-12 uint64 Addition Overflow In decodeABIString Bypasses Bounds Guard And Crashes The Process

@joanestebanr joanestebanr changed the title fix(exit-certificate): step E prevent uint64 overflow panic in decodeABIString fix(exit-certificate): AET-12 :step E prevent uint64 overflow panic in decodeABIString Jul 2, 2026
joanestebanr added a commit that referenced this pull request Jul 8, 2026
…T-02) and zero adress issue (AET-08) (#1701)

## 🔄 Changes Summary
- **Step A replaced**: addresses are discovered via a
`debug_accountRange` state dump + `Transfer` event logs per wrapped
token, instead of `debug_traceTransaction` over the whole chain history
— `O(#accounts)` vs `O(#txs)`, and it finds passive token-only holders
that tracing structurally misses. Both sources always run and merge (the
dump covers native-ETH holders and contracts; the logs cover token
holders): they have complementary blind spots, so there is no strategy
option and no fallback — an unusable `debug_accountRange` fails Step A
instead of degrading silently. The trace-based A1/A2 implementation is
removed.
- **Zero address kept in Step A** — `0x00…00` can hold value like any
other account (a plain `transfer(0x0, amount)` is not a burn and native
ETH can be sent there); dropping it left that value uncovered and the
certificate unbalanced against the LBT.
- **Extra ERC-20 holders discovered in Step A** — the `Transfer`-log
scan also covers `options.extraErc20Contracts` (deduplicated against the
wrapped tokens, and it runs even with no wrapped tokens). Step B3 only
probes `balanceOf` against the Step A address set, so a passive holder
of an extra token (no ETH/nonce/code, never touched a wrapped token) was
invisible to both sources and their collateral share flowed to
`exitAddress`; discovery stays in Step A (B3 never discovers addresses),
so `step-a-addresses.json` remains the complete address universe in
single-step mode.
- **`options.capMode` gains `"none"`, the new default** — capping the
certificate is a lossy operation and must now be opted into explicitly.
With `"none"`, Step F fails (`errCapForbidden`) as soon as any bridge
exit would have to be trimmed or dropped, on every capping path: the
mismatch cap (`ignoreBalanceMismatch=true`), the genesis pre-fund trim
(which caps even on allMatch, so `genesisPrefundETHWei` now requires a
trimming mode) and the final-certificate re-cap in the pipeline. A no-op
cap (everything fits the budget) still passes. `"amount"`/`"appearance"`
keep their previous trimming behavior.
- The Kurtosis config generator (`configuration_based_on_kurtosis.sh`)
now emits `capMode: "amount"` (its config declares
`genesisPrefundETHWei`, whose premint trim needs a trimming mode) and
`ignoreBalanceMismatch: false` (real mismatches must abort).
- **E2E harness extended with the AET-02 scenario and split into
numbered stages** (`test/e2e-exit_certificate/`): `10-run_network` →
`20-prepare_network` → `30-stop_sequencer` →
`40-generate_exit_certificate` → `45-check_exit_certificate` →
`50-submit_exit_certificate` → `60-claim_exit_certificate_funds` (the
old `run_exit_tool.sh` is gone). The sequencer stop now halts only the
block producers (`op-batcher`/`op-cl`), keeping the `op-el` RPC alive so
the tool can still read the frozen L2 state.

## ⚠️ Breaking Changes
- 🛠️ **Config**: `options.capMode` now defaults to `"none"`: runs that
relied on the previous `"amount"` default to trim the certificate (any
`genesisPrefundETHWei` run, or `ignoreBalanceMismatch=true` with real
mismatches) now fail in Step F unless `capMode` is set explicitly to
`"amount"` or `"appearance"`.
- 🛠️ **Config**: `options.ignoreOnTraceError` and
`options.stepAWindowSize` removed (the trace-based Step A and the
receipt-harvesting fallback no longer exist). `options.addressDiscovery`
(present in intermediate revisions of this PR, never released) is also
gone — Step A always runs both sources.
- 🔌 **API/CLI**: `--step a1` / `--step a2` removed (`a` has no sub-steps
anymore); `step-a1-*`, `step-a2-*` and `step-a-failed-traces.json` are
no longer produced.

## 📋 Config Updates
- 🧾 `capMode` accepts `"none"` (new default) besides `"amount"` and
`"appearance"`; `zkevm-cardona.toml` sets `capMode = "amount"`
explicitly because its `genesisPrefundETHWei` always requires trimming.

## ✅ Testing
- 🤖 **Automatic**: `go test ./tools/exit_certificate/...` and
`golangci-lint` clean. New unit tests for Step A discovery (pagination,
dialect detection, Transfer extraction, source merging, extra-ERC-20
scanning — including token dedup and the no-wrapped-tokens case —,
truncation/empty-dump/dump-unavailable guards), zero-address handling,
and capMode `"none"` (no-op pass-through, fail-on-trim, fail-on-drop,
plus RunStepF-level prefund and mismatch failures).
- 🧪 **E2E (covers the AET-02 fix end-to-end)**:
`test/e2e-exit_certificate/run_e2e_test.sh` reproduces the exact bug
scenario on a Kurtosis enclave — `20-prepare_network.sh` deploys a
`MintableERC20` on L1, bridges it to L2 and transfers part of the
wrapped balance to a **passive account** that never sends any L2
transaction (asserted: nonce 0 and zero native balance, so it is
invisible to the state dump and only discoverable through `Transfer`
logs). After generating the certificate, `45-check_exit_certificate.sh`
asserts it contains exactly two bridge exits for that ERC-20 — the
active holder and the passive recipient, each with the exact expected
amount — before the certificate is submitted, settled on L1 and the
funds claimed. With the old trace-based Step A this check fails (the
passive recipient is missing); with the new Step A it passes.
- 🖱️ **Manual**: the state-dump Step A approach was validated in #1687
against Bali (block 24,000,064): ~2m45s vs ~7h46m for the trace-based
scan, 321,581 addresses (99.95% coverage of the trace set plus 5,987
extra real holders).

## 🐞 Issues
- Closes #1699 — AET-02: passive ERC-20 recipients (accounts that only
ever *received* tokens) are structurally invisible to
`debug_traceTransaction`, so they were omitted from the certificate and
their balances wrongly absorbed into the SC-locked residual. The
`Transfer`-log discovery in the new Step A covers them.
- #1700 — the zero-address fix included here (second commit) is what
that issue asked for; the issue was already closed manually.
- Closes #1707 — AET-08: `dialBridge()` binds the bridge contract
without rejecting a zero `BridgeAddr`, so the lite syncer would silently
scan the wrong address and `BuildTree()` would return the zero hash as
the LER without any error.

Not addressed here (still open, tracked elsewhere): #1680 is already
fixed by #1684 (merged into the feature branch; it will auto-close when
the feature branch reaches `develop`), and #1693 (AET-01) is being
handled in `fix/exit-certificate-tool_aet01-native_token`.

## 🔗 Related PRs
- Imports and supersedes #1687 (its Step A lands here as the default,
not as the opt-in `aalt` step)
- Rebased on top of #1694, which already landed `genesisPrefundETHWei`,
`capMode`, `nativeSCLockedFromContracts` and the Step F agglayer LBT
dump — those changes are no longer part of this PR. The
`ignoreAddresses` option was dropped intentionally (no longer wanted).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
joanestebanr added a commit that referenced this pull request Jul 10, 2026
…deABIString (#1684)

## 🔄 Changes Summary
- Fix a uint64 integer overflow in `step_e.decodeABIString()` (SP002). A
maliciously crafted L1 ERC-20 whose `name()`/`symbol()` returns an
oversized ABI length field could make `64+strLen` wrap past `MaxUint64`
to a small value, bypassing the bounds guard and triggering a slice
out-of-range **panic** that aborts the exit certificate tool for any
chain with an unclaimed L1→L2 deposit of such a token.
- Reject length fields that don't fit in `uint64` (`big.Int.IsUint64()`)
and compare `strLen` against the remaining byte count without the
overflowing addition (`strLen > uint64(len(data))-twoABIWords`, safe
since `len(data) >= 64`).

## ⚠️ Breaking Changes
- None.

## 📋 Config Updates
- None.

## ✅ Testing
- 🤖 **Automatic**: Added `TestDecodeABIString` with regression cases —
`MaxUint64` length field (the overflow case) and a length field larger
than `uint64` — asserting no panic and `""` return, plus
valid/empty/too-short/length-exceeds-data cases. Full `go test
./tools/exit_certificate/...` passes; `go vet` and `gofmt` clean.
- 🖱️ **Manual**: n/a (deterministic decode logic covered by unit test).

## 🐞 Issues
- Closes #1680

## 📝 Notes
- The guard now also rejects length fields exceeding `uint64` range,
which `big.Int.Uint64()` would have silently truncated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
joanestebanr added a commit that referenced this pull request Jul 10, 2026
…T-02) and zero adress issue (AET-08) (#1701)

## 🔄 Changes Summary
- **Step A replaced**: addresses are discovered via a
`debug_accountRange` state dump + `Transfer` event logs per wrapped
token, instead of `debug_traceTransaction` over the whole chain history
— `O(#accounts)` vs `O(#txs)`, and it finds passive token-only holders
that tracing structurally misses. Both sources always run and merge (the
dump covers native-ETH holders and contracts; the logs cover token
holders): they have complementary blind spots, so there is no strategy
option and no fallback — an unusable `debug_accountRange` fails Step A
instead of degrading silently. The trace-based A1/A2 implementation is
removed.
- **Zero address kept in Step A** — `0x00…00` can hold value like any
other account (a plain `transfer(0x0, amount)` is not a burn and native
ETH can be sent there); dropping it left that value uncovered and the
certificate unbalanced against the LBT.
- **Extra ERC-20 holders discovered in Step A** — the `Transfer`-log
scan also covers `options.extraErc20Contracts` (deduplicated against the
wrapped tokens, and it runs even with no wrapped tokens). Step B3 only
probes `balanceOf` against the Step A address set, so a passive holder
of an extra token (no ETH/nonce/code, never touched a wrapped token) was
invisible to both sources and their collateral share flowed to
`exitAddress`; discovery stays in Step A (B3 never discovers addresses),
so `step-a-addresses.json` remains the complete address universe in
single-step mode.
- **`options.capMode` gains `"none"`, the new default** — capping the
certificate is a lossy operation and must now be opted into explicitly.
With `"none"`, Step F fails (`errCapForbidden`) as soon as any bridge
exit would have to be trimmed or dropped, on every capping path: the
mismatch cap (`ignoreBalanceMismatch=true`), the genesis pre-fund trim
(which caps even on allMatch, so `genesisPrefundETHWei` now requires a
trimming mode) and the final-certificate re-cap in the pipeline. A no-op
cap (everything fits the budget) still passes. `"amount"`/`"appearance"`
keep their previous trimming behavior.
- The Kurtosis config generator (`configuration_based_on_kurtosis.sh`)
now emits `capMode: "amount"` (its config declares
`genesisPrefundETHWei`, whose premint trim needs a trimming mode) and
`ignoreBalanceMismatch: false` (real mismatches must abort).
- **E2E harness extended with the AET-02 scenario and split into
numbered stages** (`test/e2e-exit_certificate/`): `10-run_network` →
`20-prepare_network` → `30-stop_sequencer` →
`40-generate_exit_certificate` → `45-check_exit_certificate` →
`50-submit_exit_certificate` → `60-claim_exit_certificate_funds` (the
old `run_exit_tool.sh` is gone). The sequencer stop now halts only the
block producers (`op-batcher`/`op-cl`), keeping the `op-el` RPC alive so
the tool can still read the frozen L2 state.

## ⚠️ Breaking Changes
- 🛠️ **Config**: `options.capMode` now defaults to `"none"`: runs that
relied on the previous `"amount"` default to trim the certificate (any
`genesisPrefundETHWei` run, or `ignoreBalanceMismatch=true` with real
mismatches) now fail in Step F unless `capMode` is set explicitly to
`"amount"` or `"appearance"`.
- 🛠️ **Config**: `options.ignoreOnTraceError` and
`options.stepAWindowSize` removed (the trace-based Step A and the
receipt-harvesting fallback no longer exist). `options.addressDiscovery`
(present in intermediate revisions of this PR, never released) is also
gone — Step A always runs both sources.
- 🔌 **API/CLI**: `--step a1` / `--step a2` removed (`a` has no sub-steps
anymore); `step-a1-*`, `step-a2-*` and `step-a-failed-traces.json` are
no longer produced.

## 📋 Config Updates
- 🧾 `capMode` accepts `"none"` (new default) besides `"amount"` and
`"appearance"`; `zkevm-cardona.toml` sets `capMode = "amount"`
explicitly because its `genesisPrefundETHWei` always requires trimming.

## ✅ Testing
- 🤖 **Automatic**: `go test ./tools/exit_certificate/...` and
`golangci-lint` clean. New unit tests for Step A discovery (pagination,
dialect detection, Transfer extraction, source merging, extra-ERC-20
scanning — including token dedup and the no-wrapped-tokens case —,
truncation/empty-dump/dump-unavailable guards), zero-address handling,
and capMode `"none"` (no-op pass-through, fail-on-trim, fail-on-drop,
plus RunStepF-level prefund and mismatch failures).
- 🧪 **E2E (covers the AET-02 fix end-to-end)**:
`test/e2e-exit_certificate/run_e2e_test.sh` reproduces the exact bug
scenario on a Kurtosis enclave — `20-prepare_network.sh` deploys a
`MintableERC20` on L1, bridges it to L2 and transfers part of the
wrapped balance to a **passive account** that never sends any L2
transaction (asserted: nonce 0 and zero native balance, so it is
invisible to the state dump and only discoverable through `Transfer`
logs). After generating the certificate, `45-check_exit_certificate.sh`
asserts it contains exactly two bridge exits for that ERC-20 — the
active holder and the passive recipient, each with the exact expected
amount — before the certificate is submitted, settled on L1 and the
funds claimed. With the old trace-based Step A this check fails (the
passive recipient is missing); with the new Step A it passes.
- 🖱️ **Manual**: the state-dump Step A approach was validated in #1687
against Bali (block 24,000,064): ~2m45s vs ~7h46m for the trace-based
scan, 321,581 addresses (99.95% coverage of the trace set plus 5,987
extra real holders).

## 🐞 Issues
- Closes #1699 — AET-02: passive ERC-20 recipients (accounts that only
ever *received* tokens) are structurally invisible to
`debug_traceTransaction`, so they were omitted from the certificate and
their balances wrongly absorbed into the SC-locked residual. The
`Transfer`-log discovery in the new Step A covers them.
- #1700 — the zero-address fix included here (second commit) is what
that issue asked for; the issue was already closed manually.
- Closes #1707 — AET-08: `dialBridge()` binds the bridge contract
without rejecting a zero `BridgeAddr`, so the lite syncer would silently
scan the wrong address and `BuildTree()` would return the zero hash as
the LER without any error.

Not addressed here (still open, tracked elsewhere): #1680 is already
fixed by #1684 (merged into the feature branch; it will auto-close when
the feature branch reaches `develop`), and #1693 (AET-01) is being
handled in `fix/exit-certificate-tool_aet01-native_token`.

## 🔗 Related PRs
- Imports and supersedes #1687 (its Step A lands here as the default,
not as the opt-in `aalt` step)
- Rebased on top of #1694, which already landed `genesisPrefundETHWei`,
`capMode`, `nativeSCLockedFromContracts` and the Step F agglayer LBT
dump — those changes are no longer part of this PR. The
`ignoreAddresses` option was dropped intentionally (no longer wanted).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aet bug Something isn't working exit_certificate_tool Tool to create a final exit certificate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant