Skip to content

[AAASM-1847] ✨ (core): Add gateway URL / apiKey resolver + local auto-start#47

Merged
Chisanan232 merged 20 commits into
masterfrom
v0.0.1/AAASM-1847/feat/sdk_auto_detect_local_gateway
May 23, 2026
Merged

[AAASM-1847] ✨ (core): Add gateway URL / apiKey resolver + local auto-start#47
Chisanan232 merged 20 commits into
masterfrom
v0.0.1/AAASM-1847/feat/sdk_auto_detect_local_gateway

Conversation

@Chisanan232
Copy link
Copy Markdown
Contributor

@Chisanan232 Chisanan232 commented May 22, 2026

Target

  • Task summary:

    Add zero-config gateway resolution to the Node SDK so initAssembly() with no arguments connects to a local gateway at http://localhost:7391, auto-starting aasm start --mode local --foreground if nothing is listening. Mirrors the Python SDK contract shipped in python-sdk #58 (AAASM-1846).

  • Task tickets:

  • Key point change:

    Resolution order

    1. Explicit field — initAssembly({ gatewayUrl: "...", apiKey: "..." })
    2. Environment variable — AAASM_GATEWAY_URL, AAASM_API_KEY
    3. Config file — ~/.aasm/config.yaml (soft js-yaml dependency)
    4. Local default — probe http://localhost:7391/healthz; if unreachable, spawn aasm start --mode local --foreground detached and wait up to 5s

Effecting Scope

  • Action Types:
    • ✨ Adding new something
      • 🟢 No breaking change (additive — existing callers passing both gatewayUrl and apiKey are unaffected, regression-tested)
    • ✏️ Modifying existing something
      • 🟠 Has breaking change (minor: gatewayUrl and apiKey on AssemblyConfig become optional; the config arg itself defaults to {})
  • Scopes:
    • 🧩 SDK public API — initAssembly() callable with no arguments; AssemblyConfig.gatewayUrl and apiKey now optional
    • ⛑️ Error handling — adds ConfigurationError and GatewayError, exported from src/errors
    • 🧪 Testing
      • 🧪 Unit testing — 28 new tests across tests/gateway-resolver.test.ts and tests/init-assembly-zero-config.test.ts
    • 🚀 Building
      • 🔗 Dependencies — adds js-yaml + @types/js-yaml as devDependencies only; runtime still soft-imports

Description

New files

  • src/core/gateway-resolver.ts — public resolveGatewayUrl, resolveApiKey, probeHealthz, waitForHealthz, loadConfigFile, autoStartGateway; private _seams exposed via __testing so unit tests can stub findAasmOnPath, spawnAasm, probeHealthz, loadConfigFile, and autoStartGateway without ESM mocking gymnastics
  • src/errors/configuration-error.ts, src/errors/gateway-error.ts — mirror the Python SDK exception contract
  • tests/gateway-resolver.test.ts — 25 unit tests (probe / wait / config-file / auto-start / 5-step precedence × 2 resolvers)
  • tests/init-assembly-zero-config.test.ts — 3 integration tests (zero-arg success, auto-start trigger, explicit-args regression)

Edited files

  • src/core/init-assembly.tsinitAssembly() accepts no args; resolves via resolveGatewayUrl / resolveApiKey; passes resolved values to createNativeClient
  • src/types/assembly-config.tsgatewayUrl / apiKey optional with explanatory JSDoc
  • src/errors/index.ts — re-exports the two new error classes

Test result

$ pnpm test --run
Test Files  37 passed | 1 skipped (38)
     Tests  171 passed | 2 skipped (173)
$ pnpm typecheck    # clean
$ pnpm lint         # clean

All subprocess (child_process.spawn) and network (globalThis.fetch) calls mocked in unit tests via the __testing._seams handle — no real aasm spawn, no real network.

Notes

  • Story description named src/core/init.ts — actual entry is src/core/init-assembly.ts; targeted the real module.
  • Scope expansion (approved): apiKey also became optional with the same precedence chain — necessary for the AC "no fields, no env vars → connects".
  • js-yaml is a soft (dynamic import()) runtime dep; added to devDependencies only so the parsing branch is exercised in tests. Consumers without it fall through cleanly to the local-default step.

