Files
qwen-code/packages/managed-agent-server/README.md
T
Shaojin Wen e7fe14ee83 docs(managed-agent): generate the server configuration reference from its metadata (#15939)
* docs(managed-agent): generate the server configuration reference from its metadata

The server has about 180 settings, 84 of them mapped to QWEN_MANAGED_AGENT_*
variables, and no list of them with their variables, defaults and limits:
operators read a 2000-line developer README for them.

- The server module runs spring-boot-configuration-processor once more,
  over ManagedAgentProperties only (the two Druid beans share a prefix,
  which the processor refuses), and writes the configuration metadata.
  The compiled classes are unchanged. runtime-broker.ssh is marked
  @NestedConfigurationProperty, and STRICT_HARNESS_KEYS @Getter(NONE), so
  the metadata lists the one and not the other.
- configuration-descriptions.json holds an English and a Chinese
  description for each setting and section.
- scripts/generate-managed-agent-configuration-reference.mjs writes
  docs/users/managed-agent-configuration-reference{,.zh-CN}.md from the
  metadata, the packaged application.yml (variables and defaults), the
  ${qwen.managed-agent.*} placeholders in the main sources (settings read
  outside the properties class) and the descriptions. --check fails on a
  setting without a description, a description of no setting, or a stale
  page; the reference workflow runs it after the Java build.
- The archive ships the pages as docs/configuration{,.zh-CN}.md, and the
  deployment guide and the README link them.

* fix(managed-agent): link the shipped documents by their archive names

The archive renames each document it ships, so a link written against
the repository, such as the configuration reference's language switch or
the guide's link to it, named a file the archive does not have. The
packager now points every link between shipped documents at the
document's name in the archive and leaves every other link as written.

* build(managed-agent): take the configuration processor's version from the BOM

spring-boot.version is defined nowhere in this build, so the processor
path's <version> only resolved because the compiler plugin falls back to
dependency management. Say so by naming no version.

* test(managed-agent): name no tracked Markdown in a link fixture

classify-profile.test.mjs requires every Markdown file a test names to be
in TEST_READ_MARKDOWN, and the kept-link fixture's docs/runtime-broker.md
matched packages/managed-agent-server/docs/runtime-broker.md, which no
test reads. The fixture only needs a link to a file the archive does not
ship, so it names one that does not exist.
2026-10-31 03:23:30 +08:00

2007 lines
168 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Qwen Managed Agent Server
The application lives in `packages/managed-agent-server`, alongside the Java SDK. It builds as one Maven reactor with six production modules and a verification module. The executable remains one Spring Boot service. Install the Java SDK before building the reactor; see [Runtime Broker internals](docs/runtime-broker.md).
Every configuration property, with its environment variable and default, is listed in the [configuration reference](../../docs/users/managed-agent-configuration-reference.md). It is generated from this module's configuration metadata and the descriptions in `configuration-descriptions.json`; a new property needs a description there, and a settings class declared outside `ManagedAgentProperties` needs `@NestedConfigurationProperty` on its field, or the metadata leaves it out.
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.
设计说明:[English](../../docs/design/2026-09-19-managed-agent-spring-server.md) |
[简体中文](../../docs/design/2026-09-19-managed-agent-spring-server.zh-CN.md)
## Local embedded profile
The installed CLI starts local Managed mode with `qwen serve --managed --open`.
It checks local Java 21, downloads a matching server JAR on first use, and reuses
the verified versioned cache on subsequent starts. An explicitly preinstalled
runtime can instead bundle Java 21 for offline use. It needs no MySQL, Docker,
Maven or systemd. The ordinary server profile keeps its
existing MySQL defaults. Design:
[English](../../docs/design/2026-10-20-managed-local-serve.md) |
[简体中文](../../docs/design/2026-10-20-managed-local-serve.zh-CN.md).
The `central-release` Maven profile attaches sources, Javadoc and GPG signatures
for server publication, without attaching the auxiliary operator JARs. The
executable server JAR uses the `exec` classifier. Ordinary builds keep the existing attachments and do not activate Central
publishing. The protected `Release Managed server runtime` workflow sets the
artifact version from an existing CLI release tag and publishes identical JAR
bytes and checksums to GitHub Release assets and the official OSS mirror after
full SDK/server verification. Distribution does not require Central credentials,
GPG signing or a successful Central publication. Signed Maven publication remains
an independent operation using the existing profile. The CLI's first-use download
uses OSS/GitHub and requires no Maven installation. See the
[release procedure](../../docs/users/managed-local-serve.md#publish-the-first-use-jar)
and its [Chinese version](../../docs/users/managed-local-serve.zh-CN.md#发布首次使用的-jar).
The launcher selects the `managed-local` Spring profile and supplies an
existing canonical, owner-only `QWEN_MANAGED_AGENT_LOCAL_DATA_DIRECTORY`.
The directory and its private subdirectories must be owned by the current
user with mode `0700`; symlinks, noncanonical paths and data-directory paths
containing `;` are refused. The profile creates these directories:
| Path | Persistent contents |
| ------------------------ | ----------------------------------------------------------------------------- |
| `database/managed.mv.db` | Embedded H2 database with the canonical Managed schema |
| `runtime/` | Durable local Runtime worker state |
| `workspaces/` | Application-owned Workspace storage roots and their original identity markers |
| `legacy/` | Private fallback cwd for unbound Sessions |
H2 uses MySQL compatibility mode, lowercase catalog metadata,
`DB_CLOSE_ON_EXIT=FALSE` and `WRITE_DELAY=0`. Its embedded file lock and the
launcher's data-directory lock admit one local control plane. The connection
pool retains an idle connection to keep that file lock held. No H2 TCP server,
`AUTO_SERVER`, or Web console is enabled. Database URL overrides are refused;
the profile keeps the existing Flyway admission and structure validation and
never repairs foreign, partial, history-only or unsupported catalogs.
The launcher supplies the existing Harness URL/token/digest, Session Store
base URL and binding key, Broker token and credential encryption key, Node
executable and worker entry. `QWEN_MANAGED_AGENT_WORKSPACE_ID` is the first
16 hexadecimal characters of the SHA-256 of the canonical `legacy/` path;
it identifies the fallback Runtime and Session Store, independently of
the bound Workspace IDs created through the API. Public API requests use signed authentication;
the front door retains the signing key and fixed tenant/actor identity.
Java, Harness and Broker listeners must stay on loopback. Readiness is
`GET /actuator/health/readiness`, which includes the database and Spring's
readiness state. The public front door opens after readiness and the
authenticated Harness capability probe succeed.
The profile enables verified, durable local-process workers, file tools and
Shell with `default` approval mode. The launcher creates the first Workspace
through the existing signed WebShell creation API using a persistent
idempotency key. It creates a new application-owned Workspace; the shell's
current Git checkout is not copied, symlinked or imported. The existing
Workspace ownership and grant checks still apply. Tool publication remains
disabled unless separately configured; ordinary persisted tool receipts do
not require an object store.
Keep the database, Runtime state, Workspace roots, credentials and worker
`QWEN_HOME/file-history` together across restarts. Credentials never rotate
merely because the application restarts. The local profile defaults trusted
whole-host reboot recovery to `false`; macOS supports same-boot worker
adoption only. Linux operators may explicitly enable
`QWEN_MANAGED_AGENT_RUNTIME_TRUSTED_LOCAL_REBOOT_RECOVERY` under its existing
trusted-host contract. Missing or conflicting physical identity markers
remain closed to execution; startup does not re-register or repair them.
## Maven modules and Java packages
| Module | Responsibility | Internal dependencies |
| ---------------- | --------------------------------------------------------------------------------------- | -------------------------- |
| `contracts` | Service and repository interfaces, shared values, settings views and protocol rules | None |
| `control-plane` | Session, Workspace, catalog, lifecycle, Channel, automation and extension orchestration | `contracts` |
| `runtime-broker` | Runtime execution, binding, leases, cancellation and recovery coordination | `contracts` |
| `persistence` | JDBC/memory repositories, schema and atomic database operations | `contracts` |
| `adapters` | Harness SDK, Runtime providers, HTTP clients, OSS, MQ, Git and filesystem operations | `contracts` |
| `server` | HTTP endpoints, Spring assembly, startup and maintenance entry points | All five modules |
| `verification` | Unit, integration, architecture and packaging tests; not published | All six production modules |
The parent coordinate is `com.alibaba:qwen-managed-agent-parent`. All modules share its version; the server keeps `com.alibaba:qwen-managed-agent-server`. Each production Java package belongs to one module. Keep implementation modules independent of one another: their collaboration uses contracts injected by server configuration. SQL helpers sharing a connection remain in `managedagent.persistence` or `runtimebroker.persistence`. The Broker service lives in `runtimebroker.service`; physical clients and providers live in the adapter module. Maintenance applications retain their separate component scan.
Only `server` creates executable JARs. Its ordinary library JAR and the existing `exec` and maintenance classifiers are under `server/target/`. Dependencies are ordinary JARs nested under `BOOT-INF/lib` in executable artifacts. Install the reactor before using the server POM directly or embedding the library in another Maven project:
```bash
mvn -pl server -am -DskipTests install
mvn -f server/pom.xml spring-boot:run
```
Embedding imports `ManagedAgentConfiguration`; internal callbacks additionally import `ManagedAgentInternalConfiguration`. The standalone application also enables the public web configuration. Add the ordinary server artifact as the dependency; Maven resolves the other five modules transitively. The verification module is never installed or deployed. The standalone application YAML is excluded from the ordinary library JAR.
Server configuration accessors use Lombok through the server POM's annotation processor; enable annotation processing when compiling through an IDE. Keep accessors with normalization, copying or fallback logic explicit. Lombok is excluded from executable and maintenance JARs and is not a runtime dependency of embedding applications.
See the complete Maven boundary design: [English](../../docs/design/2026-10-29-managed-agent-maven-modules.md) | [简体中文](../../docs/design/2026-10-29-managed-agent-maven-modules.zh-CN.md).
## 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](../../docs/design/2026-09-25-managed-agent-review-corrections.md)
for the original split's merge gates; earlier preview timing and recovery
results below do not establish completion of later slices.
## API contract
`contracts/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;
`verification/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](../../docs/design/2026-10-07-managed-agent-actor-roles.md), 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
whenever the cursor is below the durable floor. Rebuilding projections or pruning
all retained events never resets that floor. Message queries page directly over
`ma_message`; a scoped opaque cursor fixes the membership range and complete
ordering position while message bodies and statuses remain live. Projection
rebuild invalidates the cursor. The default transcript returns recent Items and
the actual materialized coverage for SSE handoff, without reading the whole
history or unmaterialized event tail. Export reads forward within its initial
membership range and does not promise a fixed content version. Its retained
`snapshotThroughSequence` field names that fixed range; transcript coverage
names the current read watermark. A projection behind the replay floor or an
initial export behind committed events returns retryable
`message_projection_pending`. The historical `reload_snapshot` resync action
now means reload the live message view. See the
[removal design](../../docs/design/2026-10-29-managed-message-snapshot-removal.md)
([简体中文](../../docs/design/2026-10-29-managed-message-snapshot-removal.zh-CN.md)).
`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](../../docs/design/2026-09-27-managed-agent-api-contract.md) |
[简体中文](../../docs/design/2026-09-27-managed-agent-api-contract.zh-CN.md);
Session query: [English](../../docs/design/2026-09-27-managed-agent-session-query.md) |
[简体中文](../../docs/design/2026-09-27-managed-agent-session-query.zh-CN.md);
Event replay: [English](../../docs/design/2026-09-27-managed-agent-event-replay.md) |
[简体中文](../../docs/design/2026-09-27-managed-agent-event-replay.zh-CN.md);
Durable lifecycle: [English](../../docs/design/2026-09-28-managed-agent-durable-lifecycle.md) |
[简体中文](../../docs/design/2026-09-28-managed-agent-durable-lifecycle.zh-CN.md);
Turn queries: [English](../../docs/design/2026-09-28-managed-agent-turn-queries.md) |
[简体中文](../../docs/design/2026-09-28-managed-agent-turn-queries.zh-CN.md);
Actions (Hosted permission approvals): [English](../../docs/design/2026-09-30-managed-agent-actions.md) |
[简体中文](../../docs/design/2026-09-30-managed-agent-actions.zh-CN.md);
AgentDefinition revisions: [English](../../docs/design/2026-10-01-managed-agent-definitions.md) |
[简体中文](../../docs/design/2026-10-01-managed-agent-definitions.zh-CN.md)
## On-demand tool details
Managed ReadFile and Shell cards read saved arguments, output and errors only
when expanded, through `/api/agent/web-shell/v1/tool-details/get`, without a
feature switch. Each request requires a trusted actor and current
Session/Workspace read access, rechecked before returning. It does not require
Artifact publication or download permissions and never executes a tool or starts
a Harness.
Lifecycle events remain metadata-only. Existing retained private results can be
read without backfilling public events. Allowlisted input is limited to 4 KiB;
text is limited to 8 KiB and 200 lines, with truncation reported. Closed cards do
not fetch details. Reads stay inside the saved Session/Workspace and have a
bounded history budget.
Saved version-1 Shell display records also supply structured execution details to
the shared daemon card. Their text fields share a separate 8 KiB/200-line budget,
and directory/output-file paths share an 8 KiB budget. Unsupported or malformed
structures retain the legacy text fallback; private fields are excluded.
See the complete design:
[English](../../docs/design/2026-10-19-managed-inline-tool-details.md) |
[简体中文](../../docs/design/2026-10-19-managed-inline-tool-details.zh-CN.md).
## Tool publication storage
Tool publication depends on `ToolPublicationObjectStore`; Aliyun OSS is its default implementation. Publication remains disabled unless `qwen.managed-agent.tool-publication.enabled=true`. To use another backend, register one application `@Bean` returning `ToolPublicationObjectStore` from a configuration imported into the Spring server. The default OSS auto-configuration then backs off, and no OSS endpoint, region, bucket or credentials are needed. Common publication service URL, capacities and verification deadlines remain required. Without a custom store, existing OSS configuration and client lifecycle are unchanged.
Implement atomic create without overwrite, guarded reads, idempotent physical deletion and `validateConfiguration()` for private, durable, immutable storage. Throw `ToolPublicationStorageException` with `ACCESS_DENIED` for definite storage denial or `FAILURE` for other backend failures, preserving the original cause. Even with scheduled GC disabled, failed-publication cleanup requires deletion. Do not automatically replay completed writes or a returned stream. All server instances sharing a SQL catalog must use the same byte namespace: replacing the bean does not migrate retained objects or enable fallback to another backend.
The extension applies to the Spring server. Standalone `WorkspaceRecoveryMain` and `WorkspaceCsiWorkerAckMain` still construct their documented OSS/inline storage directly; custom-backend deployments must integrate the same backend into their maintenance tooling. No additional production backend is included. Design: [English](../../docs/design/2026-10-16-managed-tool-storage-interface.md) | [简体中文](../../docs/design/2026-10-16-managed-tool-storage-interface.zh-CN.md).
## 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. |
| `projection-interval` | `1s` | Cadence of the tool-result projection pass (claim processing). Bound through a `@Scheduled` placeholder, so a suffix-less number binds as milliseconds — unlike the typed settings above. |
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.
`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. Tool-publication authorization always uses the
session journal head's validated activation columns. Missing or inconsistent
head proofs refuse publication.
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](../../docs/design/2026-09-29-managed-tool-result-public-projection.md) |
[简体中文](../../docs/design/2026-09-29-managed-tool-result-public-projection.zh-CN.md);
revalidation window: [English](../../docs/design/2026-10-02-managed-agent-query-amplification.md) |
[简体中文](../../docs/design/2026-10-02-managed-agent-query-amplification.zh-CN.md).
## Asynchronous tool publication verification
Enable `qwen.managed-agent.tool-publication.async-verification-enabled`
(default `false`) to use asynchronous admission. `verification-concurrency`
defaults to `2` and controls the dedicated verifier pool, independently of
retention and GC.
Clients that support asynchronous verification send
`X-Qwen-Tool-Publication-Async: 1` and poll the operation status after HTTP 202.
Synchronous responses remain supported when asynchronous verification is
disabled or the request does not opt in. A mode is saved
when each operation is created, so retries do not change it. `FAILED` carries
only a safe `error: {status, code}` and must be handled as a terminal error,
outside network retry handling. The adapter keeps awaiting verified success
before completing publish, seal, prefix, or finish.
For rollback, disable new async admission and leave the verifier and head
authorization running until accepted work drains. Do not downgrade the server
while pending async operations still need verification. Historical async
success replay returns the prior receipt; actual reads and final integrity
checks still validate current storage bytes.
Design and client migration contract:
[English](../../docs/design/2026-10-07-managed-tool-publication-async-verification.md) |
[简体中文](../../docs/design/2026-10-07-managed-tool-publication-async-verification.zh-CN.md).
## Experimental reference distribution
The independent Hosted Java reference distribution packages the runnable
server, operator-recovery entry point, embedded Broker component, a
configuration template, systemd unit templates, a credential bootstrap,
a phased upgrade command for the systemd layout, provenance manifest,
checksums, and deployment/upgrade instructions. Pull requests and manual dry runs build
and boot the same Linux archive; only a protected-main production dispatch
publishes an immutable prerelease. See
[Managed Agent reference deployment](../../docs/users/managed-agent-reference-deployment.md)
and the [distribution design](../../docs/design/2026-10-14-managed-agent-reference-distribution.md).
The bundled launcher also supports `--check`, a read-only preflight for
configuration, the compatibility of a single configured Harness, ports,
leftover Runtime workers and database grants; add `--online` while the server
runs. When the database check succeeds, the output includes a `capacity` line
with grouped counts from `ma_runtime_binding`, `ma_runtime_session` and
`ma_operation`.
These are durable logical records for capacity triage; they are not a count of
physical worker processes or memory usage. The line explicitly reports
physical processes and memory as unknown, and does not infer idle work or
reclaim anything. If the database check fails, capacity remains skipped.
## Prerequisites
- Java 21
- Maven 3.8.9+ (the SpotBugs gate's plugin declares that floor)
- MySQL 8
MySQL is the supported database family; full integration CI uses MySQL 8.4.
MariaDB no longer has a CI lane or an ongoing compatibility guarantee.
Existing dialect handling remains in place; this change does not reject
connections that previously worked or introduce a catalog migration.
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 Java SDK before building this module:
```bash
mvn -f ../sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true install
```
Configure and start the server:
```bash
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'
export QWEN_MANAGED_AGENT_WORKSPACE_ID='local-dev-workspace'
mvn -pl server -am -DskipTests install
mvn -f server/pom.xml spring-boot:run
```
Instead of the token and digest, `QWEN_MANAGED_AGENT_HARNESS_CREDENTIALS_FILE`
can name the Harness's own environment file. While the Harness is enabled,
the server then reads `QWEN_SERVER_TOKEN` and
`QWEN_HOSTED_HARNESS_CAPABILITY_DIGEST` from it, and
`QWEN_MANAGED_RUNTIME_BROKER_TOKEN` when the file sets it, into its
configuration, never into its process environment. It refuses to start when a
direct `QWEN_MANAGED_AGENT_HARNESS_TOKEN` or
`QWEN_MANAGED_AGENT_CAPABILITY_DIGEST` is also set, or a direct
`QWEN_MANAGED_AGENT_RUNTIME_BROKER_TOKEN` beside a file Broker token. The file
sets the Harness's `QWEN_MANAGED_RUNTIME_BROKER_URL` and
`QWEN_MANAGED_RUNTIME_BROKER_TOKEN` together or neither; a file with only one
of them, or with either one empty, refuses startup. Write each of these keys as `KEY=value` on one line,
with no whitespace around the `=` and only letters, digits and `._:/+=-` in the
value (the Broker URL may also use `[` and `]` to bracket an IPv6 literal, as
in `http://[::1]:4182`), and start comments with `#`. The server
refuses, by file and line, any line it might read differently from systemd or
a shell, such as a continuation, a carriage return from CRLF line endings, a
`;` comment, an unterminated quote or text after a closing quote.
Create a Session:
```bash
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 `ma_operation` execution units. 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.
Turn input and cancellation are recorded in the same table as `submit_input`
and `cancel` operations. Each is saved with its command, the same route reads
it back, and a retry under the key resolves to it. The Turn dispatcher stays
their only deliverer, and the Turn decides their outcome. An input completes
once a Harness admits its Turn. A cancel completes once its Turn is cancelled,
and fails with `turn_already_settled` if the Turn ended another way. Neither
blocks a lifecycle operation; an active Turn still does. Admission responses do
not carry the operation id yet. See the
[Turn command operation design](../../docs/design/2026-10-16-managed-turn-command-operations.md).
```bash
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 Turn parked in a
supported state continues on the replacement (G3 Step 3). In a Session
without Hooks or MCP, an unfinished model round reissues its saved request
and retracts the dead owner's published prefix, and a requested native-file
approval keeps its Action and deadline; a committed terminal is projected
once. Step 3 neither reissues a model round nor resumes an approval in a
Session with Hooks or MCP, and no gate covers how such a parked Turn ends. A
takeover load that can never continue (an in-flight Shell call, an unfinished
prefix continuation, a saved request that is oversized or carries image
payloads, or other evidence a replacement cannot drive) ends the Turn as `managed_runtime_recovery_blocked` with a typed reason
instead of retrying forever. 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 — close it and start a new
Session. See "Hosted recovery scope" below for what the first release
covers. 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. A delivery whose submission went unanswered is the exception (D7-2): it parks `recovery_blocked`, is never sent again, and retires its public receipt, so it holds neither later titles nor the lifecycle; the operation reconciler completes it, and shows the title, once the Session journal holds its revision's commit, and fails it once a deleted Session's complete journal lacks it ([design](../../docs/design/2026-10-17-managed-operation-reconciliation.md)). 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](../../docs/design/2026-10-08-managed-session-rename-delivery.md). 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. The deletion
request records the tombstone; 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, a typed
`SESSION_RESOURCE_GC` operation in `ma_operation` releases retired journal,
message, record, native journal snapshot and artifact owners and their `ma_resource_ref`
references in bounded transactions after the deletion grace. Unreferenced
`PUBLISHED` or `REFERENCED` immutable `MYSQL_INLINE` bodies in `ma_resource`
are then byte-dropped, keeping their identities with state `COLLECTED`.
Capture objects follow a separate `CAPTURE_GC` operation: it persists deletion
intent before physical deletion and retains its claim generation and cursor
until the outcome is settled. Live readers, recovery protection and unresolved
object writes block collection. Session/head tombstones and event-stream tail
and retention-floor counters remain. Unreferenced bodies of active Sessions
remain retained until Session retirement, except released public message bodies
that the reader-aware projection collector can reclaim. 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.
This revision starts with a new empty database. Existing development or application databases keep their historical data under the old binary; the new service refuses legacy catalogs before running its baseline. Session identities and public URLs remain stable within their own environment.
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
The shared fresh baseline creates the private Managed Session head, 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 follow `qwen.managed-agent.harness.enabled` by default, because
the Hosted Harness requires them; `QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED`
overrides that default. Serve them only on a private service listener or
trusted service network. The explicit settings are:
```bash
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 it is blank and the store listener — the internal listener when
`internal-server.port` is set, otherwise the public one — binds a blank,
`127.0.0.1`, `0.0.0.0`, `::` or `::1` address on a fixed port, the server
uses that listener's loopback `http://` URL. A public listener that serves TLS
through `server.ssl`, or any other listener, needs an explicit URL.
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. When the
global store ID is blank, the store uses the Runtime Broker's ID: its configured
ID, otherwise the hash a local-process or Kubernetes Broker derives from
`QWEN_MANAGED_AGENT_WORKSPACE_CWD`, even while the Broker is disabled. If the
Runtime Broker also has an explicit ID, startup rejects a mismatch. Keep an
existing deployment's ID unchanged, since unbound Sessions resolve their store
scope from it.
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. The SQL scan below
discovers eligible committed wake inputs after writer loss; 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`.
### Web fetch in Workspace Sessions
The main Hosted files, shell, search and team lead profiles provide `web_fetch`
with `url`, `prompt` and optional `format` (`auto`, `markdown`, `html` or `text`).
The bound Runtime fetches public HTTPS content and returns normalized text and
HTTP metadata to the conversation model. `default` and `auto-edit` require
approval with the actual destination URL; `yolo` retains its approval bypass.
Plan mode, snapshot read profiles and the dedicated CSI files profile do not
admit this tool.
The Runtime worker must use Node.js; other JavaScript runtimes are refused
because they may ignore the pinned connection dispatcher.
Every request, retry and same-host redirect checks and pins public DNS
addresses, verifies TLS and uses a dedicated connection agent. Private and
metadata addresses, URL credentials, plain HTTP and binary downloads are
refused. The request does not inherit the process-wide proxy dispatcher.
Cross-host, scheme or port redirects are returned for a new call and approval.
Text is bounded to 16 KiB after JSON encoding with a truncation notice; no
download or overflow file is written on the worker host. The worker does not
make a separate model call or retain a web cache.
Upgrade Runtime workers together with the Harness before relying on its
`web-fetch/1` capability. See the [English design](../../docs/design/2026-10-23-managed-web-fetch.md)
and [Chinese design](../../docs/design/2026-10-23-managed-web-fetch.zh-CN.md).
### Wake recovery after Harness loss
With the Session Store enabled, an independent control-plane scan discovers
committed Monitor/background Shell, channel, child-result, workflow, Automation and Session
message inputs after their writer lease expires. It runs every two seconds on a
dedicated scheduler, handles up to eight candidate Sessions per pass, and leaves
a durable 30-second retry delay after a claim. EventTransport is optional; an
acknowledged or already-claimed MQ wake cannot suppress this scan.
Only a matching committed `turn.settled` consumes an input. Recovery reattaches
the Session and lets its existing wake pump run unattempted inputs or settle
interrupted attempts honestly. It does not replay tool effects or turn unknown
results into success. Old inputs retain this policy regardless of age. Closing,
retired, private CSI and stopped or terminal child runs are excluded; Workspace grants
and mount checks still apply. A failed recovery logs the tenant and Session and
remains retryable without a permanent attempt limit. Backlog or unavailable
execution/restore evidence can delay recovery beyond the lease and retry time.
The shared V1 baseline includes a revision-stamped wake hint and retry timestamp in the
Session head. Unknown or stale hints rebuild from committed journal events;
compacted heads are excluded without marking their inputs consumed, while missing
or corrupt proof is refused. Each proof invocation reads at most 32 transactions
and yields with its complete continuation for a later claim; partial proof never
clears the hint or triggers a load. At most 128 exact-prefix continuations or
positive memos are retained. Live partials keep their slots; new long proofs wait
for completed or ineligible holders to free capacity. A new revision or process
restart begins proof at zero with the durable hint intact.
Use matching Server and Broker builds that understand the current V1 baseline.
This scan is locally verified on macOS/MySQL 8.4. Linux/Windows, real MQ-on and
deployed provider acceptance remain unverified; see also [Hosted recovery scope](#hosted-recovery-scope).
The [English design](../../docs/design/2026-10-18-hosted-wake-recovery.md) and
[Chinese design](../../docs/design/2026-10-18-hosted-wake-recovery.zh-CN.md) describe
the recovery boundary and acceptance criteria.
## 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](../../docs/design/2026-10-07-managed-automation-runtime.md).
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`. |
| `scanner-threads` | `4` | Definition rows one tick works at once on its bounded pool; the tick joins them before returning. A row leases from its claim time, never from the tick's start, so a long tick hands out no already-expired leases. |
## 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:
```bash
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 the Java SDK:
mvn -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true 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/managed-agent-server/pom.xml -pl server -am -DskipTests install
mvn -f packages/managed-agent-server/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:
```bash
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. The Hosted
Harness rejects every attach without a store descriptor; the HTTP Session
Store follows the Harness, defaults to this server's loopback listener and is
scoped by the workspace ID exported in Prerequisites, so only the Harness URL
changes:
```bash
export QWEN_MANAGED_AGENT_HARNESS_BASE_URL='http://127.0.0.1:4171'
```
It is 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 override in
place.
The ordinary daemon treats `--port` as a request and moves to the next free
port on a collision, and 4171 is exactly where a daemon displaced from 4170
lands. The Hosted Harness profile never moves: it fails to start when its
port is taken, naming the port. Start the Harness first, or confirm the
daemon's bound port in its startup line.
With both up, run from the repository root:
```bash
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.
### Native macOS Workspace development
The Java Broker can run the existing local Node.js tool worker on macOS with verified Workspace registration and durable same-boot worker adoption. Set `QWEN_MANAGED_AGENT_RUNTIME_TRUSTED_LOCAL_REBOOT_RECOVERY=false`; whole-host reboot recovery remains Linux-only. Keep durable local mode and verified Workspace recovery enabled. Use the local directory import below for automatic registration, or register storage offline using the maintenance command and configure the same mount in the Java service. A missing registration still refuses tool execution.
For the Vite entry, set `VITE_MANAGED_AGENT_WORKSPACE_BINDING=true` and `VITE_MANAGED_AGENT_ACTOR_ID` to your development actor. On the loopback Java service, set `QWEN_MANAGED_AGENT_TRUSTED_ACTOR_HEADER=X-Qwen-Actor-Id`; local directory import grants the importing actor automatically, while offline registration requires a separate Workspace grant. Configure `VITE_MANAGED_AGENT_MODEL_PRESETS` with published AgentDefinition IDs for new-task model choices; the server's `model-catalog` supplies model choices within bound Sessions. These settings supplement the Broker and Hosted Harness wiring below; enabling selectors alone does not enable tools.
macOS storage identity combines `IOPlatformUUID` with the original device, inode and nanosecond birth time from native `stat`, cross-checked against the JDK directory identity. Worker identity also checks `kern.bootsessionuuid` and the exact process start instant before re-attestation. Preserve the SQL database, private Runtime state directory and credential key across Broker restarts. macOS handles use explicit `macos` namespace labels; older Java readers refuse them. A different boot is refused even for operator stop attestation. Background Shell, Monitor, command Hooks and Linux CSI retain their existing platform restrictions. The published reference distribution's Linux qualification is unchanged.
## 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 and active delete use lifecycle v2 with reliable original-Worker stop support. Archive and unarchive require completed close proof; unarchive restores CLOSED. A live or cancelling foreground Turn and a pending approval return `turn_active`. Shell lifecycle is advertised by default; an explicit empty lifecycle profile list disables it. 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](../../docs/design/public-hosted-shell-admission.md).
### Public child agents
`QWEN_MANAGED_AGENT_WORKSPACE_CHILD_AGENTS_ENABLED` defaults to true and enables public `agent` delegation for qualified new built-in `qwen-code` Workspace Sessions. Set it to false to disable new delegation. Admission requires public Shell, a durable LOCAL process Broker, Session isolation, and a Harness advertising `public-child-agents/1`, approval selection and Shell lifecycle v2. Other deployment profiles continue to start without public child admission. Existing Sessions remain opted out. SSH, resident/CSI, custom definitions, Team, Workflow, messaging, nesting and isolated child Workspaces are excluded from this first release.
Children share the parent's directory, definition and owner, start without its conversation, and keep the effective approval mode, actual model route and Skills captured at launch. Later parent policy changes apply only to new children. Default execution is background; explicit foreground waits retain the existing mount restrictions. Each root admits at most four active children and 64 launches over its lifetime.
The optional `child_agents` / WebShell `childAgents` capability identifies orchestration support. Turning the deployment switch off blocks new launches while admitted tasks can still recover, receive approvals, stop and deliver results. Query pending approvals with `include_children=true` or WebShell `includeChildren: true`; answer each Action on its actual owner Session. Aggregation grants no new response permission.
Stopping a parent Turn durably stops new launches and requests cancellation for active children from that prompt only. A stop request is distinct from physical termination. Late results remain available through the task's `childSessionId` transcript without automatically waking cancelled work. Public children reject independent new Turns and policy/model changes. See the [bilingual design](../../docs/design/2026-10-22-managed-public-child-agents.md) for recovery and rollout acceptance.
### 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. Fresh LOCAL databases additionally require `QWEN_MANAGED_AGENT_RUNTIME_VERIFIED_WORKSPACE_RECOVERY_ENABLED=true` on Linux or macOS and a successful native storage registration before tool or child Git execution. Every logical alias maps to one physical owner; aliases of the same native mount share that owner while retaining independent logical admission fences. Registration preserves the first marker provenance, and an unmapped alias cannot acquire physical execution authority. 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.
### Upload files into a Workspace
With Workspace file access enabled and compatible Server, CLI and WebShell artifacts, select a persisted LOCAL or SSH_LOCAL Workspace and choose **Add file → Upload to workspace**. Uploads accept binary and empty files up to 8 MiB each, use an existing destination directory, and reject occupied paths without overwriting them. The composer inserts a scoped path reference for tools to use; binary bytes are not converted into a small-text snapshot or message attachment. Old CLIs leave upload disabled. CSI, folder uploads and automatic directory creation are unsupported.
Uploads require an authenticated actor with OPERATOR or OWNER access and verified storage. An execution-storage member, including a retained MCP Runtime, keeps the Workspace busy. A durable physical-storage hold excludes new execution and retirement until the original upload has a definite result. If the response is uncertain, query the original upload ID; recovery does not replay the write on another Runtime. Cancelling the browser request does not delete a file already committed. See the [English design](../../docs/design/2026-10-26-managed-workspace-upload.md) and [Chinese design](../../docs/design/2026-10-26-managed-workspace-upload.zh-CN.md) for the protocol, recovery and acceptance boundaries.
### Create empty Workspaces from WebShell
Set `QWEN_MANAGED_AGENT_WORKSPACE_CREATION_ROOT` to an existing canonical, server-owned directory to enable **New workspace → Start from scratch** in the Managed composer. The default is disabled. The root must be separate from static Workspace mounts, the default Workspace cwd, and private Runtime state. Enable Workspace files, the Session Store, the durable Session-isolated local-process Broker and verified local Workspace recovery first. Enabling this setting authorizes authenticated tenant actors to allocate independent empty roots and receive OWNER access; only enable it for tenants allowed to consume this host's storage.
`POST /api/agent/web-shell/v1/workspaces/create` accepts `displayName` (trimmed, 1–128 characters without controls) and `idempotencyKey`. It returns the regular Workspace summary without host paths. Creation freezes an actor-scoped `WORKSPACE_CREATE` receipt, derives a tenant-separated directory below the configured root, registers its native identity and marker, and atomically publishes the ACTIVE Workspace with its creator grant. The Workspace query advertises `workspaceCreation` when enabled. Existing static mount configuration continues to work.
An uncertain response must be retried with the same key and name. The page preserves that pending operation across refresh and selects the result without clearing the prompt or model. Completed replay never restores revoked access, recreates missing registry rows, or registers held storage again. Pending registration resumes in place after a process restart; failed attempts retain their owned directory for retry. There is no automatic storage deletion or arbitrary host-path import. Workspace persistence does not imply recovery of a resident Harness Session whose writer lease expired during control-plane downtime; that existing Session recovery boundary is separate from creating or reusing a Workspace. Keep the configured root and SQL database durable together. Schema V4 extends the receipt namespace after the published V3 Session-list migration while retaining all published migration resources and existing data.
### Select an existing local project directory
For a trusted single-user deployment bound to loopback, enable
`QWEN_MANAGED_AGENT_LOCAL_DIRECTORY_IMPORT_ENABLED=true` alongside Workspace files,
durable local-process execution and verified recovery. Authenticate with signed
requests or OPEN mode plus a trusted actor header. The single-user
`qwen serve --managed` launcher enables this capability with signed authentication.
Then **Workspace → Choose existing directory** browses this computer directly,
without a pre-registered parent or grant. Import keeps the project in place,
automatically registers its projects parent outside Git worktrees, grants the
current actor OWNER access, and sets the project as the initial working directory.
The parent shares file access and catalog-local leases across projects; this is not a filesystem sandbox or a global parent-directory claim. New imports use `imported-<receipt key>` storage IDs and private registration metadata below `runtime-broker.state-directory/imported-projects/<storage ID>`. Independent local catalogs may import the same project while leaving any global storage marker untouched. Whole-storage migration, physical recovery and exclusive file-history rewind are unavailable for these shared imports; own-process Runtime recovery remains available. Existing owned roots are reused while sessions run.
Private Runtime state and creation roots must stay outside imported parents.
Remote listeners cannot enable this local feature, including with the insecure
bind override. Signed mode retains signature validation and does not accept a
trusted actor header in its place. This capability is for a trusted single-user
host deployment.
The actor-authenticated directory query accepts `localPath` (empty starts at the
host home directory), and create accepts an absolute `localPath` with the name
and idempotency key. These fields are exclusive with Workspace-relative source
fields. `localDirectoryImport` advertises the capability independently; pending
receipts pin native root identity and completed receipts resolve imported roots
after restart or disabling further imports. Neither retry nor restart repairs a
missing or mismatched registration metadata or restores revoked access. Legacy `created-` imports and application-owned roots keep their global marker contract; no existing receipt or marker is converted automatically. See the
[local import design](../../docs/design/2026-10-21-managed-local-directory-import.md).
For other deployments, the existing authorized-root flow remains available.
With verified LOCAL storage enabled, **New workspace → Choose existing directory** browses the Linux or macOS execution host through Java. Select an ACTIVE ROOT Workspace you own, then choose an existing project beneath its authorized root. Administrators can register and grant a projects parent directory once; the root must remain outside all Git worktrees. The selected project stays in place and becomes the new Workspace's persistent default working directory. Other Workspaces on that storage retain the same shared file-access scope and physical-owner lease; selecting a subdirectory does not create filesystem isolation.
`POST /api/agent/web-shell/v1/workspaces/directories/query` accepts `workspaceId` and `cwdRelative`, rechecks OWNER access and native storage, and returns at most 100 real immediate child directories (scanning at most 1024 entries). Only this authorized route discloses its execution-host root path. The existing create route accepts `sourceWorkspaceId` and `cwdRelative` together to bind a new logical Workspace to the verified source storage, configuration, and policy. It does not create directories or re-register storage. The creation receipt pins source generation and directory; pending retries refuse source changes or revoked ownership. Completed replay checks the new Workspace's current access. WebShell summaries expose `defaultCwdRelative` and `canSelectDirectory`; missing values mean `.` and unsupported, respectively. The page advertises `workspaceDirectorySelection` separately from empty creation and preserves the selected directory across retries, refresh, and reselect. See the [bilingual design](../../docs/design/2026-10-16-managed-workspace-directory-selection.md).
`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 initial approval mode is pinned at Session creation and must be confirmed by the
Harness on creation and load. Built-in Workspace Sessions can select a different
policy for later Turns while retaining the original definition.
For new Workspace-bound WebShell Sessions, the shared composer queries `POST /api/agent/web-shell/v1/session-options/query` with `agentId` using the authenticated tenant actor. The response contains the effective `approvalMode` and `availableApprovalModes`; Workspace discovery advertises `approvalModeSelection` for this contract. Built-in Sessions allow `default`, `auto-edit`, and `yolo` (Full Access). Shell Full Access additionally requires the Harness feature `approval-selection/1`; without it Shell offers only `default` and `auto-edit`. The Shell deployment opt-in and its deployment default remain unchanged. Hosted execution does not support classifier `auto` or `plan`. An AgentDefinition exposes only its compiled policy, which a creation request cannot override.
The WebShell create request may include `approvalMode` only with a Workspace binding. Omission preserves the existing default and request digest; explicit selection is persisted in the Session and joins its idempotency digest. Retrying the same key reads the original pin even if deployment defaults, the AgentDefinition, or a later Session selection changed. Workspace-bound and definition-pinned Session summaries expose the current `approvalMode`, immutable `initialApprovalMode`, and `approvalRevision`. Legacy unbound built-in Sessions omit this metadata because their execution policy is supplied by the deployment. The public REST create contract is unchanged.
The only ordinary Shell profile is `hosted-workspace-shell/1`, including native `glob` alongside Read/Write/Edit, Shell and Monitor. The unshipped `/2` Shell profile was removed; development Sessions using it must be recreated. See the [consolidation design](../../docs/design/2026-10-23-managed-shell-profile.md) ([简体中文](../../docs/design/2026-10-23-managed-shell-profile.zh-CN.md)).
Eligible Workspace Sessions can change their execution approval policy. With the Harness feature `live-approval-selection/1`, a selection affects subsequent permission checks in the current Turn, and ordinary pending tools are durably allowed when the selected mode permits them. Already dispatched tools and explicit Hook or Plan-exit approvals retain their existing decisions. Built-in `qwen-code` Sessions require the Harness feature `approval-selection/1`. Stored AgentDefinition Sessions using exactly `hosted-workspace-files/1`, `hosted-workspace-files/3` or `hosted-workspace-shell/1` additionally require `agent-definition-approval-selection/1`; both features must remain available for execution after any override, including a return to the initial mode. `POST /api/agent/web-shell/v1/session-approval/query` returns the current mode, revision, available modes, and support. `POST /api/agent/web-shell/v1/session-approval/change` accepts `sessionId`, `approvalMode`, `expectedRevision`, `idempotencyKey`, and `requestId`. Only the recorded Session owner with current Workspace OPERATOR or OWNER access may change it or replay a receipt. Without live support, the Session must be idle, with no pending approval or retained Runtime. Context/lifecycle and recovery barriers always apply; Plan changes remain idle-only. The options response advertises live behavior with `liveSupported`. Upgrade the control plane and Harness together in a maintenance window, fencing old Java writers before reopening execution; new Harness tool Turns refuse a missing live-policy endpoint. See the [live approval design](../../docs/design/2026-10-20-managed-live-approval.md) ([简体中文](../../docs/design/2026-10-20-managed-live-approval.zh-CN.md)). Identical retries return the original result without another revision; different content under the same key and stale revisions conflict. The definition pin and initial creation policy remain immutable; later user Turns freeze the selected mode and revision. Creation options still expose only the compiled initial policy. Files/2, MCP, read-only child, team-lead, CSI, unknown profiles, and unbound text Sessions do not gain policy selection; `hosted-workspace-files/3` does. There is no public REST mutation endpoint. See the [stored-definition approval design](../../docs/design/2026-10-17-managed-definition-approval-selection.md) ([简体中文](../../docs/design/2026-10-17-managed-definition-approval-selection.zh-CN.md)).
The stored-definition extension needs no schema migration, but old Java readers and writers ignore custom overrides. For live approval, use the coordinated control-plane/Harness upgrade described above and stop or fence every old Java/Broker process sharing the catalog before permitting changes. After a custom override, rollback must use a compatible build or keep affected Sessions blocked while applying a forward fix. Do not mix old Java binaries, clear revisions, or rewrite definition/history to roll back. Custom initial-yolo definitions retain the existing ten-minute approval timeout fallback after an override; custom initial-default/auto-edit definitions retain their stored timeout.
Schema V6 preserves the original policy and adds a current override plus per-Turn approval snapshots. Turn admission and policy changes share the Session row lock. Immutable Turn snapshots preserve retry and recovery identity; live permission boundaries separately read the current durable Session selection. Revision zero and legacy Turns consistently omit the private `approvalSelection` field, preserving prompt retry identity across Harness capability upgrades; positive revisions always send the saved Turn snapshot. `yolo` skips ordinary file and Shell approvals, while explicit Hook ask/deny decisions retain their existing behavior. The configured approval timeout persists for new selectable Full Access Sessions and survives a later switch back to asking. Legacy Full Access definitions without a timeout keep the existing 10-minute fallback. WebShell retains an uncertain change with its original request and key across refresh, blocks send/model/cwd changes until receipt readback, and ignores older policy revisions. A Session with an override cannot execute through an older Harness. Follow the coordinated live rollout above; rollback binaries must understand the saved override and snapshots. See the [approval selection design](../../docs/design/managed-approval-selection.md).
Workspace-bound WebShell Session summaries can include `modelContext` with the actual observed `modelId`, optional latest-request `inputTokens` and resolved `contextWindowSize`, and `observedAt`. The Hosted CLI records capacity beside successful provider usage; the Java Session Store projects only committed main-model attempts into one private latest-resource pointer. Lists read these observations in one batch without scanning journal history. Older or invalid usage advances the pointer but leaves missing values unknown; cumulative billing totals and inferred capacities never substitute for them. The shared usage popover does not offer manual compression. New tasks continue to select published AgentDefinition presets. This observation projection uses existing SQL tables and resource-reference cleanup without changing the recovery format.
To allow an existing Workspace Session to change its next Turn's model, publish an explicit `qwen.managed-agent.model-catalog` list of `{modelId, label}` entries in the server configuration. The default is empty. IDs must identify configured, cached-credential models in the Hosted CLI; the browser supplies neither provider settings nor credentials. The Harness must advertise `model-selection/1`. `POST /api/agent/web-shell/v1/session-models/query` returns the catalog and current selection; `POST /api/agent/web-shell/v1/session-models/change` takes `sessionId`, `modelId`, `expectedRevision` and `idempotencyKey` and synchronously returns the saved selection. The caller needs Workspace OPERATOR or OWNER and the Session must be idle, without pending approval, context/lifecycle work or recovery. Same-key retries return their original result before mutable catalog and role gates, provided the caller still has read access. They never reapply a change.
Idle Workspace Sessions support manual context compression when the Harness advertises `context-compression/1`. Send `POST /v1/agents/sessions/{sessionId}/compress` with `Idempotency-Key` and `{ "expected_compaction_revision": 0 }`, using the Session's current `compaction_revision`; WebShell uses `/api/agent/web-shell/v1/sessions/compress` and camelCase fields. The public `context_compression` capability advertises deployment support; the WebShell `contextCompression` capability also reflects the current caller grant and execution facts. A Workspace OPERATOR or OWNER can admit the durable `context_compression` operation; the existing operation query returns `pending`, `running`, `recovery_blocked`, `completed` or `failed`, and a completed outcome is `compressed` or `noop`. Same-key retries resolve the original operation before mutable capability, revision and busy checks. Compression refuses active or queued Turns, approvals, unsettled Hooks and conflicting context work. It does not create a user Turn or change approval, Plan or Workspace context revisions. After compression, context usage stays unknown until the next main-model observation. Deploy matching Server/Broker, Harness and WebShell builds using the current V1 baseline; stop new compression admission and settle accepted operations before rollback. Existing retained journals establish their compaction revision on first read; if that evidence is unavailable, the capability is false and the revision is omitted.
Schema V5 adds the current selection to the Session and copies it into each newly admitted Turn. Retries and recovery send that Turn's fixed model, never a later Session choice. No explicit selection preserves the original AgentDefinition/deployment behavior. Selection does not change the AgentDefinition identity, instructions, tools or approval policy. Each newly committed selection clears the latest context observation, including A→B→A without a Turn, until a new successful model request supplies usage. `modelSelection` in the summary describes the next-Turn setting; `modelContext` describes an actual successful request. An unavailable selected model fails closed rather than falling back to the deployment model.
Submit `agent_id: "qwen-code"`, the existing `workspace` selection and `input`
through `POST /v1/agents/sessions`. The server persists
`hosted-workspace-shell/1` with the deployment default `default`/`auto-edit` policy 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, or the `QWEN_MANAGED_RUNTIME_BROKER_URL`
and `QWEN_MANAGED_RUNTIME_BROKER_TOKEN` variables they fall back to, to reach
this Broker. The variables keep the token off the command line, and `qwen
serve` removes them from its environment once read, so no process the Harness
starts inherits the token. When
`QWEN_MANAGED_AGENT_HARNESS_CREDENTIALS_FILE` names that environment file
and it sets the token, the control plane takes its Broker token from it and
refuses a second, direct `QWEN_MANAGED_AGENT_RUNTIME_BROKER_TOKEN`. 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 and active delete use the existing lifecycle v2 path when the Runtime can reliably stop the original Worker. Shell lifecycle is advertised by default; explicitly setting `QWEN_HOSTED_LIFECYCLE_V2_PROFILES` replaces the list, and an empty value disables it. Archive and unarchive require completed close proof; unarchive restores CLOSED. Active or cancelling foreground Turns and pending approvals return `turn_active`: cancel the original Turn, wait for settlement, then close. Background Shell, Monitor and child Sessions use the existing resource drain and cascade. Uncertain execution, output or stop evidence remains blocked for recovery. Controlled cwd
changes ship below (W2); broad Workspace capability advertisement remains
gated. Qualified new public Shell Sessions expose `agent` under the public-child admission above; public Session messaging remains unavailable.
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.
Design: [English](../../docs/design/2026-09-29-hosted-public-workspace-admission.md)
| [简体中文](../../docs/design/2026-09-29-hosted-public-workspace-admission.zh-CN.md).
### 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):
```text
"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.
Design: [English](../../docs/design/managed-agent-broker-auth.md)
| [简体中文](../../docs/design/managed-agent-broker-auth.zh-CN.md).
### Session working directory
Select the Workspace-relative working directory when creating the Session. Directory-change submission and the WebShell Change directory action have been retired. Previously admitted `CWD_CHANGE` operations still settle through their original coordinator and remain readable through operation queries; their records, fences and `session.context.changed` events are retained. Daemon/ACP relocation and tool-level directories are separate capabilities.
Design: [English](../../docs/design/2026-10-26-retire-managed-cwd-change.md) | [简体中文](../../docs/design/2026-10-26-retire-managed-cwd-change.zh-CN.md).
### 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:
```bash
export QWEN_MANAGED_AGENT_RUNTIME_BROKER_ENABLED='true'
# Omit when QWEN_MANAGED_AGENT_HARNESS_CREDENTIALS_FILE names a file that sets
# QWEN_MANAGED_RUNTIME_BROKER_TOKEN; setting both refuses startup.
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'
```
For the external scratch provisioner, set these instead of the node executable,
worker entry and state directory (the Broker token and credential key pair are
still required). External scratch also requires an explicit Workspace ID: the
worker-side cwd cannot be hashed on the Broker host, so no ID can be derived
for this kind.
```bash
export QWEN_MANAGED_AGENT_RUNTIME_BROKER_ENABLED='true'
export QWEN_MANAGED_AGENT_RUNTIME_BROKER_TOKEN="$QWEN_EXTERNAL_BROKER_TOKEN"
export QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY_ID='external-dev-v1'
export QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY="$QWEN_EXTERNAL_CREDENTIAL_KEY_BASE64"
export QWEN_MANAGED_AGENT_CAPABILITY_DIGEST="$QWEN_EXTERNAL_CAPABILITY_DIGEST"
export QWEN_MANAGED_AGENT_RUNTIME_PROVISIONER='external-scratch'
export QWEN_MANAGED_AGENT_RUNTIME_TRUSTED_LOCAL_REBOOT_RECOVERY='false'
export QWEN_MANAGED_AGENT_EXTERNAL_RUNTIME_PROVIDER_URL='https://provider.example/'
export QWEN_MANAGED_AGENT_EXTERNAL_RUNTIME_PROVIDER_TOKEN="$QWEN_EXTERNAL_PROVIDER_TOKEN"
export QWEN_MANAGED_AGENT_EXTERNAL_RUNTIME_PROVIDER_ID='external'
export QWEN_MANAGED_AGENT_EXTERNAL_RUNTIME_REQUEST_TIMEOUT='30s'
export QWEN_MANAGED_AGENT_EXTERNAL_RUNTIME_STARTUP_TIMEOUT='2m'
export QWEN_MANAGED_AGENT_WORKSPACE_ID="$QWEN_EXTERNAL_WORKSPACE_ID"
export QWEN_MANAGED_AGENT_WORKSPACE_CWD='/workspace'
```
Keep Workspace files and Shell disabled. Fill the `$QWEN_EXTERNAL_*` variables
with your own values from a secret store and pick a stable Workspace ID; they
are placeholders for operator-supplied values, not usable defaults. Set the
capability digest (`sha256:` plus 64 lowercase hexadecimal characters) even
with Harness disabled; the embedded Broker requires it. The provider URL must
be HTTP(S) without credentials, query or fragment, and the cwd is the
worker-side path. The provider implements authenticated `provision`
and `observe` routes and must persist its mapping from the Broker's logical
handle to one immutable physical resource across restarts. There is no release
route, so a closed Session's worker is not stopped and its provider TTL must be
budgeted. This increment has no tool consumer yet: Session Turns warm the worker
but the model receives no tools until the Workspace extension lands. See the
[Broker guide](docs/runtime-broker.md#external-scratch-runtime-opt-in) and the
[bilingual design](../../docs/design/2026-10-23-external-runtime-provider.md).
Local-process workers run model-directed tools, so the Broker starts them
without any environment variable whose name begins, in any letter case, with
`QWEN_MANAGED_AGENT_`, `QWEN_MANAGEDAGENT_`, `QWEN_MANAGED_RUNTIME_BROKER_`,
`SPRING_` or `OSS_`. Every other
variable of the server process still reaches the worker and its tools. Keep
unrelated secrets out of the service environment. A worker that uses the
artifact tool's `oss` publisher therefore needs its credentials under the
`ALIBABA_CLOUD_ACCESS_KEY_ID`, `ALIBABA_CLOUD_ACCESS_KEY_SECRET` and
`ALIBABA_CLOUD_SECURITY_TOKEN` names, which that publisher also accepts. Those
names are not filtered: they reach every worker the Broker starts and every
model-directed tool it runs. The Broker copies its environment into a worker
once, at launch, and never refreshes it; a Broker restart does not refresh a
durable worker that it adopts by pid. Give those names a long-lived RAM
identity that can only write the artifact bucket and prefix, never the account
key that the `OSS_` filter withholds. A short-lived STS token needs a rotation
path that re-execs the Broker before it expires, and even then an adopted
durable worker keeps its launch-time credentials. One credential set serves
every tenant and workspace on the host. This does not isolate files: a worker
runs as the service account and can read whatever that account can.
After a credential-key rotation, keep each prior key in
`previous-credential-keys` until no persisted Runtime binding references its
key ID. The brackets are not decoration: Spring's relaxed binder strips `/`,
`:` and spaces from plain map keys, so an unbracketed ID binds as a key that
matches no persisted row.
```yaml
qwen:
managed-agent:
runtime-broker:
credential-key-id: local-dev-v2
credential-key: replace-with-base64-encoded-32-byte-v2-key
previous-credential-keys:
'[local-dev-v1]': replace-with-base64-encoded-32-byte-v1-key
```
New credentials use only the active key; reads select the active or previous
key by the ID stored with the ciphertext, and an unavailable ID fails closed.
The YAML-only ring, the retirement query, the operator-recovery ring and the
single-key-pair CSI/migration entries are normative in [Rotate the Runtime credential key](../../docs/users/managed-agent-reference-deployment.md#rotate-the-runtime-credential-key),
with the rationale in the [rotation design](../../docs/design/2026-10-15-runtime-credential-key-rotation.md).
Two optional knobs change how the Broker listens and how long it waits for a
dispatched v3 execution's result:
```bash
# 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 seconds: startup refuses a value below the 1s floor but accepts
# a stale milliseconds-style override (1800000 becomes 1800000 seconds),
# so always write the suffix. 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'
```
The same unit rule applies to every `qwen.managed-agent.*` duration setting: a
suffix-less number binds as seconds except for `runtime-broker.retention.max-age`,
which binds as days, and the millisecond-bound settings below.
Millisecond-bound settings — `auth.allowed-drift` (so
a stale `300000` keeps meaning five minutes instead of widening the
signature-replay window to ~83 hours), the sub-second `events.batch-interval`
and `events.materialize-interval`,
`runtime-broker.child-workspace-git-timeout`, `runtime-broker.retention.scan-delay`,
`automation.scan-delay`,
`automation.lease`, `automation.late-tolerance` and `automation.lookback`
— bind a suffix-less number as milliseconds. Always write the suffix (`90s`,
`500ms`). At startup the server reads each seconds-convention setting's
written value and warns on a bare integer — the shape of a stale
milliseconds-style override — naming what the number binds as now (seconds)
and what it would have bound before (milliseconds); a suffixed value never
warns. Cadences bound through `@Scheduled` placeholders —
`artifacts.projection-interval`, `channels.scan-delay`,
`child-relay.scan-delay`, `child-workspace.scan-delay`,
`dispatch.scan-delay`, `events.replay-floor-interval`,
`message-relay.scan-delay`, `session-worktree.scan-delay` and
`wake-recovery.scan-delay` — also read a bare number as milliseconds,
and the startup warning cannot see them, so always write the suffix
there.
For the local-process and Kubernetes provisioners, 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 derivation does not apply to
`external-scratch`, which requires an explicit ID because its worker-side cwd
cannot be hashed on the Broker host.
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 active credential key and
every configured previous key must decode to exactly 32 bytes and protect
persisted Runtime seeds and static Runtime credentials with AES-256-GCM. New
writes use only the active key; reads select the exact key ID stored with the
ciphertext. 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](../../docs/design/2026-09-27-local-runtime-adoption.md).
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](../../docs/design/2026-09-28-local-reboot-recovery.md)
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.
Managed Server and Runtime Broker share the single 38-table baseline in
[`persistence/src/main/resources/db/managed-schema/V1__managed_schema.sql`](persistence/src/main/resources/db/managed-schema/V1__managed_schema.sql).
Until the first formal Managed Agent release, schema changes update this V1 baseline
in place. Do not add V2+ migrations or increment the database schema version before
that release. Recreate development catalogs when the baseline changes; pre-release
database upgrades are not maintained.
The Server loads it through Flyway; Broker initialization reads the same resource.
Both refuse a legacy, partial, foreign, or history-only catalog before schema writes.
An already initialized complete baseline is structurally validated; a Broker-created
baseline may be registered by Flyway only after that validation. Schema initialization
does not repair or backfill old data.
Use a new MySQL 8.4/InnoDB database for the new environment. Quiesce old admissions
and settle in-flight work before cutover; retain the old environment/database for
historical reads. New Sessions belong to the new database, which must remain available
if routing is rolled back. The retired SQL/Java migration chain and upgrade tests are
[archived outside the runtime classpath](../../docs/archive/managed-agent-schema/README.md).
The former separate Broker `schema.sql` has been removed.
### Terminal Runtime record retention
Runtime JDBC retention is disabled by default. It removes old terminal
executions, Sessions and bindings in bounded batches on a dedicated scheduler;
it does not stop workers or delete Workspace files. Configure
`qwen.managed-agent.runtime-broker.retention` with these environment variables:
| Variable | Default | Meaning |
| ------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `QWEN_MANAGED_AGENT_RUNTIME_RETENTION_ENABLED` | `false` | Enable cleanup when the Runtime Broker is also enabled |
| `QWEN_MANAGED_AGENT_RUNTIME_RETENTION_MAX_AGE` | `30d` | Minimum age of eligible terminal records; must be at least 1ms; unit-less values use days; explicit units such as `12h` are preserved |
| `QWEN_MANAGED_AGENT_RUNTIME_RETENTION_BATCH_SIZE` | `100` | Per-tick bound on scanned bindings, scanned children and total deleted rows; 1–1000 |
| `QWEN_MANAGED_AGENT_RUNTIME_RETENTION_SCAN_DELAY` | `1m` | Positive delay between ticks, at least 1ms |
Recovery records, Workspace holders and CSI retirement keep their bindings.
Child Session lineage also keeps a binding until its canonical parent child_run
has a committed terminal projection; a missing projection remains protected.
This lets cascade and relay repair a stopped child's original Runtime identity.
Lineage and permanent relay classifications are never collected here.
A `:process` execution also keeps its whole binding family while its owning
Session has an unsettled background Shell child_run, including top-level
Sessions and Workspace isolation. This preserves the process-status evidence
used by Session-open never-started reconciliation until those Shell records
settle; the association conservatively protects the whole owning Session.
The lookup deduplicates owning Sessions by both raw value and binary identity
before checking their records on the sweep connection.
Publication and CSI ACK references keep the original execution and its parents,
including already collected publications; unreferenced siblings may be removed.
Nonterminal children, live operation/dispatch leases and managed bindings without
stop evidence remain retained. Binding slots and generation counters, placement
guards, harness drains and storage fences remain durable. Successful ticks log
scanned, skipped and deleted counts and duration; failures retry on the next tick.
Enabling retention allows eligible, unreferenced historical records to be deleted
once they age past `max-age`; it does not guarantee deletion by that age.
Publication and CSI ACK references retain executions and their parent sessions and
bindings regardless of age, including COLLECTED publications.
After cleanup, existing missing-record responses apply. Callers must not reuse
expired Runtime Session IDs or idempotency keys. Initialize a fresh database with
the single canonical 38-table V1 baseline and keep cleanup disabled. Legacy
catalogs are rejected; no in-place upgrade or old-data migration is provided.
Verify repeated initialization preserves terminal receipts, retain the old
environment for historical reads and settle its in-flight work before cutover.
Shared publication, CSI operation/work and Session record variants are checked
by their typed discriminators; their metadata and physical owner headers are
not collected. Every publication writer must use the locking protocol, and
retention stays disabled until the exact-identity Hosted cleanup
release-proof contract in [#13992](https://github.com/QwenLM/qwen-code/issues/13992)
is delivered and verified. An owed cleanup descriptor can outlive its RELEASED binding;
collection then makes the original release conflict and blocks the Hosted
Session. This PR does not implement that release-proof contract.
Disable retention before reverting to older writers;
disabling prevents future cleanup but cannot restore deleted records. See the
[retention design](../../docs/design/runtime-broker-jdbc-retention.md).
### 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):
```yaml
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-child admission above controls `agent`) 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 and active delete require lifecycle v2 and reliable original-Worker stop support. The canonical Shell profile advertises v2 by default; explicit lifecycle environment values override that list. 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](../../docs/design/2026-09-26-managed-workspace-execution.md)
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](../../docs/design/2026-09-30-hosted-file-history.md)
for record capacity and rewind semantics.
### Verified original-mount recovery (W1a)
The current fresh 38-table baseline requires this physical mount guard for LOCAL tool and child Git execution in a trusted, single-host OpenJDK 21 `local-process` deployment on Linux or macOS with a persistent, unambiguous root birth time. Enable `QWEN_MANAGED_AGENT_RUNTIME_VERIFIED_WORKSPACE_RECOVERY_ENABLED=true` and complete offline registration before admitting that work. Text-only Turns can run without registration; LOCAL tool acquisition fails with `409 workspace_unavailable` when the physical mapping is absent. Taking the next tool Turn after a Broker restart also requires `durable-local-process=true`. macOS requires `trusted-local-reboot-recovery=false` and supports same-boot adoption only. Linux whole-host restart succeeds only while the registered physical identity still matches. The shared V1 baseline stores registration, independent `mount_birth_time` and mount fence in `ma_storage` and `ma_storage_owner`. An unregistered mount is never registered from the directory found at startup.
Marker v2 stores birth time as a canonical `Instant` string. Linux reads creation time, mtime, device and inode in one JDK `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 Linux birth time equal to mtime is also conservatively refused; prepare the project layout or update the root's mtime offline before retrying. macOS reads native nanosecond birth time with `stat` and cross-checks its device/inode against the JDK canonical-directory snapshot, because JDK 21 truncates macOS birth time to seconds. A positive native macOS birth time is independent even when it equals mtime; fresh empty roots keep their original birth time. 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 with `Qwen-Code/verified-workspace/v2` as input. Its key is the trimmed machine-id UTF-8 bytes on Linux or the original `IOPlatformUUID` UTF-8 bytes on macOS; neither raw identity is stored in the marker.
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.
The first-release contract initializes fresh catalogs with one complete shared V1 baseline and marker v2. Server, Broker, Harness and WebShell must use matching builds. Earlier development catalogs and checkpoints are unsupported; keep any data that still needs historical access with its original deployment, and use a fresh database and state directory for this build. Do not bypass refusal with Flyway `repair`, `outOfOrder` or manual history edits. See the [first-release contract](../../docs/design/2026-10-23-managed-first-release-contract.md) ([简体中文](../../docs/design/2026-10-23-managed-first-release-contract.zh-CN.md)).
Stop all processes and external jobs that can write the storage, account for
old Runtime holders and verify the original root before registration. Initialize
the new database with the canonical V1 baseline, then run the private maintenance entry from this module on
the same execution 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:
```bash
mvn -q -pl server -am -DskipTests -Dgpg.skip=true install
mvn -q -pl server exec:java \
-Dexec.mainClass=com.alibaba.qwen.code.managedagent.maintenance.WorkspaceStorageRegistrationMain \
-Dexec.args='register tenant-a storage-a /absolute/canonical/workspace-a <operation-uuid> --offline-confirmed'
```
In the reference distribution, run it as
`bin/qwen-managed-agent-admin storage-registration register tenant-a storage-a /absolute/canonical/workspace-a '<operation-uuid>' --offline-confirmed`;
`bin/qwen-managed-agent-admin` with no arguments lists every maintenance entry it
wraps. The same entry is available from a shipped Spring Boot fat jar, without the source checkout or Maven:
```bash
java -cp /path/to/app.jar \
-Dloader.main=com.alibaba.qwen.code.managedagent.maintenance.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 new deployment before admission. 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](../../docs/design/2026-09-29-managed-workspace-w1-recovery.md).
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:
```bash
docker build -f packages/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:
```bash
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
Run Maven from the parent directory. Unit and H2 tests run in `verification` with the normal test phase; all original profile names and test families remain available from the parent reactor. Focused commands such as `mvn -Dtest=ModuleArchitectureTest,ManagedAgentEmbeddingTest test` build required production modules first. Test reports are under `verification/target/{surefire-reports,failsafe-reports}` and aggregate coverage under `verification/target/site/jacoco-aggregate`. Do not run the verification POM alone against stale installed same-version artifacts. `mvn clean verify` checks the compiled module boundaries, the existing test families and packaged server artifacts. The embedding-host fixture is under `verification/src/test/embedding-host`; after installing the production reactor, run its normal and `embedded` profiles to verify an independent host. `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 projection, and the independent-JVM Managed
Session Store crash/takeover path:
```bash
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.
The `hosted-harness-mysql` profile additionally clears every `ma_` base table
before each top-level `Hosted*IT` class, while retaining the schema and Flyway
history. Its JUnit extension is enabled only in that profile; one fork and
disabled JUnit parallelism keep cleanup ahead of the next class's contexts.
Use a database exclusively owned by this test invocation. Class teardown must
still stop all contexts and child processes. State is preserved between methods,
nested classes, and context restarts inside one class. See the
[isolation design](../../docs/design/2026-10-20-hosted-it-database-isolation.md)
([简体中文](../../docs/design/2026-10-20-hosted-it-database-isolation.zh-CN.md)).
## Managed extension packages
Complete package distribution and the seven-component Managed profile are opt-in.
Set `QWEN_MANAGED_AGENT_EXTENSION_PACKAGES_ENABLED=true`,
`QWEN_MANAGED_AGENT_EXTENSION_ARTIFACT_DIRECTORY` to a durable private directory,
and `QWEN_MANAGED_AGENT_EXTENSION_ALLOWED_ACTORS` to the permitted operator actor
keys. The configured Workspace OPERATOR grant is also required. See the
[configuration example](distribution/config/application.example.yml) for the
separate Runtime execution profile: Linux LOCAL and SSH require delegated cgroup
v2; native macOS requires the explicit `developer-process-group` mode. Existing
Runtime bindings do not acquire the new execution contract automatically.
Replicas must share the authoritative artifact directory. A Runtime cache is
reconstructible and cannot replace that directory. Published versions and encrypted
settings revisions are retained; this increment does not implement garbage
collection.
In WebShell, select the Managed Workspace and Session, open Extensions, prepare an
original Git/npm/directory/archive source for its target platform, then publish it.
An update explicitly selects the catalog extension ID. Select package versions and
an exact saved settings revision before applying to an idle Session. Saving settings
creates a complete immutable revision; values never appear in read responses.
Upgrading a package preserves its selected settings revision. An unfinished apply
has separate Continue original operation and Restore previous version controls;
unknown outcomes continue to block inference until the original owner reconciles.
Public clients use `/v1/agents/workspaces/{workspaceId}/extensions` for bounded
binary publication and catalog queries and `/v1/agents/sessions/{sessionId}/extensions`
for configuration queries and application. The OpenAPI contract documents the
settings and operation subresources. The Java SDK exposes `ManagedExtensionsClient`;
the TypeScript daemon SDK can prepare and stream an original package. Creation's
optional `extensionPins` freezes the initial selection before the first model call.
Omitting it retains the existing Session path.
Package assets use an explicit read-only extension source. Model tool schemas list
the selected public pins; `read_file` and `glob` accept package-relative paths or
`extension://<extensionId>/<path>`. Shell selects `extension_context` and receives
`QWEN_EXTENSION_ROOT`, while its working directory remains the Session directory.
Ordinary Workspace tools retain their existing containment rules. Executable
preflight checks target interpreters and directly resolvable commands without
running Hooks or connecting MCP; complex authored Shell expressions are checked
when their approved invocation executes. Argument placeholders inside nested Shell
substitution, backticks or heredocs are rejected explicitly.
See the linked [English design](../../docs/design/2026-10-24-managed-extension-packages.md)
and [Chinese design](../../docs/design/2026-10-24-managed-extension-packages.zh-CN.md).
Local focused tests, macOS developer receipts and the existing Linux static-extension
gate below establish different boundaries. Complete package Linux/SSH cleanup,
fault injection and real-provider qualification remain separate acceptance gates.
## Linux extension process acceptance
`HostedExtensionsIT` is collected by the existing `hosted-harness-mysql` profile. It uses the public AgentDefinition and Session APIs, real MySQL, the packaged Harness and durable native worker, a stdio MCP process, and command Hooks in a delegated cgroup v2 subtree. It verifies static Skill bytes, extension revision pinning across Harness replacement, Hook allow/deny, cancellation of a Hook and its descendant, safe continuation, and physical close. A separate MCP fault case writes and fsyncs an effect before disconnecting, then verifies that a replacement Harness refuses recovery without replaying the effect or reporting successful close.
After building the root packages and bundle and installing the current Java SDK, run this focused slice from `packages/managed-agent-server` on Linux with JDK 21, Node 22+, disposable MySQL 8.4, and Maven already inside the delegated cgroup ancestor:
```bash
QWEN_MANAGED_HOOK_CGROUP_ROOT=/sys/fs/cgroup/your-delegated-root \
mvn -Phosted-harness-mysql -Dit.test=HostedExtensionsIT \
-Dnode.executable=/absolute/path/to/node \
-Dqwen.cli.entry="$PWD/../../dist/cli.js" \
-Dmysql.url='jdbc:mysql://127.0.0.1:3306/managed_agent_test' \
-Dmysql.user=root -Dmysql.password= verify
```
Missing Linux/MySQL/cgroup prerequisites are not acceptance. This selector verifies only the extension class; it does not establish that the complete Hosted profile passed. Evidence is saved before forced fixture teardown under `verification/target/failsafe-reports/hosted-extension-evidence/` and is included in the existing CI report upload. The interrupted MCP case leaves its public Turn `RUNNING` with durable `ma_turn.error_code = hosted_runtime_recovery_blocked` and ordinary execution `UNKNOWN`, rejects new input and close with `409 turn_active`, and does not replay the physical effect; successful cleanup is established by the separate healthy-close case. This gate uses a controlled loopback model and local stdio transport, so it does not establish upstream-model, remote transport, reboot, cross-machine, SSH MCP/Hook, or production acceptance. See the complete [English design](../../docs/design/2026-10-16-managed-extension-acceptance.md) and [Chinese design](../../docs/design/2026-10-16-managed-extension-acceptance.zh-CN.md).
## 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 `ma_tool_execution` record (exactly one `SETTLED` row). Hosted
tool intent and receipt events also expose tool names and lifecycle status
through public `item.tool_call.updated` events without O2 publication. Raw
arguments and output remain behind the existing publication policy. The
Hosted model loop follows `model.maxSessionTurns` (default `-1`); it no
longer imposes a separate 16-round limit. See the
[progress design](../../docs/design/2026-10-16-managed-webshell-progress.md).
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):
```bash
npm run build && npm run bundle
mvn -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true install
mvn -f packages/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:
```bash
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:
```bash
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 failover E2E
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 failover E2E CI job.
The G3 arms replace owners without an operator choosing the next one. With
`--harness-only`, the Spring control plane stays alive and only the Hosted
Harness is killed, so the live control plane must adopt the next Harness
generation:
```bash
npm run test:e2e:managed-harness-restart-failover
npm run test:e2e:managed-harness-restart-inflight-failover
npm run test:e2e:managed-harness-restart-continuation-failover
npm run test:e2e:managed-harness-restart-model-round-failover
```
The model-round arm creates a Session with no Workspace, publishes a prefix
from its first model round, and kills the Harness twice. Each replacement must
reissue the same saved model request and retract the prefix before its own
text appears. The arm then requires one terminal event, only the final answer
in the public transcript, no tool execution, and a next Turn whose model
history holds one copy of the prompt and answer and none of the prefixes.
The Step 3 packaged gates take over a parked native-file approval or model
continuation through a Harness-only replacement. Each keeps the original
identities (the Action for the approval gates, the Runtime binding and the
prepared or settled execution), confirms the original Runtime cleanup,
completes a next Turn, and closes the Session:
```bash
npm run test:e2e:managed-harness-restart-approval-allow
npm run test:e2e:managed-harness-restart-approval-deny
npm run test:e2e:managed-harness-restart-approval-cancel
npm run test:e2e:managed-harness-restart-approval-expiry
npm run test:e2e:managed-harness-restart-continuation-closeout
```
Five arms from #14481 step 2 layer one fault on the Allow path. The first
crashes the replacement Harness as soon as the decision is durably recorded;
the second holds the Runtime Broker's execution `:start` so the crash lands
after the `PREPARED` row and before the physical dispatch; the third crashes
after the approved result commits while the post-tool model round is held, so
its reissue must reuse the saved request and retract the published prefix. All
three leave the original Action, prepare key, digest and Runtime identity in
place, start a third Harness generation, and require exactly one settled
execution, one file effect and one terminal. The other two drop the control
plane's reply after it committed; the retry under the same `Idempotency-Key`
must land on the original record, so a lost decision produces one
`ACTION_RESPONSE` operation and a lost Turn admission produces one Turn row:
```bash
npm run test:e2e:managed-harness-restart-approval-after-decision
npm run test:e2e:managed-harness-restart-approval-after-prepare
npm run test:e2e:managed-harness-restart-approval-after-result
npm run test:e2e:managed-harness-restart-approval-decision-reply-lost
npm run test:e2e:managed-harness-restart-approval-admission-reply-lost
```
Two more cover the next barriers of the same matrix. The terminal
reply-loss arm commits `turn.settled`, loses its reply (and every bounded
retry of it) until the crash, and restarts the Harness; the replacement
projects the committed terminal, and no second store commit is required. The
missing-terminal arm refuses the write itself, so the replacement must
compensate the payable checkpoint exactly once. Both keep one original
Action, one settled execution, one file effect, one terminal, the recovered
model answer and the original cleanup:
```bash
npm run test:e2e:managed-harness-restart-approval-terminal-reply-lost
npm run test:e2e:managed-harness-restart-approval-terminal-write-failed
```
A refused cleanup release followed by another crash (C3) refuses the release
until the Turn is terminal and its Harness is killed. The owed cleanup is then
paid by the replacement through the control plane's debt retry, not by the
Turn path: the arm requires the replacement's own release, one `confirmed`
event and no remaining `HOSTED_CLEANUP` projection:
```bash
npm run test:e2e:managed-harness-restart-approval-cleanup-failed
```
The control plane retries that debt itself. A periodic scan reads the dedicated
managed database (never an embedded host's business database), lists outstanding
`HOSTED_CLEANUP` owners whose Session has no active Turn, resolves the owed
Turn from the cleanup descriptor, and issues the same prompt-scoped
cleanup-only load an operator recheck uses. The scan is keyset-paginated and
wraps around, advancing past unreadable descriptors so a stuck prefix cannot
starve later debts. Retries back off in memory from 10 seconds to 5 minutes; an
attempt that loses the daemon or its reference drops the connector's cached
attachment so the next one loads the replacement generation, while a named
refusal of live work only backs off. A Session with live work, an unsupported
profile or unreadable evidence keeps its debt and waits for the operator
recheck route. See
[the cleanup debt design](../../docs/design/2026-10-28-hosted-cleanup-debt-retry.md).
A lost takeover-load reply (A4's load half) drops the reply to the replacement's
first drive load once, via a test-gated Harness hook (`QWEN_E2E_DROP_LOAD_REPLY`,
never set in production). The control plane redrives the same load into the
now-active driver; the arm asserts the reply was actually lost, then requires the
original Turn to project exactly one terminal with the recovered answer — the
redrive restates the active attachment instead of wedging the Turn:
```bash
npm run test:e2e:managed-harness-restart-approval-load-reply-lost
```
A crash between the main stream's durable Finished marker and the assistant
commit (B3a) holds the replacement inside that window via a test-gated Harness
hook (`QWEN_E2E_CRASH_BEFORE_ASSISTANT_COMMIT`, never set in production), set
only on the generation that must crash. Once its post-tool round records
Finished, the hook holds before the assistant commit and the runner kills it
there; the next generation must reissue the saved request deep-equal and
project one terminal with the recovered answer, rather than compensate an
already-committed output:
```bash
npm run test:e2e:managed-harness-restart-approval-finished-before-commit
```
They write their evidence under `.qwen/investigations/g3-step3-implementation/`;
`G3_VERIFY_REPORT_DIR` overrides the directory. The remaining rows stay
focused or integration tested for now: mixed calls and rewritten inputs
across a replacement, Hook/MCP/child uncertainty, the attempt and stream
identities plus the live SSE view, the round and cost budgets, and the
mixed-version reader/bundle closure (#14481).
Two arms keep a former owner alive while its replacement finishes the Turn
and then wake it with `SIGCONT`:
```bash
npm run test:e2e:managed-continuation-frozen-owner-failover
npm run test:e2e:managed-frozen-control-plane-failover
```
The first freezes the original Harness, so it covers the journal: the woken
writer commits nothing. The second freezes the original Spring and Harness
together during a no-tool model round. It starts a second Spring and Harness
on the same MySQL, and the new Harness reissues the round to completion.
After the wake, the MySQL general log must show the former Spring's claim
`UPDATE`s carrying its stale owner id, and the former Harness must log the
store's refusal of its stale writer grant. None of these attempts may change
the Session binding, the Turn row, the public events or the journal, as far as
the snapshot listed in D11 of the replaceable-Harness design can see. All of
these arms are steps of the Hosted failover E2E CI job (`hosted-failover-e2e`),
which runs daily or on demand. It builds the CLI and packages the server
itself, so a failing test in the Hosted MySQL job does not skip the arms, and
each arm runs even when an earlier one failed.
### Hosted recovery scope
Ordinary LOCAL and SSH_LOCAL Managed Workspace Sessions may execute concurrently in the same directory, with one active Turn per Session. An UNKNOWN tool result on a READY binding retains its original Runtime Session and cannot be replayed, but does not exclude another ordinary Session. Existing LOST, DRAINING, OPERATOR_RECOVERY and attested RECOVERY_BLOCKED placement barriers remain in force. Physical registration, fencing, migration and maintenance still require every execution membership and unproven writer to drain. Shared files have no transactional isolation. Past execution overlap does not disable file undo. Undo requires other current memberships to release, then briefly excludes new acquisitions; existing file-level history checks still apply. Resident/private CSI and existing Agent/Workflow child-admission restrictions are unchanged. See the [concurrency design](../../docs/design/2026-10-21-managed-workspace-concurrency.md) ([简体中文](../../docs/design/2026-10-21-managed-workspace-concurrency.zh-CN.md)).
The baseline stores ordinary execution memberships per Runtime Session. Use matching Server/Broker builds with a fresh catalog. CSI reservation and physical-maintenance ownership retain their existing roles. Development catalogs using singleton holders require their original deployment to finish or recover and release those holders; they cannot be reused by this build.
The [SQL wake scan](#wake-recovery-after-harness-loss) has separate local
macOS/MySQL 8.4 evidence; it does not yet have Linux/Windows, real-MQ or deployed
provider acceptance. The scope below does not extend those claims.
The first Hosted release supports recovery for one Java control-plane instance
on one trusted Linux host. That control plane replaces reachable Hosted Harness
process generations one after another, and a Runtime Session stays exclusive
to its Session. Recovery covers only the profiles and parked states that are
explicitly supported and accepted on the released artifacts. The gates above
accept the in-flight and continuation Runtime takeovers, the native-file
approval waits and the first model round of a no-tool Session, all without
Hooks or MCP. An in-flight Shell call and unknown external effects stay
blocked rather than replayed, and Step 3 neither reissues a model round nor
resumes an approval in a Session with Hooks or MCP. Journal writer fencing,
durable engine ownership, capability and journal-contract checks and
fail-closed recovery apply throughout. A refused recovery is never replayed
through another Runtime or the Legacy engine.
The frozen control-plane arm shows that a woken former control plane's stale
claim is refused and changes nothing, and a store test pins the bind guards
that claim never reaches. Neither makes two live control planes a supported
deployment. Routing between them is out of scope: a new Turn could
be claimed by either one. Cross-host takeover, physical-ownership handoff
between hosts, real providers and mixed-version clusters still need their own
acceptance before they are supported.
The `--big-output` long-answer mode is also Workspace-bound:
```bash
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:
```bash
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](../../docs/design/workspace-storage-migration.md) and [Chinese design](../../docs/design/workspace-storage-migration.zh-CN.md). 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.
The current baseline includes the Workspace migration schema. Earlier development catalogs require fresh disposable fixtures; do not edit Flyway history to reuse them.
The private artifact is `qwen-managed-agent-server-0.1.0-alpha-workspace-migration.jar`; the reference distribution runs the same entry as `bin/qwen-managed-agent-admin workspace-migration`. 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:
```bash
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. Static Skills and administrator-provisioned MCP/Hook pins are supported as described below. 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. Opting into `hosted-workspace-files/3` needs the same drain and upgrade before its approval selection is enabled. Empty permission policies inherit the deployment approval mode (default `yolo`); use `default` for deployments that require approval.
`skills` accepts up to 16 static `{ "name": "review", "description": "Review changes", "body": "Read the diff and report concrete defects.", "base_directory": ".qwen/skills/review" }` entries for a Workspace-bound tool profile. `base_directory` is optional and defaults to `.`; it is a canonical POSIX path relative to the Session's working directory, matching Runtime `read_file` and `glob`, never the Harness host. Absolute paths, traversal, empty or dot components, colons, backslashes and control characters are rejected. Names are unique and use letters, numbers, `_`, `:`, `.`, or `-` (at most 128 UTF-8 bytes); descriptions are nonblank and at most 4096 UTF-8 bytes, bodies are nonblank and at most 32768 bytes, directories are at most 4096 bytes, and the compact JSON catalog is at most 48 KiB. Invalid Unicode and extra Skill fields are rejected. Bodies grant no tool permissions and cannot register Hooks, override the model, or discover host-installed skills. The connector publishes the catalog as a digest-addressed `managed-agent-skills` resource; a replacement Harness validates and reads the original reference.
Public and WebShell Workspace Session creation also accept an optional `skills` array in the same static format. The Session catalog permits 256 entries, 512 KiB per body and 2 MiB compact JSON; AgentDefinition admission retains the smaller limits above. Both origins use the same `managed-agent-skills` resource kind, whose shared resource validators apply the 2-MiB bound; the smaller definition limits are enforced at admission. The array adds to the pinned AgentDefinition Skills (including for the built-in `qwen-code` agent); duplicate names or combined limits are rejected. The effective catalog is saved immutably with the creation receipt, and changing Skill content under the same idempotency key conflicts. Empty or omitted arrays preserve existing behavior. The WebShell uses the enabled Skills from its existing Skills management page; creation imports an immutable instruction snapshot from the explicitly selected trusted daemon Workspace. Upload and enable/disable remain on that page. Manual-only and user-hidden invocation flags are preserved; source asset directories, permission grants, model overrides and Hooks are not imported. This requires a durable Session Store and a Harness advertising `session-skills/2`; deploy compatible Java, SDK, Harness, and WebShell artifacts together. The existing Skills page owns discovery and installation; the Managed execution Runtime does not discover source directories implicitly.
`mcp_servers` accepts up to 16 unique `{ "server_id": "docs", "server_revision": 1, "definition_digest": "<64 lowercase hex characters>" }` pins. Select an explicitly allowlisted `hosted-workspace-mcp/1` or `hosted-workspace-shell/1` tool profile in `QWEN_MANAGED_AGENT_DEFINITION_TOOL_PROFILES`. The legacy MCP profile requires pins and retains its no-Shell/no-backup contract. Ordinary `shell/1` accepts optional pins and keeps Read/Write/Edit, foreground/background Shell, file backups and rewind on the same Session Runtime owner. Other profiles, foreground-only SSH and dedicated CSI resident Workspaces reject nonempty pins. Plan creation/change is explicitly unavailable for Agents with MCP pins; Session options do not advertise it. Static pins do not import daemon settings or enable configuration editing. The Runtime deployment owns the matching server manifest, endpoints, transport and credentials. `hook_catalog` similarly accepts `{ "catalog_id": "review", "catalog_revision": 1, "definition_digest": "<64 lowercase hex characters>" }` for any Workspace-bound tool profile, selecting a Runtime-provisioned Hook catalog. Revisions are positive JavaScript-safe integers. Neither field accepts executable recipes or credentials. Create/update continues to store future configuration shapes without interpreting them; the limits and supported shapes above are enforced at Session admission. An old definition containing an unsupported shape remains rejected there.
Roll out the compatible Runtime manifest, Broker and Hosted Harness before admitting configured Sessions. The Harness must advertise `workspace-shell-mcp/1` for ordinary Shell with MCP. Set `QWEN_HOSTED_LIFECYCLE_V2_PROFILES` to include `hosted-workspace-shell/1` for that combination, or `hosted-workspace-mcp/1` for the legacy profile, before public close/delete. Ordinary Shell with MCP requires the lifecycle v2 close path rather than direct Hosted DELETE. Preserve reliable Runtime stop support; configuration admission does not bypass these existing lifecycle gates. MCP/Hook load requests carry pins from the original admitted AgentDefinition revision, and Skills use the saved resource reference. Updating a definition affects new Sessions only. Unknown execution outcomes continue to require reconciliation of the original Runtime operation. Missing, corrupt or conflicting resources fail loading explicitly. Existing Sessions without extension fields keep their prior behavior for non-MCP profiles. The MCP profile requires nonempty server pins during create and attach/load validation. Older storage admission could retain an empty-pin MCP profile even though Hosted attachment already rejected it; audit stored definitions and Sessions before rollout rather than assuming none exist. Retain historical access separately and settle or close affected Sessions on the previous deployment before replacement. Publish a corrected definition revision for new Sessions; existing Session pins are not rewritten.
### Session MCP management
Deploy matching Java, Java SDK, Broker, Runtime and WebShell artifacts. The Harness must advertise `session-mcp-management/1`, and `QWEN_HOSTED_LIFECYCLE_V2_PROFILES` must enable the Session's exact `hosted-workspace-shell/1` or `hosted-workspace-mcp/1` profile. The initial implementation uses ordinary local-process Workspace workers; foreground-only SSH, dedicated CSI resident profiles, read-only children and other tool profiles do not gain management. An ordinary Shell Session may add its first MCP server without being recreated.
Use the Session's **Manage MCP** entry or the MCP management page. Add or edit selects an exact server revision published in that tenant and Workspace's Runtime-private `QWEN_MANAGED_MCP_CONFIG` manifest. Remove, enable, disable, reload and restart apply to the current Session. The manifest remains deployment administration: this interface neither accepts arbitrary executable recipes nor imports daemon settings, credentials or OAuth state. Inventory exposes only the published identifier, revision, digest and transport. Keep historical manifest revisions available for existing Sessions.
Management reattaches only to a Session resident in the current Harness and never triggers cleanup, recovery or wake dispatch. After a Harness restart, continue or recover the Session through its normal execution flow first; management otherwise returns `hosted_mcp_session_load_required`. A new empty Session may initialize through management without sending a Turn. Command status never creates a Session, and uncertain creation never loads a cold journal implicitly. Cleanup-only recovery attachments also refuse management until a normal execution load upgrades them.
The original AgentDefinition and Skills stay pinned. Explicit management commands append independent Session MCP settings revisions; they do not change other Sessions or future defaults. Each snapshot retains at most 32 server identities including removed tombstones, with at most 16 enabled selections; the existing shared Runtime connection quota still applies. Removed and disabled selections survive Session reload. Direct detach remains governed by the existing lifecycle protocol. The Session retains its original owner and MCP history until ordered close, so Plan remains unavailable after MCP management even when every server is removed. File/Shell tools, approvals, backups and rewind retain their ordinary behavior. Restart drains the selected connection before reconnecting, without restarting a shared worker.
Public routes are `GET /v1/agents/sessions/{sessionId}/mcp-management`, `POST /v1/agents/sessions/{sessionId}/mcp-management/commands` with `Idempotency-Key`, and `GET /v1/agents/sessions/{sessionId}/mcp-management/commands/{operationId}`. WebShell has corresponding POST query, submit and operation-query routes under `/api/agent/web-shell/v1/session-mcp/management/`. Commands include `action`, `serverId` and `expectedRevision`; add/edit also include the exact selected `server` pin. Management requires Workspace execution permission and a public active Session. The settings commit rechecks idle/Plan/lifecycle admission atomically; a static settings revision or catalog is not proof of a healthy connection.
An unconfirmed operation keeps its original identity. Status queries are read-only; explicit retry first queries that original operation, then resumes the saved original command only while it remains unsettled. The UI can retain unconfirmed public command metadata across a tab reload when the provider has an explicit tenant-and-actor `productScope`; otherwise it retains it only in memory. Neither mode stores executable recipes or credentials. Unknown physical outcomes block replacement rather than replaying on another Runtime. See the complete [English design](../../docs/design/2026-10-22-managed-session-mcp-management.md) and [Chinese design](../../docs/design/2026-10-22-managed-session-mcp-management.zh-CN.md) for ownership, authorization and qualification boundaries.
### Shell lifecycle handoff
Initial v2 close/delete admission preserves an already-live writer for one configured Session Store writer-lease duration, measured using database time. Ordinary renewal and writes remain fenced; idempotent replay, failed resubmission and claim retries never refresh the allowance. Effects still requires a valid lease. A delivery or child cascade beyond this finite handoff can still require recovery; expired writers are not revived. See the [lifecycle design](../../docs/design/2026-10-23-managed-shell-lifecycle.md) ([简体中文](../../docs/design/2026-10-23-managed-shell-lifecycle.zh-CN.md)).