5 Commits
Author SHA1 Message Date
jinyeandQwen-Coder f3af3dd857 refactor(managed-agent): Split server into Maven modules (#15871)
* refactor(managed-agent): split server into Maven modules

Separate contracts, orchestration, persistence, adapters, assembly, and verification while retaining the existing service delivery model. Move tests and build consumers together and document local evidence and pending CI acceptance.

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(managed-agent): reduce configuration and SDK boilerplate

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(managed-agent): narrow contracts and simplify test scaffolding

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): repair verification after module migration

Resolve shared fixtures and worker paths from the Maven module root, update CI packaging assertions, and align existing test fixtures with the extracted contracts. Restore restricted Workspace constructors and remove redundant test visibility changes without altering inherited MySQL coverage.

Co-authored-by: Qwen-Coder <[email protected]>

* test(managed-agent): align remaining Harness and OSS fixtures

Preserve SDK refusal messages and register the managed OSS adapter at fixture injection points. Keep the existing retry and client shutdown assertions intact.

Co-authored-by: Qwen-Coder <[email protected]>

* style(managed-agent): order Hosted cleanup Harness imports

Co-authored-by: Qwen-Coder <[email protected]>

* test(managed-agent): fix recovery smoke fixture and import order

Supply the required transcript coverage in the recovery browser fixture and wait for history before testing a complete handoff. Preserve its existing safety and submission assertions. Order the cleanup coordinator test imports for Checkstyle.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-30 14:56:35 +00:00
Shaojin Wenand易良 e7a67ee4e3 feat(managed-agent): give the Hosted Harness its Broker through the environment (#15244)
* feat(managed-agent): give the Hosted Harness its Broker through the environment

The Hosted Harness reached the Runtime Broker only through command-line
options, so the shipped unit expanded the Broker token into argv, where
any local user can read it, and the token was stored again in
server.env for the control plane. A busy Harness port also moved the
Harness to another port that the control plane could not reach.

harness.env now carries QWEN_MANAGED_RUNTIME_BROKER_URL and
QWEN_MANAGED_RUNTIME_BROKER_TOKEN, which qwen serve already reads and
deletes from its environment, and the unit no longer passes Broker
arguments. Every secret gate lists the token too: core's child and
${VAR} denylist, the ACP scrub list, the channel worker environment and
the control plane's Runtime worker filter. The Hosted Harness profile
fails on a busy port instead of moving.

The Harness credentials file may carry the Broker token for the control
plane, refusing a second direct copy and binding nothing on a conflict;
both Broker lines follow the strict credential rules. Preflight adds a
broker-url row that compares the file's URL with the Broker's address
and warns when the file has none, and the broker row's fix names the
file only when startup read it. The bootstrap, templates, systemd unit
and guides keep the token in harness.env only and tell upgraded hosts
to replace QWEN_HOSTED_HARNESS_BROKER_ARGS.

* fix(managed-agent): check the Harness Broker URL against the actual listener

The broker-url preflight row matched loopback addresses by class, so a
Broker bound to 127.0.0.1 passed a file URL on 127.0.0.2. It failed any
URL that differed from the bind address, although a proxy or TLS front
end may forward it. And it ignored the scheme once a port was given, so
https to the plain-HTTP Broker passed.

The row now passes only a direct http URL: the Broker's port, and a host
that resolves to the bound address itself or, for a wildcard bind, to an
address of this machine. https to that same listener fails, since the
embedded Broker serves plain HTTP. Any other URL warns that a proxy may
forward it, which preflight cannot verify, so it no longer fails --check.

* fix(managed-agent): hold the Harness Broker URL to the Harness's own rules

- broker-url first applies the rules managed-runtime-broker-url.ts applies
  at Harness startup (an http or https origin, plain http only on loopback),
  reads the host of a registry authority such as an underscore name, and
  names the direct loopback URL, or the TLS-terminating proxy for a
  non-loopback bind, in its fix.
- A broker check fails a non-loopback bind without
  runtime-broker.allow-non-loopback, which the Broker refuses at startup.
- The Harness credentials file requires the Broker URL and token together,
  refuses a key containing whitespace such as `export KEY=value` (systemd
  ignores it, a shell assigns it), and lets the URL bracket an IPv6 literal.
- qwen serve warns when --managed-runtime-broker-token is on the command
  line; serve tests cover the environment fallback, its removal and flag
  precedence.
- The upgrade runbook names the server.env token to delete, the server
  README lists the Broker prefix among the stripped ones, and the bootstrap
  test checks stdout for every generated secret.

* fix(managed-agent): read the Harness Broker URL the way the Harness parses it

The broker-url preflight row predicted the Harness with java.net.URI and a
hand-written loopback check, which disagree with Node's WHATWG URL parser in
both directions. It failed http://127.1:4182, http:/127.0.0.1:4182 and
http://127.0.0.1:4182/./, which the Harness takes as its loopback origin, and
passed http://::1:4182, http://127.0.0.08:4182 and port 99999, which the
Harness refuses before it starts.

- HarnessBrokerUrl follows the URL Standard for the part of an http(s) URL a
  credentials-file value can reach: the scheme, the slashes after it, the
  IPv4, IPv6 and ASCII-domain host parsers, the port and the path's dot
  segments. An xn-- label needs IDNA, so the row warns that it cannot read
  that URL instead of failing.
- managed-runtime-broker-url.fixtures.json records the Harness's verdict for
  each spelling; the CLI test holds the Harness to it and HarnessBrokerUrlTest
  holds preflight to it.
- The fix's direct URL is built from the address the bind resolves to, so a
  ::ffff:127.0.0.1 bind yields http://127.0.0.1:4182.
- Docs: the upgrade backs up server.env and harness.env and a rollback
  restores them; the guide names the row's no-URL warning; the operator pages
  allow [ and ] in the URL key; the server README states the URL/token pairing
  rule.

* fix(managed-agent): keep the Broker pair across a daemon update restart

A daemon self-update replaces `qwen serve` through execve, which keeps
the PID, so systemd never hands the replacement the Broker pair from
harness.env again, and serve has already removed the pair from its own
environment. Hand the update restarter the resolved pair and add it to
the replacement's environment only, beside the server token.

Also from the same review round:
- preflight decides the plain-http, path and ends-in-a-number refusals
  for a host with an xn-- label without IDNA, and leaves only an https
  URL to such a host unclassified;
- the Java fixture loop allows only the two documented approximations, a
  scheme or path read before the host and an xn-- host read without
  IDNA; the shared fixture gains cases for both;
- the CLI fixture loop fails on a verdict it does not know instead of
  passing on any throw;
- the broker-url row brackets an IPv6 Broker address;
- the credentials file refuses an empty Broker URL or token, which the
  Harness takes as set and then refuses to start with;
- the deployment guide no longer says a wildcard Broker bind is reached
  through a proxy.

* fix(managed-agent): warn on an IPv6 URL for an IPv4 wildcard Broker bind

The JDK binds 0.0.0.0 to IPv4 alone under -Djava.net.preferIPv4Stack=true,
so a Harness given http://[::1]:P cannot reach a Broker bound there, while
the broker-url row passed it as served. Preflight cannot see the server's
JVM options, which may come from an environment it does not share, so a
wildcard bind now serves only addresses of its own family, and an IPv6 URL
to the IPv4 wildcard warns and names the IPv4 loopback URL. An https URL
there still fails. The JDK binds :: only on a dual-stack socket, so the
IPv6 wildcard keeps serving IPv4 too.

* fix(managed-agent): warn on an https Broker URL across address families

When the Broker binds the IPv4 wildcard and the URL names an IPv6
address, the Broker's own socket takes it only from a dual-stack JVM.
Under -Djava.net.preferIPv4Stack=true another listener, such as a TLS
proxy, may hold that address and forward to the Broker, so preflight
cannot tell an https URL there apart from a working deployment. It now
warns for both schemes and fails https only for an address the Broker's
plain-HTTP socket takes in either mode.

---------

Co-authored-by: 易良 <[email protected]>
2026-10-25 20:29:23 +08:00
jinyeandQwen-Coder 28415ddff3 refactor(managed-agent): Consolidate server and broker sources (#14277)
* refactor(managed-agent): Consolidate server and broker sources

Move the combined Java application beside sdk-java and remove the standalone Runtime Broker artifact. Update build, development, packaging and CI references while preserving Java package identities, runtime protocols and schema resources.

Co-authored-by: Qwen-Coder <[email protected]>

* docs(managed-agent): Preserve historical evidence and verification guidance

Restore commit-pinned H3 probe inputs and align the Java verification environment contract with the consolidated server layout.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): Run hosted Broker tests under Bash

Keep Maven property arguments intact on Windows and stop before Broker tests when the SDK install fails. Exercise both successful and failed SDK installation in the workflow regression tests.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): Quote the hosted Broker shell value

