Skip to content

feat(sdk): compose local agents with Desktop and Workstation - #222

Open
cm2435-hcomp wants to merge 44 commits into
mainfrom
charlie/placement-python-sdk
Open

cm2435-hcomp wants to merge 44 commits into
mainfrom
charlie/placement-python-sdk

Conversation

@cm2435-hcomp

@cm2435-hcomp cm2435-hcomp commented Sep 30, 2026 •

Copy link
Copy Markdown

What

Expose local agent execution through the existing Python Agent API client, composed with local Desktop or remote Workstation environments. Own the local runtime lifecycle, authenticated attachment, command interruption and session resources; keep the hosted client default unchanged. Permission prompts run on the main thread.

Why

Each product currently has to manage its own local agent process and device bridge. The SDK should own that lifecycle behind the existing session interface.

How

Add Client(mode="local"), await AsyncClient.local(), and local runtime/process ownership, authenticated attachment, session resources and interruption through existing device bridges. Execute OS permission preflight on the main thread.

Validation and dependencies

P4; depends on P1/P2 HAI contracts/drivers/runtime and the matching P3 generation overlays. Runtime manifest defaults are preserved from the existing release, so this PR MUST NOT publish or merge until a verified compatible runtime and dependency floors are pinned. Explicit candidate overrides are for QA only.

Validation: 225 non-integration SDK tests pass, 12 skipped, on current main with the review runtime sources. Prior live QA covered local and cloud environments, file round trips, follow-ups and owned-command Stop. SDK workstation work already merged in #220 is excluded from this diff.

QA scope: real local-agent/local-environment and local-agent/cloud-environment runs cover files, follow-ups, Stop and reuse. Candidate hosted-agent combinations and packaged visual parity remain unverified. HoloWork integration is deferred.

Stack navigation: P1 contract → P2 runtime; P3 generation + P4 SDK; P5 checks: runtime, generation, SDK pins; P6 CLI. P7 duplicate pin cleanup follows only after P6 ships. HoloWork integration is subsequent work.

Review guide: intent, architecture, service tradeoffs and merge order.

CI dependency constraint: the published hai-drivers dependency lacks DesktopCommandRunner, so the real SDK/driver Stop regression fails collection in hosted CI until the compatible driver is available. It passes with candidate drivers locally; it has not been skipped to hide the dependency. Changed Python files pass Ruff lint and format.

Lifecycle refinements from review: downloads run outside the per-port spawn lock; idle probes close their HTTP pools; failed cancellation retains the exit retry; startup cleanup preserves the original error; credential cleanup uses the startup lock; asynchronous startup has an awaited, cancellation-safe factory. Regression tests exercise these boundaries.


Note

High Risk
Large new surface area: process spawn, verified binary downloads, loopback auth/HMAC, and session/bridge lifecycle that affects local desktop/workstation execution and release gating.

Overview
Adds local agent execution on top of the existing session API via Client.local() / await AsyncClient.local(), while the default Client() still targets the hosted Agents API.

A new hai_agents_local.runtime layer starts or attaches to loopback hai-agent-runtime (PATH, explicit binary, or sha256-verified download from pin.json), enforces the shared recipe, and uses HMAC challenge/proof HTTP clients so traffic cannot be served by a squatter on the port. Optional Inference routes model calls to cloud (HAI_API_KEY) or a self-hosted endpoint without leaking hosted keys. Context managers close() / aclose() cancel client-owned sessions, stop bridges, and shutdown_if_idle() owned runtimes when no other active sessions remain.

Local sessions wire bridges to the runtime token with verify_runtime, run macOS permission preflight on the main thread before async bridge startup, and improve stop/cancel (driver interrupt on bridge stop, HTTP 410 channel closure, unconfirmed bridge stops, exit-time cancel retries). Release workflow gains a runtime-pin macOS job and scripts/bump_runtime.py to bump pinned runtime digests; README documents the local-agent flow.

Reviewed by Cursor Bugbot for commit f4659fd. Bugbot is set up for automated code reviews on this repo. Configure here.

@cm2435-hcomp

Copy link
Copy Markdown
Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/sessions.py Outdated
Comment thread src/hai_agents/client.py Outdated
Comment thread src/hai_agents_local/manager.py
Comment thread src/hai_agents_local/runtime/state.py
@cm2435-hcomp

Copy link
Copy Markdown
Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/manager.py

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents/client.py
Comment thread src/hai_agents_local/runtime/runtime.py
…t.local()

