Repository navigation
feat(managed-agent): broker authentication and broker-provisioned writer credentials - #13210
Conversation
afa75f3 to
92298a4
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
|
Addressed the review round in 3648e4b: Criticals
Triage items: requireOwner reads the creator row null-tolerantly (no more 500 on a missing row); the writer credential policy is a required injection (fails closed). Suggestions: fixed — bound-mode test for the publication-writer path, workspace term of the MAC pinned, drift window validated (>=1s) and exercised, unparsable timestamp 401 pinned, store base-url/internal-port coherence guard, plaintext non-loopback harness URL refused, envelope-shaped 404 from the routing filter, connector address acceptance checked, TLS/plaintext split warned, signed-hosted-create replay test, WebShell creator recording test, CLI refusal body now carries the reason, producer omit-when-unset test, passthrough assertion covers all six fields, lowercase-hex and undecoded-path/raw-query documented, stale self-mint prose corrected, loopback trust-boundary caveat added, spec wording scoped to covered routes and the missing-tenant 400. Intentionally unchanged (design decisions, now documented): ownerless Sessions (anonymous open-mode or pre-V31) answer approvals tenant-scoped, matching their read ACL (§8.4 amended); the attach payload fields stay additive without a protocol bump — the upgrade order (CLI before broker config) is called out in Risk & Scope. 中文说明已在 3648e4b 处理本轮评审:
|
3648e4b to
21ff724
Compare
…ter credentials Close the five gateway-free security gaps from QwenLM#13180: - signed auth mode: the broker authenticates the tenant/actor header pair itself via HMAC (X-Qwen-Signature + timestamp), with startup guards that refuse non-loopback binds, weak keys, a contradictory trusted-actor stand-in, and a context path that would bypass the path filters - writer credentials are broker-provisioned HMAC bindings over (tenant, workspace, session) and enforced at every session-store entry point; the harness receives them through the attach payload - /internal/** can bind to its own loopback connector with 404 routing between the two surfaces - sessions record their creator (V28), so hosted approvals are answerable over HTTP and workspace approvals bind to the authenticated actor - the TS client refuses plaintext http:// broker URLs on non-loopback hosts unless the broker opts in via the attach payload Includes the bilingual design doc (docs/design/managed-agent-broker-auth) and verification coverage: signed-mode E2E, dual-port routing E2E, the binding sweep over every store entry point, and the hosted ownership chain. Co-authored-by: Qwen-Coder <[email protected]>
- refuse internal-server.port equal to server.port at startup: the surface routing filter classifies by local port, and equal port numbers on different addresses would serve /internal/** on the public address - refuse reusing the signing key as the writer binding key: the two HMAC keys protect different domains - log the resolved auth mode, internal listener and writer binding state at startup, as the design doc promises - log the refused managed session store descriptor in the hosted harness so the transport guard's remedy stays discoverable Co-authored-by: Qwen-Coder <[email protected]>
main gained its own V28 and V30 migrations while this branch was in review; move the creator column to V31 to keep the Flyway sequence unambiguous. Co-authored-by: Qwen-Coder <[email protected]>
The v1.28 Unauthorized response description changed; the generated client must match the contract. Co-authored-by: Qwen-Coder <[email protected]>
- sign the request body, query string and Idempotency-Key too, so a captured signature authorizes exactly one request (review R1-3) - resolve the public port to the Spring Boot default 8080 when server.port is unset, so the equal-port guard cannot slip (R1-1) - read the session creator row null-tolerantly instead of 500ing on a missing row (triage #3), and make the writer credential policy a required injection so it fails closed (triage #5) - startup guards: allowed-drift >= 1s, loopback store base-url must name the internal listener's port, and an enabled harness over plaintext non-loopback http is refused (the attach payload carries credentials) - the routing filter answers the standard error envelope, the internal connector checks its address property was accepted, and a TLS public listener beside a plaintext internal one is warned about - docs: OpenAPI scheme descriptions, README recipes and both design docs now match the implementation; the ownerless fallback is documented as tenant-scoped for anonymous and pre-migration Sessions - tests pin the drift window, unparsable timestamps, body/query/key coverage, the workspace term of the writer MAC, WebShell creator recording, the publication-writer credential check, and the producer's omit-when-unset payload Co-authored-by: Qwen-Coder <[email protected]>
main took V31 (workspace recovery bundle) and V32 (workspace session close) while the review round was in flight; move the creator column to V33. Co-authored-by: Qwen-Coder <[email protected]>
main took V33 (agent definitions) during the latest rebase. Also make WorkspaceRecoveryStoreTest compare the session table byte[]-tolerantly: creator_actor_key is the first VARBINARY column in that table, and byte[] compares by reference inside Map.equals, which failed the immutability assertion on MySQL. Co-authored-by: Qwen-Coder <[email protected]>
21ff724 to
201051a
Compare
…olumn set The retention IT pins the schema at V31 and then creates a session, but the store insert now always names creator_actor_key (V34), which a database at the prerequisite version does not have yet. Write the pre-upgrade session with the V31-era column set directly, then let the close and retirement flow run through the store as before. Co-authored-by: Qwen-Coder <[email protected]>
…tract gaps - Cover the bare POST /v1/agents route and normalized path spellings by deciding filter coverage on the routed path (shared PublicSurface predicate), while the canonical string keeps signing the raw URI. - Bound the buffered signed body (auth.max-signed-body-bytes, default 10 MiB) and answer 413 before the signature comparison; refuse a repeated Idempotency-Key header with 400. - Classify the internal surface on the routed path as well, so encoded or parameter-carrying spellings cannot cross the listener boundary; the routing filter now shares the injected ObjectMapper. - Refuse a plaintext session-store base URL whose host falls outside the client's literal-only loopback set, guard spring.mvc.servlet.path alongside context-path, and enumerate the guards allow-insecure-bind skips in the startup posture line. - Qualify the creator-only responder wording in the OpenAPI contract with the ownerless fall-through, declare the 401 refusals per route, and pin both with contract tests. - Restore the transaction boundary on the legacy 9-arg insertSessionCommand default, share one test signer between the integration tests, and carry writer_credential_invalid in the shared error fixture. Co-authored-by: Qwen-Coder <[email protected]>
|
Addressed the R2 round in 8dc87e3 (plus the MariaDB IT fix in e86c2a5: the retention IT pins the schema at V31, so its pre-upgrade session is now written with the legacy column set instead of the current store insert). Criticals
Suggestions
Already in place / reply-only
Design docs (EN + zh-CN) updated for the new fail-closed details, guards and limits; the OpenAPI 401 sweep is reflected in the risk section. 中文说明已在 8dc87e3 处理 R2 轮(另有 e86c2a5 修复 MariaDB IT:保留 IT 把 schema 钉在 V31,其升级前会话改用以当时列集直写)。 严重项
建议项:R2-5 查询串签名钉住;R2-6 共享 ObjectMapper;R2-7 posture 行枚举被跳过的守卫;R2-8 启动拒绝客户端回环集之外的明文 store URL(含 已有/仅答复
设计文档(中英)已同步新的 fail-closed 细节、守卫与上限;OpenAPI 401 逐路由声明已反映到风险节。 |
main landed V34 as managed_tool_output_collection; move the creator column to V35 and update the design docs, the OpenAPI wording and the test references. Co-authored-by: Qwen-Coder <[email protected]>
…oker-auth # Conflicts: # packages/sdk-java/managed-agent-server/src/main/resources/openapi/managed-agent-public-api.openapi.json
The first merge commit swept in a locally regenerated NOTICES.txt whose content depends on the host's installed tree; CI regenerates it on Linux and expects the upstream content. Co-authored-by: Qwen-Coder <[email protected]>
- Narrow the replay and body-bound claims to what the code guarantees (captured signatures stay fixed to method/path/query/tenant/actor/body; the body bound is per request, with the worker pool as the aggregate ceiling). - Presize the signed-body buffer, pin the raw-URI signing split and the chunked bound, and add a route-table coverage test for the shared public-surface predicate. - Guard a plaintext non-loopback tool-publication service URL, normalize a blank internal address, warn for a non-loopback internal listener without TLS, and stop classifying a bare 127 as loopback. - Declare 413 per operation with a shared PayloadTooLarge component, state the ownerless approval fall-through's create-command step, and parse the shared fixture structurally in the qwencode test. Co-authored-by: Qwen-Coder <[email protected]>
|
Sandbox verification round 4 (run 37254095813) disposition / 第四轮沙箱验证处置: Finding #1 (413 contract gap) → fixed by narrowing the claim. The PR body now reads: "the 401 refusal is declared per route on the covered surface, and 413 on every covered route that declares a request body" — which is exactly true at head (401: 58/58; 413: 32/32 on requestBody-declaring operations). Both language halves updated; no code or contract change. Finding #2 (unreachable Finding #3 (the timeout class is unpinned) → deferred, with reasoning. A source-scanning guard test over The 415 observation (unsupported media type answers 500 — pre-existing, Noted with appreciation: the 12-cell charset A/B (including the 中文:发现 #1 已通过收窄主张修复(正文改为「401 逐路由声明,413 覆盖所有声明请求体的覆盖路由」,与 head 实测完全一致,双语已同步);发现 #2 已修复(证据段改引本通道臂 A 的公开复现 run);发现 #3 延后并说明理由(当前 59/59 全覆盖有独立普查实测,抖动门是确定性绊线,自扫描守卫测试相对收益偏脆——记入延后清单,若有回归漏网再补);415 观察为 PR 外既有问题,记入跟进 issue 候选。12 格 charset A/B 与回归测试的非空验证正是该修复所需的度量,致谢。 |
|
@qqqys One pointer for the next pass's gate #4: the wire-level regression the round-6 review asked for exists — 中文:给下一轮的门 #4 指路——R6 要求的 wire 级回归测试在 |
|
@qwen-code /triage |
yiliang114
left a comment
There was a problem hiding this comment.
LGTM at 64fe514, reviewed against the threat model. All five goals check out in code: SignatureAuthFilter verifies HMAC over method+URI+query+tenant+actor+timestamp+body-hash+idempotency-key with MessageDigest.isEqual, bounded buffered body, repeated-key refusal, and the routed-path coverage rule (so percent-encoding can't slip past); auto mode refuses non-loopback binds without a key; the writer credential is broker-issued HMAC over the scope (>=32-byte key enforced, checked before any state lookup, legacy TOFU preserved when unbound); the TS client refuses plaintext non-loopback with a literal-only loopback set (no DNS resolution to rebound through); and V40's creator_actor_key gives hosted sessions a durable owner. The startup guards enumerate every skipped check. Migrations don't collide with in-flight V36-V39.
|
Sandbox verification round 5 (run 37261576399) disposition — post-merge / 第五轮(合并后)处置: 61/61 assertions, flakiness gate ✅, and the correction is accepted with thanks — the narrowed 413 sentence now reads true as measured (32/32 on requestBody-declaring covered operations). That correction closed three rounds of a finding that was really a denominator mismatch. Finding #1 (approval-ownership wording) → fixed in the body. The Risk & Scope sentence now carries the qualifier in both languages: loopback open mode keeps legacy behavior except that an anonymous hosted Session (no recorded creator, no recorded create command) is tenant-owned for approvals, exactly as design doc §4.4 tier 3 specifies. The bounding analysis is right: in signed mode the signing key already confers tenant-wide authority, so tier 3 grants nothing new there; the widening exists only on the default loopback posture, where caller-chosen tenant headers are the trust model by design. Finding #2 (waitFor timeout class unpinned) → deferred, disposition unchanged from round 4 — the measured mutation (M1 survives green) is exactly the property we accepted: the class is complete at head (59/59) and the flakiness gate remains the deterministic tripwire for any future regression on this file. Both language halves of the body were updated and the local draft is in sync. With the PR merged at 中文:61/61 断言全过、抖动门 ✅,并接受更正——收窄后的 413 表述经实测为真(32/32),三轮旧发现实为分母口径差异。发现 #1 已在正文修复(双语补上限定:回环 open 模式除「匿名 hosted Session 的审批归租户共有」外保持既有行为,与设计文档 §4.4 第三级一致);边界分析正确——signed 模式下签名密钥本就有租户级权限,放宽只存在于默认回环姿态(其信任模型本就是调用方自选租户头)。发现 #2 维持第四轮延后处置(M1 存活正是我们接受的属性:类已完整,抖动门是未来的确定性绊线)。PR 已合并,验证闭环完成;415 观察保留在跟进清单。 |
Absorbs main's broker authentication (#13210) and the cold-load refusal hardening (#13361). Two conflict hunks in hosted-harness-session.ts keep both semantics: main's restore-refusal diagnostics reuse the single pre-close verdict read, and the typed checkpoint_blocked decline now fires only for a drive takeover — a bare load or a cancellation-only load keeps the baseline retriable 409 (main's new witness pins the bare shape; a new drive-shape witness pins the decline). R10 (qwen-code-ci-bot review 5410379695, four Criticals) shared one root cause: a plain attach was minted while the journal held an unsettled Turn with no live owner of the wait. - R10-1: the attached cancellation redrive of a parked no-tool Turn no longer mints a plain attach; no kernel is consulted on that arm, so there was no inapplicable to mirror — the load keeps the cold arm's retriable refusal, drive keeps its typed decline. - R10-2: the prompt route guards the journal itself — a fresh promptId is refused 409 hosted_prompt_recovery_required whenever any input is unsettled, whatever the takeover answered; no admission can stack on a parked Turn's mid-flight checkpoint. - R10-3: all three plain-attach epoch migrations (plus the recovered path's) move ONLY the epoch and keep the consumed watermark, so a committed-but-undelivered tail — including turn.settled — replays instead of being skipped behind the exclusive cursor; the plain cancel route answers an honest 409 when nothing live can be aborted while the journal is unsettled, and the CANCELLING arm adopts + streams on that coded refusal instead of re-issuing a no-op cancel. - R10-4: a null epoch proves only that the admission REPLY was lost; the withdraw arm resubmits once, and on the coded duplicate-admission 409 adopts the attach epoch via a split-expectation recordRecoveryAdmission (turn epoch vs session epoch — a session can still carry an earlier Turn's), keeping the watermark; without a prior mark the refusal names a different Turn's work, so it burns the retry budget and records the daemon's own code on exhaustion. Also: recordRecoveryAdmission's epoch CAS predicates are null-safe with split turn/session expectations (a real-store adoption witness proves `IS NULL` matches and a wrong expected session epoch still refuses); the R9 journal-replay's detached async writer now answers a failed identity read with the retriable refusal instead of hanging the request; the re-prove identity guard gains its workspaceId-mismatch witness; the sibling wholesale store mock exports ManagedSessionStoreHttpError. Witnesses: CLI suites 243/243 across the three hosted files (new R10 tests plus the merge-restored main witness), HarnessCoordinatorTest 51/51, ManagedAgentServerIntegrationTest 29/29 (real H2), full module suites managed-agent-server 744/744 and qwencode 181/181, npm run build, typecheck, eslint.
…#13434) The baseUrl guard added in QwenLM#13210 rejects non-loopback plaintext hosts unless allowInsecureHttp is set. Nine construction sites in http-managed-session-store.test.ts still used the fake host http://session-store.test without the opt-in, so 12 tests have failed on main since that merge and now block CI on every PR that includes latest main. Pass allowInsecureHttp: true to those fixtures; the dedicated guard-negative test is untouched.
- pin both halves of the parseLine pair: the record half now asserts the normalized message (so a raw-as-record substitution goes red), and a new case keeps one slot per physical record that fails validation - witness the non-object owner-payload disjunct that stands between a primitive systemPayload on disk and a TypeError from the 'in' operator - widen the restore-head grant table to all five disjuncts and the journal-vs-head agreement test to all three, via disjoint headOverrides - make the fake store honour the requested page limit so the two-page count derives from production's limit=100 request - merge resolution: opt the fake-host store constructions into allowInsecureHttp after main's plaintext-HTTP guard (#13210) Each new row was mutation-probed: it goes red when its own disjunct (or the limit) is removed and stays green on pristine source. Co-authored-by: Qwen-Coder <[email protected]>
Resolve the overlap with the broker-auth work (#13210): - bridgeTypes.test.ts (add/add): union of both suites, plus the 127-prefixed DNS-name reject rows the loopback predicate's per-octet numeric test is pinned by, and a row covering the opt-in below. - bridgeTypes.ts: the HTTPS-off-loopback rule predates the broker-provisioned allowInsecureHttp opt-in that arrived with this merge; honor it (mirroring http-managed-session-store.ts) so the field stays reachable through the bridge parser, and correct the loopback predicate's JSDoc premise — a four-label name whose labels are not all numeric survives the URL parser verbatim. - hosted-harness-session.test.ts: keep main's loopback baseUrl in the bounded-store actual call; the factory-refusal test's descriptor now carries allowInsecureHttp so the store factory is what rejects it. Co-authored-by: Qwen-Coder <[email protected]>
Resolve the http-managed-session-store.test.ts conflict by keeping both sides' new suites (this PR's six witness tests and main's #13341 fake-server override suite) and adopting main's bootStoresAndSession helper. Review round: opt the five new createHttpManagedSessionStores call sites into allowInsecureHttp so the six tests they set up actually run (R4-1) — plaintext guard from #13210 reached this branch through the merge. Drop this branch's per-call-site CommonJS module-scope pins in terminal-image-renderer.test.ts and test-efficacy.integration.test.ts now that main pins the same shims at suite/builder level (R4-2); verified green under an ambient {"type":"module"} TMPDIR, and mermaidImageRenderer returns to 13 failed if main's builder pin is removed. Co-authored-by: Qwen-Coder <[email protected]>
QwenLM#13341) * test(core): close QwenLM#12693 post-merge review test and hygiene gaps Second batch of QwenLM#12693 post-merge review follow-ups — the test-coverage and hygiene findings that remained after the correctness PR: - The HTTP store suite never drove a failing or inconsistent server: the fake now takes page/receipt overrides and stored-integrity edits, pinning the multi-page read, contiguous-revision, bytes-vs-metadata, empty-page, nextRevision, echo-receipt mismatch and 409 resource-missing paths. - A sealed Managed session can now be seen to survive daemon archive through the sealed-writer fallback (red if that arm is removed; mutation-checked). - The managed-resume rejection test stubs QWEN_RUNTIME_DIR — ambient exports outrank setRuntimeBaseDir and redirected the hook to the ambient runtime. - sink success-path coverage: session_source and file_history_snapshot domain round-trips, carried system subtypes through the message channel, and the remaining three turn_result guard disjuncts; the unmapped-refusal fixture now uses a genuinely unmapped subtype (rewind instead of the mapped file_history_snapshot). - The live-vs-cold projection difference is documented at both sites and pinned by a test asserting the deliberate narrower live door. - parseLine returns the validation it already ran, so transcript indexing no longer validates every record twice; the accumulator's four unavailability reasons and the empty-transcript default gain a collocated suite. - isLockRecord's managed-sealed branch reuses isValidCommitProof instead of re-implementing it; the misplaced recoverUncommittedTail safety contract now sits on the method that truncates; the unused de-facto-private export of buildManagedSessionRestoreProjection is dropped. * test(core): cover the restore head mismatch and align the 409 comment - The journal-vs-durable-head check now has a witness: a server head that overstates committedSequence is refused after the page loop. - The staged-survival assertion's comment no longer overclaims a literal retry of the same missing resource (review R1-3). * test(core): address the round-1 review on the test batch Addressing the seven Suggestion findings from the auto-review, each with the mutation witness the review asked for: - The restore-journal head guards now have witnesses too, via headOverrides: a storageVersion/recoveryStatus mismatch is refused by the grant check, an overstated committedSequence fails journal-vs-head, a stale eventsDigest fails metadata-vs-records, and a page that promises the end while the head is ahead is refused (the 'did not advance' branch was already pinned). - The 409 test drives a real retry of the same transaction with a staged ref it carries, asserting the retried commit body still posts bytesBase64 — exactly the regression that would appear if a failure path ever released staged entries. - SessionExecutionEngineAccumulator.parseLine's returned `{ value, record }` pair is pinned independently on both halves, proving the raw line value survives where the normalized record drops fields. - The hot/wide projection distinction is documented as width, not lifecycle — `projectManagedSessionRecords` is named as the reader-facing list used on live paths too (`readActiveTranscriptChain`) — and the pin test's title and inline comment say the same. - `turn_result` case rejection now asserts both halves of the canCarry/write agreement on the same record object. - Carried system subtypes assert their message channel (one message.committed, zero domain.committed) before the cold round-trip. - QWEN_RUNTIME_DIR is deleted once in each package's setup file (preload of every suite) instead of two per-test stubs, closing the class: 15 ambient failures in session-transcript-reader and 4 ACP lock failures on writer-lease reproduce on ambient machines and now pass. * test(core): address the round-2 review on the test batch - pin both halves of the parseLine pair: the record half now asserts the normalized message (so a raw-as-record substitution goes red), and a new case keeps one slot per physical record that fails validation - witness the non-object owner-payload disjunct that stands between a primitive systemPayload on disk and a TypeError from the 'in' operator - widen the restore-head grant table to all five disjuncts and the journal-vs-head agreement test to all three, via disjoint headOverrides - make the fake store honour the requested page limit so the two-page count derives from production's limit=100 request - merge resolution: opt the fake-host store constructions into allowInsecureHttp after main's plaintext-HTTP guard (QwenLM#13210) Each new row was mutation-probed: it goes red when its own disjunct (or the limit) is removed and stays green on pristine source. Co-authored-by: Qwen-Coder <[email protected]> * test(cli): scope fake renderer scripts as CommonJS under a poisoned tmpdir Deterministic verification rejected the round-2 commit: 15 packages/cli tests in mermaidImageRenderer, terminal-image-renderer and test-efficacy failed with renderer results of kind 'unavailable'. The fake mmdc/chafa/git executables those suites write under os.tmpdir() are extensionless CommonJS scripts, and the runner's /tmp/package.json carries "type": "module", so node executed them as ES modules and they crashed on require/__dirname ('require is not defined in ES module scope'), which the renderers then report as chafa being unavailable. Write a {"type":"commonjs"} package.json next to each fake so the nearest-scope lookup pins the module system regardless of what sits above the host's temp directory. Verified both ways: the three files are green with the poisoning in place and stay green with TMPDIR pointed at a clean directory. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: qwen-code-ci-bot <[email protected]> Co-authored-by: Qwen-Coder <[email protected]>
Conflicts resolved: - managed-agent-public-api.openapi.json: #13210 took v1.30.0 with its gateway-free qwenSignature scheme this morning; the W2 cwd contract now ships v1.31.0, with both changelog sentences kept and the contract's two (v1.30) feature references re-anchored to (v1.31). - README.md: keep both feature sections - main's broker authentication and writer credentials first, the W2 controlled cwd change after, with the settle retry budget recorded in its refusal paragraph. - web-shell generated client: regenerated from the resolved contract, so it carries the v1.31 text and main's 413 payload_too_large arm. The cwd migration is V45 after upstream took V40-V44 this morning.
…#13565) Move the merged-PR history of the Managed Agent dual-path proposal (QwenLM#12380) out of the issue body into a bilingual ledger under docs/design/. The issue body had come within 10 KB of GitHub's 256 KiB limit, so the body will keep only the delivery snapshot and the open PRs, and merged rows move here rewritten to their merged final state. The ledger carries every merged row the issue tracked up to its 2026-10-03 reconcile (adding the missing merge commit to the five earliest rows and normalising the Chinese state cells), rewrites the eight rows the issue still listed as open although their PRs had merged (QwenLM#13141, QwenLM#13166, QwenLM#13174, QwenLM#13210, QwenLM#13214, QwenLM#13217, QwenLM#13218, QwenLM#13247), and adds rows for the 48 managed-agent PRs merged between that reconcile and main 0c13502 that had no row yet. PRs closed without merging (QwenLM#13087, QwenLM#13336) sit in their own table. Later merges land at the next reconcile. Co-authored-by: wenshao <[email protected]>
What this PR does
Adds the authentication/credential layer for the Managed Agent Runtime Broker that #13180 asks for, with the bilingual design doc at
docs/design/managed-agent-broker-auth.md/managed-agent-broker-auth.zh-CN.md:qwen.managed-agent.auth.mode=signed) has the broker verify an HMAC signature overMETHOD + path + query + tenant + actor + timestamp + SHA-256(body) + Idempotency-Keyitself (headersX-Qwen-Actor-Id,X-Qwen-Signature-Timestamp,X-Qwen-Signature), installing theAuthenticatedTenantActorprincipal without any external gateway.auto(the default) resolves to the existing open behavior on loopback and refuses to start on a non-loopback address, so a production listen address can never come up unauthenticated. Startup guards also reject a weak signing key, a contradictorytrusted-actor-headerconfiguration, a non-rootserver.servlet.context-pathorspring.mvc.servlet.path(which would silently bypass the path-prefix filters), a plaintext non-loopbacksession-store.base-urlortool-publication.service-base-url, an internal listener on the same port number as the public one (the surface routing is port-based), and reusing the signing key as the writer binding key. Signed requests buffer their body underqwen.managed-agent.auth.max-signed-body-bytes(default 10 MiB) before verification — an over-limit body answers 413payload_too_largeand a repeatedIdempotency-Keyheader answers 400, both before the signature comparison.qwen.managed-agent.session-store.binding-keyset (>= 32 bytes), the writer token becomes an HMAC over(tenantId, workspaceId, sessionId)issued through the harness attach payload (ManagedSessionStoreConnection.writerToken), and every session-store entry point rejects self-minted tokens with403 writer_credential_invalid— including during a free lease window. The credential deliberately excludes the writer id, so a rebooted harness re-acquires with the same credential after the old lease lapses. Without the key, loopback deployments keep the legacy first-writer-wins behavior.qwen.managed-agent.internal-server.port/.addressbind/internal/**to their own connector; a routing filter answers 404 for the wrong surface on either port. Leaving loopback — public or internal — requires signed mode or a configured binding key respectively (allow-insecure-bindis the documented escape hatch).managed_agent_session.creator_actor_key, recorded from the authenticated actor at creation for both hosted and workspace Sessions.requireOwnerresolves the creator column, then the legacy workspace creation row, then (neither recorded) falls back to the tenant-scoped semantics non-workspace Sessions already had — so hosted approvals can finally be answered over HTTP, and workspace approvals bind to the authenticated actor instead of a guessable header value.createHttpManagedSessionStoresrefuses a non-loopbackhttp://base URL unless the broker opts in (qwen.managed-agent.session-store.allow-insecure-http=true, forwarded through the attach payload asallowInsecureHttp).Why it's needed
Without a gateway in front, the pre-change arrangement composes into cross-tenant read/write of any hosted Session's durable transcript: assert a tenant header, wait out a writer lease window (up to 300 s), acquire the writer with a self-minted token, and rewrite history — verified end-to-end in this PR's reproduction (a forged
requested → decided: allowapproval lifecycle was committed into a victim Session's journal and served back by the public actions API). The broker is being built toward multi-tenant hosted deployment; this PR installs the perimeter the controls assumed. Default loopback dev/E2E topologies keep working with zero new configuration.Reviewer Test Plan
How to verify
-Dgpg.skip=true):cd packages/sdk-java/managed-agent-server && mvn test— the new coverage:Issue13180SignedModeTest(unsigned/wrong-key/stale → 401, signed works, cross-tenant 404),Issue13180InternalPortTest(each surface 404s on the other's port) andIssue13180HardenedVerificationTest(the original attack chain re-run against the hardened broker: takeover 403, forged commit 403, cross-tenant replay 403, hosted approval answered by its creator),ManagedSessionStoreBindingTest(self-minted refused at every store entry point, reboot re-acquire),BrokerSecurityTest(mode resolution + every startup guard),SignatureAuthFilterTest,WriterCredentialPolicyTest,ManagedActionsTest#hostedSessionsAnswerThroughTheirRecordedCreator(ownership fallback chain).cd packages/core && npx vitest run src/managed-runtime/http-managed-session-store.test.ts(plaintext guard table incl.127.example.comnot counting as loopback),cd packages/acp-bridge && npx vitest run src/bridgeTypes.test.ts,cd packages/cli && npx vitest run src/serve/hosted-harness-session.test.ts(payload passthrough + unusable-descriptor 400).ToolPublicationStoreTesttiming cases fail on this machine both before and after the change (measured on the unmodified base; unrelated claim-expiry probes).Evidence (Before & After)
N/A — non-UI server/SDK change. Before/after behavior is pinned by
Issue13180HardenedVerificationTest(attacks fail in the hardened configuration); the before-half is reproduced publicly by the sandboxed verification lane's three-arm A/B, whose arm A drives the #13180 chain against the base build and lands the forged commit (e.g. run 37254095813).Tested on
Environment (optional)
OpenJDK 21, Maven 3.9, H2 (MySQL mode) for the broker tests; Node 22 for the TS suites.
Risk & Scope
-Pmysql-integration) need a database service and were not run locally; the 401 refusal is declared per route on the covered surface, and 413 on every covered route that declares a request body.allow-insecure-http, or move the store to https/loopback); (2) enablingbinding-keyon a new broker requires harnesses on this CLI version or later (older clients reject the provisionedwriterTokenfield). Existing databases migrate via V40; pre-migration Sessions keep working through the legacy ownership fallback.docs/design/managed-agent-broker-auth.md,docs/design/managed-agent-broker-auth.zh-CN.md.Linked Issues
Closes #13180
中文说明
本 PR 做什么
为 Managed Agent Runtime Broker 实现 #13180 要求的认证/凭证层,双语设计文档见
docs/design/managed-agent-broker-auth.md/managed-agent-broker-auth.zh-CN.md:qwen.managed-agent.auth.mode=signed)让 broker 自行校验覆盖METHOD + path + query + tenant + actor + timestamp + SHA-256(body) + Idempotency-Key的 HMAC 签名(头X-Qwen-Actor-Id、X-Qwen-Signature-Timestamp、X-Qwen-Signature),无需外部网关即可安装AuthenticatedTenantActorprincipal。auto(默认)在回环地址上保持现有 open 行为,在非回环地址上拒绝启动,生产监听地址不可能在无认证的情况下起来。启动守卫还会拒绝过弱的签名密钥、与trusted-actor-header的矛盾配置、非根server.servlet.context-path或spring.mvc.servlet.path(它会静默绕过路径前缀过滤器)、非回环明文的session-store.base-url或tool-publication.service-base-url、与公网同号的内部监听端口(路由按端口分类),以及签名密钥与绑定密钥复用。签名请求在验签前按qwen.managed-agent.auth.max-signed-body-bytes(默认 10 MiB)缓冲请求体——超限应答 413payload_too_large,重复Idempotency-Key头应答 400,均先于签名比对。qwen.managed-agent.session-store.binding-key(≥32 字节)后,writer token 变为对(tenantId, workspaceId, sessionId)的 HMAC,经 harness attach 载荷(ManagedSessionStoreConnection.writerToken)下发;会话存储的所有入口都以403 writer_credential_invalid拒绝自铸 token——包括租约空窗期。凭证刻意不含 writer id,harness 重启后可在旧租约失效后用同一凭证重新 acquire。未配置时,回环部署保持既有的首写者赢行为。qwen.managed-agent.internal-server.port/.address把/internal/**绑到独立连接器;路由过滤器对两个端口上的错面请求一律 404。离开回环——公网或内部——分别要求 signed 模式或已配置的 binding-key(allow-insecure-bind是文档化的逃生门)。managed_agent_session.creator_actor_key,hosted 与 workspace 会话在创建时都会从已认证 actor 记录。requireOwner依次解析 creator 列、遗留的 workspace 创建行,最后(都无记录时)回退到非 workspace 会话已有的租户级语义——hosted 审批终于能通过 HTTP 应答,workspace 审批绑定到已认证 actor 而非可猜测的头值。createHttpManagedSessionStores拒绝非回环的http://base URL,除非 broker 显式 opt-in(qwen.managed-agent.session-store.allow-insecure-http=true,经 attach 载荷以allowInsecureHttp转发)。为什么需要
没有网关时,改动前的安排可组合成对任意 hosted 会话持久 transcript 的跨租户读写:伪造租户头、等一个租约空窗(最长 300 秒)、用自铸 token 夺取 writer、重写历史——本 PR 的复现端到端验证了这条链(伪造的
requested → decided: allow审批被写入受害者会话的持久 journal,并经公共 API 读回)。该特性正走向多租户外托管部署,本 PR 把这些控制所假设的边界真正建立起来。默认的回环 dev/E2E 拓扑零新配置继续可用。评审者测试计划
如何验证
-Dgpg.skip=true):cd packages/sdk-java/managed-agent-server && mvn test——新增覆盖:Issue13180SignedModeTest(未签名/错密钥/过期 → 401,签名可用,跨租户 404)、Issue13180InternalPortTest(两个面在对方端口 404)、Issue13180HardenedVerificationTest(对加固后的 broker 重放原始攻击链:夺取 403、伪造提交 403、跨租户重放 403、hosted 审批可被创建者应答)、ManagedSessionStoreBindingTest(自铸 token 在每个存储入口被拒、重启后重新 acquire)、BrokerSecurityTest(模式解析 + 全部启动守卫)、SignatureAuthFilterTest、WriterCredentialPolicyTest、ManagedActionsTest#hostedSessionsAnswerThroughTheirRecordedCreator(属主回退链)。cd packages/core && npx vitest run src/managed-runtime/http-managed-session-store.test.ts(明文守卫表,含127.example.com不算回环)、cd packages/acp-bridge && npx vitest run src/bridgeTypes.test.ts、cd packages/cli && npx vitest run src/serve/hosted-harness-session.test.ts(载荷透传 + 不可用描述符 400)。ToolPublicationStoreTest有两个既有的计时用例在本机改动前后都失败(已在未改动的基线上实测;与本 PR 无关的 claim 过期探针)。证据(前后对比)
N/A——非 UI 的服务端/SDK 改动。前后行为由
Issue13180HardenedVerificationTest(加固配置下攻击失败)钉住;before 半侧由沙箱验证通道的三臂 A/B 公开复现——臂 A 在 base 构建上驱动 #13180 攻击链并成功落地伪造提交(见 run 37254095813)。测试平台
环境(可选)
OpenJDK 21、Maven 3.9、H2(MySQL 模式)跑 broker 测试;Node 22 跑 TS 套件。
风险与范围
-Pmysql-integration)需要数据库服务,本地未跑;401 拒绝已在覆盖面的每条路由上逐一声明,413 覆盖所有声明了请求体的覆盖路由。allow-insecure-http,或把 store 改为 https/回环);(2) 在新 broker 上启用binding-key要求 harness 使用本版或更新的 CLI(旧客户端会拒绝下发的writerToken字段)。现有数据库经 V40 迁移;迁移前的会话通过遗留属主回退继续工作。docs/design/managed-agent-broker-auth.md、docs/design/managed-agent-broker-auth.zh-CN.md。关联 Issue
Closes #13180