* docs(managed-agent): design AgentDefinition execution (D8b and D8c)
Proposes how a Session pins a stored AgentDefinition revision (D8b) and how
the definition's fields take effect (D8c): permission_policy and tools through
the existing per-Session approval mode and tool profile pins (Java only), then
model and instructions through the Hosted Harness. Content that cannot take
effect refuses admission instead of being dropped. The D8a design's open
questions now link here.
Decisions for review are listed in section 7.
* docs(managed-agent): address D8b/D8c design review
- Default the definition tool-profile allowlist to files/1; files/2 becomes
an operator opt-in after the #13166 rollout, since Java has no switch.
- Add W1 recovery's source validator as the fourth qwen-code check and keep
the pinned revision and digest in the recovery source.
- Refuse approval_timeout_ms in v1 instead of accepting a value the
connector would not deliver.
* feat(managed-agent): pin and compile stored AgentDefinition revisions (D8b+D8c-1) (#12867)
Session admission resolves a stored definition: an explicit agent_revision
pins that revision, an omitted one resolves the latest revision locked
under FOR UPDATE in the creation transaction, and a missing agent or
revision answers 404 agent_not_found. The pinned (agent_id, revision,
digest) lands on the new agent_definition_digest column (V53), and replay
returns the original Session without resolving again.
D8c-1 compiles permission_policy and tools into the approval mode and
tool profile the Session already pins: shell profiles never pair with
yolo, unbound Sessions stay model-only, and selectable profiles sit
behind the qwen.managed-agent.definition-tool-profiles allowlist
(default hosted-workspace-files/1). model, instructions, skills,
mcp_servers and environment_template_id refuse with 409
agent_definition_unsupported naming the field in error.field instead of
being dropped.
The four hard-coded qwen-code gates relax to qwen-code-or-pinned:
later-Turn admission, Workspace execution authorization and W1 recovery
sources, which now carry the pinned revision and digest. PublicSession
gains the optional agent_digest and the contract becomes 1.34. Two
design amendments ride along: FOR UPDATE implements the stated locked
read (H2 has no FOR SHARE) and the refusal field is named error.field,
matching the envelope shape.
* chore(web-shell): regenerate managed agent API types for error.field
* test(managed-agent): include V55 in migration upgrade assertions
The MariaDB integration run applies the new definition-digest migration correctly. Keep the strict ordered upgrade expectation in sync while preserving the applied-history checks.
Co-authored-by: Qwen-Coder <[email protected]>
* fix(managed-agent): correct three review findings on stored-definition execution
- insertChildSessionCommand now copies agent_definition_digest from the
parent, so a child of a stored-definition Session is no longer born
with a NULL digest (non-executable, and poisoning W1 recovery).
- hosted-harness-session: a deadline expiry keeps naming the deadline;
only a generic error outcome is reclassified as model_unavailable.
- README and the definition-service javadoc no longer claim Sessions do
not use stored definitions.
Co-authored-by: Qwen-Coder <[email protected]>
* fix(managed-agent): accept published agent instructions in W1 recovery
The W1 recovery reader's PUBLISHED allowlist covered only the three
managed-tool-result kinds, so reading a definition Session's digest-
addressed instructions resource threw resource_layout_unsupported and
failed the whole storage's capture/verify (R1-3). The reader now also
accepts managed-agent-instructions up to 64 KiB, mirroring the
allowance ManagedSessionStore.verifyStoredResource already carries,
and the mixed-storage capture test publishes non-empty instructions
through the production publisher and reads them back through the
recovery RPC.
Also fixes two test/doc findings from the same review round: the two
compiler-test @CsvSource text blocks were passed as value, so JUnit
executed one row per annotation (14 of 16 rows dead) — now textBlock =
(R1-50); and the OpenAPI route descriptions for createAgent and
createSession (plus their WebShell twins) still claimed Sessions do
not use definitions and public callers cannot select the profile,
which this PR's own admission changes falsify (R1-7, R1-6). WebShell
generated types regenerated.
* fix(managed-agent): round-2 review — migration version, stop-load pin, store guard, doc truths
R2-3 (fails-closed): the new migration took V58, below versions main
already shipped (V60/V62/V64), so with Flyway's default outOfOrder=false
it resolves-but-never-applies on any upgraded database and the
agent_definition_digest column every Session INSERT and read touches
silently does not exist. Renamed to V66, the next free version.
R2-1: messageStopAttachment is the third LoadHarnessSession site and the
only one that never carried the pinned agentDefinition, so a
definition-pinned child Session's message stop was refused with 409
hosted_agent_definition_conflict. The load now carries the pin, and
aMessageStopOfAPinnedSessionCarriesTheAgentDefinition pins it red/green.
R2-2: Session admission pinned a stored definition even when the Managed
Session Store the Harness create needs was disabled — a supported-looking
config (harness on, store off, files off) answered 202 for a Session that
could never run. validateWorkspaceFiles now refuses that shape at startup,
and ManagedAgentPropertiesTest witnesses it.
R1-28: two of the four falsified statements were still standing — the
README's revision-source sentence (a pinned Session records the revision
resolved at admission, not QWEN_MANAGED_AGENT_REVISION) and the
AgentDefinitionRequest javadoc's 'no field changes Session execution yet'.
Also from the same round: the 64 KiB agent-instructions bound is now one
named constant (MAX_AGENT_INSTRUCTIONS_BYTES) instead of four bare
literals across publisher, both verifiers and admission (R2-5); the
createAgent description's stale (v1.34) label is now v1.41 and names the
operator allowlists that gate model and tool profile (R2-13), the
webShellCreateSession description no longer promises a fixed server-owned
profile for definition-pinned Sessions (R2-6), and the workspaceTurns
predicate wording now matches the implementation (active and undeleted,
both branches on the frozen execution profile) (R2-11); the generated
managed-agent-api.ts was regenerated.
* fix(managed-agent): pin V66 in the workspace migration IT list
842d8a62e8 renumbered the agent definition digest migration from V58 to
V66, but WorkspaceMigrationMySqlIT still asserted the old pinned version
list. upgradesCurrentMainWithoutChangingAppliedMigrations therefore
failed with the applied set [48..57, 60, 62, 64, 66] against the expected
[48..57, 58, 60, 62, 64].
Align the list with the migrations actually present in the tree, keeping
installed_rank (ascending version) order.
Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmv32n54k1x
* docs(managed-agent): name V66 as the definition-digest migration in the design
Review round 2 renamed the migration V58→V66 (main had since shipped
V60/V62/V64 and Flyway never applies a version below the applied
maximum), but §10 of both design docs still said 'the database
migration is V58'. Both now name V66 with the reason, matching the
shipped V66__managed_session_agent_definition_digest.sql. The PR
description's V55 references are updated the same way, keeping the
two historical repair notes as records with the superseded numbering
marked inline.
---------
Co-authored-by: Qwen-Coder <[email protected]>
6.5 KiB
Managed AgentDefinition Revisions (Stage D8a)
Status: implemented in this change Date: 2026-10-01 Issue: #12867, part of #12380 Builds on: API contract (D1) and Session query (D2)
1. Problem
The contract declares POST /v1/agents, GET /v1/agents/{agentId} and
POST /v1/agents/{agentId} with the AgentDefinitionRequest and
AgentDefinition schemas, but all three are planned. A Session records the
fixed revision from QWEN_MANAGED_AGENT_REVISION and the only accepted agent
is qwen-code, so there is nowhere to keep a definition or its history.
The bare collection path also skipped the tenant scope: TenantContextFilter
matched the /v1/agents/ prefix, which /v1/agents does not have.
#12867 splits D8 into three slices. D8a stores definitions and serves the routes. D8b makes a Session pin a stored revision. D8c makes definition fields change Harness execution and needs its own design.
2. Goals
- Serve the three routes with the contract's request and response schemas.
- Keep revisions append-only and immutable, each with a digest of its canonical content.
- Make create and update idempotent under their
Idempotency-Key. - Scope definitions to the tenant, including the bare collection path.
3. Non-goals
- Sessions keep pinning
qwen-codeand the configured revision (D8b). - No definition field changes model selection, instructions, tools or permissions (D8c).
- A separate publish step, listing definitions, deleting them, and the role matrix from section 10 of the contract.
4. Design
4.1 Storage
Flyway V33, following main's V32 Workspace session-close migration, adds two tables. managed_agent_definition holds one row per
revision, keyed by tenant, agent ID and revision number, with the digest, the
request's JSON and the creation time. Rows are only inserted.
managed_agent_definition_command records each create or update under its
tenant and Idempotency-Key, with the request digest and the revision the
command answered with.
4.2 Identity and digests
The server generates agent IDs as agent_ plus 32 hex digits; the request has
no ID field. GET/POST /v1/agents/{agentId} answer 404 agent_not_found for
any ID that is not of that form. The reserved names are answered elsewhere:
literal routes outrank {agentId} in Spring's path matching, so
/v1/agents/sessions (GET and POST) and /v1/agents/workspaces (GET) reach the
Session and Workspace routes, while a reserved name whose method has no literal
sibling — POST /v1/agents/workspaces — still reaches the definition route and
answers 404. Either way the reserved names never resolve to a stored row, and
neither do padded or case-folded variants of a real ID.
The content is the request with absent and null optional fields removed; the
contract declares skills, mcp_servers and metadata nullable for that
reason. Array items must be objects, so a null item answers 400. The
digest is the SHA-256 of the canonical JSON (keys sorted at every level), so
key order does not change it, and neither does an explicit null for one of
the optional top-level fields. Null stripping is one level only: a null
nested inside an open map such as metadata does change the digest. The
request digest also covers the operation and, for an update, the agent ID.
4.3 Semantics
- Create stores revision
1and answers202. - Update stores the next revision. When the content digest equals the latest revision's, it stores nothing and answers with that revision.
- Replay. The same key and request digest answer the recorded revision
with
X-Qwen-Idempotent-Replay: true. The same key with a different request, including a create key reused for an update, answers409 idempotency_conflict. - Concurrency. Revision rows and the command commit in one transaction.
When a concurrent request commits first, the losing write rolls back and
reads the command committed under its key: the same request replays that
result. Otherwise a concurrent update that took the revision number answers
409 agent_revision_conflictwithout recording the command, so its key can be retried, and a different request under the same key answers409 idempotency_conflict. A no-op update may precede an overlapping content-changing update; its response and later replays still name the recorded revision, even if that revision is no longer the head when the response arrives. - Get reads the latest revision, or the one named by
revision(a decimal number from1). Anything else answers404 agent_not_found. - The response carries
id,object: "agent",revision,digest,created_atand the request'smetadata. It does not echo the content.
4.4 Tenant scope
TenantContextFilter now also covers the exact path /v1/agents, so every
route requires X-Qwen-Tenant-Id and refuses a mismatched trusted actor with
403 actor_scope_mismatch. Reads and updates in another tenant answer 404.
4.5 Contract
The three routes move to implemented with descriptions, create and update
declare 400, and the contract is v1.29.0. The version assumes #13112 lands
v1.28 first.
5. Open questions for D8b and D8c
- Which fields take effect, and which stay stored only until their stage
lands (for example
skillsandmcp_serverswith Stage H)? - Is a stored revision usable at once, or does a publish step decide which revision new Sessions use?
- How do
toolsandpermission_policymap onto the frozen Hosted tool profiles and the approval mode a Session pins today?
AgentDefinition execution (D8b and D8c) proposes answers to these questions.
6. Verification
ManagedAgentDefinitionTest: creation and replay, unchanged and changed updates, revision reads, key conflicts, tenant scope, server-generated IDs, validation, and canonical digests.ManagedAgentDefinitionServiceTest: a write that loses a race replays the committed result for the same request and keeps its conflict otherwise.ManagedAgentApiContractTest: the three routes against their schemas, including400,404and409, and the tenant filter refusal on the bare collection path.TenantContextFilterTest: the bare path requires a tenant, and an unrelated path with the same prefix is not filtered.