Skip to content

proxy: the ask callback is told which invocation asked - #735

Open
ken-jo wants to merge 1 commit into
anthropics:mainfrom
ken-jo:feat/ask-callback-attribution
Open

ken-jo wants to merge 1 commit into
anthropics:mainfrom
ken-jo:feat/ask-callback-attribution

Conversation

@ken-jo

@ken-jo ken-jo commented Oct 16, 2026

Copy link
Copy Markdown

Fixes #734.

What

filterNetworkRequest passes a second argument to the ask callback:

const attributionKey = encodedCommand
  ? decodeSandboxedCommand(encodedCommand)
  : undefined
// … per-command lists are looked up by the same key …
const answer: unknown = await sandboxAskCallback(
  { host, port },
  { attributionKey },
)
  • SandboxAskContext = { attributionKey: string | undefined } is new in
    sandbox-schemas.ts and exported from the package root.
  • SandboxAskCallback gains context?: SandboxAskContext. The library always
    passes it. It is optional so that a one-argument callback, or code that
    calls a callback directly, still type-checks.
  • The per-command list lookup now reuses the decoded key instead of decoding
    it inline. Its behaviour does not change.
  • README: a paragraph and an example under the ask-callback section.

Why this value

It is the value the proxy already treats as "which invocation". It is the key
the violation store records the denial under (encodedCommand, decoded) and
the key registerCommandNetworkLists is looked up by. An embedder can use
one id for all three. It is cut to 100 characters, as every carrier cuts it.

The key is passed even when no wrap in this process registered it. The
callback does not go through resolveCommandText, for two reasons:

  • commandTextsByKey is a bounded FIFO, so a long-running command's entry can
    age out while it still makes connections.
  • The embedder needs the id it issued, not the command text.

Because the key is the client's choice, the type doc and the README call it a
label, not a credential, as #591 does for the per-command lists.

Compatibility

  • Existing callbacks are unaffected. JavaScript ignores the extra argument,
    and TypeScript accepts a one-parameter function for a two-parameter type.
  • On a release without this change the callback is called with one argument,
    so context is undefined there. Unlike askCallbackDenyReason, an
    embedder can detect this from the call itself, so no feature flag is added.
    A flag can be added if you would rather have one.
  • No behaviour change for anyone who does not read the argument. The decision
    order and the strict-allowlist path are untouched.

Tests

New describe('ask callback attribution') in
test/sandbox/update-config.test.ts, against a real proxy and a loopback
listener:

  • the callback sees the commandId the username names (HTTP CONNECT);
  • it sees the same id over SOCKS5;
  • the bare srt username gives attributionKey: undefined;
  • the key equals the decoded encodedCommand of the recorded violation;
  • a key no wrap registered is passed as the bytes it decodes to;
  • a 128-character id arrives cut to 100 characters;
  • a host a per-command list already allows never reaches the callback.

Six of the seven fail without the change. The seventh guards the decision
order.

bun test test/sandbox/update-config.test.ts test/public-exports.test.ts:
100 pass. npm run typecheck, eslint and prettier --check are clean on the
changed files.

Related: #591 (per-command lists and deny reasons), #722 (another
caller-supplied per-wrap option, for the pty a command is spawned on).

An embedder that runs several commands at once through the one
process-wide proxy cannot tell, inside SandboxAskCallback, which of them
opened the connection it is asked about. The proxy already knows: the
username carries the invocation's attribution key, and the same function
hands it to recordOutboundDeny and to the per-command list lookup. Without
it the embedder cannot ask on behalf of the right command, and cannot
answer differently for different kinds of process sharing the proxy.

The callback now receives a second argument, { attributionKey }: the
decoded key, cut to 100 characters like every carrier cuts it, or
undefined when the username names none. It is the key the violation store
records a denial under and registerCommandNetworkLists is looked up by.
The key is chosen by the sandboxed client, so it is documented as a label
to compare with ids the embedder issued, not a credential. It is passed
whether or not this process wrapped a command under it, so a long-running
command's key does not depend on the bounded command-text registry.

The parameter is optional in the type, so an existing one-argument callback
and code that invokes a callback directly both still type-check. A release
without this change calls the callback with one argument, which an embedder
can detect as context === undefined.

This branch has not been deployed

No deployments
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.

The ask callback cannot tell which invocation opened the connection

1 participant