Skip to content

fix(platform): run agent connector calls in process, bounded - #4544

Merged
yannickmonney merged 9 commits into
mainfrom
fix/connector-bridge-in-process
Oct 8, 2026
Merged

yannickmonney merged 9 commits into
mainfrom
fix/connector-bridge-in-process

Conversation

@yannickmonney

Copy link
Copy Markdown
Contributor

An agent's connector call through the sandbox bridge ran the connector's body as node -e <program> inside the agent's own session, with the organization's connector secrets embedded in the program — readable by the agent (same uid) through /proc/<pid>/cmdline or ps while the call ran — plus a Node cold start (~40 ms, ~48 MB) and a runnerd live-exec slot per call.

Changes

fix(platform): run agent connector calls in process, not in the sandbox

The in-sandbox connectors bridge passed the turn's own session as
execSessionId, so each live yaml-js body ran as node -e <program> in
the agent's session. The program embeds the body's scope as a string
literal, credential.secrets included: while the call ran, the org's raw
connector key sat on a command line any process of that session (the
agent, same uid) could read through /proc//cmdline or ps. Each
call also paid a Node cold start (~40 ms, ~48 MB), took one of the
session's four live-exec places (parallel tool calls then failed with
"sandbox produced no result"), and added ~150-250 ms.

The bridge now dispatches without a session, so the body runs on the
in-process live runner that automation runs and chat already use: the
shipped catalog is the only code it runs (organizations cannot upload
connector bodies), ctx.http is the same policed live host, and the
credential stays in the platform process. The connectors spec states it
as CONN-R13, held by the bridge route test.

No caller passes execSessionId any more. The session-bound path it
reaches (the sandbox-exec runner, engine_exec_runner, portable-live,
the hostcall token and route, and the device relay's allowance for that
route in the sandbox service) is left in place: removing it spans the
platform's engine, routes and env and the sandbox service, which is not
a self-contained change.

fix(platform): bound agent connector calls that run in process

Moving agent bridge calls onto the in-process live runner lost two bounds
the sandbox gave them: four live execs per session, and a kill when a
body overran its deadline. In the shared backend process the deadline
only released the caller; the body went on calling its vendor, and an
agent retrying a slow call (a recursive Drive listing on a large account)
started another body each time. Bridge calls also gained ctx.files, so an
agent's read of an attachment stored blobs and file records in the
organization's store, which the sandbox path never did.

  • A yaml-js body's host gets a signal that aborts at the body's time
    limit, so every ctx.http and ctx.files request after it rejects and the
    body unwinds at its next host call (a test shows 9 vendor requests
    after the limit without it, none with it).
  • The bridge runs at most four of a session's calls at once per backend
    process; a further call gets a busy refusal the agent can retry.
  • Bridge calls run without a file store, as they always did.
  • runConnectorAction no longer accepts a session to run a body in; the
    now unused session-bound machinery is recorded in the contract debt
    ledger, and comments that described it as live are corrected.

CONN-R14 states the bounds; a service-level test holds the runner choice
(CONN-R13) with a yaml-js example instead of a native connector.

Verification

  • vitest: 31 files / 555 tests across lib/connectors, backend/domains/connectors, backend/core/connectors, the bridge and connector-caller suites, and the domain-spec guard.
  • The new deadline test fails without the fix (9 vendor requests after the limit) and passes with it (none).
  • @tale/platform typecheck clean; type-aware oxlint clean on the touched files.
  • Adversarial review: verdict fix-then-ready; its major finding (unbounded in-process bodies) and minors (file store side effect, dead session path, GlitchTip example) are addressed in the second commit.

Part of the sandbox footprint and robustness pass (#4533, #4538 and siblings).

The in-sandbox connectors bridge passed the turn's own session as
execSessionId, so each live yaml-js body ran as `node -e <program>` in
the agent's session. The program embeds the body's scope as a string
literal, credential.secrets included: while the call ran, the org's raw
connector key sat on a command line any process of that session (the
agent, same uid) could read through /proc/<pid>/cmdline or ps. Each
call also paid a Node cold start (~40 ms, ~48 MB), took one of the
session's four live-exec places (parallel tool calls then failed with
"sandbox produced no result"), and added ~150-250 ms.

The bridge now dispatches without a session, so the body runs on the
in-process live runner that automation runs and chat already use: the
shipped catalog is the only code it runs (organizations cannot upload
connector bodies), ctx.http is the same policed live host, and the
credential stays in the platform process. The connectors spec states it
as CONN-R13, held by the bridge route test.

No caller passes execSessionId any more. The session-bound path it
reaches (the sandbox-exec runner, engine_exec_runner, portable-live,
the hostcall token and route, and the device relay's allowance for that
route in the sandbox service) is left in place: removing it spans the
platform's engine, routes and env and the sandbox service, which is not
a self-contained change.
Moving agent bridge calls onto the in-process live runner lost two bounds
the sandbox gave them: four live execs per session, and a kill when a
body overran its deadline. In the shared backend process the deadline
only released the caller; the body went on calling its vendor, and an
agent retrying a slow call (a recursive Drive listing on a large account)
started another body each time. Bridge calls also gained ctx.files, so an
agent's read of an attachment stored blobs and file records in the
organization's store, which the sandbox path never did.

- A yaml-js body's host gets a signal that aborts at the body's time
  limit, so every ctx.http and ctx.files request after it rejects and the
  body unwinds at its next host call (a test shows 9 vendor requests
  after the limit without it, none with it).
- The bridge runs at most four of a session's calls at once per backend
  process; a further call gets a busy refusal the agent can retry.
- Bridge calls run without a file store, as they always did.
- runConnectorAction no longer accepts a session to run a body in; the
  now unused session-bound machinery is recorded in the contract debt
  ledger, and comments that described it as live are corrected.

CONN-R14 states the bounds; a service-level test holds the runner choice
(CONN-R13) with a yaml-js example instead of a native connector.
…t refuses

The host signal and the caller's deadline fired on the same tick, so a
caller could be told 'request aborted' by the body's next host call
instead of the body's time-limit error. Host calls now start refusing
100 ms past the limit.
@yannickmonney
yannickmonney force-pushed the fix/connector-bridge-in-process branch from 9f1380c to 0964095 Compare October 8, 2026 12:45
codeRunnerForSession lost its last caller when connector bodies moved in
process; the program runner beneath it stays with the rest of the
session-bound path listed in the contract debt ledger.
…in-process

# Conflicts:
#	services/platform/tests/manual/reference/automation.md
@yannickmonney
yannickmonney merged commit b320da6 into main Oct 8, 2026
82 checks passed
@yannickmonney
yannickmonney deleted the fix/connector-bridge-in-process branch October 8, 2026 20:35
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