Mirrors the Python SDK's exception contract for the resolver chain
landing in this Sub-task (AAASM-1847 / E17 S-G). ConfigurationError
signals "cannot resolve gateway config" (e.g. aasm absent from PATH);
GatewayError signals "gateway present but unreachable / not ready".
Both extend Error directly — node-sdk has no AssemblyError base yet.
Makes the two new error classes consumable by SDK callers via
``import { ConfigurationError } from "@agent-assembly/sdk/errors"``.
Lays down the new module that will host the zero-config resolution
logic for initAssembly (AAASM-1847 / E17 S-G). Exports the default
gateway URL, healthz path, probe / auto-start timeouts (ms — Node
convention), env-var names, and the auto-start argv tuple. No behavior
yet.
Async global-fetch GET against ``{baseUrl}/healthz`` with an
AbortController-driven default 500ms timeout. Any network / timeout
error is swallowed and surfaces as false — keeps the local-dev probe
near-instant when nothing is listening.
Three behaviors: 2xx → true (also pins the /healthz suffix), fetch
reject → false, non-2xx → false (table-driven across 400/404/500/503).
globalThis.fetch is stubbed via vi.fn — no real network.
Polls the healthz endpoint until success or timeout. Used after
autoStartGateway to know when the freshly-spawned local CP is ready
to accept connections. Default 5000ms budget per Story AC; final
re-probe after the deadline ensures borderline races resolve cleanly.
Three behaviors: success on first probe, success after two prior
fetch rejections (verifies the poll-then-sleep loop body), and false
when the timeout elapses with no success. Stubs globalThis.fetch
rather than spyOn the same-module probeHealthz export — ESM named
exports aren't mutable from spyOn.
Reads ~/.aasm/config.yaml when present. js-yaml is treated as a soft
dependency via dynamic import() — missing module returns an empty
record so the resolver falls through to the local-default step.
File-missing, OS errors, parse errors, and non-object payloads all
collapse to the same empty result. Tilde-expansion via os.homedir().
Required for the loadConfigFile happy-path test in the upcoming
commit. js-yaml stays a soft (dynamic-import) dependency at runtime —
SDK consumers without it will simply skip the YAML config-file step.
The dev pin lets the test suite exercise the parsing branch.
Four behaviors: missing file → {}, well-formed YAML → parsed mapping,
non-mapping root (top-level list) → {}, malformed YAML → {}. Uses
mkdtempSync for an isolated tmp dir; no real ~/.aasm/ touched.
Defines the two private primitives that autoStartGateway will compose:
defaultFindAasmOnPath walks process.env.PATH (with .exe/.cmd suffixes
on Windows) and defaultSpawnAasm launches the binary detached with
stdio ignored — the docker-style daemon hand-off. Both are routed
through a mutable _seams object exposed via ``__testing`` so tests
can stub them without ESM module-mocking gymnastics.
Composes the seams: findAasmOnPath → ConfigurationError when missing,
spawnAasm to launch detached, waitForHealthz to confirm readiness or
raise GatewayError after the configured timeout. Matches the Python
SDK's _auto_start_gateway contract semantically.
Three behaviors: aasm-missing → ConfigurationError with install hint,
spawn succeeds + healthz ready → resolves and the path is passed to
the spawn seam, spawn succeeds but timeout elapses → GatewayError.
Stubs done via the __testing._seams handle plus fetch — no real
subprocess, no real network.
Implements the 4-step precedence chain (explicit > AAASM_GATEWAY_URL >
~/.aasm/config.yaml agent.gateway_url > local default with probe +
auto-start). probeHealthz / loadConfigFile / autoStartGateway are now
routed through ``_seams`` too so unit tests can stub each step
independently without spawning real aasm or hitting the network.
Five tests exercising the 4-step chain: explicit > env > config >
local-default; the local-default branch is split into probe-hit (no
auto-start) and probe-miss (auto-start invoked with the canonical
URL). __testing._seams handles cross-step stubbing without touching
the real network or PATH.
Mirrors resolveGatewayUrl's 4-step precedence for apiKey: explicit
field → AAASM_API_KEY env → config-file agent.api_key → empty default.
No auto-start path — local mode is unauth-accepting per the Epic, so
the empty fallback is the documented default rather than an error.
Four tests mirroring resolveGatewayUrl's chain: explicit > env >
config > empty-default. No rejection on missing — empty string is the
documented local-mode default.
gatewayUrl and apiKey are now optional on AssemblyConfig and the
config arg itself defaults to {}, so initAssembly() with no
arguments works per the Story AC. The body calls resolveGatewayUrl /
resolveApiKey to fill the gaps via the 4-step precedence chain.
Bundled with the type change to keep this commit bisectable — type
relaxation alone left init-assembly.ts uncompilable.
Two scenarios: probe-hit (no auto-start invoked, ctx returned bound
to localhost default) and probe-miss (autoStartGateway invoked with
the canonical URL). Uses __testing._seams to short-circuit the
resolver and skip any real subprocess or network activity.
Story AC: existing callers passing both gatewayUrl and apiKey must be
unaffected by the resolver path. Stubs probeHealthz and
autoStartGateway with sentinels that throw if invoked — proves the
resolver short-circuits on explicit args.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

