Skip to content

[Phase 1] TypeScript SDK — frame encoding with canonical request signing and test parity with Python SDK#40

Open
shayankashif123 wants to merge 6 commits into
c2siorg:mainfrom
shayankashif123:feature/sdk-frame-encoding-signing
Open

[Phase 1] TypeScript SDK — frame encoding with canonical request signing and test parity with Python SDK#40
shayankashif123 wants to merge 6 commits into
c2siorg:mainfrom
shayankashif123:feature/sdk-frame-encoding-signing

Conversation

@shayankashif123

Copy link
Copy Markdown

Implements the TypeScript SDK wire path with canonical request
signing, closing the cross-SDK HMAC mismatch gap identified in #34.
Closes #34.

Problem

JavaScript's JSON.stringify does not sort keys by default. Without canonicalization, the TypeScript SDK would produce different HMACs than the Python SDK for semantically identical payloads — the Go
sidecar would reject every TypeScript request with HMAC verification failed. This is the exact bug #32 fixed for Python and Go. This PR builds the TypeScript implementation correctly from day one.

What Was Implemented

src/models.ts
Decision enum with correct wire bytes, decisionFromByte(),
SanitiseResult, ChunkResult, FirewallError, FirewallConnectionError.

src/frame.ts
sortKeysRecursive() — recursive key sort at all nesting levels.
canonicalPayload() — parse, sort, compact re-encode, throws
CanonicaliseError on invalid JSON.
signedMessage() — version(1B) || length(4B BE) || nonce(16B) ||
canonical_payload. Matches Python/Go layout exactly.
encodeRequest(), decodeRequest(), encodeResponse(), decodeResponse().

src/transport.ts
Async UDS transport via net.createConnection.
Injectable dialer for deterministic unit tests.
Exponential backoff retry — 100ms/200ms, 3 attempts.
Chunked response reassembly for fragmented TCP delivery.
FirewallConnectionError after retry exhaustion.

src/firewall.ts
Firewall class with all four async hook methods.
onPrompt / onContext / onToolCall / onMemory.
HMAC key resolution from ACF_HMAC_KEY env variable.
RiskContext payload matches Python SDK _build_payload() exactly.

Cross-SDK Guarantee

Given the same nonce, TypeScript and Python produce identicalHMAC signatures for semantically identical JSON regardless of key ordering. Verified in frame.test.ts:

"produces identical HMAC for equivalent payload key orders
— core cross-SDK guarantee"

Unicode normalization: \u0041 → A matches Go encoding/json behaviour.

Test Coverage

models.ts 16 tests — enum values, error hierarchy
frame.ts 21 tests — canonicalization, interop, frame codec
transport.ts 7 tests — retry logic, chunked reassembly
firewall.ts 22 tests — constructor, hooks, decisions, errors
──────────────────────────────────────────
Total 66 tests 0 failures

All tests use mocks — no sidecar required for CI.
Zero external dependencies — Node stdlib only.

Relates To

What This Does Not Include

  • LangGraph adapter (src/adapters/langgraph.ts) — deferred
  • Windows named pipe support in transport — deferred
  • Async hook variants beyond the four v1 hooks — deferred

- FastAPI app with GET /health and POST /validate endpoints
- In-process regex rule engine (6 rules, 24+ patterns)
- Pre-filter + sidecar two-layer enforcement flow
- Pydantic request/response contracts
- 82 tests — all passing, no sidecar required
- SDK zero-dependency contract preserved
…ry framing, and HMAC signing inputs.

transport layer: IPC connection handling, retry logic, and response frame retrieval/decoding.
@VibhorGautam

Copy link
Copy Markdown
Contributor

the typescript signing path closing out #34 is a clear win. the new sortKeysRecursive and canonicalPayload in frame.ts give js the deterministic byte-level output python needed for hmac parity, and the three npm scripts (test, test:e2e, test:parity) line up with the go sidecar's expectations

two things worth flagging that i think are separate asks from the core work:

split the api/ directory out. the pr title is "TypeScript SDK frame encoding with canonical request signing" but the diff also adds api/main.py (264 lines), api/rules/engine.py (175 lines), api/models.py, api/config/rules.yaml, and docs/api.md (352 lines). that's a separate python fastapi facade with its own rules engine, and it reads like work from a different proposal. mixing it with the sdk wire-path work makes this hard to review and risks the #34-closing part getting stalled behind api-level design questions. strongly recommend:

python-side parity for canonicalisation. sortKeysRecursive solves the ts side, but sdk/python/acf/firewall.py still builds the payload with json.dumps(ctx, separators=(",", ":")) and no sort_keys=True. python dicts preserve insertion order so a dev who always inserts keys in the same order is fine in practice, but any nested object where python's and typescript's insertion order diverges will still produce different hmacs. has the parity test actually been run against the live sidecar with payloads containing nested objects whose key order would differ between the two sdks? if yes, great - maybe worth linking the test output in the pr. if not, adding sort_keys=True to json.dumps on the python side is a two-character change that closes the parity gap at the source

on the ci additions. 24 lines added to .github/workflows/ci.yml for typescript build + test on node 20 with cache. looks solid. worth making the ts test step a required status check if it isn't already, otherwise typescript regressions land silently

on readme. the new "Build the TypeScript SDK" and test-command sections read clean. minor nit: the parity test line says "against live sidecar" but a reader coming fresh might not realise they need to export ACF_HMAC_KEY and start the sidecar in another shell first. a one-line prerequisites note would save some env-related issues from users trying it

once the split happens i'd +1 part A quickly. part B (the api/ facade) would benefit from its own issue and design discussion

@shayankashif123

Copy link
Copy Markdown
Author

That plan makes total sense. I’ll go ahead and split the work into two distinct PRs to make the review process cleaner.

The Split:

PR A (Fixes #34): This will focus on the TypeScript SDK wire path (models/frame/transport/firewall), canonical signing, TS tests, and the new CI job. I’ll also include the relevant README updates here.

PR B: This will be dedicated to the Python API facade (api/* + docs/api.md) so we can have a separate architectural discussion there.

Parity & CI Updates:
I’ll run some live sidecar scenarios to verify canonicalization parity between Python and TS, specifically looking for any key-order issues in nested objects. If I find a gap, I’ll patch the Python serialization and include it in the relevant PR.

I’m also setting the TypeScript check as a required status in CI and adding the ACF_HMAC_KEY / sidecar prerequisite to the README as we discussed.

I'll get these pushed shortly. Once PR A is isolated, I'll ping you for a final look to close out #34.

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.

[Phase 1] TypeScript SDK — Implement frame encoding with canonical request signing and test parity with Python SDK

2 participants