* 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>
81 KiB
Qwen Managed Agent Server
Standalone Spring Boot control plane for the Qwen Code Hosted Harness. It has
no DataWorks dependency. The default loopback listen address keeps the
header-asserted tenant model: a trusted upstream sends X-Qwen-Tenant-Id
and the server uses that value on every database read and write. Leaving
loopback requires authentication: set QWEN_MANAGED_AGENT_SERVER_ADDRESS
and configure signed mode (QWEN_MANAGED_AGENT_AUTH_MODE=signed with
QWEN_MANAGED_AGENT_AUTH_SIGNING_KEY) so the broker verifies each request's
HMAC signature itself, or put an authenticated gateway in front. With the
default auto mode a non-loopback address refuses to start. See "Broker
authentication and writer credentials" below.
Integration status
The Hosted path supports durable no-tool Sessions and private Workspace tool profiles. G0 adds opt-in public creation with an initial Workspace Turn (file tools, or mandatory-approval foreground Shell under the Shell opt-in) through the production Broker. This is not complete Workspace lifecycle, in-flight recovery or distributed provisioning support. See the G0 section below for its exact deployment and admission boundary, and the historical review corrections for the original split's merge gates; earlier preview timing and recovery results below do not establish completion of later slices.
API contract
src/main/resources/openapi/managed-agent-public-api.openapi.json is the
single source for the public and WebShell routes. ManagedAgentApiContractTest
compares the mapped routes, the ApiModels records and real responses with it;
src/test/resources/openapi/contract-known-gaps.txt lists the differences that
a later slice still has to close; none remain after D4. The WebShell client types are generated from the
same file by npm run generate:managed-agent-api in packages/web-shell.
The test-tree SurfaceRegistry names every mounted route, internal ones
included, with its admission rule class; SurfaceRegistryGateTest fails any
mounted route the registry lacks, so a new public or WebShell route needs both
a spec operation and a registry entry (actor-roles design, D5).
Sessions created for the built-in qwen-code agent record the agent revision
from QWEN_MANAGED_AGENT_REVISION (default 1); a Session pinned to a stored
definition records the revision it resolved at admission. POST /v1/agents,
GET /v1/agents/{id} and
POST /v1/agents/{id} store tenant-scoped, immutable AgentDefinition
revisions; a Session pinned to one executes that stored revision (see "Stored
agent execution" below). Every response carries X-Request-Id, which error
envelopes repeat as request_id and the logs print. Events keep the schema and
projection versions they were accepted with. They keep their Item and Part
identity too, except after Harness recovery retracts output: the retracted
deltas lose their text and identity, later deltas may name other Parts, and a
stream.reconciled event announces it. A client that sees one reloads the
Items and resumes after their snapshot_through_sequence. A
cursor below a Session's replay floor gets 409 cursor_expired from the JSON
event query and one agent.session.resync_required frame from either stream
only while the Snapshot backs the floor
(replay_floor_sequence <= snapshot_through_sequence); a stream reconciliation
discards the Snapshot without lowering the floor, and during that rebuild such
a cursor is served from the retained events.
GET /v1/agents/sessions/{id}/turns lists a Session's Turns newest first with
an opaque cursor, and GET /v1/agents/sessions/{id}/turns/{turnId} reads one.
Design: English |
简体中文;
Session query: English |
简体中文;
Event replay: English |
简体中文;
Durable lifecycle: English |
简体中文;
Turn queries: English |
简体中文;
Actions (Hosted permission approvals): English |
简体中文;
AgentDefinition revisions: English |
简体中文
Managed tool results (O3)
O3 publishes durable Hosted foreground Shell outcomes to Items, events and
Managed WebShell. Downloads read immutable stdout/stderr after the writer is
sealed, without reviving a Harness. The API requires a trusted actor and a
current Workspace read grant, checked at request admission and then once per
read-revalidation-interval while a download is in flight; a tenant header
alone cannot authorize it.
O3 requires O2 publication to be configured, including
qwen.managed-agent.tool-publication.verification-bytes-per-second and
qwen.managed-agent.tool-publication.max-verification-timeout. These required
O2 verification settings are separate from the O3 content-read timeout below.
All settings below use the qwen.managed-agent.artifacts prefix:
| Setting | Default | Meaning |
|---|---|---|
enabled |
false |
Enable projection and public reads when O2 object storage is configured. Receipt sources are recorded even while disabled. |
publish-original |
false |
Approve original stream representations for current Workspace readers. |
publish-preview |
false |
Additionally approve bounded previews for every Session reader; requires original publication approval. |
max-concurrent-reads |
4 |
Maximum simultaneous content responses per server process. |
read-timeout |
2m |
Elapsed-time budget checked between stream chunks, capped by the fixed two-minute output read lease; storage requests also use the storage client's timeouts. |
read-revalidation-interval |
5s |
How often an in-flight download re-runs the access check (workspace grant, read policy, session lifecycle); PT0S re-verifies every chunk. A revocation lands at the first chunk boundary after the window's end; chunks written inside the window still reach the client — up to 1 MiB for a Range request, and up to a full read-timeout's worth of a streaming download. Once the re-check denies, no further chunk is written. |
A product can replace ManagedArtifactPolicy for narrower publication or
actor rules. Published previews persist in shared events. Policy changes do
not automatically reproject historical results; content requests use the
current read policy at admission and once per revalidation window thereafter.
Configure the policy before enabling projection.
Two sibling knobs tune the same relaxation elsewhere:
qwen.managed-agent.events.read-grant-recheck-interval (default 5s) bounds
how often a live event stream re-checks the Workspace read grant — PT0S
restores the per-event check — and
qwen.managed-agent.tool-publication.journal-head-authorization (default
false) switches tool-publication authorization from the journal scan to the
session journal head's activation columns; enable it only after every writer
in the fleet runs the V36 schema's code (the rolling-window self-healing is
covered in the
query-amplification design
§9).
Original reads are capped at 1 MiB per Range request; full downloads use
bounded segment buffers and stream with backpressure. Deployments must retain
O2 roots and validate real OSS and slow-reader limits before enabling this
feature. O3 does not enable public Shell execution or garbage collection.
Design: English | 简体中文; revalidation window: English | 简体中文.
Prerequisites
- Java 21
- Maven 3.8.9+ (the SpotBugs gate's plugin declares that floor)
- MySQL 8
Run the packaged CLI with qwen serve --profile hosted-harness as a separate
process (the credentialed launch is spelled out in the Full WebShell
dual-path development entry below). It supports durable no-tool Sessions and
the opt-in Workspace file Turns described in the G0 section below.
Install the two sibling libraries once when building this module outside a Maven reactor:
mvn -f ../qwencode/pom.xml -DskipTests -Dgpg.skip=true install
mvn -f ../runtime-broker/pom.xml -DskipTests install
Configure and start the server:
export SPRING_DATASOURCE_URL='jdbc:mysql://127.0.0.1:3306/qwen_managed_agent'
export SPRING_DATASOURCE_USERNAME='qwen'
export SPRING_DATASOURCE_PASSWORD='replace-me'
export QWEN_MANAGED_AGENT_HARNESS_ENABLED='true'
export QWEN_MANAGED_AGENT_HARNESS_BASE_URL='http://127.0.0.1:4170'
export QWEN_MANAGED_AGENT_HARNESS_TOKEN='replace-me'
export QWEN_MANAGED_AGENT_CAPABILITY_DIGEST='sha256:replace-with-64-hex-characters'
mvn spring-boot:run
Create a Session:
curl -sS http://127.0.0.1:8080/v1/agents/sessions \
-H 'Content-Type: application/json' \
-H 'X-Qwen-Tenant-Id: demo' \
-H 'Idempotency-Key: create-1' \
-d '{"agent_id":"qwen-code","input":[{"type":"input_text","text":"hello"}]}'
The returned id is an RFC UUID and is the canonical identity used by
the public API, Hosted Harness transcript, and Runtime Broker. The server does
not maintain a separate public-to-Harness Session mapping.
Public Session lifecycle
Close, archive and delete are durable operations (Flyway V17). Each answers
202 with a command operation that
GET /v1/agents/sessions/{id}/operations/{operationId} reads back, also after a
delete; the WebShell adapter offers the same routes. The public control plane
owns lifecycle state and tenant/idempotency checks, while the Hosted Harness
remains the private title authority and the Runtime Broker owns execution
bindings.
curl -sS -X PATCH \
http://127.0.0.1:8080/v1/agents/sessions/$SESSION_ID \
-H 'Content-Type: application/json' \
-H 'X-Qwen-Tenant-Id: demo' \
-H 'Idempotency-Key: rename-1' \
-d '{"title":"investigate checkout failure"}'
curl -sS -X POST \
http://127.0.0.1:8080/v1/agents/sessions/$SESSION_ID/close \
-H 'X-Qwen-Tenant-Id: demo' \
-H 'Idempotency-Key: close-1'
curl -sS \
http://127.0.0.1:8080/v1/agents/sessions/$SESSION_ID/operations/$OPERATION_ID \
-H 'X-Qwen-Tenant-Id: demo'
curl -sS -X POST \
http://127.0.0.1:8080/v1/agents/sessions/$SESSION_ID/archive \
-H 'X-Qwen-Tenant-Id: demo' \
-H 'Idempotency-Key: archive-1'
curl -sS -X POST \
http://127.0.0.1:8080/v1/agents/sessions/$SESSION_ID/unarchive \
-H 'X-Qwen-Tenant-Id: demo' \
-H 'Idempotency-Key: unarchive-1'
curl -sS -X DELETE \
http://127.0.0.1:8080/v1/agents/sessions/$SESSION_ID \
-H 'X-Qwen-Tenant-Id: demo' \
-H 'Idempotency-Key: delete-1'
Close and delete reject an active Turn and seal input as soon as they are
admitted. A background worker then closes the Hosted Harness Session, waits
until no Harness holds its journal writer under an unexpired lease (the
holding Harness seals it when closing), drains the Runtime binding (fenced by
the persisted Session status the Broker checks on the warm admission resolve
for an unbound Session, so the fence survives restarts; an in-process flag is
kept only for a Session row that has vanished) and completes the operation; a
failed
attempt is retried with the dispatch backoff until it succeeds, so a 202
never means that tools stopped (a Harness whose capability digest no longer
matches retries the same way, logged with the permanent reason — nothing
may complete honestly before an operator realigns the versions, because a
confirmation requires the Harness's own acknowledgement). When the Hosted
Harness restarts, a live
control plane adopts the new process generation: the connector renegotiates
once instead of failing every bound Session, pending Turns re-attach through
the takeover load as their retries come due, and the Session's bound boot ID
moves to the new generation without a Java restart. A takeover load that can
never continue (its parked state is not one a replacement can drive) ends the
Turn as managed_runtime_recovery_blocked with a typed reason instead of
retrying forever — in this slice that includes a Turn parked mid model round,
whose safe reissue is the named Step 3 follow-up. The Session row and its
generation binding survive that decline, but the declined Turn's input stays
unsettled in the journal, so the Session cannot admit a further Turn until
the Step 3 reissue lands — close it and start a new Session. A Harness whose journal writes stopped after a failed commit answers every close with 503 until it restarts. A delete of a closed or archived Session
needs no Harness. Archive accepts only a closed Session and completes at once;
unarchive restores it to closed. Rename waits for the Harness to durably commit
session_metadata. When a rename failure is recorded, its PENDING command becomes FAILED
while retaining its receipt and request digest. The same
key retries the same content with the replay flag set; changed content or a
different Session conflicts. A successful concurrent request can still complete
the receipt, and a failing sibling cannot overwrite that completed outcome. It
cannot complete a retired receipt once a later rename has completed either: that
sibling answers 409 session_mutation_superseded and the newer public SQL title stays.
The latest title attempt is now a durable delivery with a monotonically increasing per-Session revision. The Harness checks this revision inside its journal commit and uses a stable command identity per attempt, so a late older versioned write cannot undo a newer versioned title and a lost reply can be replayed without another write. A failed same-key request retried after a later rename receives a new revision and can still win. Pending same-key retries keep their original revision. A recovery worker redelivers the latest unfinished title and completes its public receipt; older completion attempts cannot affect the newer delivery. Both inline requests and recovery workers claim before sending; failure cleanup requires the current unexpired owner. Fresh disabled or unsupported Harness requests are refused before admission. Workspace migration also refuses unfinished title delivery even when the public command is FAILED. Close, archive, delete and cwd changes wait for this delivery to settle, including when its public command is FAILED, so sealing cannot strand a title already accepted by the Harness. The Harness must advertise title protocol version 1 before the SDK sends a versioned title. Plain private SDK title writes remain supported and preserve the watermark. See the concurrency design. If the command store
is unavailable during cleanup, the original API failure is preserved and the
same key can resume its receipt when storage returns. Only an in-flight
lifecycle change blocks another one. A retry with the same key from the same
actor returns the original operation once it has completed.
Harness attachment uses strict create/load semantics: create returns 409 for
an existing private Session authority, while load returns 404 for a missing
authority and never initializes one. The Java connector attempts strict create for a new binding and loads on
conflict or uncertain creation outcome. A known existing binding only loads.
The Java connector caches an attachment per (tenantId, sessionId),
so one tenant's cached reference is never handed to another by the connector.
The Harness itself keys its in-memory Session by sessionId alone and does
not compare the presented tenant, Store endpoint or workspace on attach; the
only attach-time identity fence is the Harness writer generation, so tenant
isolation on attach is the caller's responsibility in this slice. A presented
Harness writer generation is checked: an attach whose writerId is not this
process's boot ID fails closed with 409 hosted_harness_generation_mismatch —
the restart-generation failure described above. That
managed_session_store_conflict fence is target design for the integration
slice, not shipped behavior.
Delete writes a public tombstone: get and list stop returning the Session,
while its operations stay readable. Completed deletion permanently marks an existing
private journal DELETED, clears its writer and recovery references, and fences
new writes and recovery. Close and archive keep output pinned. Deletion does
not physically erase the journal, events or resources; output collection stays
disabled by default and requires the retention deployment gates. With both
qwen.managed-agent.tool-publication.enabled and
qwen.managed-agent.tool-publication.gc-enabled true, collection also frees
stream-capture bytes (background Shell and foreground leftovers): eligible
PUBLISHED MYSQL_INLINE rows of the managed-tool-result-{content,page,manifest}
kinds with no journal reference on a permanently retired Session are
byte-dropped after the same deletion grace under a per-Session claim ledger
(qwen_managed_session_resource_collection), keeping their identities with
state COLLECTED. Reads of the retired Session stay fenced with
tool_output_session_retired exactly as before; a workspace-recovery read
that physically reaches a collected byte copy fails the named check
resource_collected instead of the generic resource_layout_unsupported or
resource_corrupt verdict a pre-V65 broker produces.
The Phase 1 schema has not been released. A development database created by an
older revision with harness_session_id must be recreated before running this
revision; the service fails Flyway validation instead of silently rewriting
existing public Session URLs.
The public listener intentionally ignores end-user Authorization. The
optional Runtime Broker listener still requires a separate machine bearer and
must remain private.
Private Managed Session store
Flyway V4 creates the private Managed Session journal and resource tables. The
internal routes under /internal/managed-session-store/v1/** provide
database-time writer leases and generations, head compare-and-set,
idempotent transaction receipts, exact JSONL transaction bytes, paged restore
reads, atomic checkpoint-pointer advancement, and transactional resources up
to 64 KiB. Callers must provide the trusted tenant header and a writer
credential in X-Qwen-Managed-Writer-Token; only its SHA-256 is persisted.
Without QWEN_MANAGED_AGENT_SESSION_STORE_BINDING_KEY the credential is a
fresh Base64URL secret the caller mints (first writer wins); with a binding
key it must be the broker-issued HMAC over the Session scope and self-minted
secrets are rejected. Restore,
transaction-page, and resource reads require the same current, unexpired
writer secret.
Restore transaction pages are bounded to 8 MiB of unencoded record bytes even when the requested item limit is larger. Unknown head states, unsafe counters, or missing transaction revisions fail closed as storage corruption.
The routes are disabled by default. Enable them only on a private service listener or trusted service network:
export QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED='true'
export QWEN_MANAGED_AGENT_SESSION_STORE_BASE_URL='http://127.0.0.1:8080'
export QWEN_MANAGED_AGENT_SESSION_STORE_WRITER_LEASE_DURATION='60s'
export QWEN_MANAGED_AGENT_WORKSPACE_ID='workspace-demo'
QWEN_MANAGED_AGENT_SESSION_STORE_BASE_URL must be reachable from the Hosted
Harness. When both the Harness and Store are enabled, Java includes a scoped
Store descriptor in each new private Hosted Session request. The ordinary
daemon rejects that descriptor, while the Hosted Harness uses the TypeScript
HTTP adapter with the descriptor's broker-issued writer credential (or a
self-generated secret when no binding key is configured). Workspace-bound Sessions use
their persisted Workspace ID for the Store scope; unbound Sessions use
QWEN_MANAGED_AGENT_WORKSPACE_ID. The public Session, private journal and Runtime
binding retain one (tenantId, workspaceId, sessionId) identity. The global ID
must still be set explicitly when enabling the Session Store; if the Runtime
Broker also has an explicit ID, startup rejects a mismatch.
This activates the durable create and cold-load paths for newly created Hosted
Sessions. The load path rebuilds the Harness state from the scoped Store and
does not require a Pod-local transcript. The Store boundary has an
independent-JVM crash/takeover proof against real MySQL. A deterministic
multi-process check also kills the real Java and Hosted Harness owners, deletes
their local homes, and proves that replacement owners complete a second Turn
with the first Turn's restored context. A separate recovery slice supports one
known pending tool execution: the replacement Harness starts or polls the
original Broker identity, commits its settled receipt as results_ready, and
Java replaces the Harness boot and public event epoch under the Turn dispatch
lease, durably records the replacement attachment watermark before invoking
checkpoint-bound continuation with the original public prompt identity, and
advances the cursor after the response. Unit, contract, and H2 coordinator/store
tests verify that this path does not resubmit the Prompt or replay the tool. Do
not advertise general automatic cross-Pod recovery yet: multi-tool recovery,
recovered cancellation, event/checkpoint reconstruction after a
mid-continuation Harness crash, the full multi-process in-flight failure matrix,
OSS-backed resources, and scheduler recovery are still pending. Resources
larger than 64 KiB fail with
managed_session_oss_disabled until the immutable OSS path is implemented.
Production deployments must add mTLS or equivalent service authentication;
the tenant and writer headers are scope and fencing inputs, not a substitute
for transport identity. Responses under the private prefix use
Cache-Control: no-store.
Managed automation (H6)
The control plane runs the H6 automation runtime for persistent
definitions: a definition is created, revised, read and retired under the
Workspace-bound Session its creator owns (/v1/agent-automations), a
scanner derives each definition's due slots on this host's tz database,
claims the definition under a lease with a fence, records every occurrence
decision in its own ledger (V57) and fires a run as one idempotent Hosted
Harness operation; a manual run is the same operation keyed by its
Idempotency-Key. The Session journal's schedule and automation_run
chains stay the authority; the ledger locates, leases and indexes. The
per_run target and the delivery policy are refused until their slices
land. Design:
managed automation runtime.
All settings below use the qwen.managed-agent.automation prefix:
| Setting | Default | Meaning |
|---|---|---|
enabled |
false |
Run the scanner and admit the mutation routes (QWEN_MANAGED_AGENT_AUTOMATION_ENABLED); the read routes answer either way. |
scan-delay |
10s |
Fixed delay between scanner ticks. |
lease |
60s |
How long one scanner holds a definition; a stalled scanner yields it afterwards, and the fence it held no longer writes. |
late-tolerance |
5m |
A slot older than this when the scanner sees it is late and follows the definition's catch-up policy (none, latest, bounded). |
lookback |
24h |
Slots older than this are neither fired nor recorded; it bounds every catch-up by age. |
max-slots-per-tick |
1000 |
Slots returned per definition per tick: the walk itself follows the lookback window (its minutes are enumerated regardless), and the result set's surplus is cut at the oldest end and logged. |
concurrency |
4 |
Non-terminal runs an allow definition may hold at once; beyond it an occurrence is recorded skipped with reason count_limit. |
Full WebShell dual-path development entry
The full WebShell can keep an ordinary Qwen daemon for its existing chat, workspace, settings, and terminal surfaces while routing only the Managed panel to this Spring service.
The one-shot launcher starts the ordinary daemon and the private Hosted
Harness from TypeScript source, writes the Harness wiring
(QWEN_MANAGED_AGENT_HARNESS_*, the rotating capability digest, and the
HTTP Session Store that Hosted Sessions require) to a
spring.env under the OS temp directory — kept outside the served
workspace, mode-0600 on POSIX (on Windows NTFS ACLs scope the per-user temp
directory instead, and a PowerShell spring.env.ps1 sibling is written next
to it) — waits for /actuator/health on the Spring service
(--skip-java-wait bypasses), then opens the WebShell with the Managed
panel selected:
npm run dev:managed-agent
# In a second terminal, before the Java health wait expires (10 min).
# Once per clone, and re-run after pulling changes to qwencode/runtime-broker (~12 s):
mvn -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true install
mvn -f packages/sdk-java/runtime-broker/pom.xml -DskipTests install
# One-time, on a fresh MySQL 8 (creates the database and user the URL names;
# the performance_schema SELECT covers Flyway's boot probe — without it the
# probe is denied (ERROR 1142) and startup stops at 'foreign_key_checks'):
mysql -u root -e "CREATE DATABASE qwen_managed_agent CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'qwen'@'localhost' IDENTIFIED BY 'replace-me'; CREATE USER 'qwen'@'127.0.0.1' IDENTIFIED BY 'replace-me'; GRANT ALL ON qwen_managed_agent.* TO 'qwen'@'localhost'; GRANT ALL ON qwen_managed_agent.* TO 'qwen'@'127.0.0.1'; GRANT SELECT ON performance_schema.user_variables_by_thread TO 'qwen'@'localhost'; GRANT SELECT ON performance_schema.user_variables_by_thread TO 'qwen'@'127.0.0.1';"
# (official MySQL images enable skip-name-resolve, so 'qwen'@'localhost' alone never
# matches TCP clients; a containerized MySQL sees the gateway address — grant at
# 'qwen'@'%' or the container-visible host instead)
# Every run; the launcher prints this spring.env path at startup (on Windows,
# the printed path is the spring.env.ps1 sibling):
source <printed spring.env path>
export SPRING_DATASOURCE_URL='jdbc:mysql://127.0.0.1:3306/qwen_managed_agent'
export SPRING_DATASOURCE_USERNAME='qwen'
export SPRING_DATASOURCE_PASSWORD='replace-me'
mvn -f packages/sdk-java/managed-agent-server/pom.xml spring-boot:run
The daemon, Harness and Java URLs print at startup with the spring.env
path, and the full Managed URL (which carries the daemon token) prints on an
interactive terminal; ports auto-increment when busy. The launcher verifies
only that something Spring-Boot-shaped answers /actuator/health — it
cannot prove that Spring loaded this run's spring.env, so restart Spring
whenever the launcher (and its rotating token and digest) restarts. If every
Turn then fails with hosted_harness_rejected in the panel: a Harness
400 means the Session Store wiring in spring.env did not load
(invalid_managed_session_store — an env file from an older run), while a
Harness 401 means a launcher restarted without restarting Spring —
re-source the new spring.env and restart Spring.
To wire the pieces by hand instead, keep the ordinary daemon on 4170 (the
vite proxy's default) and start the private Hosted Harness on a distinct
port. The profile refuses to start without credentials, so the launch
reuses the same credential pair the Prerequisites section exports for
Spring — CLI-side the token travels as QWEN_SERVER_TOKEN and the
capability digest as QWEN_HOSTED_HARNESS_CAPABILITY_DIGEST — plus
--no-web, which the profile requires and no environment variable supplies:
QWEN_SERVER_TOKEN="$QWEN_MANAGED_AGENT_HARNESS_TOKEN" \
QWEN_HOSTED_HARNESS_CAPABILITY_DIGEST="$QWEN_MANAGED_AGENT_CAPABILITY_DIGEST" \
qwen serve --profile hosted-harness --port 4171 --hostname 127.0.0.1 --no-web
— and Spring must point at it with the matching base URL. Spring's HTTP
Session Store stays off by default, and the Hosted Harness rejects every
attach without a store descriptor, so Spring also needs the three values
the one-shot launcher writes into spring.env — the connector refuses to
start when the store is enabled with a blank base URL or workspace ID.
Export all four together:
export QWEN_MANAGED_AGENT_HARNESS_BASE_URL='http://127.0.0.1:4171'
export QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED='true'
export QWEN_MANAGED_AGENT_SESSION_STORE_BASE_URL='http://127.0.0.1:8080'
export QWEN_MANAGED_AGENT_WORKSPACE_ID='local-dev-workspace'
All four are read once at JVM startup, so if mvn spring-boot:run is
already up on the 4170 value exported in Prerequisites, restart it with the
overrides in place.
--port is a request, not a guarantee: qwen serve moves to the next free
port on a collision, and 4171 is exactly where a daemon displaced from 4170
lands. Confirm each server's bound port in its startup line before exporting
the base URL above.
With both up, run from the repository root:
QWEN_DAEMON_URL=http://127.0.0.1:4170 \
QWEN_MANAGED_AGENT_JAVA_URL=http://127.0.0.1:8080 \
npm run dev:managed-agent-web
Open
http://127.0.0.1:5174/?managed=1&managedProvider=java&tenant=local-java-demo.
The standard WebShell entry proxies its existing routes to the ordinary daemon
and /api/agent/web-shell/v1/** to Spring. Use
managedSession=<sessionId> to deep-link a Managed Session. If the ordinary
daemon requires authentication, append its token as the usual #token=...
fragment.
managedProvider=java and tenant are local-development conveniences and are
honored only by the Vite development entry. Production hosts should construct
createJavaManagedAgentProvider(...) themselves and pass it to
WebShellWithProviders.managedAgentProvider; a trusted upstream must derive
the tenant instead of trusting a browser query parameter. Products that do not
have an ordinary daemon can continue to render the exported
ManagedAgentWebShell directly. Neither browser mode receives the private
Harness or Runtime Broker credentials.
Embedded Runtime Broker
Public foreground Shell admission
Stored AgentDefinition Sessions keep their compiled profile and approval mode;
the Shell switch changes only the built-in agent's default profile selection.
Every Shell Session still requires the switch and valid asking approval for
fresh execution. Harness load copies retain the definition identity alongside
suppressChildAgents.
QWEN_MANAGED_AGENT_WORKSPACE_SHELL_ENABLED=true selects the server-owned
hosted-workspace-shell/1 profile for newly created built-in qwen-code Workspace Sessions on both
public REST and WebShell, including empty creation. It defaults to false and
requires Workspace files plus its deployment prerequisites. Set
QWEN_MANAGED_AGENT_APPROVAL_MODE to default or auto-edit; Shell opt-in with
yolo, an empty or unknown mode fails startup. Shell always asks permission.
Existing Sessions keep their stored profile and approval mode.
Disabling only the Shell flag refuses fresh Turns on existing Shell Sessions
with 409 workspace_unavailable; original idempotency keys, accepted approval
responses, cancellation and settlement remain supported while Workspace files
remain enabled and persisted approval remains default or auto-edit. Files-off
or invalid approval can refuse attachment; neither case is bypassed by a
capability. Files Sessions retain their existing behavior. Shell Session responses
add foreground_shell (public) or foregroundShell (WebShell); false disables
fresh sending. Files and unbound Sessions omit the field, with no schema default.
workspaceTurns expresses creator/grant permission, not sufficient Shell Turn
admission: fresh sending also requires foregroundShell=true. Controlled W2 cwd
changes do not execute Shell and retain their own files, actor, path and
context-revision admission; the Shell flag is not a general Session freeze.
Shell close/archive/unarchive/delete remain unavailable, and are refused before creating a lifecycle operation. Existing G3 recovery refusals remain in force; no dispatched Shell is automatically replayed. Keep the opt-in off until the deployment passes #12904, #13010 and public FG6f physical failure gates. See the complete design.
Initial Workspace Turn (G0)
QWEN_MANAGED_AGENT_WORKSPACE_FILES_ENABLED=true opts in to an initial
Workspace Turn supplied with public Session creation: Read/Write/Edit under
hosted-workspace-files/1, or file tools plus mandatory-approval foreground Shell
under hosted-workspace-shell/1 when the Shell opt-in above is enabled. The
WebShell creation adapter uses the same admission. This requires the Hosted
Harness and HTTP Session Store, default or auto-edit approval for Shell
(yolo is also valid for files), and a local-process, session-isolated
Broker with configured runtime-broker.workspace-mounts. Registry entries must
use managed-runtime-tools/1 and preapproved-workspace-tools/1, and their
tenant/storage identity must have a deployment mount. The trusted ingress must
provide an AuthenticatedTenantActor principal with read/create grants; a caller
header alone does not authenticate an actor. For local runs and the
packaged-stack E2E, which have no ingress,
QWEN_MANAGED_AGENT_TRUSTED_ACTOR_HEADER names a request header whose value is
then trusted as the actor for the request's tenant. It is disabled by default;
never enable it where untrusted clients can reach the server.
QWEN_MANAGED_AGENT_APPROVAL_MODE defaults to yolo. In default and
auto-edit, any caller with a read grant on the bound Workspace can list and
inspect pending permission Actions through the public API or WebShell;
answering one needs the Session's recorded owner or a caller holding OPERATOR
or above, and that read grant as well. A Shell Session is the exception:
only its recorded owner may answer, because that answer runs a command. Responses are durable, idempotent
operations; their final result follows the committed Harness decision.
QWEN_MANAGED_AGENT_APPROVAL_TIMEOUT defaults to 10m and accepts 1s to 24h.
The approval mode is pinned at Session creation and must be confirmed by the
Harness on creation and load.
Submit agent_id: "qwen-code", the existing workspace selection and input
through POST /v1/agents/sessions. The server persists
hosted-workspace-shell/1 with mandatory default/auto-edit approval when the
Shell deployment opt-in is enabled, otherwise hosted-workspace-files/1 with the
configured file approval mode. This also applies to empty bound creation. The
Session Store uses the persisted Workspace ID; public callers cannot choose the
profile. W1c offline migration remains files/1-only and refuses persisted Shell
definitions with migration_profile_unsupported. Configure the
Harness's deployment-owned --managed-runtime-broker-url and
--managed-runtime-broker-token options to reach this Broker. A repeated creation key returns
the original Session/Turn; changed input conflicts. Disabling the Workspace files
opt-in refuses creation with input, including replays, while empty bound creation
remains available. The Shell opt-in selects the persisted profile; it does not
independently admit creation input. Upgrade the Harness before enabling the
Shell flag: an older Harness ignores suppressChildAgents and would advertise
the child agent tool. The directory mounted for a Workspace is trusted deployment data,
not a filesystem sandbox.
Later Turns may be submitted by any caller holding OPERATOR on the bound
Workspace under the same opt-in while the Session's creator-keyed execution
facts hold (the Workspace registry still backs the binding and stays ACTIVE,
and the actor recorded by the Workspace create command keeps OPERATOR or
above; the WebShell adapter's per-caller workspaceTurns capability flag
mirrors exactly that rule, though the public surface publishes no such
flag). Shell Sessions also require the Shell opt-in and valid persisted
approval for fresh sending. workspaceTurns does not certify Shell admission;
Shell clients also require foregroundShell=true. Such a caller may cancel
the Session's running Turns and rename the Session — a readable actor below
OPERATOR gets 403 session_operation_forbidden, and an admitted OPERATOR
blocked by the shape or fact gates gets 409 workspace_unavailable.
Workspace close follows its separate close capability and lifecycle admission.
Public Shell close, archive, unarchive and delete stay unavailable, including
protocol-0. Archive, delete and unarchive of non-Shell Sessions follow their
separate retention capabilities after reliable Workspace close. Controlled cwd
changes ship below (W2); broad Workspace capability advertisement remains
gated. Public foreground Shell does not publish the child agent tool, and it
has no Session message funnel because that funnel shares the child
orchestrator's write chain.
Background Shell, Monitor and complete in-flight recovery remain separate slices.
The existing EmbeddedRuntimeBroker is used through production configuration;
no direct store admission or test Broker replacement is needed.
Broker authentication and writer credentials
QWEN_MANAGED_AGENT_AUTH_MODE selects how the public surface
(/v1/agents/** — including the bare POST /v1/agents collection route —
and the WebShell adapter) authenticates its caller:
auto (the default) resolves to open when server.address is loopback
and refuses to start otherwise, open keeps the header-asserted tenant and
the optional trusted-actor stand-in for local runs, and signed requires
every public request to carry X-Qwen-Actor-Id,
X-Qwen-Signature-Timestamp (epoch seconds, within
QWEN_MANAGED_AGENT_AUTH_ALLOWED_DRIFT, default 5m, minimum 1s) and
X-Qwen-Signature: v1=<lowercase hex HMAC-SHA256> over the canonical string
below, keyed by QWEN_MANAGED_AGENT_AUTH_SIGNING_KEY (at least 32 bytes):
"qwen-broker-auth-v1\n" + METHOD + "\n" + undecoded request path + "\n"
+ raw query string (empty when absent) + "\n" + tenant + "\n" + actor + "\n"
+ timestamp + "\n" + lowercase hex SHA-256 of the raw request body + "\n"
+ the Idempotency-Key header value (empty when absent)
A repeated Idempotency-Key header answers 400 invalid_request, and a body
beyond QWEN_MANAGED_AGENT_AUTH_MAX_SIGNED_BODY_BYTES (default 10 MiB)
answers 413 payload_too_large. Signed mode cannot be combined with
QWEN_MANAGED_AGENT_TRUSTED_ACTOR_HEADER.
QWEN_MANAGED_AGENT_SESSION_STORE_BINDING_KEY switches writer tokens from
self-minted to broker-provisioned: the writer credential becomes an HMAC
over (tenantId, workspaceId, sessionId) that the Broker hands to the
Harness in the attach payload, and the store rejects any other token,
including during a free lease window. A configured key must be at least 32
bytes. A Broker that intentionally serves the store over plaintext HTTP
inside a trusted network sets
QWEN_MANAGED_AGENT_SESSION_STORE_ALLOW_INSECURE_HTTP=true, which the
attach payload forwards to the Harness so its client accepts the URL. The
internal surface
(/internal/**) may move to its own listener via
QWEN_MANAGED_AGENT_INTERNAL_SERVER_PORT and
QWEN_MANAGED_AGENT_INTERNAL_SERVER_ADDRESS (default 127.0.0.1); either
port then answers 404 for the other surface's routes. Leaving loopback —
public or internal — requires signed mode or a configured binding key
respectively, unless QWEN_MANAGED_AGENT_AUTH_ALLOW_INSECURE_BIND=true
explicitly overrides the guard. Loopback is a trust boundary only as strong
as the host: a shared host that runs untrusted workloads (including
model-generated commands) should configure a binding key even on loopback.
Controlled cwd change (W2)
Under the same QWEN_MANAGED_AGENT_WORKSPACE_FILES_ENABLED opt-in, any caller
holding OPERATOR on a bound Session's Workspace moves its relative directory
within the same Workspace:
curl -sS -X POST \
http://127.0.0.1:8080/v1/agents/sessions/$SESSION_ID/cwd \
-H 'Content-Type: application/json' \
-H 'X-Qwen-Tenant-Id: demo' \
-H 'Idempotency-Key: cwd-1' \
-d '{"cwd_relative":"services/api","expected_context_revision":1}'
202 admits a durable cwd_change operation; it does not activate the
directory. The change is same-Workspace only and requires the caller's OPERATOR
role plus the Session's creator-keyed execution facts (the registry still backs
the binding and stays ACTIVE, and the recorded create actor keeps OPERATOR or
above; the V56-persisted initiator is re-checked at settlement the same way, so
demoting either actor fails the operation with workspace_unavailable), an idle
Session (409 session_context_busy while a Turn or another operation is open)
and a matching expected_context_revision (409 context_revision_conflict
otherwise); a retry with the same key returns the original operation even after
it completes. A background worker verifies the target against the deployment
mounts and commits one transaction that bumps cwd_relative and
context_revision, marks the operation completed and appends
session.context.changed; poll the operation through the existing query route
or await the event. A target the mount cannot verify fails the operation with
failure_code and leaves the Session's binding untouched; refused changes
never retry, while a mount fault the probe cannot reach (a stale export)
retries internally up to an 8-attempt budget and then fails the same type,
releasing the Session back to turns and lifecycle operations. Legacy Sessions answer 400 unsupported_feature, an unreadable
actor 404 session_not_found and a readable actor below OPERATOR 403 session_operation_forbidden, matching the sibling lifecycle refusals; a
probe refusal the turn layer would share also uses 409 workspace_unavailable, and so does an admitted OPERATOR once the
creator-keyed facts moved. The WebShell adapter offers the same flow as
/api/agent/web-shell/v1/sessions/cwd/change plus /operations/query.
Subsequent turns acquire a fresh Runtime Session and install the new context
before tools run, so an unverifiable change can never redirect tool execution.
Broker deployment
The Broker starts before the first Hosted Harness connection, so the supported startup order is Spring/Broker first, Hosted Harness second, traffic last. The Harness SDK handshake is lazy and occurs on the first admitted Turn.
For the single-node local-process provisioner, also set:
export QWEN_MANAGED_AGENT_RUNTIME_BROKER_ENABLED='true'
export QWEN_MANAGED_AGENT_RUNTIME_BROKER_TOKEN='replace-me'
export QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY_ID='local-dev-v1'
export QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY='replace-with-base64-encoded-32-byte-key'
export QWEN_MANAGED_AGENT_WORKSPACE_CWD='/absolute/authorized/workspace'
export QWEN_MANAGED_AGENT_RUNTIME_STATE_DIRECTORY='/absolute/private/state'
export QWEN_MANAGED_AGENT_NODE_EXECUTABLE='/absolute/path/to/node'
export QWEN_MANAGED_AGENT_RUNTIME_WORKER_ENTRY='/absolute/path/to/dist/cli.js'
export QWEN_MANAGED_AGENT_CLI_ENTRY='/absolute/path/to/dist/cli.js'
Two optional knobs change how the Broker listens and how long it waits for a dispatched v3 execution's result:
# Default false: the Broker refuses to bind a non-loopback address. This face
# is plaintext HTTP with one global bearer token and no per-tenant
# authorization, so set it only behind a layer that terminates TLS and
# authorizes callers — restricting the network alone still puts that token on
# the wire, and whoever reads it owns every execution the Broker admits.
export QWEN_MANAGED_AGENT_RUNTIME_BROKER_ALLOW_NON_LOOPBACK='false'
# Default 30m, minimum 1s: how long the Broker keeps polling the worker for
# a dispatched v3 execution's result. When the window lapses the execution
# is marked UNKNOWN instead of polling on, so a value shorter than your
# longest tool call degrades that call to UNKNOWN. A suffix-less number
# binds as milliseconds, which startup refuses. Raising it above 30m buys
# nothing on the shipped path: the TypeScript client stops observing a v3
# execution at its own fixed 30-minute deadline.
export QWEN_MANAGED_AGENT_RUNTIME_BROKER_V3_RESULT_WINDOW='30m'
When QWEN_MANAGED_AGENT_WORKSPACE_ID is omitted, the server derives the same
16-character SHA-256 workspace ID that Qwen Code uses from the canonical
workspace path. An explicitly configured ID must match that value or startup
fails before traffic is accepted.
The Hosted file profile connects through the private Broker endpoint, which
defaults to http://127.0.0.1:4182. When enabled, the embedded
Broker always uses the Spring DataSource and Flyway-managed Runtime tables;
it does not fall back to in-memory repositories. The credential key must decode
to exactly 32 bytes and protects persisted Runtime seeds and static Runtime
credentials with AES-256-GCM. By default, local worker ownership is durable:
the Broker registers every launch and a restarted Broker adopts the same live
worker. This requires Linux and fails startup elsewhere; on such hosts set
QWEN_MANAGED_AGENT_RUNTIME_DURABLE_LOCAL_PROCESS=false together with
QWEN_MANAGED_AGENT_RUNTIME_TRUSTED_LOCAL_REBOOT_RECOVERY=false to keep
worker ownership ephemeral (a restarted Broker then cannot adopt it). The state directory
must be persistent local storage, owned by the Broker user with mode 0700,
without symlinks, outside every configured Workspace root. The expected
owner is resolved from the process UID, so a numeric UID without a passwd
entry is fine. Workers and tools
must be trusted; same-UID hostile tools and multi-host or remote storage are
unsupported. Keep the host machine ID, SQL credential key, placement mapping,
state directory and worker command stable across Broker restarts. Shutdown and
late lease discard detach from registered workers instead of killing them.
/etc/machine-id must be nonempty and stable, and Linux must expose the PID
and time namespaces (/proc/self/ns/pid and /proc/self/ns/time; the latter
requires Linux 5.6 or newer with CONFIG_TIME_NS). An empty or malformed
identity fails startup the same way as an absent one, naming both opt-out
switches. The service
manager must let workers survive a Broker exit: systemd's default
KillMode=control-group kills them, as does restarting a container whose main
process is the Broker. Configure the service to leave child workers running
(for example, systemd KillMode=process) and use an init that reaps orphaned
processes. The Broker recognizes Z/X workers as exited even before they are
reaped.
Missing or damaged records and worker death do not authorize replacement;
worker death does not prove escaped writers stopped. No host reboot reclamation
is enabled by this option alone. Old v1 handles cannot be upgraded by guessing identity.
This option does not retire idle workers or prune their registration and lock
files. With session isolation, each Hosted Session can retain a separate idle
worker across Broker restarts; budget process, memory and state-directory growth
for it. Physical cleanup needs an evidence-preserving lifecycle;
do not delete records to reclaim capacity.
See the adoption design.
Trusted same-host Linux reboot recovery is also on by default. It requires
durable local mode with the local-process provisioner, so a deployment that
opts out of durable local workers or uses another provisioner must set
QWEN_MANAGED_AGENT_RUNTIME_TRUSTED_LOCAL_REBOOT_RECOVERY=false. A changed kernel boot ID on the original machine can prove
that original local writers stopped; worker-only death still cannot. The
service scans eight saved bindings every five seconds, independently of current
Session grants, and clears only the original SQL holder after all execution
receipts become terminal. Recovery never starts a replacement worker or replays
an unknown execution. A later authorized request may create a new generation.
Keep the same Broker user, local disks, machine identity and SQL keys; remote
writers, restored/cloned snapshots and external jobs that recreate writers are
outside this contract. Where a matching boot identity cannot be trusted as stop
evidence, set the option to false. The
reboot recovery design
distinguishes portable test evidence from the dedicated Linux reboot acceptance
gate completed at W0e-3 head 8c2b626c. A systemd soft reboot is not stop
proof because it does not change the kernel boot ID.
The Kubernetes adapter's real-cluster fault matrix remains a production gate. This standalone reference keeps the one configured directory for legacy unbound Sessions. Persisted bound Sessions use the private Workspace execution path below.
Flyway V12 aligns the Runtime tables with the Broker's own schema.sql, which
its JDBC repositories are written against. RuntimeBrokerFlywaySchemaTest
fails when the two definitions differ, so a change to either one needs a
matching change to the other. V12 replaces two primary keys. MySQL rejects this
when sql_require_primary_key is set: V12 fails before it changes anything, and
Flyway records the failure. Unset the variable, run Flyway repair, and start
the server again.
Private Workspace tool execution (W0c-3)
The worker entry is the built CLI bundle; the server launches it with
managed-runtime-worker. Configure canonical existing roots using Spring
configuration (all Brokers sharing the database must use the same mappings):
qwen:
managed-agent:
runtime-broker:
workspace-mounts:
- tenant-id: tenant-a
storage-id: storage-a
root: /absolute/canonical/workspace-a
An empty mapping list rejects bound Session execution. This path requires
local-process provisioning and session isolation. The Session must be
created through W0b with a Registry configuration reference of
managed-runtime-tools/1 and policy reference of
preapproved-workspace-tools/1. The original creator must still have read and
create grants. Other frozen configuration pairs are refused.
The private Broker can acquire, execute Read/Write/Edit/foreground Shell, and
release these Sessions. One Runtime Session holds each tenant/storage pair
until the original worker closes its execution gate. Lost or ambiguous
responses retain the SQL holder; there is no timeout-based takeover. The
provider and file tools do not confine access to the mount root: Read/Write/Edit
and Shell can reach other paths allowed by the worker's host permissions.
Foreground Shell may create detached descendants. Use this only with trusted
local workloads. The W0e recovery above handles trusted host reboot; it
does not provide physical isolation or recovery after worker-only death.
Public bound Turn admission is limited to the initial Workspace Turn described
in G0 above (file tools, or mandatory-approval foreground Shell under the Shell
opt-in; the public Shell tool list does not include the child agent tool) and
later Turns submitted by any caller holding OPERATOR on the Workspace under the
same opt-in while the Session's creator-keyed execution facts hold (the WebShell
adapter's per-caller workspaceTurns capability flag mirrors that same rule;
the public surface publishes no such flag). Shell Sessions additionally require
the Shell opt-in and valid persisted approval for fresh sending; workspaceTurns
does not by itself admit a Shell Turn, which also requires foregroundShell=true.
Such a caller may also cancel the Session's running Turns and rename the Session.
Later Turns run under the creator's Workspace grants, so an actor without a read
grant keeps the existing session_not_found invisibility, a readable actor below
OPERATOR is refused session_operation_forbidden, and an admitted OPERATOR whose
Session lost the creator-keyed facts meets the family's domain
workspace_unavailable. Public Shell close, archive, unarchive and delete stay
unavailable, including protocol-0. Public close follows its separate close
capability and lifecycle admission. Archive, delete and unarchive follow their
separate retention capabilities after reliable Workspace close;
the Shell profile reaches public creation only through the deployment Shell
opt-in described above; callers cannot select it.
See the bilingual execution design
for the exact boundary.
Hosted files/Shell file history additionally requires persistent worker backup
storage. Configure an absolute QWEN_HOME in the Broker/worker environment and
mount it as durable storage writable by the worker OS user. Backups are stored
at $QWEN_HOME/file-history/<Harness Session ID>/; without the override they
use the worker user's ~/.qwen/file-history/. Preserve this directory alongside
the Workspace and SQL database. A Workspace mount alone does not preserve these
backup bytes, and referenced backups must survive worker/container restarts.
The stock image runs as UID 10001; derived images must provision appropriate
write access for their worker user.
Roll out the Broker and worker bundle before the Hosted Harness. An older Broker rejects raw-history control, including the bind before a read-only tool turn. The Harness releases a definite rejected bind and ends the turn with an error; it does not run unbacked writes. Missing backups also refuse tool turns until the original backup data is restored. Unknown or partial effects still require operator recovery. See the bilingual file-history design for record capacity and rewind semantics.
Verified original-mount recovery (W1a)
W1a's physical mount guard is opt-in for process restart in a trusted, single-host OpenJDK 21/Linux local-process deployment with a persistent, unambiguous root birth time. Taking the next tool Turn after a Broker restart requires durable-local-process=true. Whole-host restart succeeds only while the registered physical identity still matches. Flyway
V25 adds a persistent storage registration, independent mount_birth_time and mount fence. Leave
QWEN_MANAGED_AGENT_RUNTIME_VERIFIED_WORKSPACE_RECOVERY_ENABLED=false while
upgrading every Broker and Harness instance. An unregistered mount is refused
once the option is enabled; it is never registered from the directory found at
startup.
Marker v2 stores birth time as a canonical Instant string preserving nanoseconds. The guard reads creation time, mtime, device and inode in one unix attribute snapshot and rejects creation time at/before epoch or equal to mtime, which OpenJDK 21 may return when birth time is unsupported. A real birth time equal to mtime is also conservatively refused; prepare the project layout or update the root's mtime offline before retrying. Mtime is not an identity field, and ordinary mtime changes do not invalidate an unchanged birth time. The marker contains an application-specific HMAC-SHA256 host ID, keyed by the trimmed machine-id UTF-8 bytes with Qwen-Code/verified-workspace/v2 as input, rather than the raw machine ID.
Keep the storage root outside every Git worktree, with Session cwd in a child project directory. .qwen-managed-storage.json is an administrator maintenance file: do not read/write it through model tools or subject it to Git cleanup/stash. Tools are not confined by this layout. Missing or conflicting markers close admission; completed registrations never automatically republish them, including on a same-UUID retry. Marker repair needs a separate design.
Prerelease W1 V21/V24 databases and marker v1 cannot be directly upgraded to V25/v2. Experimental W1 V24 conflicts with main’s Actions V24; renaming the migration does not upgrade an existing database. Do not bypass the mismatch with Flyway repair, outOfOrder or manual history edits. Preserve backups and design an explicit offline migration for deployments with retained data; only disposable test deployments may be rebuilt.
Stop all processes and external jobs that can write the storage, account for
old Runtime holders and verify the original root before registration. Apply
Flyway migrations, then run the private maintenance entry from this module on
the same Linux host. Give a stable canonical lowercase, hyphenated 36-character UUID to each operation and reuse it after a
crash. Database credentials come from W1_JDBC_URL, W1_JDBC_USER and
W1_JDBC_PASSWORD environment variables:
mvn -q -DskipTests compile exec:java \
-Dexec.mainClass=com.alibaba.qwen.code.managedagent.store.WorkspaceStorageRegistrationMain \
-Dexec.args='register tenant-a storage-a /absolute/canonical/workspace-a <operation-uuid> --offline-confirmed'
The same entry is available from a shipped Spring Boot fat jar, without the source checkout or Maven:
java -cp /path/to/app.jar \
-Dloader.main=com.alibaba.qwen.code.managedagent.store.WorkspaceStorageRegistrationMain \
org.springframework.boot.loader.launch.PropertiesLauncher \
register tenant-a storage-a /absolute/canonical/workspace-a '<operation-uuid>' --offline-confirmed
inspect <tenant> <storage> <canonical-root> is read-only. The same entry also
accepts fence or restore-original with a mount revision and the exact
operation UUID; both require --offline-confirmed. A fence has no timeout and requires every holder field to be clear after exact-owner cleanup. Entering a new fence and restoring both require the intact registered root identity and marker. W1 cannot force-fence, re-register or repair a changed identity or missing marker; admission stays closed until the verified original mapping is re-presented, or a separately designed offline repair is performed. Stop new admissions, settle or cancel original executions, prove writers stopped, release holders and stop service/external writer processes before fencing.
restore-original only reopens the still-verified original mapping after its
holder is clear. Both commands accept retries with the same operation UUID;
restoring increments the mount revision, so a delayed old fence cannot reopen
maintenance. Completed registration and restore retries revalidate identity and marker; concurrent same-operation retries do not advance the revision twice. Inspect reports state, revision, active/completed operation, holder and independent identity/marker status, including while fenced. The flag records the operator's offline check; the program
cannot stop arbitrary processes or external writers itself. Do not use it on
a live shared storage.
After registration, enable
QWEN_MANAGED_AGENT_RUNTIME_VERIFIED_WORKSPACE_RECOVERY_ENABLED=true on the
whole upgraded deployment. On each new attachment, model submission, Runtime
claim and execute, the server compares the configured canonical root against
the SQL registration, Linux host/device/inode/birth-time identity and the root marker.
A missing or conflicting marker, replacement root, missing saved cwd or fenced
storage blocks new work; original execution status/cancel and authorized
history remain on their saved identities. The marker is a continuity check,
not a backup or protection against a malicious same-UID writer. See the
W1 design.
Hosted Workspace cold-load validation is always enabled, independently of the Java mount-guard option. Omitted tool profile and Shell captureBytes use the saved definition; supplied values must match exactly. Saved approval settings remain pinned. Integrity checks run before new model work or Broker prepare/execute and cover retained private resources plus complete remote Shell output, including pages, segments and empty-stream seals. Preserve O2 recovery of original results_ready, consumed-final and not_started receipts. An incomplete receipt may produce a blocked ACK or original-history repair before load is refused, so refusal does not promise zero journal writes or ACKs. Restore validation uses a fixed committed cut, and continuation still requires current writer ownership and authorization. Missing old resources or unsupported recovery domains block loading. Passive Harness loading does not implement unknown-execution cleanup; use original Broker execution identities. Rollback to old binaries requires entry points to remain stopped because those binaries ignore the fence columns. Public
Workspace next-turn admission for any caller holding OPERATOR on the bound
Workspace, while the Session's creator-keyed execution facts hold, under the G0 opt-in
described above has landed; public Workspace resume still requires
product-route integration, and this internal guard is not a public resume
capability yet.
Build the container from the repository root:
docker build -f packages/sdk-java/managed-agent-server/Dockerfile .
The stock image contains the Java control plane only. Use the static Runtime provisioner, or provide a derived image/mount with Node.js and the Qwen worker artifacts, before enabling the local-process provisioner in a container.
The image keeps the application's 127.0.0.1 defaults, so a plain
docker run -p 8080:8080 publishes nothing: -p DNATs to the container's
bridge IP, and a loopback listener is unreachable through it. Publishing is
an explicit opt-in, and the server refuses a non-loopback bind under the
shipped auto auth mode, so the command must name an authentication
posture — signed mode, as the introduction recommends (see "Broker
authentication and writer credentials" above). The signing key and the
datasource password are passed valueless so Docker forwards them from the
shell environment — the = spelling would leave either secret in the
command's argv for the container's whole lifetime. A valueless -e
forwards the variable of the same name, so export
QWEN_MANAGED_AGENT_AUTH_SIGNING_KEY and SPRING_DATASOURCE_PASSWORD before
the run. The datasource must also be
named: the Prerequisites' 127.0.0.1 default is the container itself from
inside, so the image cannot boot against it:
docker run -p 8080:8080 \
-e QWEN_MANAGED_AGENT_SERVER_ADDRESS=0.0.0.0 \
-e QWEN_MANAGED_AGENT_AUTH_MODE=signed \
-e QWEN_MANAGED_AGENT_AUTH_SIGNING_KEY \
-e SPRING_DATASOURCE_URL='jdbc:mysql://<db-host>:3306/qwen_managed_agent' \
-e SPRING_DATASOURCE_USERNAME='qwen' \
-e SPRING_DATASOURCE_PASSWORD \
<image>
or the deliberately unauthenticated
-e QWEN_MANAGED_AGENT_AUTH_ALLOW_INSECURE_BIND=true override.
The qwen user the recipe names needs the grants from the Full WebShell
entry's MySQL one-liner — including SELECT on
performance_schema.user_variables_by_thread, granted at the
container-visible host — or Flyway's boot probe is denied (ERROR 1142)
and startup stops.
Publishing the API needs no Runtime Broker face: the broker ships disabled
by default, and a non-loopback broker bind is a separate opt-in — the broker
refuses one unless QWEN_MANAGED_AGENT_RUNTIME_BROKER_ALLOW_NON_LOOPBACK=true
is also set (see Broker deployment above).
The opt-in makes the authentication consequence load-bearing. Under the
shipped default — auto resolving to open — there is no HTTP
authentication: tenancy is whatever X-Qwen-Tenant-Id says. Signed mode
HMAC-authenticates every request's tenant and actor headers; the
insecure-bind override publishes with request authentication off. Either way the tenant-scoped API is
exposed to anything that can route to the listener. That exposure does
not depend on publishing a port: on the default bridge network the API
also answers on the container's own bridge address, so every process on
the Docker host and every peer container on that bridge can reach it
with no -p at all. Run a published deployment behind an ingress that
authenticates the tenant before setting X-Qwen-Tenant-Id, map ports
only inside your own network policy, and attach the container only to
networks whose peers you trust.
Managed Session Store verification
Unit and H2 contract tests run with the normal Maven test phase. mvn verify
additionally runs the SpotBugs high-confidence gate (Maven 3.8.9+): a new
warning fails the build, and a false positive goes into
spotbugs-excludes.xml with a justification in the PR. To run the static
gates without the test suite, use mvn verify -DskipTests — it also runs
Checkstyle and the Spring Boot repackage, and the full suite includes
environment-sensitive timing tests that can fail on a local machine, so CI is
the arbiter; for SpotBugs alone, run mvn compile spotbugs:check. The
optional real-MySQL profile also verifies schema upgrade,
exact bytes, public Item/Snapshot projection, and the independent-JVM Managed
Session Store crash/takeover path:
mvn -Pmysql-integration \
-Dmysql.url='jdbc:mysql://127.0.0.1:3306/managed_agent_test' \
-Dmysql.user=root \
-Dmysql.password= \
verify
Use a disposable database: the integration test creates and deletes fixture rows within the selected schema.
Real-model end-to-end check
The full-chain script starts Spring with its embedded Runtime Broker and runs
both the Hosted Harness and the worker from the packaged dist/cli.js: the
Harness as node dist/cli.js serve --profile hosted-harness, and each worker,
launched by the Broker, as node dist/cli.js managed-runtime-worker. No
separate worker bundle exists. The G0 integration test
(HostedPublicWorkspaceIT) uses the same packaged dist/cli.js.
The real-model run below creates its Session through the public route as a
Workspace-bound G0 Session, and proves the physical tool execution through
the durable qwen_tool_execution record (exactly one SETTLED row) rather
than a public item.tool_call.* event — Broker-worker tool calls are not
published without O2 tool publication. The run needs live model credentials,
so no CI job executes it; it has been run locally as evidence (macOS,
qwen3.8-max), and a green CI run therefore says nothing about this mode. The
script also needs java, mysqld, mysql and mysqladmin on PATH; it
starts its own temporary MySQL server and exits before starting anything else
when a command or a required file is missing.
Build the required artifacts first, then run (the Maven steps need Maven
3.8.9+ — the SpotBugs gate rides the verify phase that install traverses):
npm run build && npm run bundle
mvn -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true install
mvn -f packages/sdk-java/runtime-broker/pom.xml -DskipTests install
mvn -f packages/sdk-java/managed-agent-server/pom.xml clean package
npm run test:e2e:managed-agent-server -- --model moonshot/kimi-k3
For the deterministic durable-owner failover check, use the same built artifacts and run:
npm run test:e2e:managed-session-failover
This mode uses a local fake model, completes one Turn, kills the Spring and Hosted Harness process trees, deletes their old local homes, starts replacement owners against the same MySQL store, and verifies that the second Turn sees the first Turn's prompt and answer.
The in-flight and continuation variants run the same replacement-owner proof
through a physical Workspace file tool execution. The runner configures the
G0 public Workspace admission for every mode — it seeds the Workspace
registry and access grant as deployment data, enables the G0 file admission,
and uses QWEN_MANAGED_AGENT_TRUSTED_ACTOR_HEADER for its local actor — and
the real-model check and both tool-driven variants create their Sessions
through the public route as Workspace-bound Sessions, while
--session-failover deliberately stays unbound to exercise the plain
durable-owner takeover:
npm run test:e2e:managed-inflight-failover
npm run test:e2e:managed-continuation-failover
Both modes require Linux: the replacement owner retires the dead worker's
Runtime binding through the durable local-Worker reclaim (#12380 W0e), which
runs on Linux only. The runner enables durable-local-process for these modes
and refuses other platforms with an explicit error; the Hosted MySQL CI job
runs both.
The in-flight mode holds the first Broker :start request after the Harness
has durably committed its await_runtime checkpoint, kills the original
Spring and Hosted Harness process trees, deletes their homes, and starts
replacement owners. It requires the replacement Harness to use the original
executionCallId, execute the physical tool exactly once, continue the
original Prompt without replay, and commit one public terminal event. The
continuation mode kills the Harness after the first published text chunk and
requires one tool execution, one further continuation, only the replacement's
answer in the public transcript, and one terminal event. Both modes run in the
Hosted MySQL CI job.
The --big-output long-answer mode is also Workspace-bound:
npm run test:e2e:managed-big-output
It streams a long answer, verifies the complete public text and stored record,
deletes the original Harness and Runtime homes, and checks the complete answer
in a cold replacement's model context. A short answer remains inline. This
mode executes no tools and has no Runtime binding to reclaim, so it keeps
durable-local-process disabled and does not require Linux.
A zero-delay run checks the real-model path as shown above; a controlled cold-start delay additionally tests output before Runtime readiness:
npm run test:e2e:managed-agent-server -- \
--model moonshot/kimi-k3 \
--runtime-delay-ms 45000
That run additionally requires the first model event to precede Runtime readiness whenever the delay reaches the 15 seconds the acceptance criterion names. A deterministic controlled-model proof of the same ordering is tracked in #12941.
The real-model check extracts only the selected model provider, its referenced
environment credential, the selected model, and the authentication policy
from the supplied settings file into a private temporary Qwen home. It does
not copy hooks, MCP servers, extensions, tools, permissions, or other provider
credentials. The runner removes that file, the MySQL data directory,
workspaces, and child processes on exit. Override the source with
--settings /path/to/settings.json; credentials are never printed by the
runner.
W1c offline Workspace migration
See the English design and Chinese design. Run only after every service is upgraded, admission/dispatch is disabled, accepted work is settled, Harness writers are stopped, and automatic restart is disabled. The source remains accessible on the same trusted Linux host. Export the original canonical absolute QWEN_HOME in the private maintenance and registration commands as well as the Broker/Harness environment, with no symlink components. fileHistoryRoot must equal the canonical $QWEN_HOME/file-history directory. Preserve that home and its independent history volume; do not move it with the Workspace.
W1c adds Flyway V48–V50 after main's V47 channel-persistence migration, preserving all published migration bytes and applied history. Databases using earlier unpublished W1c migration numbers require fresh disposable fixtures; do not repair production Flyway history to reuse them.
The private artifact is qwen-managed-agent-server-0.1.0-alpha-workspace-migration.jar. Set W1_JDBC_URL, W1_JDBC_USER, W1_JDBC_PASSWORD, W1_RUNTIME_CREDENTIAL_KEY_ID, and W1_RUNTIME_CREDENTIAL_KEY to the original deployment database and Broker credential key. The full request file contains version (the JSON integer 1), migrationOperationId, tenantId, storageId, fenceOperationId, captureOperationId, mountRevision, sourceRoot, targetRoot, bundleRoot, fileHistoryRoot, stateDirectory, nodeExecutable, and cliEntry. Fence admission is storage-scoped for bound Sessions; unbound legacy Sessions are outside that ownership. Metadata transactions share the tenant placement lock, so other storages of the same tenant may wait until those transactions finish; file scans and physical retirement hold no such lock. Paths must be canonical absolute deployment paths; roots and the durable Runtime state directory cannot overlap. IDs must be distinct UUIDs. Keep this exact request file for retries.
Before starting a new operation, the original Runtime state directory and retained history directory must exist at canonical paths, and the unchanged Linux identity reader must prove the history volume, including unambiguous birth time. Run maintenance as the original service user; the Runtime state directory must belong to that UID with exact POSIX 0700 permissions, checked by the same durable provider validator without creating or changing it. Failure returns migration_state_unavailable or migration_history_unverified before creating a migration row/fence or retiring placements. The target copy is still prepared after retirement. Run java -jar <migration.jar> retire <request.json> --offline-confirmed first. Then use the existing registration command to fence the original revision with the request's fence ID, and capture the W1b bundle with the request's capture ID. Prepare the target Workspace through the external offline copy procedure, preserving modes and the copied source marker. Run prepare and then promote with the same arguments. The first prepare and every new promote attempt verify sealed content, live source, target, retained history and original physical identities. Replaying prepare after PREPARED or any completed operation returns the saved receipt without rescanning; use promote for fresh transition verification. inspect <request.json> reads progress and the original receipt. abort <request.json> --offline-confirmed requires all placements retired and W1a still fenced; it does not restart anything or remove target artifacts. After abort or invalidation, prepare a fresh external target copy matching the new capture before starting a new operation; foreign marker or temporary files are rejected.
If the original history root has birth time equal to mtime, including when the first Session backup creates it, preflight conservatively returns migration_history_unverified. With all writers stopped, confirm the filesystem exposes a genuine persistent birth time, then update only the original history directory's mtime as the service user and retry retire with the same request:
touch -m -- "$QWEN_HOME/file-history"
Do not recreate or move the directory, change QWEN_HOME, or modify retained backups. Mtime is not an identity field; the root's device, inode and birth time must remain unchanged. A filesystem without a provable birth time remains unsupported after touch.
Promotion increments mount revision once. Successful prepare and promote commits clear the migration's previous error code. Update the deployment's Workspace root and restart Broker/Harness with the original QWEN_HOME before opening admission. An old deployment mapping fails closed. New file Turns and undo use fresh Runtime identities. Source rows, messages, journal, keys and backup names remain unchanged. Failure preserves the fence; missing stop proof, unsupported profiles or drift require diagnosis. Reverse migration is a new verified operation at a higher revision.
The target marker is the sole manifest exception and must match the copied source marker or the exact operation-pinned target marker. Do not hand-edit it. No online drain, directory copying, public migration route, Shell/MCP/Hook migration or source-lost recovery is provided. Uninitialized retained members without a verifiable frozen private definition are refused. Production Linux/MySQL acceptance evidence must be recorded separately from injected-identity tests.
Stored agent execution
Sessions pin a stored AgentDefinition revision and digest at admission. permission_policy and tools select the Session's immutable approval mode and hosted tool profile. model accepts {} or { "id": "configured-model" }; instructions accepts UTF-8 text up to 64 KiB. Skills, MCP servers, environment templates and per-definition approval timeouts remain unsupported and are refused explicitly.
Set QWEN_MANAGED_AGENT_DEFINITION_MODELS to comma-separated IDs configured in the Harness's deployment modelProviders; it defaults to empty. Each ID must resolve to one configured provider route with deployment credentials. An absent or ambiguous route or unresolved credentials fails the Turn with model_unavailable before inference. Credentials never belong in definitions. Enable and configure the Managed Session Store for stored-definition execution; instructions are delivered as digest-addressed resource references and verified on load.
QWEN_MANAGED_AGENT_DEFINITION_TOOL_PROFILES defaults to hosted-workspace-files/1. Before opting into hosted-workspace-files/2, drain old workers and upgrade every provisioner according to #13166. Empty permission policies inherit the deployment approval mode (default yolo); use default for deployments that require approval.