❌ Patch coverage is 84.66258% with 25 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/gateway-resolver.ts 82.01% 25 Missing ⚠️

📢 Thoughts on this report? Let us know!

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot

See analysis details on SonarQube Cloud

@Chisanan232
Copy link
Copy Markdown
Contributor Author

Chisanan232 commented May 23, 2026

🤖 Claude Code review record — AAASM-1847 ST-2

Reviewer: Claude Code (Opus 4.7, 1M context)
Review date: 2026-05-23
Branch head: v0.0.1/AAASM-1847/feat/sdk_auto_detect_local_gateway

CI status

Check group Result
test-matrix (Node 18 / 20 / 22 / 24 × ubuntu / macos / windows — 12 jobs) ✅ all 12 green
napi-build (ubuntu / macos / windows × Node 20 / 22 — 6 jobs) ✅ all 6 green
module-system-smoke (Node 18 / 20 / 22)
quality (pre-commit checks)
coverage-and-analysis
publish-docs build
codecov/patch
SonarCloud Code Analysis ⚠️ Quality Gate Failed — 1 Security Hotspot (the child_process.spawn(aasmPath, …) line in defaultSpawnAasm; aasmPath is the absolute path that came back from defaultFindAasmOnPath after walking process.env.PATH + existsSync validation, so the hotspot is a false positive — flagged because the spawn argument is a variable rather than a literal)
mergeStateStatus UNSTABLE (sonar-only)

24 of 25 checks green. Per project policy "test coverage / SonarQube failures are ignorable until acceptance signs off", treating the sonar hotspot as non-blocking.

Scope coverage vs ticket

Cross-checked PR diff against AAASM-1847 description:

  • src/core/gateway-resolver.ts (+244) — all six helpers (probeHealthz, waitForHealthz, loadConfigFile, defaultFindAasmOnPath + defaultSpawnAasm, autoStartGateway, resolveGatewayUrl, resolveApiKey)
  • src/core/init-assembly.ts edited — initAssembly() accepts no args; calls resolvers
  • src/types/assembly-config.tsgatewayUrl / apiKey optional with JSDoc
  • src/errors/configuration-error.ts + gateway-error.ts + barrel re-export
  • Tests in tests/gateway-resolver.test.ts (25) + tests/init-assembly-zero-config.test.ts (3) — flat layout matches existing node-sdk convention, not tests/unit/core/ as the ticket spec proposed
  • js-yaml + @types/js-yaml added as devDependencies only; runtime still soft-imports via dynamic import() and falls through cleanly when absent
  • __testing._seams handle exposed so unit tests stub PATH lookup + spawn without ESM module-mocking gymnastics

Acceptance criteria coverage

AC Status Evidence
initAssembly({}) no-args connects to local default tests/init-assembly-zero-config.test.ts > AAASM-1847 AC: initAssembly() with no args resolves the local default
AAASM_GATEWAY_URL env override tests/gateway-resolver.test.ts > resolveGatewayUrl > uses AAASM_GATEWAY_URL over config + default
aasm on PATH + probe miss → auto-start + connect spawns aasm and resolves when healthz becomes ready (pins argv + detached:true + unref())
aasm absent + probe miss → ConfigurationError with install hint throws ConfigurationError when aasm is not on PATH
5s auto-start timeout → GatewayError throws GatewayError when the spawned gateway never becomes ready
Existing callers unaffected explicit gatewayUrl + apiKey bypass the resolver entirely (sentinel stubs throw if invoked)
Unit tests mock spawn + HTTP vi.fn() stubs on globalThis.fetch + __testing._seams.spawnAasm throughout

End-to-end smoke (run 2026-05-23 against aa-gateway --mode local)

  • ✅ Probe-hit: initAssembly({ mode: 'sdk-only' }) → context with activeAdapters: ['langchain-js', 'openai-agents']
  • ✅ Aasm-missing: throws ConfigurationError: No gateway found at http://localhost:7391 and 'aasm' is not on PATH. Install it with: npm install -g @agent-assembly/cli (or pnpm add -g)

Smoke output captured in agent-assembly #727.

Verdict

Approved for merge. CI green except the ignorable sonar hotspot (false positive on validated-path spawn). Scope complete, all 7 ACs covered with proof tests, end-to-end smoke passes. Behaviour cross-checks identical with the Python and Go sibling PRs.

@Chisanan232 Chisanan232 merged commit 34172a7 into master May 23, 2026
25 of 26 checks passed
@Chisanan232 Chisanan232 deleted the v0.0.1/AAASM-1847/feat/sdk_auto_detect_local_gateway branch May 23, 2026 03:49
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