Follow the repository yamllint single-quote rule without changing the parsed workflow configuration.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-16 00:28:58 +00:00
8679b7ca75 fix(managed-agent): e2e runner and image robustness from the #12692 R2 review (#13344)
* fix(managed-agent): e2e runner and image robustness from the #12692 R2 review

Six R2 review follow-ups on #12692, re-verified against the current
script (post-#13263) before patching:

- waitUntil surfaces the last predicate error in its timeout message
  instead of swallowing it, and every iteration is bounded by the
  remaining deadline so a hung predicate can no longer outlive
  timeoutMs several-fold.
- crashProcess refuses to kill a child that already died BY SIGNAL
  (exitCode stays null but signalCode is set), instead of throwing a
  raw ESRCH that hides the descriptive pre-exit diagnostic.
- An interrupt delivered after the last poll now exits 1 with the
  signal name; success JSON can no longer appear for a run the
  operator explicitly stopped.
- runMysql and mysqladmin pass --no-defaults first, so a developer's
  ~/.my.cnf client options cannot auth-fail the scratch
  empty-password server (both mysqld invocations already did).
- The packaged-jar lookup is version-independent in both the runner
  (exactly-one jar assertion over target/) and the Dockerfile COPY
  (wildcard that fails on zero or multiple matches).
- The image listens beyond loopback by default: ENV
  QWEN_MANAGED_AGENT_SERVER_ADDRESS=0.0.0.0 and
  QWEN_MANAGED_AGENT_RUNTIME_BROKER_HOST=0.0.0.0, so a documented
  docker run -p no longer refuses traffic; the application.yml
  loopback defaults stay host-appropriate.

* fix(scripts-e2e): unbreak the failover lanes from the #13344 review

Three findings on the R2-fix follow-up, each verified against the CI
failure on hosted-harness-mysql before patching:

- The packaged-jar lookup assumed exactly one qwen-managed-agent-server-*.jar
  in target/, but the pom attaches classified repackage executions
  (workspace-bundle, operator-recovery) besides the unclassified server
  jar, so the cardinality check could never pass and every
  test:e2e:managed-*-failover step died at module scope. The script now
  reads the project <version> from the pom — the one following the
  module's own <artifactId>, not the <parent> block's — and names the
  unclassified jar exactly.
- The Dockerfile wildcard COPY fails the image build outright: multiple
  matches into a file destination is a Docker error, and no workflow
  builds this image to catch it. The build stage now selects the
  unclassified jar by classifier exclusion, fails unless exactly one
  exists, and publishes it under the fixed name the runtime stage COPYs.
- waitUntil's stall timer was discarded live: a fast success idled the
  runner up to the largest timeout after the success JSON. The timer is
  unref'd.

Script suite 14/14 green (new pins: pom-version jar naming, the
classifier-exclusion line, stallTimer.unref()); Prettier and ESLint
clean. The pom regex and the Dockerfile selection loop were both
executed locally against a three-jar target before committing.

* fix(scripts-e2e): harden the image guard and record the publish consequence

Second review round on the jar/bind changes, each verified before patching:

- The glob default arm trusted sh's non-nullglob behaviour: with zero
  matches the pattern itself reaches the counting arm, passes count=1
  vacuously, and the build dies at cp with a glob in the message. The
  loop now breaks on a non-existent match candidate and the guard names
  the expectation loudly (expected exactly one unclassified server jar,
  found N).
- The wide bind was necessary (-p DNATs to the bridge IP) but its
  consequence went unrecorded while README.md:6-8 still says the server
  listens on the loopback deliberately: the API has no HTTP
  authentication, so tenancy is whatever X-Qwen-Tenant-Id says, and
  docker run -p on a shared host publishes that surface. README's
  container section and the Dockerfile ENV comment now say this
  plainly; the pre-existing private-ingress gate stays with the wider
  lifecycle deferrals (#13302 family).
- Two pins watch the bind and the guard in the script suite (both go
  red against the old text); the Reviewer Test Plan again matches the
  current head.

Suite 15/15 green; Prettier applied.

* fix(scripts-e2e): close the second round's eight follow-ups

Each verified against the runner before patching:

- The synthetic 'predicate stalled' rejection no longer overwrites the
  real predicate error; undici's cause (ECONNREFUSED) is what the
  diagnostic surfaces instead of 'fetch failed'.
- MySQL client invocations run with an isolated empty HOME: MySQL
  honours ~/.mylogin.cnf even with option files disabled, so a
  developer's mysql_config_editor credential would otherwise defeat
  the flag at exactly the scratch empty-password servers it was for.
  spawnSync probes carry their own timeouts, as a sync-blocking call
  prevents the race deadline from firing inside an iteration.
- One 'has this child exited' spelling (childExited) replaces the four
  drifted phrasings that produced the original ESRCH.
- The AST extractor exists once; the port-allocation test calls the
  shared helper instead of maintaining a second copy.
- The two surfaces that pick the server jar are named similarly in
  test intent (pom-derived exact name in the runner, classifier
  exclusion with a loud guard in the Dockerfile) so their ownership
  no longer contradicts, and the pom regex is executed against the
  real pom in the suite rather than only grepped.

Suite 16/16 green; ESLint clean; Prettier applied.

* fix(scripts-e2e): opt the image bind in explicitly and close round three

Third review round, each finding reproduced or probed before patching:

- The published image no longer binds the managed-agent control plane
  on 0.0.0.0 by default (R1-1): the module has no HTTP authentication,
  so the ENV pair published a tenant API anyone routing to the port
  could drive. The image keeps the application.yml loopback defaults;
  publishing is now an explicit docker run -e opt-in documented in the
  README's container section, including the no--p bridge exposure.
- .dockerignore excludes target/ in both spellings, so a developer's
  stale Maven output cannot ride the build context into the stage the
  jar cardinality guard inspects.
- waitUntil: the stall sentinel is compared by identity and recorded
  only when no real predicate error was seen; an answered (falsy)
  iteration clears a stale earlier error; and the remaining budget is
  handed to the predicate so synchronous probes bound themselves.
- runMysql surfaces the spawn-level reason (a timeout no longer reads
  as a bare 'MySQL command failed: '), and the isolated MySQL client
  HOME hangs off the runner's one reclaimed scratch root.
- Tests: extraction lists name childExited so the early-exit branch is
  reachable; the pom regex is lifted from the runner source and pinned
  by shape instead of a copied literal (a version bump stays green);
  the double transpile is gone.

Suite 23/23 green (7 existing + 16 added by this PR); build, typecheck
and lint clean; 14 mutation probes each red their witness.

* fix(scripts-e2e): floor the last MySQL probe and close round four (#13344)

Fourth review round, each finding reproduced or probed before patching:

- R4-1 (Critical): the container-publishing recipe named
  QWEN_MANAGED_AGENT_RUNTIME_BROKER_HOST=0.0.0.0 without
  ALLOW_NON_LOOPBACK=true, which the broker requires for a non-loopback
  bind — an operator enabling the broker got a crash-looping container.
  Dropped the broker line from the README recipe and the Dockerfile
  comment and pointed at the Broker deployment section; publishing the
  API never needs the broker's HTTP face. A pairing pin now reds any
  documented broker wildcard bind that omits the flag.
- R1-8: waitUntil handed the final poll a remaining budget below one
  client round trip, so the deadline message named a wedged client when
  the lease was simply unexpired. runMysql floors its spawn timeout at
  2s, re-admitting up to ~2s of overshoot per wedged poll against the
  ~10s the budget threading removed.
- R3-1: the mysqlClientHome pin was a whole-file count, blind to a
  fifth client invocation without the override. Assert per call site
  over the runner's AST: every spawnSync mysql/mysqladmin/mysqld passes
  --no-defaults, and the mysql/mysqladmin clients use the isolated HOME
  (the mysqld launches deliberately keep the real one).
- R3-3: the loopback-by-default guarantee is pinned at application.yml's
  own default, not only at the Dockerfile declining to override it.
- R3-2: the waitUntil extraction list lived in six pasted copies; one
  WAIT_UNTIL_DEPS inventory with one loadWaitUntil() loader now serves
  all six, and the loader takes injected new Function parameters (used
  by the crashProcess and runMysql cases).
- R4-2: the jar-selection guard is exercised as shell — the RUN stage's
  own text run under a probed sh against zero/one/three/two-unclassified
  fixture target/ populations, asserting exit status and message.

Suite 24/24 green (7 existing + 17 added by this PR); build, typecheck
and lint clean; seven mutation probes red their witnesses, with the
guarded broker variant staying green as the control.

* fix(scripts-e2e): gate the jar guard on cp and sharpen the MySQL scan (#13344)

Fifth review round, each finding probed before patching:

- R4-2 (Critical): the executable jar-guard case gated on sh alone,
  but its fixture ends in cp — on a Git-Bash-only Windows PATH sh.exe
  resolves without the coreutils, so the merge-queue lane would red at
  the status assertions, and where sh is absent the in-body return
  recorded a pass for a guard that never ran. The case now probes the
  capability it consumes (sh -c 'command -v cp') and skipIf-gates, so
  a host without either reports skipped, matching the repo's own
  hasSha256sum probe spelling.
- R3-1: the per-call-site scan matched spawnSync only, missing the
  long-lived start(mysqld, ...) launch (3 of the 4 MySQL process
  launches); a collapsed population still passed the lower-bound
  assertion; and toContain pinned the flag's presence, not its
  position, though MySQL honors --no-defaults only as the first
  option. The scan now covers spawnSync/spawn/start, asserts the
  exact four-call inventory, and pins --no-defaults as each call's
  first array element.
- R1-8: the floor pin exercised only the clamp side; the
  default-timeout call now also pins that a budget above the floor
  reaches spawnSync unchanged (10_000), so a Math.max-to-constant
  regression reds the case.

R1-12 stays open for a maintainer: it asks for a PR-body edit (the
stale Image bind bullet and the 15/15 count), which this loop cannot
perform — it has no GitHub write credentials.

Suite 24/24 green; build, typecheck, lint and prettier clean; six
mutation probes red their witnesses (clamp-to-constant; --no-defaults
dropped from the start() launch; a fifth unprotected client; the flag
moved off index 0; cardinality guard disabled; a cp-less PATH reports
the jar-guard case skipped).

* fix(scripts-e2e): document a bootable publish recipe and close round five (#13344)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(scripts-e2e): boot the documented container recipe and pin the round-two gaps (#13344)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(scripts-e2e): pass the recipe's datasource password valueless and pin the round-three gaps (#13344)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(scripts-e2e): name both valueless credentials in the publish recipe's export instruction (#13344)

* fix(scripts-e2e): cover the third pom classifier in the image jar guard (#13344)

The base-conflict merge from main added the workspace-migration repackage
classifier to the pom while the Dockerfile's exclusion list stayed at two
arms, so the cardinality guard counted two unclassified jars and the
documented image build failed at the RUN stage with no CI lane building
the image to catch it. Add the missing arm, extend the shell-fixture
populations with the real four-artifact target/ layout, and derive the
exclusion set from the pom's own <classifier> elements in the suite so a
fourth classifier reddens the Test job instead of an operator's docker
build. The three prose copies of the artifact count are retired rather
than renumbered, so they cannot drift again.

Also close the two remaining round-one test-hardening findings: the
port-allocation test now loads through the shared load() helper instead
of a second local new Function spelling, and the image ENV pins anchor on
the instruction (case-insensitive, leading whitespace tolerated) rather
than the conventional uppercase column-0 spelling.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(scripts-e2e): defer the freeze wake record and name stalls in waitUntil errors (#13344)

- The --freeze arm printed its fencedFormerWriter record eagerly inside
  the try, so a SIGINT right after it left a success-shaped document on
  stdout for a run that exits 1, and the record flipped to before the
  main result. Defer it like resultJson and print it after the main
  result, restoring the base ordering.
- When the deadline's own poll stalled, waitUntil reported a stale
  earlier error as the final state. The message now reads "predicate
  stalled; last error before the stall: ..." so both facts survive.
- README: the documented MySQL grants did not boot the server (Flyway's
  performance_schema.user_variables_by_thread probe is denied with
  ERROR 1142 and startup stops at 'foreign_key_checks'); add the SELECT
  grant to the one-liner and mention it in the container section.

* fix(scripts-e2e): floor the waitUntil stall bound so a final in-flight poll can answer (#13344)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(scripts-e2e): print the README's full grant set in the dev recipe hint (#13344)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(scripts-e2e): re-key the in-flight poll test off the clock for re-entry margin (#13344)

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code[bot] <qwen-code[bot]@users.noreply.github.com>
2026-10-11 16:42:22 +00:00
Shaojin WenandQwen-Coder aca4d03c7e feat(managed-agent): Add dev:managed-agent one-shot dev launcher (#13218)
* feat(managed-agent): Add dev:managed-agent one-shot dev launcher

Developing against the Managed dual-path WebShell previously meant
starting an ordinary daemon, a private Hosted Harness, and the Spring
control plane by hand, keeping their ports, tokens, and capability
digest consistent by copy-paste.

The new launcher mirrors daemon-dev: it serves the daemon and the
Hosted Harness from TypeScript source with rotating tokens and digest,
writes the Spring wiring to the git-ignored (0600)
.qwen/managed-agent-dev/spring.env, waits for /actuator/health on the
Java side (--skip-java-wait bypasses), then opens the WebShell Vite dev
server with the Managed panel selected. Ports auto-increment when busy,
and any child exiting aborts the group so the stack cannot half-start.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): Harden dev:managed-agent against review findings

Round-1 review found 4 Critical and 14 Suggestion-level issues in the
launcher; this commit addresses all 18:

- Windows teardown now uses taskkill /T so the dev.js grandchild that
  owns the sockets cannot be orphaned (R1-1).
- Equal --daemon-port/--harness-port is rejected, the daemon probe
  excludes an explicit harness port, and both health gates assert
  responder identity through /capabilities (the harness must present
  this run's capability digest), so a busy-port bump can no longer be
  reported as a healthy mis-wired stack (R1-2).
- The two serve children boot in serialized stages: dev.js destructively
  re-stages the shared browser-use runtime on every boot, and the
  parallel spawns raced on it (R1-3).
- The token-bearing open path travels via QWEN_WEB_SHELL_OPEN_PATH,
  consumed as server.open in the web-shell Vite config, instead of a
  --open argv that npm echoed and cmd.exe would split on '&' (R1-4,
  R1-17).
- spring.env moved to the OS temp directory (mode 0600) so the harness
  bearer token no longer sits inside the served workspace; a PowerShell
  sibling is written for Windows, a warning fires when a previous run's
  file (and a possibly stale Spring) exists, and the Spring recipe now
  carries the datasource exports and one-time sibling installs in both
  README and the startup banner (R1-5, R1-6, R1-11, R1-12, R1-14).
- --tenant is validated against Spring's own pattern, waitForHttpOk
  carries the last HTTP status/error into its timeout, a Java timeout
  no longer tears down the healthy local stack, signal handlers live
  inside the entry guard, and the isMain guard is symlink-safe (R1-8,
  R1-9, R1-10, R1-16, R1-18).
- Exported builders (kill plan, harness wiring, serve stages, web
  launch, URL) with mutation-witness tests pin the contracts the first
  round showed unpinned (R1-7, R1-13, R1-15).

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): Probe the web port on Vite's bind family

Vite binds 'localhost', which resolves to ::1 only on IPv6-first hosts,
but the web-port probe listened on 127.0.0.1 and called a held port
free — observed in a two-launcher dry run where the second launcher's
probe passed and Vite silently bumped past the probed value (the
printed URL then names the wrong port; the TTY URL also used the IPv4
spelling that cannot connect to an ::1-only bind). The probe now takes
a host parameter and the web probe uses 'localhost', matching Vite's
own bind attempt by construction; the printed URL uses the localhost
spelling. A unit witness binds a server on 'localhost' and requires
the probe to walk past it.

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): Close round-2 review findings on dev:managed-agent

The second review round (2 Critical, 8 Suggestions) found the round-1
remedies themselves creating new edges; this commit addresses all ten:

- The env-file write is now fail-closed: the directory is keyed by uid
  and a hash of the checkout root (multi-user and multi-worktree runs no
  longer collide or cross-truncate), created 0700, verified owned, each
  target lstat-checked and refused when it is not our regular file (a
  planted symlink is bounced, its victim untouched), and 0600 is applied
  by chmod so it holds on the overwrite path too. POSIX-only
  enforcement: NTFS owns the boundary on Windows, where stat mode masks
  and chmod read-only would have broken the launcher outright (R1-11).
- Teardown now covers SIGHUP plus a synchronous exit hook, so a closed
  terminal tab or dropped SSH session no longer orphans the credentialed
  serves that detached:true would strand (R2-1); registrations stay
  inside the entry guard and are pinned via TEARDOWN_SIGNALS.
- Explicitly pinned ports are probed and rejected when busy, because
  qwen serve bumps on EADDRINUSE regardless, which previously meant a
  60-second misleading timeout (R2-2); Vite gets --strictPort and a
  pinned --host so it fails closed instead of drifting past the probed
  port (R2-3).
- waitForHttpOk captures err.cause.code (ECONNREFUSED now visible
  through undici's generic 'fetch failed') and clears stale accumulators
  so a fresher error supersedes an older response (R1-9 residual).
- The Java gate applies its siblings' identity logic through
  springHealthExpectBody instead of certifying any 2xx (R2-6).
- The PowerShell recipe line is uncommented and self-enabling
  (Set-ExecutionPolicy -Scope Process), emitted by an exported
  buildSpringRecipeLines that also carries the database/user creation
  step matching the reference e2e, mirrored into the README (R1-14,
  recipe gap).
- Effectful decisions moved into exported, injectable helpers
  (executeKillPlan, runServeStages, ensurePortFree) with interleaving
  and argv witnesses, and nine loose oracles tightened (rotation, ::1
  family fixture, daemon argv, launch env/argv, sharper retry message,
  the vite env-consumer case in web-shell, the Authorization header, the
  harness port in both base URLs, ambient-credential collision) (R2-4,
  R2-5).

Dry-run witnesses: staged healthy with digest pairing, recipe lines and
the env-file layout (0700 dir, 0600 file, no ps1 on POSIX) verified,
pinned-busy fails in milliseconds with exit 1, and a live SIGHUP drains
daemon, harness and Vite with no orphaned listeners.

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): Skip the web-shell spawn once teardown has begun

Self-audit after the round-2 fixes found two edges: a child dying
during the Java wait now lands in the non-fatal continue branch, which
would print the Managed URL and spawn Vite into a group teardown was
already reaping (the exit hook caught it, but the output misleads); and
killChild's re-entrancy comment overclaimed a no-op second pass — the
group/taskkill plans do not set child.killed, so a second pass is a
redundant signal to a dying tree, which is safe but not a no-op.

Guard the spawn on shuttingDown and correct the comment. No behavior
change on the happy paths; the script suite stays at 60/60.

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): Close round-3 review findings on dev:managed-agent

The third review round (4 Critical, 18 Suggestions), including three
defects the round-2 fixes introduced, now addressed together with the
review's batching recommendation:

- shutdown() counted only signal-dead children (the >>continue<<
  predicate read 'signalCode === null'), so Ctrl-C/SIGHUP launched
  process.exit immediately, orphaned every live server, and a sole
  signal-killed child exited 0. The probe now reads isLiveChild, which
  the suite pins in both directions (R2-1).
- The credential writer's checks resolved through symlinks: a planted
  link at the directory walked the lstat refusal, and a dangling link
  at a target skipped it entirely. The writer now lstats everything and
  refuses any symlink up front (R1-11), with planted-link victim-
  untouched cases.
- Teardown now hears SIGQUIT (Ctrl-\) and escalates to SIGKILL after
  the 5 s grace instead of just exiting (R2-1/R3-1); the exit-hook
  comment narrows to what an in-process hook can promise instead of
  claiming coverage for unhandled signals and hard aborts.
- Pinned-port checks moved from launcher start to each stage's own
  spawn step, so a port taken between the daemon's 60 s boot and the
  harness's spawn can no longer be silently bumped past (R2-2); the
  web port resolves at spawn time through an exported launch seam, so
  a squatter in the Java window cannot turn --strictPort into a full
  teardown (R2-3, R3-2).
- The Java gate's teardown path is silent: no 'stays up' continuation
  line while children are reaping (R3-4), extracted into a runJavaGate
  seam with an injected health wait so the path is test-reachable.
- healthFailureAction left only a literal-'continue' call site; the
  dead policy table, the unreachable abort branch and its describe are
  removed per the review's KISS option (R3-5).
- The MySQL recipe grants the 'qwen' user at both 'localhost' and
  '127.0.0.1' (official images enable skip-name-resolve) with a
  container-topology note, and a pairing test ties the granted
  hosts/password to the exported SPRING_DATASOURCE_* literals (R2-7,
  R3-10).
- Test-oracle batch (R3-3, R3-6..R3-9, R3-14, R3-15): default-runner
  witness for the kill plan, detached flag via an exported
  spawnDevProcess, first-run mkdir 0700 gate, stalled-responder
  rejection, both uid arms, stages[1].args pin, : prefix pins for
  the ps1 BASE_URL/digest lines, the printed ps1 path tied to the
  writer's filename, '503' replaced by the port-safe 'last response'
  suffix assertion with an in-range budget and a missing-finally close,
  the ephemeral-port-safe family fixture (property, not exact port),
  usable-on-Windows branch-shape cases decoupled from POSIX mode
  assertions, and parsed-actionable-line checks for the PowerShell
  recipe. ensurePortFree's unused probeHost parameter is gone (R3-13),
  and the env-transport comments now record the browser-hop argv the
  token still rides (R3-12).

78/78 in the script suite; live dry run healthy (dual-user recipe,
argv clean), and a real SIGQUIT drains daemon, harness and Vite with
no orphaned ports.

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): Wire the Session Store every Managed Turn needs

Real-stack verification on the PR thread found F1: since #12754 the
Hosted Harness answers 400 invalid_managed_session_store to any POST
/session without a managedSessionStore descriptor, and Spring only
sends one when its HTTP Session Store is enabled — so the launcher's
recipe brought up a perfectly healthy stack in which every Managed
Turn failed with hosted_harness_rejected. The stub-Spring dry-run and
env-name checks in earlier rounds never drove a real Turn through the
wiring. Adopts the reviewed candidate patch (wenshao assets-pr13218
@ ef748da, measured 3/3 Turns against a freshly created DB) and adds
the remaining notes:

- spring.env/ps1 now also carry QWEN_MANAGED_AGENT_SESSION_STORE_*
  (enabled, base URL = --java-url since the Harness calls Spring back,
  and a dev workspace id), so the recipe by itself produces a usable
  Session Store (F1).
- SERVER_PORT is emitted from --java-url for plain-http loopback URLs,
  so a non-default --java-url no longer leaves Spring on 8080 while
  the launcher waits elsewhere (N2).
- Thrown messages no longer carry the '[managed-agent-dev]' prefix that
  main()'s catch prepends once (N3).
- '--java-url' is trimmed before validation, so surrounding whitespace
  can't pass the checks and then fail every health attempt (N5).
- The README states the hosted_harness_rejected diagnostic mapping
  (Harness 400 = Session Store not loaded; 401 = stale Spring) (N1).
- The first-run 0700/0600 writer case is re-gated for POSIX semantics
  (R4-1); content/branch-shape cases keep running everywhere.

The supervisor data point from N4 (SIGKILL of the launcher) stays a
deliberate follow-up on the launcher family, unchanged.

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): Gate the POSIX-arm writer case on the host, not the flag

The round-5 review found the split's new case reproducing R4-1's exact
failure on the Windows lane one level up: 'writes the env content and no
ps1 sibling on the POSIX arm' injected isWinPlatform: false unguarded, so
on a real Windows runner the production 0700 gate reads the synthesized
777 directory mode and writeSpringEnvFiles throws before either
assertion — the mechanism and message R4-1 described, on the case I
believed the split had made portable. It now gates on the host like its
six siblings.

Also adopts the round-3 verification's wording note: the two sibling
installs are labeled once per clone, and re-run after pulling changes
to qwencode or runtime-broker, since a snapshot install before a main
pull leaves spring-boot:run compiling against stale artifacts (the
banner builder and the README recipe say the same thing either way).

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): Close the actionable slice of the round-4 deferred list

Five items from the round-4 review body's deferred Suggestions were
covered here because each is a small consistency hole in the seam this
PR just built:

- main() re-checks pinned ports at t0 before writeSpringEnvFiles runs,
  so a busy --daemon-port/--harness-port no longer rotates credentials
  and prints the banner before failing at stage spawn the same run
  (R4-14); the per-stage recheck stays for the boot window it was added
  for.
- launchWebShell re-consults the teardown flag after resolveWebPort, so
  a launcher dying mid-probe no longer prints a Managed URL for a group
  the exit hook is about to reap (R4-10), with a witness that flips the
  flag mid-resolve.
- findWebPort is now held against a stand-in bound to this host's
  localhost resolution at DEFAULT_WEB_PORT, so the family walk is
  operational rather than vacuous (R4-17).
- installTeardownHandlers' captured handlers are asserted to be
  functions, not only named functions (R4-19).
- The vi.mock child-process fixture spawns fake children with
  pid=undefined, so teardown code can never signal a real process
  through the registry it is pushed into (R4-20).

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-10-04 14:41:46 +00:00