Hand-written runtime management lives outside the generated package, so a
codegen sync cannot wipe it. Local clients are built with Client.local() and
AsyncClient.local(), keeping the generated constructors and their typing.
HTTP(S)_PROXY no longer receives the local runtime bearer token.
Every request to a local runtime carries a fresh X-Hai-Runtime-Challenge, and
every response must carry X-Hai-Runtime-Proof, the HMAC-SHA256 of the
challenge keyed by the runtime token. Unproven responses raise
LocalRuntimeError, so a server squatting the runtime port never receives the
bearer token and its commands never reach a device bridge.

/health is proven before any bearer-authenticated request, both when
spawning and when attaching.
Closing the client that started the runtime terminated it for every
attached client. close() now stops the runtime only when no session other
than this client's own is still active; shutdown() still stops it
unconditionally.
…to charlie/placement-sdk-pins

# Conflicts:
#	src/hai_agents_local/runtime/manifest.py

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/runtime/runtime.py Outdated
Comment thread src/hai_agents_local/runtime/runtime.py
Comment thread src/hai_agents_local/transport.py Outdated
@abonneth

abonneth commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Pushed review fixes directly (as agreed).

Commit What Why
d21f77f Local code moves to hai_agents_local/runtime/; new Client.local() / await AsyncClient.local() The generated package is overwritten on every sync. Hosted Client(...) keeps its typed constructor.
0f5f9a4 Loopback HTTP uses trust_env=False Proxy env vars could route the local bearer through a proxy.
547d2a3 Every request sends a challenge; responses must carry an HMAC proof from the runtime's token Any process on the port could collect the bearer and feed commands to the bridges.
1446f8e Token and pid files are written atomically, only after the child proves the port A failed spawn could overwrite a live runtime's token.
8e9b38f HTTP 410 is a clean bridge stop Every normal session end was logged as a crash and sent a second cancel.
f738f10 cancel_session(id, *, request_options) matches the generated signature; close() only cancels live sessions, and 404/409 count as stopped cancel_session(id=...) raised TypeError. close() raised once old sessions were evicted.
d02c2de close() stops an owned runtime only when no other client's session is active One client closing killed sessions of other clients attached to the same runtime.
1d6640b 15 s SIGTERM grace before SIGKILL The runtime needs up to 10 s to release environments on shutdown.
c2c9a96 Publish blocks unless the pinned runtime downloads, verifies and serves the shared recipe 0.1.8 has no shared recipe, so a release today would ship a broken Client.local().

Tests: 228 passed, 12 skipped. The release gate was checked both ways: it fails on the 0.1.8 pin and passes against a runtime that serves the shared recipe.

Needs a runtime release with identity proofs plus a pin bump before this can ship.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/runtime/runtime.py
Comment thread src/hai_agents_local/sessions.py
@abonneth

abonneth commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Folded #223 into this PR (fast-forward, no other changes). Merge order unchanged.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/runtime/runtime.py

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/hai_agents_local/runtime/runtime.py
@abonneth

abonneth commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Simplification pass (6 commits, net -139 lines, no behavior change)

Change Why Lines
Drop LocalRuntime.force_kill() / health() No callers; pid file kept (still read by stop tooling) -20
README: local section = usage + 1 inference line Internal/volatile details drift -40
verify_runtime set once in sessions._localize (class default on bridge) Was threaded through 7 signatures -13
Runtime pin moved to runtime/pin.json; bump script = load, validate, dump No regex rewriting of Python source; same validation (partial bump, unknown platform, placeholder sha) -31
Shared base for sync/async local sessions (state, close-failure handling) Only the awaits differ 0
One-line docstrings, drop narrating comments Readability -35

Tests

  • pytest: exit 0, 247 (was 245; +2 new bump-script cases), same 13 skips
  • ruff check + format --check clean on changed files
  • Wheel build: pin.json ships; manifest imports from an installed wheel with identical URLs
  • client.py unchanged

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f4659fd. Configure here.

def _claim(runtime: LocalRuntime, inference: typing.Optional[Inference]) -> bool:
if inference is not None and not runtime.owned:
raise ValueError("inference selection cannot reconfigure an existing runtime; choose a free local port")
return runtime.owned

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attached clients never stop leftover runtime

High Severity

Client.local() only marks the original spawner as owner. If that client closes while another attached client still has sessions, shutdown_if_idle leaves the process running, and later attachers never claim ownership. No later close() stops the child, so the runtime and its credential files stay up until a manual kill.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f4659fd. Configure here.

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.

2 participants