You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
WebAssembly guest blocks on a synchronous ZeroKMS call and cannot run on the edge — export the two halves of a batch so the host does the I/O #1064
Updated 2026-10-05 (PDT). The Go SDK design is PR cipherstash/stack#1070: docs/plans/2026-10-04-plan-builder.md, "The Go SDK", and the principles in docs/sdk-design-principles.md (ADR-0008). Consistent with this issue. ADR-0008 records the principles every language SDK follows; the TypeScript schema builder is the TypeScript spelling of a declaration. The napi shell links the same generated EQL dispatch as the Go guest, so the EQL decision for Go is the decision for every binding.
Background
stack-encrypt, our Rust field-level encryption library, runs inside a WebAssembly (WASI) guest for hosts that cannot or should not load a native library: Go today (languages/golang/stackencrypt/guest/, hosted by wazero), and in future the edge runtimes (Cloudflare Workers, Supabase Edge, Deno Deploy, browsers). The guest reaches ZeroKMS, our key management service, through one host import, transport_send (packages/stack-guest-abi/src/transport.rs).
Problem
transport_send is synchronous from the guest's point of view, and the ABI relies on that ("block_on never parks"). Under wazero a host function may block a goroutine, so Go is fine. In a browser, Deno, Bun or an edge worker, a WebAssembly import may not block the event loop, so the guest as built cannot run there. Asyncify, JSPI and a worker with Atomics.wait are each fragile workarounds.
Proposal
From "Where the design strains", item 2, of docs/plans/2026-10-04-plan-builder.md (PR cipherstash/stack#1052):
Make the guest sans-I/O: export the two halves of a batch. The first export builds a plan's Pending and returns the ZeroKMS requests it needs; the host performs the round trip in its own idiom (a fetch promise, a goroutine); the second export takes the responses and finishes. Pending already separates building requests from dispatching them, so this exposes an existing seam rather than adding one.
Keep the synchronous path for Go if it stays simpler there, or move Go onto the same two halves; decide in the PR.
Required before the TypeScriptwasm-inlineedge binding is built, not before. Native shells (napi-rs, PyO3) never meet this.
Relationship to other work
Blocked by #1059 (dynamic::record as a lowering, so the guest runs the builder).
Decision (2026-10-18): a step loop, not exactly two halves.
Today the guest blocks on the host's transport_send import in three places. The step loop has to cover all three:
se_init: one load-keyset round trip for the default keyset.
Resolving a named keyset: the first use is one more load-keyset.
Every encrypt and decrypt: the batched generate_keys / retrieve_keys, issued when a Pending is awaited.
token_get is not a problem: the host hands over a token it already holds.
One operation can need two round trips (a load-keyset, then generate_keys), so a fixed split into two exports does not fit. Instead, each export returns one of two answers:
done: here is the output; or
needs I/O: here are the HTTP requests (method, URL, headers, body) and a handle.
The host performs the requests in its own idiom (fetch, a goroutine), calls se_resume(handle, responses), and repeats until the answer is "done".
In the guest, the transport becomes a future that records its request and returns Poll::Pending. Each operation's future is kept in a handle table and polled again when its responses arrive. Stack Encrypt itself is unchanged: Pending already builds its requests before dispatching them, and only the guest's transport changes.
Still to decide, with these recommendations:
Go moves onto the same loop. One ABI is easier to keep correct than two, and Go's loop is a few lines. Then the Go tests exercise the path the edge runtimes will use.
This expands the decision above into a design to review before any code is written.
Background
The guest is Stack Encrypt compiled to WebAssembly (languages/golang/encrypt/guest). A host loads the guest and calls its exports, such as se_encrypt_record. The host is Go today, through the wazero runtime, and in future an edge runtime: Cloudflare Workers, Deno or a browser. The guest can't open a network connection itself. Whenever it needs ZeroKMS, our key service, it calls a function the host provides, transport_send (packages/stack-guest-abi/src/transport.rs). The host performs that HTTP request and hands back the response.
The problem
transport_send is synchronous: the guest calls it and waits until the response is back. The guest's async code relies on this. Each export runs its future with futures::executor::block_on, and the connection's comment says "block_on never parks", because the response is always already there.
The guest waits on the network in these places:
se_cipher_init loads the default keyset (one load-keyset call).
se_keyset, and the first use of a keyset named in a call's options, loads that keyset (one more load-keyset).
se_encrypt_record sends one generate_keys batch. se_decrypt_record sends one retrieve_keys batch, per keyset under {"any"}. Each batch is split into requests of 500 keys, sent one after another.
se_term and se_query_term mint no data key, but they can still hit cases 1 and 2.
One operation can need more than one round trip, for example a load-keyset and then a generate_keys.
Why Go is fine and the edge is not. In Go, a host function may block its goroutine, so transport_send simply does the HTTP call. In a JavaScript runtime, a WebAssembly import must return without waiting, and fetch only returns a promise. There is no way to block on it, so the guest as built can't run there.
There's a cost in Go too.Client.call holds the client's mutex for the whole export, network time included (languages/golang/encrypt/client.go). Every caller waits for the slowest ZeroKMS round trip. And a call cancelled mid-request is cancelled while the guest is running, which is why one cancelled call closes the shared client (#1169).
The proposal
Every export that might need the network returns one of two answers:
done: the output, exactly as today; or
needs I/O: a handle and a list of HTTP requests.
The host performs the requests however it likes. Then it calls se_resume(handle, responses), which again returns done or needs I/O. It repeats until the answer is done.
Only the transport changes. WasiHostConnection::send stops calling the import. It records its request in the operation's slot and returns Poll::Pending until se_resume fills that slot with the response.
Each export polls its future once, instead of using block_on. If the future is still pending, the guest stores it in a handle table and returns the requests it recorded.
Stack Encrypt, stack-kms and Pending don't change: they already build their requests before sending them.
Handles:
A handle is a number. It refers to one paused operation: its boxed future and the requests it's waiting on.
It lives until the operation finishes (done or failed), or until the host calls se_abort(handle).
se_shutdown drops every handle.
An unknown or finished handle is a status error. It never panics.
Several handles can be open at once, so a host can run calls concurrently.
One request or several per step:
{"any"} and the 500-key split could send several requests per step, and the list allows that.
Today stack-kms sends them one at a time (max_concurrent_reqs is pinned to 1). The first version keeps it that way: one request per step, and the list holds one entry.
Raising the limit later changes no ABI.
The messages
These are typed messages, read and written with serde over frames, the same as the control messages in #1252. No FfiValue.
// guest → hostenumStep{Done(Output),// the export's output, as todayNeedsIo{handle:u32,requests:Vec<Request>},}structRequest<'a>{method:&'astr,// "POST"url:&'astr,headers:Vec<(&'astr,&'astr)>,// includes the bearer tokenbody:&'a[u8],}// host → guest, in the order of `requests`structResponse<'a>{status:i32,// HTTP status, or negative: transport failureheaders:Vec<(&'astr,&'astr)>,body:&'a[u8],// can carry wrapped keys}
Request and Response replace the arguments and output slots of transport_send, field for field. The negative-status rule is unchanged, so map_response stays the same.
Errors, cancellation and secrets
A failed request is a response with a negative status, as today. The guest maps it to STATUS_KMS_TRANSPORT and the operation ends.
Key material waiting between steps: a paused future may hold generate_keys results, a keyset's keys or a request body. All of these are already wiped on drop: Zeroizing, and the protected types in stack-kms. Dropping a handle (done, aborted or at shutdown) wipes them, and the leak tests extend to an aborted handle.
Buffers: request bodies and the host's response buffers go through the existing registry and are wiped by se_dealloc, as today.
Auth
token_get stays as an import. The host hands over a token it already holds, and that never waits. One host-side change goes with it: on the edge, refreshing a token is itself a fetch, so the host refreshes before calling into the guest, and again before se_resume if the token is close to expiring. Go's auth.Strategy already refreshes before handing a token over.
The credential guest (languages/golang/auth) makes its own refresh requests over the same transport_send. It has the same edge problem, and it can get the same loop later, separately.
Go and the edge
Go moves onto the loop (decided above), and Go users notice nothing: Client drives the loop inside each method. Two improvements follow:
the client's mutex is held only while the guest runs, not across the network, so concurrent callers stop queueing behind each other's I/O;
the hermetic Go tests exercise the exact path the edge will use.
A JavaScript host is the same loop with await fetch(..) in place of http.Do. That is the wasm-inline edge binding's job, and it isn't part of this issue.
the guest's polled transport and handle table, and se_resume and se_abort;
the Go loop;
delete the transport_send import from the encrypt guest.
Alternatives considered
Asyncify (Binaryen) or JSPI (JavaScript Promise Integration). These let a WebAssembly import wait on a promise. Asyncify rewrites the whole module: it gets bigger and slower, and the rewrite must be redone on every build. JSPI is a newer browser and V8 feature that wazero doesn't have, so Go and the edge would end up on different code paths.
A worker thread blocked with Atomics.wait. This needs shared memory and a second thread, and Cloudflare Workers allow neither.
Async host imports. WebAssembly imports have no async form outside the component model, and wazero doesn't support that.
Exactly two exports per operation, "prepare" then "finish" (this issue's original proposal). It breaks when one operation needs two round trips, such as a keyset load and then a key batch. Splitting at every await point by hand would double the export surface and repeat the control flow the futures already express.
Open questions
Should se_cipher_init and se_keyset use the loop too, or keep a separate synchronous path? I recommend the loop everywhere, so the guest has one transport.
Should we allow several requests per step now, which would speed up {"any"} across keysets? Or keep one request per step and leave that for later?
Should a host be allowed to drive two handles at once on one instance? If yes, Go's mutex covers each step, not each operation, and the guest's caches (keysets, the endpoint) must stay correct while two operations are paused.
Should the edge's token refresh stay on the host, or become one of the loop's requests (a "needs a token" step)? I recommend keeping it on the host.
Should the credential guest's refresh requests get the same loop in this issue, or in a separate one?
Decisions, corrections and risks for the step loop
A second, independent review checked the design comment above against the code on next. This records what was decided and what that design got wrong or left out. Where this and the comment above disagree, this comment wins.
Decisions on the open questions
Cipher init and keyset loading use the loop too.se_cipher_init and se_keyset already reach ZeroKMS through the same transport as every other export, and the edge cannot run cipher init without the loop. So there is one transport, and the old transport_send import is retired. Two things to add:
Cipher init needs an "in progress" state. Today a second se_cipher_init is refused only once a cipher exists, which would not be true while an init handle is paused.
A paused init holds the client key until it finishes or is aborted.
The interface carries a list of requests, but a step sends one for now. Allowing several requests in one step later needs no change to the interface. This will not speed up {"any"} on its own: decrypt fetches each keyset's keys one after another in Stack Encrypt (pending.rs). Fetching them at the same time is a separate Stack Encrypt change, to be filed on its own.
A host may have several handles open at once. Without this, the Go client would still hold its lock across the network. Go takes its lock for each step, not for the whole operation. The encrypt guest's caches already allow this. Each lookup releases its lock before the round trip, the endpoint is set once, and nothing holds a lock across an .await. The rules:
There is a hard limit on open handles. Past it, a call fails with STATUS_STATE, so a host that forgets se_abort cannot grow the table without limit.
Handle ids only ever increase and are never reused, so a stale se_resume cannot reach another operation.
The guest keeps a single last-error slot. A host reads a failed step's error before it steps any other handle.
se_shutdown drops every open handle before the cipher and before the buffers are wiped.
Token refresh stays on the host, in Go as well as on the edge. Before each step, the host holds a fresh token, and token_get only hands it over. (Correction 1 below explains why Go needs a change for this.) The refresh margin must be longer than the longest operation. A token refreshed between steps only applies to requests the guest builds after it. A request already handed out carries the token it was built with.
The credential guest gets the loop in a separate issue, at low priority. It is not needed for the edge: an edge host already gets tokens from @cipherstash/auth/wasm-inline, which runs over fetch. Its loop is also harder. It waits on an async lock during refresh, borrows its strategy table for the whole call, and the Go host holds a file lock across a refresh. Its part of the Go cancellation problem is fixed by fetching the token before each step, not by the loop.
Corrections to the design above
In Go, token_get can wait on the network. The design says it "never waits". In Go it calls Strategy.Token(ctx) (encrypt/transport.go), which can refresh through the credential guest. That makes an HTTP request while the client's lock is held, under the caller's context. The fix is decision 4: Go gets the token outside the lock, before each step, and token_get returns the token it holds.
More changes than the transport. Today each operation borrows the cipher, through a RefCell, and borrows the host's input buffers. A future kept in a handle must own its inputs and share the cipher, for example through an Rc, and every export changes to do that. The guest interface docs also change their promise. The host's plaintext now lives for the whole operation, network waits included, not just for one call.
The loop removes only half of the cancellation problem. wazero's WithCloseOnContextDone closes the module when a call starts with a context that is already done. So the host runs each step under context.WithoutCancel(ctx), as guest.Free already does. Between steps it checks ctx.Err(), and when that is set it calls se_abort. With that in place, WithCloseOnContextDone can probably be removed.
Several handles are allowed (decision 3). The design both stated this and asked it as an open question.
The export is se_cipher_init, not se_init.
Checked and correct:
every export runs its future with block_on;
a request of more than 500 keys is split, and the parts are sent one after another;
{"any"} makes one key fetch per keyset;
Client.call holds its lock across the network;
a term mints no data key but may load a keyset;
the request body, bearer header and response body are Zeroizing.
Risks the design missed
The waker. Requests of more than 500 keys go through .buffered(), which polls an inner future again only after its waker has fired. If the guest polls once with a waker that does nothing, it stalls after se_resume: still pending, with no new request. The transport must store the waker on the first poll, and se_resume must wake it before polling again. This happens even with one request at a time.
"Pending, with no request" needs a defined outcome:STATUS_INTERNAL, and the handle is dropped. The encrypt guest cannot reach it today, but the credential guest's async lock would.
Wiping across the move into a box. Moving a future that holds the client key, or a copy of the plaintext, into a box leaves the old stack copy unwiped. TestClientKeyDoesNotRemainInGuestMemory scans all of the guest's memory. Extend it to a handle paused and then aborted, and add a plaintext search, in the same style as the errorBody test, for an encrypt aborted part way.
The request buffer holds secrets: the bearer token, and a body with key contexts. A host frees it on every path, including when fetch throws (in JS, finally { se_abort; se_dealloc }). On the edge, the bytes copied into the fetch body stay in the JS heap, unwiped. The protect-ffi wasm build already has this exposure, but the Go guest does not, so document it.
Handle lifetime: this is the limit and the no-reuse rule in decision 3. A JS host that abandons a promise chain would otherwise keep plaintext and keys alive until se_shutdown.
Tests that pin today's design need rewriting, not extending:
TestImportSurfaceIsWASIPlusTransport;
the sends == 1 transport counter;
TestInterruptedCallClosesTheClient;
the reason comment next to max_concurrent_reqs(1).
The deterministic build never pauses, so the hermetic suite cannot exercise the loop. Model a test of two interleaved handles on the stub-server tests, such as TestNewClientIssuesTheLoadKeysetRequest.
Scope. Today's @cipherstash/stack/wasm-inline is a different engine: protect-ffi's wasm-bindgen build of cipherstash-client, with async exports and HTTP over fetch. The step loop is for a new Stack Encrypt edge binding.
token_get still writes into guest memory through se_alloc, so the guest interface docs keep that rule even after transport_send is retired.
Activity
coderdan commentedon Oct 18, 2026
Decision (2026-10-18): a step loop, not exactly two halves.
Today the guest blocks on the host's
transport_sendimport in three places. The step loop has to cover all three:se_init: oneload-keysetround trip for the default keyset.load-keyset.generate_keys/retrieve_keys, issued when aPendingis awaited.token_getis not a problem: the host hands over a token it already holds.One operation can need two round trips (a
load-keyset, thengenerate_keys), so a fixed split into two exports does not fit. Instead, each export returns one of two answers:The host performs the requests in its own idiom (
fetch, a goroutine), callsse_resume(handle, responses), and repeats until the answer is "done".In the guest, the transport becomes a future that records its request and returns
Poll::Pending. Each operation's future is kept in a handle table and polled again when its responses arrive. Stack Encrypt itself is unchanged:Pendingalready builds its requests before dispatching them, and only the guest's transport changes.Still to decide, with these recommendations:
dynamic#1252's typed control messages, rather than built onFfiValuefirst.coderdan commentedon Oct 18, 2026
Design: the step loop
This expands the decision above into a design to review before any code is written.
Background
The guest is Stack Encrypt compiled to WebAssembly (
languages/golang/encrypt/guest). A host loads the guest and calls its exports, such asse_encrypt_record. The host is Go today, through the wazero runtime, and in future an edge runtime: Cloudflare Workers, Deno or a browser. The guest can't open a network connection itself. Whenever it needs ZeroKMS, our key service, it calls a function the host provides,transport_send(packages/stack-guest-abi/src/transport.rs). The host performs that HTTP request and hands back the response.The problem
transport_sendis synchronous: the guest calls it and waits until the response is back. The guest's async code relies on this. Each export runs its future withfutures::executor::block_on, and the connection's comment says "block_onnever parks", because the response is always already there.The guest waits on the network in these places:
se_cipher_initloads the default keyset (oneload-keysetcall).se_keyset, and the first use of a keyset named in a call's options, loads that keyset (one moreload-keyset).se_encrypt_recordsends onegenerate_keysbatch.se_decrypt_recordsends oneretrieve_keysbatch, per keyset under{"any"}. Each batch is split into requests of 500 keys, sent one after another.se_termandse_query_termmint no data key, but they can still hit cases 1 and 2.One operation can need more than one round trip, for example a
load-keysetand then agenerate_keys.Why Go is fine and the edge is not. In Go, a host function may block its goroutine, so
transport_sendsimply does the HTTP call. In a JavaScript runtime, a WebAssembly import must return without waiting, andfetchonly returns a promise. There is no way to block on it, so the guest as built can't run there.There's a cost in Go too.
Client.callholds the client's mutex for the whole export, network time included (languages/golang/encrypt/client.go). Every caller waits for the slowest ZeroKMS round trip. And a call cancelled mid-request is cancelled while the guest is running, which is why one cancelled call closes the shared client (#1169).The proposal
Every export that might need the network returns one of two answers:
The host performs the requests however it likes. Then it calls
se_resume(handle, responses), which again returns done or needs I/O. It repeats until the answer is done.Inside the guest:
WasiHostConnection::sendstops calling the import. It records its request in the operation's slot and returnsPoll::Pendinguntilse_resumefills that slot with the response.block_on. If the future is still pending, the guest stores it in a handle table and returns the requests it recorded.Pendingdon't change: they already build their requests before sending them.Handles:
se_abort(handle).se_shutdowndrops every handle.One request or several per step:
{"any"}and the 500-key split could send several requests per step, and the list allows that.max_concurrent_reqsis pinned to 1). The first version keeps it that way: one request per step, and the list holds one entry.The messages
These are typed messages, read and written with serde over frames, the same as the control messages in #1252. No
FfiValue.RequestandResponsereplace the arguments and output slots oftransport_send, field for field. The negative-status rule is unchanged, somap_responsestays the same.Errors, cancellation and secrets
STATUS_KMS_TRANSPORTand the operation ends.se_abort(handle). That drops the future. The guest was never running during the I/O, so nothing is left half-done. This makes the Go SDK: one cancelled or timed-out call closes the shared encrypt.Client for every later caller #1169 class of bug go away rather than needing a workaround.generate_keysresults, a keyset's keys or a request body. All of these are already wiped on drop:Zeroizing, and the protected types in stack-kms. Dropping a handle (done, aborted or at shutdown) wipes them, and the leak tests extend to an aborted handle.se_dealloc, as today.Auth
token_getstays as an import. The host hands over a token it already holds, and that never waits. One host-side change goes with it: on the edge, refreshing a token is itself afetch, so the host refreshes before calling into the guest, and again beforese_resumeif the token is close to expiring. Go'sauth.Strategyalready refreshes before handing a token over.The credential guest (
languages/golang/auth) makes its own refresh requests over the sametransport_send. It has the same edge problem, and it can get the same loop later, separately.Go and the edge
Go moves onto the loop (decided above), and Go users notice nothing:
Clientdrives the loop inside each method. Two improvements follow:A JavaScript host is the same loop with
await fetch(..)in place ofhttp.Do. That is thewasm-inlineedge binding's job, and it isn't part of this issue.Order of work
dynamic#1252's typed messages, soStep,RequestandResponseare typed from the start.next. The rework changes only how requests travel, and the fixture proves stored bytes don't move.se_resumeandse_abort;transport_sendimport from the encrypt guest.Alternatives considered
Atomics.wait. This needs shared memory and a second thread, and Cloudflare Workers allow neither.Open questions
se_cipher_initandse_keysetuse the loop too, or keep a separate synchronous path? I recommend the loop everywhere, so the guest has one transport.{"any"}across keysets? Or keep one request per step and leave that for later?coderdan commentedon Oct 18, 2026
Decisions, corrections and risks for the step loop
A second, independent review checked the design comment above against the code on
next. This records what was decided and what that design got wrong or left out. Where this and the comment above disagree, this comment wins.Decisions on the open questions
se_cipher_initandse_keysetalready reach ZeroKMS through the same transport as every other export, and the edge cannot run cipher init without the loop. So there is one transport, and the oldtransport_sendimport is retired. Two things to add:se_cipher_initis refused only once a cipher exists, which would not be true while an init handle is paused.{"any"}on its own: decrypt fetches each keyset's keys one after another in Stack Encrypt (pending.rs). Fetching them at the same time is a separate Stack Encrypt change, to be filed on its own..await. The rules:STATUS_STATE, so a host that forgetsse_abortcannot grow the table without limit.se_resumecannot reach another operation.se_shutdowndrops every open handle before the cipher and before the buffers are wiped.token_getonly hands it over. (Correction 1 below explains why Go needs a change for this.) The refresh margin must be longer than the longest operation. A token refreshed between steps only applies to requests the guest builds after it. A request already handed out carries the token it was built with.@cipherstash/auth/wasm-inline, which runs overfetch. Its loop is also harder. It waits on an async lock during refresh, borrows its strategy table for the whole call, and the Go host holds a file lock across a refresh. Its part of the Go cancellation problem is fixed by fetching the token before each step, not by the loop.Corrections to the design above
token_getcan wait on the network. The design says it "never waits". In Go it callsStrategy.Token(ctx)(encrypt/transport.go), which can refresh through the credential guest. That makes an HTTP request while the client's lock is held, under the caller's context. The fix is decision 4: Go gets the token outside the lock, before each step, andtoken_getreturns the token it holds.RefCell, and borrows the host's input buffers. A future kept in a handle must own its inputs and share the cipher, for example through anRc, and every export changes to do that. The guest interface docs also change their promise. The host's plaintext now lives for the whole operation, network waits included, not just for one call.WithCloseOnContextDonecloses the module when a call starts with a context that is already done. So the host runs each step undercontext.WithoutCancel(ctx), asguest.Freealready does. Between steps it checksctx.Err(), and when that is set it callsse_abort. With that in place,WithCloseOnContextDonecan probably be removed.se_cipher_init, notse_init.Checked and correct:
block_on;{"any"}makes one key fetch per keyset;Client.callholds its lock across the network;Zeroizing.Risks the design missed
The waker. Requests of more than 500 keys go through
.buffered(), which polls an inner future again only after its waker has fired. If the guest polls once with a waker that does nothing, it stalls afterse_resume: still pending, with no new request. The transport must store the waker on the first poll, andse_resumemust wake it before polling again. This happens even with one request at a time."Pending, with no request" needs a defined outcome:
STATUS_INTERNAL, and the handle is dropped. The encrypt guest cannot reach it today, but the credential guest's async lock would.Wiping across the move into a box. Moving a future that holds the client key, or a copy of the plaintext, into a box leaves the old stack copy unwiped.
TestClientKeyDoesNotRemainInGuestMemoryscans all of the guest's memory. Extend it to a handle paused and then aborted, and add a plaintext search, in the same style as theerrorBodytest, for an encrypt aborted part way.The request buffer holds secrets: the bearer token, and a body with key contexts. A host frees it on every path, including when
fetchthrows (in JS,finally { se_abort; se_dealloc }). On the edge, the bytes copied into thefetchbody stay in the JS heap, unwiped. The protect-ffi wasm build already has this exposure, but the Go guest does not, so document it.Handle lifetime: this is the limit and the no-reuse rule in decision 3. A JS host that abandons a promise chain would otherwise keep plaintext and keys alive until
se_shutdown.Tests that pin today's design need rewriting, not extending:
TestImportSurfaceIsWASIPlusTransport;sends == 1transport counter;TestInterruptedCallClosesTheClient;max_concurrent_reqs(1).The deterministic build never pauses, so the hermetic suite cannot exercise the loop. Model a test of two interleaved handles on the stub-server tests, such as
TestNewClientIssuesTheLoadKeysetRequest.Scope. Today's
@cipherstash/stack/wasm-inlineis a different engine: protect-ffi's wasm-bindgen build ofcipherstash-client, with async exports and HTTP overfetch. The step loop is for a new Stack Encrypt edge binding.token_getstill writes into guest memory throughse_alloc, so the guest interface docs keep that rule even aftertransport_sendis retired.feat(stack-guest-abi): a step loop, so a guest operation pauses on it…
feat(golang)!: the crypto guest steps instead of blocking on transpor…
feat(golang)!: the crypto guest steps instead of blocking on transpor…
feat(stack-guest-abi): a step loop, so a guest operation pauses on it…
feat(golang)!: the crypto guest steps instead of blocking on transpor…
feat(stack-guest-abi): a step loop, so a guest operation pauses on it…
feat(golang)!: the crypto guest steps instead of blocking on transpor…
feat(stack-guest-abi): a step loop, so a guest operation pauses on it…
feat(golang)!: the crypto guest steps instead of blocking on transpor…
feat(stack-guest-abi): a step loop, so a guest operation pauses on it…
feat(golang)!: the crypto guest steps instead of blocking on transpor…
feat(stack-guest-abi): a step loop, so a guest operation pauses on it…
feat(golang)!: the crypto guest steps instead of blocking on transpor…
feat(stack-guest-abi): a step loop, so a guest operation pauses on it…
feat(golang)!: the crypto guest steps instead of blocking on transpor…
fix(golang): queue callers past the guest's paused cap, and give step…