mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* docs(managed-agent): repair the R2 review's doc findings on #12692
Six doc findings from the post-merge R2 review round on #12692,
re-verified against current main (2c591ecc08) before patching, plus
two adjacent corrections in the same files:
- The README's dual-path setup assigned 127.0.0.1:4170 to both the
private Hosted Harness and the ordinary daemon; the second process
to bind would EADDRINUSE. The private harness now moves to a
distinct port (4171) with the matching harness base URL, keeping
the ordinary daemon on the proxy's default 4170.
- Preview-only design links in the storage-event design pair are
downgraded to plain-text references naming the integration-preview
documents, in both languages — clicking them 404'd, and the
delegated normative requirements were unverifiable from this repo.
- The English document's Related-designs entries pointed at .zh-CN
twins despite English versions existing; they now link the
in-tree English files, and the closing paragraph names the two
integration-snapshot documents as out-of-tree references.
- The rollout-ordering stage labels (C, D, E, G, W0, W1, O4) gain a
one-line glossary at first use naming proposal #12380 as their
source, in both languages.
- The zh-CN verification record claims one daemon-side panel where
the English says all panels; both instances are singularized to
match the English original.
- The durable-store pair's §4.2 claim that attachment coalescing
compares the full descriptor identity and fails closed with
managed_session_store_conflict is narrowed to the shipped behavior
(coalescing on (tenantId, sessionId) only), in both languages;
the fence stays named as integration-slice target design.
- The review-corrections bullet claiming executions:prepare and
executions/{id}:start 501 is updated: both are live routes again,
and only executions/{id}:resolve still 501s, in both languages.
* docs(managed-agent): close the launch/contract gaps from the #13343 review
Six findings on the doc repair, each verified against the cited code
before patching:
- The fenced `qwen serve --profile hosted-harness` block could not
start: the profile requires a bearer token, --no-web and a sha256
capability digest, none present in the block or section. The launch
now reuses the Spring credential pair (QWEN_SERVER_TOKEN /
QWEN_HOSTED_HARNESS_CAPABILITY_DIGEST spellings) plus --no-web --
which has no environment fallback.
- The 4171 base-url export sat after the foreground
`mvn spring-boot:run` step, while the value is read once at JVM
startup. The section now states the restart requirement and warns
that --port is a request: on EADDRINUSE the serve walker puts a
displaced ordinary daemon exactly on 4171.
- The bare :101 command reference points at the credentialed launch.
- README 209-211 asserted the fail-closed
managed_session_store_conflict fence as shipped while the design
pair this commit edits narrows it to target design. The README
sentence now matches: coalescing on (tenantId, sessionId) -- so
another tenant cannot reuse the in-memory Session -- with neither
endpoint/workspace/generation comparison nor the fence as shipped;
same correction applied to the previously self-contradictory
design-doc sentence in both languages (tenant is part of the
coalescing key, not on the uncompared list).
- Review-corrections gate item 1 still claimed the profile rejects
startup and that acquire/control/release 501; only
executions/{id}:resolve still 501s. Reworded in both languages,
keeping the genuinely open parts (real tool turn, durable-store
adapter/worker bundle, crash recovery).
Two pins in scripts/tests/managed-agent-server-e2e.test.js hold the
repaired claims: every fenced hosted-harness launch block carries
--no-web plus token and digest sources (reds if any are stripped),
and the 4171 export pairs with a JVM-startup restart note (reds if
the clause is removed). Suite 9/9 green; Prettier clean.
* docs(managed-agent): name the attach-time generation fence and pin docs to the profile validator
The R2 review found the narrowed attachment contract still overstated the
gap: a presented Harness writer generation IS compared on every attach
(contract middleware boot-id 409 and the store-descriptor writerId 409,
both before coalescing), so the README and the durable-store design pair
now say the fence applies to the generation while endpoint/workspace
remain uncompared and managed_session_store_conflict stays target design.
The README pins move from hand-copied substrings to the real
validateHostedHarnessProfile, so a fenced launch block that the profile
would reject (missing token/digest/--no-web, non-loopback hostname, an
unseen fence spelling) reds the suite; the dual-path slice test gains a
fail-closed closing anchor and a wrap-tolerant caveat match, and a new
pin asserts the attachment paragraph names the generation rejection.
Co-authored-by: Qwen-Coder <[email protected]>
* test(managed-agent): pin the hosted-harness README launch literally
The grammar-based pin derived argv/env from the fenced block and mirrored
serve.ts option mapping by hand; every shell spelling or flag it did not
model failed open, and each review round found new corners. Match the
launch block exactly instead, cross-check that every $VAR the block
references is assigned in a fenced README block, and take the CLI-side
credential env names from the production constants so a rename there
reddens the pin instead of stranding the README spelling.
* docs(managed-agent): correct the attach contract's tenant-isolation claim
The Harness keys its in-memory Session by sessionId alone, so the README
and the durable-store design doc cannot promise (tenantId, sessionId)
coalescing: name the connector-side cache as the per-tenant layer and
the writer generation as the only attach-time fence, in both languages.
Also fold the three launch-pin suggestions into the e2e test: scope the
$VAR provenance scan to the Prerequisites section, and parse the
validator fixture and the Spring base-URL port from the pinned launch
block so the copies cannot drift apart.
* docs(managed-agent): supersede the foundation boundary, pin fence polarity
Co-authored-by: Qwen-Coder <[email protected]>
* docs(managed-agent): wire the Session Store into the by-hand dual-path recipe
* docs(managed-agent): order the Session Store exports before the Spring restart
R6-1: the by-hand dual-path recipe exported the three Session Store values
after its only Spring restart cue, so an in-order reader restarted onto a
store-disabled JVM and every attach failed 400 invalid_managed_session_store.
All four exports now precede the single restart, whose read-once caveat is
re-scoped to cover them, and the Spring-side values are no longer called
"the launch".
R9-1: gate 1 of the review-corrections doc still named real tool turns, the
copied E2E script's durable-store adapter and worker bundle, and crash
recovery as unproven although the hosted-harness-mysql CI job gates all
three (HostedWorkspaceToolTurnIT, HostedProcessCrashIT, three owner-failover
modes). Narrowed to the gate-4 recovery signalling, in-flight recovery,
complete Workspace lifecycle and distributed provisioning, in both
languages.
Pins: the dual-path slice now asserts the store exports precede the restart
cue, and a doc-oracle case reddens when either language re-lists CI-gated
Hosted capabilities as unproven while the tool-turn IT and the failover
lane exist; both went red on the pre-fix text before the docs changed.
---------
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
This commit is contained in:
11 files changed
+315
-31
No files matched your search
@@ -78,8 +78,8 @@ QWEN_MANAGED_AGENT_JAVA_URL=http://127.0.0.1:8080 \
|
||||
|
||||
- 单测覆盖显式选择 Java、tenant header 透传,以及未选择时行为保持不变。
|
||||
- 在现有 daemon 代理旁验证精确的 Java 代理目标。
|
||||
- 用真实浏览器跑通 Java、Hosted Harness、Runtime Broker、Tool Runtime,并在同一页面打开 daemon
|
||||
所有的面板。
|
||||
- 用真实浏览器跑通 Java、Hosted Harness、Runtime Broker、Tool Runtime,并在同一页面打开一个
|
||||
daemon 侧面板。
|
||||
|
||||
## 验收标准
|
||||
|
||||
@@ -93,6 +93,6 @@ QWEN_MANAGED_AGENT_JAVA_URL=http://127.0.0.1:8080 \
|
||||
## 验证结果
|
||||
|
||||
完整页面成功加载已有 Java Managed Session,新回合精确返回 `FULL_SHELL_OK`,随后在同一个浏览器页面
|
||||
打开了 daemon 所有的 Status 面板。开发代理能够同时返回 Java Session 列表和 daemon health。定向
|
||||
打开了那个 daemon 侧 Status 面板。开发代理能够同时返回 Java Session 列表和 daemon health。定向
|
||||
WebShell 测试覆盖了启动适配、代理注册,以及 daemon workspace context 尚未就绪时 Managed 面板保持
|
||||
可用的行为。
|
||||
@@ -244,17 +244,17 @@ RestoreSet is a versioned immutable cross-store manifest binding a stable set ID
|
||||
|
||||
Reconcile lost ACKs between component completion and manifest publication by the original operation; do not create another set or release the barrier early. Release the maintenance barrier under current authorization only after component verification and conditional publication succeed. On failure retain durable progress and converge under the original operation. A successful backup proves only its declared boundary, not future restoration eligibility.
|
||||
|
||||
A shared timestamp is not consistency evidence. [MySQL binlog/PITR](https://dev.mysql.com/doc/refman/8.0/en/point-in-time-recovery-binlog.html) and GTID do not prove external side effects, and [VolumeSnapshot](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/) does not automatically provide application consistency. `RestoreBundle` remains a single-Session recovery response using a valid basis within the verified collection; RestoreSet does not replace the authority. See [storage contracts](managed-agent-session-storage.md#restore-set) for closed records/references, minimumReader, and capability rules.
|
||||
A shared timestamp is not consistency evidence. [MySQL binlog/PITR](https://dev.mysql.com/doc/refman/8.0/en/point-in-time-recovery-binlog.html) and GTID do not prove external side effects, and [VolumeSnapshot](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/) does not automatically provide application consistency. `RestoreBundle` remains a single-Session recovery response using a valid basis within the verified collection; RestoreSet does not replace the authority. See the storage contracts (`#restore-set`) of the integration-preview document `managed-agent-session-storage.md` for closed records/references, minimumReader, and capability rules.
|
||||
|
||||
<a id="quarantined-restore"></a>
|
||||
|
||||
### 9.2 Quarantined Restore and Missing Intervals
|
||||
|
||||
Restoration starts quarantined: no dispatch, automatic wakeups, or GC. Ordinary reads also require the complete current safety evidence in Section 9.3. Isolate old Java/Harness storage-write rights separately from old Runtime physical write channels; a new DB epoch or credential version does not prove an old Shell stopped. Original-node/storage evidence, RWOP, and same-volume handover follow [Endpoint Section 17](2026-09-21-managed-runtime-endpoint-recovery.md#hosted-runtime-profile).
|
||||
Restoration starts quarantined: no dispatch, automatic wakeups, or GC. Ordinary reads also require the complete current safety evidence in Section 9.3. Isolate old Java/Harness storage-write rights separately from old Runtime physical write channels; a new DB epoch or credential version does not prove an old Shell stopped. Original-node/storage evidence, RWOP, and same-volume handover follow the Endpoint Section 17 (`#hosted-runtime-profile`) of the integration-preview document `2026-09-21-managed-runtime-endpoint-recovery.md`.
|
||||
|
||||
Enumerate execution gaps from the backup boundary through completion of recovery isolation using original durable ledgers, protected backups/audit data, and original-owner evidence that survive that backup's loss interval. No command/dispatch row in the restored database does not prove non-execution. Continue only when evidence covers the complete interval, original effects are settled, and results/history/consumption positions agree. If the interval cannot be enumerated, block all potentially affected Sessions/Workspaces rather than treating a known subset as complete. Original-execution queries do not provision/prepare/execute.
|
||||
|
||||
Replay deletion/key-revocation facts and restore only still-eligible resources. If the real ACL authority is unavailable or may have rolled back, grant no new rights and open no ordinary reads; technical restoration does not restore a user's old permissions. `accepted_unresolved` closes only the business case: physical UNKNOWN persists, without tool success, volume release, or continuation of the original model. New tasks cannot bypass the original volume barrier either; see [reconciliation](managed-agent-recovery-operations.md#unknown-reconciliation) and [revocation](managed-agent-control-protocol.md#dynamic-authorization).
|
||||
Replay deletion/key-revocation facts and restore only still-eligible resources. If the real ACL authority is unavailable or may have rolled back, grant no new rights and open no ordinary reads; technical restoration does not restore a user's old permissions. `accepted_unresolved` closes only the business case: physical UNKNOWN persists, without tool success, volume release, or continuation of the original model. New tasks cannot bypass the original volume barrier either; see the reconciliation (`#unknown-reconciliation`) of the integration-preview document `managed-agent-recovery-operations.md` and the dynamic-authorization contract (`#dynamic-authorization`) of the integration-preview document `managed-agent-control-protocol.md`.
|
||||
|
||||
<a id="recovery-safety-watermark"></a>
|
||||
|
||||
@@ -268,7 +268,7 @@ In full-DR deployments, first idempotently preregister deletion/revocation inten
|
||||
|
||||
Do not miss concurrent deletion/revocation during restore verification. Recheck current authority and safety watermarks before opening reads or issuing grants, then keep subsequent reads/renewals subject to current permissions. Cross-system reads are not a distributed atomic transaction; remain quarantined without a valid admission barrier. Without evidence, ordinary SSE/history/Range/export/receipt content stays blocked. Only separately authorized isolated diagnostics may access necessary material, without model wakeups or ordinary export.
|
||||
|
||||
Deletion/revocation evidence must cover every backup or replica still capable of restoring the affected old data; short command-idempotency TTLs cannot remove it. This is a G/W1 full-DR enablement requirement, not an obligation to add infrastructure for the initial C/E + W0 online loop. Without it, report that a backup exists or restoration is unverified, never safe automatic recovery.
|
||||
Deletion/revocation evidence must cover every backup or replica still capable of restoring the affected old data; short command-idempotency TTLs cannot remove it. The stage labels used here — `C`, `D`, `E`, `G`, `W0`, `W1`, `O4` — name the staged-delivery slices of proposal #12380, not any phase numbering local to this document. This is a G/W1 full-DR enablement requirement, not an obligation to add infrastructure for the initial C/E + W0 online loop. Without it, report that a backup exists or restoration is unverified, never safe automatic recovery.
|
||||
|
||||
<a id="compliance-deletion"></a>
|
||||
|
||||
@@ -291,7 +291,7 @@ Expose completion through a controlled versioned public projection, with origina
|
||||
|
||||
Replay deletion constraints before any disaster-recovery reads open. An [OSS delete marker](https://www.alibabacloud.com/help/en/oss/developer-reference/deleteobject) does not erase historical versions. Delivered user exports cannot technically be recalled; do not claim their cleanup without evidence. Cryptographic erasure requires every relevant decryptable copy and recoverable key path to be irrecoverable. Never destroy another owner's shared key or promise both complete erasure and complete restoration.
|
||||
|
||||
RPO/RTO, retention windows, the specific CSI driver, backup deletion, and vendor contracts remain undecided; do not guess values. Unconfigured/unvalidated automatic recovery or GC stays disabled. UNKNOWN is not a legal basis for indefinite sensitive-data retention: separate retention policy and authorized compliance handling are required. Business risk acceptance also does not fabricate physical settlement. See [Session storage](managed-agent-session-storage.md#deletion-settlement) for detailed lifecycle/reference rules.
|
||||
RPO/RTO, retention windows, the specific CSI driver, backup deletion, and vendor contracts remain undecided; do not guess values. Unconfigured/unvalidated automatic recovery or GC stays disabled. UNKNOWN is not a legal basis for indefinite sensitive-data retention: separate retention policy and authorized compliance handling are required. Business risk acceptance also does not fabricate physical settlement. See the deletion-settlement rules (`#deletion-settlement`) of the integration-preview document `managed-agent-session-storage.md` for detailed lifecycle/reference rules.
|
||||
|
||||
## 10. Backend Selection and Deployment Configuration
|
||||
|
||||
@@ -333,7 +333,7 @@ Do not switch MySQL/PG or MQ arbitrarily per request. Switching requires pausing
|
||||
| Old Java/Harness returns after lease handover | Both database acceptance and private journal commits validate generations. MQ ordering does not provide this protection. |
|
||||
| Unknown tool outcome or missing Workspace | Enter `recovery_blocked`; neither execute automatically a second time nor substitute an empty workspace for recovery. |
|
||||
|
||||
Retain tenant authorization for all queries, replay, resource downloads, and internal notifications. `X-Qwen-Tenant-Id` conveys scope from a trusted entry point, not identity credentials. Internal MQ metadata and private Harness records must not pass directly to the frontend. Preserve explicit public projection and sensitive-field filtering. Hosted checks current ACL on existing SSE streams and each subsequent content-delivery boundary under the [dynamic authorization contract](managed-agent-control-protocol.md#dynamic-authorization); connection-time permission is not permanent authorization. Internal original-ID receipt acceptance may continue without restoring user reads or model progress.
|
||||
Retain tenant authorization for all queries, replay, resource downloads, and internal notifications. `X-Qwen-Tenant-Id` conveys scope from a trusted entry point, not identity credentials. Internal MQ metadata and private Harness records must not pass directly to the frontend. Preserve explicit public projection and sensitive-field filtering. Hosted checks current ACL on existing SSE streams and each subsequent content-delivery boundary under the dynamic-authorization contract (`#dynamic-authorization`) of the integration-preview document `managed-agent-control-protocol.md`; connection-time permission is not permanent authorization. Internal original-ID receipt acceptance may continue without restoring user reads or model progress.
|
||||
|
||||
## 12. Rollout and Code Changes
|
||||
|
||||
@@ -380,4 +380,4 @@ Performance comparisons record first-text and inter-chunk latency p50/p95/p99, t
|
||||
|
||||
Before production, determine the existing RocketMQ platform/version, active Session count and event rate, replay window `W`, maximum tolerable backlog, failure guarantees of database/MQ acknowledgements, history/resource retention, and allowed recovery point/time objectives. Interface boundaries and rollout planning do not require these values immediately; production capacity, cleanup, and cross-node recovery configuration do.
|
||||
|
||||
Related designs in the integration snapshot: `docs/design/2026-09-19-managed-agent-spring-server.zh-CN.md`, `docs/design/2026-09-20-managed-agent-dual-path-web-shell.zh-CN.md`, `docs/design/managed-agent-session-storage.md`, and `docs/design/managed-agent-session-harness-runtime.md`. This document adds storage and event boundaries without treating prior design documents as implementation validation.
|
||||
Related designs: [2026-09-19 Managed Agent Spring server](2026-09-19-managed-agent-spring-server.md) and [2026-09-20 dual-path WebShell](2026-09-20-managed-agent-dual-path-web-shell.md) in this tree; `managed-agent-session-storage.md` and `managed-agent-session-harness-runtime.md` in the integration snapshot. This document adds storage and event boundaries without treating prior design documents as implementation validation.
|
||||
@@ -244,17 +244,17 @@ RestoreSet 是有版本、不可变的跨存储 manifest,绑定稳定集合 ID
|
||||
|
||||
组件完成和 manifest 发布之间丢 ACK 时查询原 operation,不另造恢复集合或提前解屏障。全部组件核验、条件发布成功后才按当前权限释放维护屏障;失败保留持久进度并由原 operation 收敛。备份成功只证明所声明边界,不批准将来恢复。
|
||||
|
||||
共同时间戳不是一致性证明;[MySQL binlog/PITR](https://dev.mysql.com/doc/refman/8.0/en/point-in-time-recovery-binlog.html)和 GTID 不证明外部副作用,[VolumeSnapshot](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/)不自动提供应用一致性。`RestoreBundle` 仍是单 Session 的恢复响应,读取已验证集合里的合法基础;RestoreSet 不代替 authority。封闭记录/引用、minimumReader 及 capability 规则见[存储规范](managed-agent-session-storage.zh-CN.md#restore-set)。
|
||||
共同时间戳不是一致性证明;[MySQL binlog/PITR](https://dev.mysql.com/doc/refman/8.0/en/point-in-time-recovery-binlog.html)和 GTID 不证明外部副作用,[VolumeSnapshot](https://kubernetes.io/blog/2020/12/10/kubernetes-1.20-volume-snapshot-moves-to-ga/)不自动提供应用一致性。`RestoreBundle` 仍是单 Session 的恢复响应,读取已验证集合里的合法基础;RestoreSet 不代替 authority。封闭记录/引用、minimumReader 及 capability 规则,见集成预览文档 `managed-agent-session-storage.zh-CN.md` 的存储规范(`#restore-set`)。
|
||||
|
||||
<a id="quarantined-restore"></a>
|
||||
|
||||
### 9.2 隔离恢复与缺失区间
|
||||
|
||||
恢复启动默认隔离,不派发、不自动唤醒、不运行 GC;普通读取也须等 §9.3 的完整当前安全依据。旧 Java/Harness 的存储写权与旧 Runtime 的物理写通道分别隔离,新 DB epoch 或凭据版本不能证明旧 Shell 停止。原节点/存储证明、RWOP 和同卷交接遵守 [Endpoint §17](2026-09-21-managed-runtime-endpoint-recovery.zh-CN.md#hosted-runtime-profile)。
|
||||
恢复启动默认隔离,不派发、不自动唤醒、不运行 GC;普通读取也须等 §9.3 的完整当前安全依据。旧 Java/Harness 的存储写权与旧 Runtime 的物理写通道分别隔离,新 DB epoch 或凭据版本不能证明旧 Shell 停止。原节点/存储证明、RWOP 和同卷交接,遵守集成预览文档 `2026-09-21-managed-runtime-endpoint-recovery.zh-CN.md` 的 Endpoint §17(`#hosted-runtime-profile`)。
|
||||
|
||||
从备份边界到恢复隔离完成之间,必须依靠不随该备份丢失的原持久账本、受保护备份/审计及原 owner 证据枚举执行缺口。恢复库自己没有 command/dispatch 行不是未执行证明。仅当现有证据可覆盖完整区间、原效果已结算且结果/history/消费位置一致时才允许续跑;区间无法枚举就阻塞可能受影响的全部 Session/Workspace,不能把已知子集当作完整清单。原执行查询不 provision/prepare/execute。
|
||||
|
||||
重放删除/密钥撤销事实后只恢复仍有资格的资源。真实 ACL 权威不可用或尚可能是回滚旧版本时,不签新 grant、不开放普通读取;技术恢复成功不恢复用户旧权限。`accepted_unresolved` 仅关闭业务案件,物理 UNKNOWN 保留,不生成工具成功、不解卷、不恢复原模型。新任务也不能绕开原卷屏障,见[对账](managed-agent-recovery-operations.zh-CN.md#unknown-reconciliation)及[撤权](managed-agent-control-protocol.zh-CN.md#dynamic-authorization)。
|
||||
重放删除/密钥撤销事实后只恢复仍有资格的资源。真实 ACL 权威不可用或尚可能是回滚旧版本时,不签新 grant、不开放普通读取;技术恢复成功不恢复用户旧权限。`accepted_unresolved` 仅关闭业务案件,物理 UNKNOWN 保留,不生成工具成功、不解卷、不恢复原模型。新任务也不能绕开原卷屏障,见集成预览文档 `managed-agent-recovery-operations.zh-CN.md` 的对账(`#unknown-reconciliation`)与集成预览文档 `managed-agent-control-protocol.zh-CN.md` 的动态授权契约(`#dynamic-authorization`)。
|
||||
|
||||
<a id="recovery-safety-watermark"></a>
|
||||
|
||||
@@ -268,7 +268,7 @@ RestoreSet 是有版本、不可变的跨存储 manifest,绑定稳定集合 ID
|
||||
|
||||
恢复验证期间的并发删除/撤权也不能漏掉:开放读取/签发 grant 前重新核验当前权威与安全水位,并让后续读取/续租持续服从当前权限。跨系统读取不冒充分布式原子事务;无法形成有效准入屏障时继续隔离。没有证据时,普通 SSE/历史/Range/export/回执内容均 blocked,仅可经独立授权的隔离诊断访问必要材料,不唤醒模型、不允许普通导出。
|
||||
|
||||
删除/撤销依据至少覆盖所有仍能恢复相关旧数据的备份与副本,不能按短命令幂等 TTL 清除。该前置属于 G/W1 完整灾备开放条件,不强迫 C/E+W0 在线闭环新增基础设施。缺少它可以报告备份存在或恢复未验证,不能宣称安全自动恢复。
|
||||
删除/撤销依据至少覆盖所有仍能恢复相关旧数据的备份与副本,不能按短命令幂等 TTL 清除。本文出现的阶段标记(`C`、`D`、`E`、`G`、`W0`、`W1`、`O4`)指提案 #12380 的分阶段交付切片,不是本文内部的阶段编号。该前置属于 G/W1 完整灾备开放条件,不强迫 C/E+W0 在线闭环新增基础设施。缺少它可以报告备份存在或恢复未验证,不能宣称安全自动恢复。
|
||||
|
||||
<a id="compliance-deletion"></a>
|
||||
|
||||
@@ -291,7 +291,7 @@ RestoreSet 是有版本、不可变的跨存储 manifest,绑定稳定集合 ID
|
||||
|
||||
删除约束必须在任意灾备开放读取前重放。[OSS delete marker](https://www.alibabacloud.com/help/en/oss/developer-reference/deleteobject)不等于历史版本已清除;已交付的用户导出无法技术追回,不能无依据承诺清理它。密钥擦除只有所有相关可解密副本及可恢复 key 路径都不可恢复才成立,不销毁他人共享 key,不同时承诺彻底擦除和完整恢复。
|
||||
|
||||
RPO/RTO、保留窗口、具体 CSI 驱动、备份删除及供应商契约尚未选定,不猜数值;未配置/未验证时相应自动恢复或 GC 不开放。UNKNOWN 不是无限期保留敏感数据的法律依据,需独立保留策略和有权合规处理;业务风险接受也不伪造物理结算。更细生命周期与引用规则见[Session 存储](managed-agent-session-storage.zh-CN.md#deletion-settlement)。
|
||||
RPO/RTO、保留窗口、具体 CSI 驱动、备份删除及供应商契约尚未选定,不猜数值;未配置/未验证时相应自动恢复或 GC 不开放。UNKNOWN 不是无限期保留敏感数据的法律依据,需独立保留策略和有权合规处理;业务风险接受也不伪造物理结算。更细生命周期与引用规则,见集成预览文档 `managed-agent-session-storage.zh-CN.md` 的删除结算(`#deletion-settlement`)。
|
||||
|
||||
## 10. 后端选择与部署配置
|
||||
|
||||
@@ -333,7 +333,7 @@ qwen:
|
||||
| 租约切换后旧 Java/Harness 回来 | 数据库接受事务和私有日志提交均校验 generation;MQ 顺序能力不承担此防护。 |
|
||||
| 工具结果未知或 Workspace 缺失 | `recovery_blocked`,不自动执行第二次,也不创建空工作区冒充恢复。 |
|
||||
|
||||
所有查询、重放、资源下载与内部通知都保留租户鉴权。`X-Qwen-Tenant-Id` 是可信入口传递的范围,不是身份凭证。MQ 内部元数据和私有 Harness 记录不得直接透传前端;保留当前显式公开投影及敏感字段过滤。Hosted 按[动态授权契约](managed-agent-control-protocol.zh-CN.md#dynamic-authorization)在现有 SSE 和每个后续内容交付边界核对当前 ACL,不把连接建立时的权限当成永久资格。内部原 ID 收件可继续,但不会恢复用户读取或模型推进。
|
||||
所有查询、重放、资源下载与内部通知都保留租户鉴权。`X-Qwen-Tenant-Id` 是可信入口传递的范围,不是身份凭证。MQ 内部元数据和私有 Harness 记录不得直接透传前端;保留当前显式公开投影及敏感字段过滤。Hosted 按集成预览文档 `managed-agent-control-protocol.zh-CN.md` 的动态授权契约(`#dynamic-authorization`)在现有 SSE 和每个后续内容交付边界核对当前 ACL,不把连接建立时的权限当成永久资格。内部原 ID 收件可继续,但不会恢复用户读取或模型推进。
|
||||
|
||||
## 12. 落地顺序与代码改造
|
||||
|
||||
@@ -380,4 +380,4 @@ P4 是明确的后续设计门槛,不是当前接口已经实现的能力。
|
||||
|
||||
上线前需明确:现有 RocketMQ 平台及版本、活跃 Session 数与事件速率、续传窗口 `W`、最大可容忍积压、数据库/MQ 确认的故障保证、历史与资源保留期、允许的恢复点和恢复时间。接口和分阶段设计不依赖立刻确定这些数值;容量、清理和跨节点恢复的生产配置依赖它们。
|
||||
|
||||
参考现有方案:集成快照中的 `docs/design/2026-09-19-managed-agent-spring-server.zh-CN.md`、`docs/design/2026-09-20-managed-agent-dual-path-web-shell.zh-CN.md`、`docs/design/managed-agent-session-storage.md` 与 `docs/design/managed-agent-session-harness-runtime.md`。本文补充其存储与事件边界,不把已有设计文档视作功能验证结果。
|
||||
参考现有方案:本仓库中的 [2026-09-19 Managed Agent Spring server](2026-09-19-managed-agent-spring-server.zh-CN.md) 与 [2026-09-20 双通道 WebShell](2026-09-20-managed-agent-dual-path-web-shell.zh-CN.md);集成快照中的 `managed-agent-session-storage.md` 与 `managed-agent-session-harness-runtime.md`。本文补充其存储与事件边界,不把已有设计文档视作功能验证结果。
|
||||
@@ -109,7 +109,7 @@ Archive and delete reject `ACCEPTED`, `RUNNING`, or `CANCELLING` Turns instead o
|
||||
|
||||
Archive clears the live event cursor but retains the last Harness generation as a private-authority existence sentinel. After unarchive, the next Turn cold-loads that Session before binding a new generation. A Session without a sentinel also probes load first and creates only on an explicit not-found response, covering Sessions whose first private write was metadata rather than a Turn. The private `create` path rejects an existing authority with `409`, while `load` returns `404` instead of initializing an empty authority; this keeps retry recovery from silently replacing or fabricating conversation history.
|
||||
|
||||
A live Hosted attachment is additionally bound to the normalized Store endpoint, tenant, workspace, and Harness writer generation. Hot attach, concurrent cold-load coalescing, and restore races all compare that identity and fail closed with `managed_session_store_conflict` instead of letting another tenant or Store descriptor reuse the in-memory Session. Lease duration may change without changing storage identity.
|
||||
In this slice the Java connector caches a live Hosted 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 hot attach, concurrent cold-load coalescing, and restore races compare neither the presented tenant nor a normalized Store endpoint or workspace. A presented Harness writer generation is checked instead — an attach whose `writerId` is not the Harness process's boot ID fails closed with `409 hosted_harness_generation_mismatch` — and the `managed_session_store_conflict` fail-closed fence is target design for the integration slice rather than shipped behavior. Lease duration may change without changing storage identity.
|
||||
|
||||
The first delete slice is deliberately a soft tombstone. `GET` and list APIs hide a `DELETED` Session, while the committed public deletion event remains replayable to an already connected client. Private journal/resource bytes are retained until writer sealing, execution reconciliation, legal hold, retention, and garbage collection are implemented. This slice must not be advertised as physical erasure.
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ DELETE /v1/agents/sessions/{sessionId}
|
||||
|
||||
归档会清除在线事件游标,但保留最后一代 Harness generation,作为私有权威已经存在的哨兵。取消归档后,下一次 Turn 会先冷加载该 Session,再绑定新的 generation。没有哨兵的 Session 也会先探测 load,只有明确收到 not-found 才执行 create,从而覆盖首次私有写入是元数据而不是 Turn 的 Session。私有 `create` 路径遇到已有权威时返回 `409`,而 `load` 在权威不存在时返回 `404`,不会顺手初始化空权威,从而避免重试恢复静默替换或凭空生成会话历史。
|
||||
|
||||
在线 Hosted attachment 还会绑定到规范化后的 Store endpoint、tenant、workspace 和 Harness writer generation。热 attach、并发冷加载合并和 restore race 都会比较这组身份;一旦不同,就以 `managed_session_store_conflict` fail closed,而不会让另一个 tenant 或 Store 描述复用内存中的 Session。lease 时长变化不改变存储身份。
|
||||
当前切片中,Java connector 以 `(tenantId, sessionId)` 缓存在线 Hosted attachment,因此一个租户的缓存引用不会被 connector 交给另一个租户;Harness 本身仅以 `sessionId` 作为内存 Session 的键,热 attach、并发冷加载合并与 restore race 都不比较所呈现的租户,也不比较规范化的 Store endpoint 或 workspace;但 Harness writer generation 会被检查——`writerId` 与 Harness 进程 boot ID 不一致的 attach 会以 `409 hosted_harness_generation_mismatch` fail closed;`managed_session_store_conflict` 的 fail-closed 围栏属于集成切片的目标设计而非已交付行为。lease 时长变化不改变存储身份。
|
||||
|
||||
第一阶段删除有意只实现软 tombstone。`GET` 和列表 API 会隐藏 `DELETED` Session,而已连接客户端仍可重放已经提交的公共删除事件。私有 journal/resource 字节会继续保留,直到 writer seal、execution 对账、legal hold、保留期和垃圾回收全部实现。因此不能把当前能力宣称为物理擦除。
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ This review targets the current PR #12692 split. The four earlier design pairs w
|
||||
- Public text parts aggregate only adjacent deltas of the same kind. Tools and reasoning delimit parts so snapshot hydration preserves visible chronology.
|
||||
- Continuation retraction only clears output from the selected Harness boot and event epoch. In the same transaction it invalidates the derived projection, resets materialization progress, and publishes `stream.reconciled` after commit so browsers reload retained history. This repairs projection consistency; it does not prove the missing private checkpoint recovery protocol.
|
||||
- Unreachable workers without local process ownership remain `UNKNOWN`; a failed attestation is not evidence that the old process has stopped.
|
||||
- `executions:prepare` and `executions/{id}:start` return a non-retryable 501 until the Broker implements a real durable dispatch fence. Operator resolution also returns 501 until it has a durable service API. The previous mapping dispatched during prepare and only read during start, violating the proposed protocol. The existing create-and-dispatch route retains its semantics.
|
||||
- `executions:prepare` and `executions/{id}:start` are live Broker routes again (`RuntimeBrokerHttpServer` dispatches them to `prepareExecution`/`startExecution`); `executions/{id}:resolve` still returns a non-retryable 501 until it has a durable service API. The previous mapping dispatched during prepare and only read during start, violating the proposed protocol. The existing create-and-dispatch route retains its semantics.
|
||||
- The Java 21 database CI job now installs local SDK/Broker dependencies and runs the Spring module's unit tests, Checkstyle, and MySQL-profile integration tests against a separate database. Changes to the shared contract fixture also trigger the workflow.
|
||||
- WebShell maps Java `running` and `cancelling` states, disables actions on inactive Sessions, and preserves tool failure and identity fields in both streamed events and items.
|
||||
|
||||
## Remaining integration gates
|
||||
|
||||
1. The embedded transport rejects acquire/control/release with 501. Current main contains a Broker client foundation, but `qwen serve --profile hosted-harness` explicitly rejects startup because the Broker-backed Session loop is not implemented; the remote durable-store adapter and worker bundle assumed by the copied E2E script are also unavailable. A successful real tool turn and crash-recovery proof require these dependencies to be integrated together.
|
||||
1. Only `executions/{id}:resolve` still returns a non-retryable 501; acquire/control/release are live Broker routes. Credentialed with a bearer token, `--no-web` and the capability digest, `qwen serve --profile hosted-harness` boots on loopback and serves the private no-tool Managed Session API. What remains unproven is the durable recovery block signalling named in gate 4, in-flight recovery, complete Workspace lifecycle, and distributed provisioning. The opt-in Workspace file-tool Turn, the HTTP durable-store adapter and the three owner-failover E2E modes run in the hosted-harness-mysql CI job.
|
||||
2. Private Store acquisition accepts a caller-selected writer token and tenant. The writer lease fences successive writers but does not authenticate a service. A trusted service identity or enforced private ingress policy must cover these routes independently of browser-facing Agent routes.
|
||||
3. Harness-level drain only retires a Session in process memory. It does not stop the worker, persist retirement, or revoke all private Broker access. Archive/delete must not be advertised as Runtime teardown.
|
||||
4. The HTTP adapter now fails closed with 409 for Broker `UNKNOWN`, but it cannot yet communicate the durable recovery block promised by the design. Recovery needs an explicit compatible unknown-outcome contract.
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
- 公共文本 Part 只聚合相邻且同类的 delta;工具与思考事件切分 Part,保证快照还原后的显示顺序。
|
||||
- continuation 回退只清除指定 Harness boot 和 event epoch 的输出。在同一事务中失效派生投影、重置物化进度,提交后发布 `stream.reconciled`,使浏览器重新读取保留的历史。这修复投影一致性,不代表缺失的私有 checkpoint 恢复协议已被证明。
|
||||
- 无本地进程所有权且无法连接的 worker 保持 `UNKNOWN`;attestation 失败不能证明旧进程已停止。
|
||||
- Broker 实现真正的持久化派发屏障前,`executions:prepare` 与 `executions/{id}:start` 返回不可重试的 501。operator resolution 在具备持久化服务 API 前也返回 501。此前 prepare 会派发执行、start 仅查询,违背设计协议。已有 create-and-dispatch 路由保留原语义。
|
||||
- `executions:prepare` 与 `executions/{id}:start` 已恢复为真实路由(`RuntimeBrokerHttpServer` 将其分派到 `prepareExecution`/`startExecution`);仅 `executions/{id}:resolve` 在具备持久化服务 API 前仍返回不可重试的 501。此前 prepare 会派发执行、start 仅查询,违背设计协议。已有 create-and-dispatch 路由保留原语义。
|
||||
- Java 21 数据库 CI 任务现已安装本地 SDK/Broker 依赖,并使用独立数据库运行 Spring 模块的单测、Checkstyle 和 MySQL profile 集成测试。共享契约 fixture 变更也会触发工作流。
|
||||
- WebShell 正确映射 Java `running`、`cancelling`,禁止在非活动 Session 上操作,并在流事件和 Item 中保留工具失败状态及身份。
|
||||
|
||||
## 剩余集成门禁
|
||||
|
||||
1. 内嵌 transport 对 acquire/control/release 返回 501。当前 main 已有 Broker client 基础代码,但 `qwen serve --profile hosted-harness` 会明确拒绝启动,因为尚未实现 Broker 支持的 Session 循环;所复制 E2E 脚本需要的远程 durable-store adapter 和 worker bundle 也不可用。必须共同集成这些依赖,才能证明真实工具 Turn 与崩溃恢复。
|
||||
1. 仅 `executions/{id}:resolve` 仍返回不可重试的 501;acquire/control/release 已是真实 Broker 路由。提供 bearer token、`--no-web` 与 capability digest 后,`qwen serve --profile hosted-harness` 可在 loopback 启动并提供私有 no-tool Managed Session API。仍未证明的是门禁 4 所述的持久化恢复阻塞信号、in-flight 恢复、完整 Workspace 生命周期与分布式 provisioning。opt-in 的 Workspace 文件工具 Turn、HTTP durable-store adapter 以及三种 owner-failover E2E 模式均在 hosted-harness-mysql CI 任务中运行。
|
||||
2. 私有 Store 的 acquire 接受调用方自行选择的 writer token 和 tenant。writer 租约约束先后写入者,但不认证服务身份。必须为这些路由提供可信服务身份或强制私网入口策略,并与浏览器 Agent 路由区分。
|
||||
3. Harness 级 drain 仅在进程内存中标记 Session 退休,不停止 worker、不持久化退休状态,也不撤销全部私有 Broker 访问。不能把 archive/delete 宣称为 Runtime 回收。
|
||||
4. HTTP 适配器现在对 Broker `UNKNOWN` 返回 409 并拒绝继续,但尚不能表达设计承诺的持久化恢复阻塞。恢复需要明确且兼容的未知结果契约。
|
||||
|
||||
@@ -10,6 +10,15 @@ execution APIs that do not exist on main, and incompatible Java and TypeScript
|
||||
execution contracts. The agreed scope is to make the foundations buildable and
|
||||
fail closed, without importing the preview architecture.
|
||||
|
||||
A later integration split lifted the fail-closed boundary this document
|
||||
records: credentialed with a bearer token, `--no-web`, and the capability
|
||||
digest, `qwen serve --profile hosted-harness` now boots on loopback and
|
||||
serves the private no-tool Managed Session API, and of the Broker routes
|
||||
only `executions/{id}:resolve` still returns a non-retryable 501. The
|
||||
statements below describe the foundation PR's shipped state.
|
||||
[Managed Agent split: review corrections](2026-09-25-managed-agent-review-corrections.md)
|
||||
records the current boundary.
|
||||
|
||||
## Scope and decisions
|
||||
|
||||
`qwen serve --profile hosted-harness` fails before opening a listener, even with
|
||||
|
||||
@@ -9,6 +9,14 @@ PR #12691 是从 #12358 提取的基础设施,不是可运行的 Hosted Harnes
|
||||
以及 Java 与 TypeScript 执行契约不兼容。本次约定范围是恢复构建并在能力不足时
|
||||
明确拒绝操作,不引入预览分支的完整架构。
|
||||
|
||||
后续的集成拆分解除了本文档记录的 fail-closed 边界:提供 bearer token、
|
||||
`--no-web` 与 capability digest 后,`qwen serve --profile hosted-harness`
|
||||
现在可在 loopback 启动并提供私有 no-tool Managed Session API;Broker 路由中
|
||||
仅 `executions/{id}:resolve` 仍返回不可重试的 501。下文描述的是该基础设施
|
||||
PR 交付时的状态,当前边界以
|
||||
[Managed Agent 拆分:评审修正](2026-09-25-managed-agent-review-corrections.zh-CN.md)
|
||||
为准。
|
||||
|
||||
## 范围与决策
|
||||
|
||||
`qwen serve --profile hosted-harness` 在监听端口之前失败,即使凭据完整也不例外。
|
||||
|
||||
@@ -121,8 +121,9 @@ revalidation window: [English](../../../docs/design/2026-10-02-managed-agent-que
|
||||
- MySQL 8
|
||||
|
||||
Run the packaged CLI with `qwen serve --profile hosted-harness` as a separate
|
||||
process. It supports durable no-tool Sessions and the opt-in Workspace file
|
||||
Turns described in the G0 section below.
|
||||
process (the credentialed launch is spelled out in the Full WebShell
|
||||
dual-path development entry below). It supports durable no-tool Sessions and
|
||||
the opt-in Workspace file Turns described in the G0 section below.
|
||||
|
||||
Install the two sibling libraries once when building this module outside a
|
||||
Maven reactor:
|
||||
@@ -228,9 +229,17 @@ 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.
|
||||
An in-memory Hosted attachment is bound to one normalized Store endpoint,
|
||||
tenant, workspace, and Harness writer generation; an attach or cold-load race
|
||||
with a different identity fails closed.
|
||||
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
|
||||
@@ -362,9 +371,46 @@ 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, start an ordinary `qwen serve` on port 4170 in addition to the
|
||||
private Hosted Harness used by Spring, then run from the repository root:
|
||||
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. Spring's HTTP
|
||||
Session Store stays off by default, and the Hosted Harness rejects every
|
||||
attach without a store descriptor, so Spring also needs the three values
|
||||
the one-shot launcher writes into `spring.env` — the connector refuses to
|
||||
start when the store is enabled with a blank base URL or workspace ID.
|
||||
Export all four together:
|
||||
|
||||
```bash
|
||||
export QWEN_MANAGED_AGENT_HARNESS_BASE_URL='http://127.0.0.1:4171'
|
||||
export QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED='true'
|
||||
export QWEN_MANAGED_AGENT_SESSION_STORE_BASE_URL='http://127.0.0.1:8080'
|
||||
export QWEN_MANAGED_AGENT_WORKSPACE_ID='local-dev-workspace'
|
||||
```
|
||||
|
||||
All four are read once at JVM startup, so if `mvn spring-boot:run` is
|
||||
already up on the 4170 value exported in Prerequisites, restart it with the
|
||||
overrides in place.
|
||||
|
||||
`--port` is a request, not a guarantee: `qwen serve` moves to the next free
|
||||
port on a collision, and 4171 is exactly where a daemon displaced from 4170
|
||||
lands. Confirm each server's bound port in its startup line before exporting
|
||||
the base URL above.
|
||||
|
||||
With both up, run from the repository root:
|
||||
|
||||
```bash
|
||||
QWEN_DAEMON_URL=http://127.0.0.1:4170 \
|
||||
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
transpileModule,
|
||||
} from 'typescript';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { QWEN_SERVER_TOKEN_ENV } from '../../packages/cli/src/serve/channel-worker-env.js';
|
||||
import { HOSTED_HARNESS_CAPABILITY_DIGEST_ENV } from '../../packages/cli/src/serve/hosted-harness-contract.js';
|
||||
import { validateHostedHarnessProfile } from '../../packages/cli/src/serve/hosted-harness-profile.js';
|
||||
|
||||
const read = (file) =>
|
||||
readFileSync(new URL(`../../${file}`, import.meta.url), 'utf8');
|
||||
@@ -28,6 +31,13 @@ const namedScripts = (text) => [
|
||||
),
|
||||
];
|
||||
|
||||
// Fenced shell blocks of a markdown text, bodies only; language-less fences
|
||||
// are invisible to this scan.
|
||||
const fencedShellBlocks = (text) =>
|
||||
[
|
||||
...text.matchAll(/```[ \t]*(?:bash|sh|shell|console|zsh)\n([\s\S]*?)```/g),
|
||||
].map((match) => match[1]);
|
||||
|
||||
describe('managed-agent-server e2e runner', () => {
|
||||
it('keeps service and proxy ports distinct when an ephemeral port repeats', async () => {
|
||||
const source = createSourceFile(
|
||||
@@ -239,4 +249,215 @@ describe('managed-agent-server e2e runner', () => {
|
||||
expect(namedScripts('see `scripts/nope.ts`')).toEqual(['scripts/nope.ts']);
|
||||
expect(namedScripts('packages/foo/scripts/nope.ts')).toEqual([]);
|
||||
});
|
||||
|
||||
it('pins the fenced hosted-harness launch block in the server README to a startable form', () => {
|
||||
// The oracle is the profile validator itself, not a copy of its rules:
|
||||
// the documented launch must supply everything
|
||||
// validateHostedHarnessProfile rejects for missing, or the launch fails
|
||||
// at startup while this pin stays green. The block is matched exactly —
|
||||
// a grammar that parses the fence into argv/env fails open on every
|
||||
// shell spelling it does not model. The CLI-side credential names come
|
||||
// from the production constants so renaming one reddens this pin instead
|
||||
// of stranding the README's spelling.
|
||||
const readme = read('packages/sdk-java/managed-agent-server/README.md');
|
||||
const fencedBlocks = fencedShellBlocks(readme);
|
||||
const command =
|
||||
'qwen serve --profile hosted-harness --port 4171 --hostname 127.0.0.1 --no-web';
|
||||
const launchBlocks = fencedBlocks.filter((block) =>
|
||||
block.includes(command),
|
||||
);
|
||||
expect(
|
||||
launchBlocks,
|
||||
'the README must fence exactly one hosted-harness launch block',
|
||||
).toHaveLength(1);
|
||||
expect(launchBlocks[0]).toBe(
|
||||
[
|
||||
`${QWEN_SERVER_TOKEN_ENV}="$QWEN_MANAGED_AGENT_HARNESS_TOKEN" \\`,
|
||||
`${HOSTED_HARNESS_CAPABILITY_DIGEST_ENV}="$QWEN_MANAGED_AGENT_CAPABILITY_DIGEST" \\`,
|
||||
command,
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
// A `$VAR` reference defers to a name the reader was told to export in
|
||||
// the Prerequisites section; a renamed or dropped export there expands
|
||||
// to empty in the reader's shell and the launch dies at startup, so
|
||||
// every name the launch block references must be assigned inside that
|
||||
// section — an assignment in a fence anywhere else in the README never
|
||||
// reaches the reader's shell.
|
||||
const prereqStart = readme.indexOf('## Prerequisites');
|
||||
const prereqEnd = readme.indexOf('## Public Session lifecycle');
|
||||
expect(prereqStart).toBeGreaterThan(-1);
|
||||
expect(prereqEnd).toBeGreaterThan(prereqStart);
|
||||
const assigned = new Set();
|
||||
for (const fenced of fencedShellBlocks(
|
||||
readme.slice(prereqStart, prereqEnd),
|
||||
)) {
|
||||
for (const match of fenced.matchAll(
|
||||
/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/gm,
|
||||
)) {
|
||||
assigned.add(match[1]);
|
||||
}
|
||||
}
|
||||
for (const match of launchBlocks[0].matchAll(
|
||||
/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g,
|
||||
)) {
|
||||
expect(
|
||||
assigned.has(match[1]),
|
||||
`the launch block references $${match[1]}, which the Prerequisites section does not assign`,
|
||||
).toBe(true);
|
||||
}
|
||||
// The validator's input is parsed from the pinned block so the fixture
|
||||
// cannot drift from the documented launch: an edit that drops --no-web
|
||||
// or widens --hostname must redden this oracle, not only the exact-text
|
||||
// pin above. mode stays the constant serve.ts hardcodes — the launch
|
||||
// carries no flag for it — and the deferred `$VAR` credentials stand in
|
||||
// as conforming values so the validator judges the launch's shape rather
|
||||
// than the placeholder spelling.
|
||||
expect(() =>
|
||||
validateHostedHarnessProfile({
|
||||
profile: 'hosted-harness',
|
||||
hostname: /--hostname\s+(\S+)/.exec(launchBlocks[0])[1],
|
||||
port: Number(/--port\s+(\d+)/.exec(launchBlocks[0])[1]),
|
||||
mode: 'http-bridge',
|
||||
token: 'documented-value',
|
||||
serveWebShell: !launchBlocks[0].includes('--no-web'),
|
||||
hostedHarnessCapabilityDigest: `sha256:${'a'.repeat(64)}`,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('pairs the 4171 base-url export with a startup-order note in the dual-path entry', () => {
|
||||
// The base URL is read once at JVM startup, so the section that moves
|
||||
// Spring to 4171 must say the value applies before (or via a restart
|
||||
// of) `mvn spring-boot:run`, or the reader's running server stays on
|
||||
// the 4170 value exported in Prerequisites.
|
||||
const readme = read('packages/sdk-java/managed-agent-server/README.md');
|
||||
const start = readme.indexOf(
|
||||
'## Full WebShell dual-path development entry',
|
||||
);
|
||||
const end = readme.indexOf('## Embedded Runtime Broker');
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
const slice = readme.slice(start, end);
|
||||
// The export must point at the port the documented launch binds, not at
|
||||
// a second copy of the number: a launch-block port bump mirrored into
|
||||
// the launch pin above would otherwise leave this assertion green while
|
||||
// Spring keeps calling the old port and every Managed Turn fails.
|
||||
const launchBlocks = fencedShellBlocks(readme).filter((fenced) =>
|
||||
fenced.includes('qwen serve --profile hosted-harness'),
|
||||
);
|
||||
expect(
|
||||
launchBlocks,
|
||||
'the README must fence exactly one hosted-harness launch block',
|
||||
).toHaveLength(1);
|
||||
const port = /--port\s+(\d+)/.exec(launchBlocks[0])[1];
|
||||
expect(
|
||||
slice,
|
||||
'the Spring base URL must point at the port the launch block binds',
|
||||
).toContain(
|
||||
`QWEN_MANAGED_AGENT_HARNESS_BASE_URL='http://127.0.0.1:${port}'`,
|
||||
);
|
||||
// The caveat sentence is hard-wrapped in the README, so match across the
|
||||
// line break; the fallback is the restart clause, not the bare word
|
||||
// `restart` that any unrelated sentence in the slice could supply.
|
||||
expect(slice.replace(/\s+/g, ' ')).toMatch(
|
||||
/read once at JVM startup|restart it with the override/i,
|
||||
);
|
||||
// The by-hand recipe re-anchors Spring to the Prerequisites environment,
|
||||
// which never names the Session Store, and application.yml defaults the
|
||||
// store off — a reader who follows that path wires Spring without a
|
||||
// store descriptor and the Harness answers every attach with
|
||||
// 400 invalid_managed_session_store, so the slice must name the switch.
|
||||
expect(slice).toContain('QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED');
|
||||
// The store switch is read once at JVM startup exactly like the base
|
||||
// URL: exported below the restart cue it never reaches the reader's
|
||||
// JVM, and every attach fails with 400 invalid_managed_session_store
|
||||
// while the recipe reads complete — so the exports must land before the
|
||||
// single restart. The cue is hard-wrapped, so compare on the collapsed
|
||||
// slice; a missing cue fails closed through the -1.
|
||||
const flat = slice.replace(/\s+/g, ' ');
|
||||
expect(
|
||||
flat.indexOf('QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED'),
|
||||
'the Session Store exports must precede the Spring restart cue',
|
||||
).toBeLessThan(flat.indexOf('restart it with the override'));
|
||||
});
|
||||
|
||||
it('keeps the review-corrections merge gates behind the CI-gated Hosted proofs', () => {
|
||||
// The hosted-harness-mysql CI job runs HostedWorkspaceToolTurnIT (a real
|
||||
// file-tool Turn through the packaged worker) and the three
|
||||
// owner-failover E2E modes against the production HTTP durable-store
|
||||
// adapter, all fail-closed via the failsafe includes and
|
||||
// check-failsafe-reports.js. While both oracles stand, the
|
||||
// review-corrections gates must not re-assert those capabilities as
|
||||
// unproven — the foundation-boundary banner names that document the
|
||||
// current authority, so the false claim reaches integrators in either
|
||||
// language.
|
||||
const toolTurnIt =
|
||||
'packages/sdk-java/managed-agent-server/src/test/java/com/alibaba/qwen/code/managedagent/HostedWorkspaceToolTurnIT.java';
|
||||
expect(
|
||||
existsSync(new URL(`../../${toolTurnIt}`, import.meta.url)) &&
|
||||
read('.github/workflows/sdk-java.yml').includes(
|
||||
'test:e2e:managed-session-failover',
|
||||
),
|
||||
'the Hosted tool-turn IT and the failover E2E lane must both exist for this oracle to mean anything',
|
||||
).toBe(true);
|
||||
for (const [file, heading, claim] of [
|
||||
[
|
||||
'docs/design/2026-09-25-managed-agent-review-corrections.md',
|
||||
'## Remaining integration gates',
|
||||
/what remains unproven is[^.]*\./gi,
|
||||
],
|
||||
[
|
||||
'docs/design/2026-09-25-managed-agent-review-corrections.zh-CN.md',
|
||||
'## 剩余集成门禁',
|
||||
/仍未证明[^。]*。/g,
|
||||
],
|
||||
]) {
|
||||
const doc = read(file);
|
||||
expect(doc, `${file} must keep its merge-gates section`).toContain(
|
||||
heading,
|
||||
);
|
||||
const gates = doc.slice(doc.indexOf(heading));
|
||||
for (const [sentence] of gates.matchAll(claim)) {
|
||||
expect(
|
||||
sentence,
|
||||
`${file} lists CI-gated Hosted capabilities as unproven`,
|
||||
).not.toMatch(
|
||||
/tool turns|durable-store adapter|worker bundle|工具 Turn/i,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('pins the attach-time generation fence in the Harness attachment contract', () => {
|
||||
// The attachment paragraph publishes which identities an attach does NOT
|
||||
// compare. The Harness keys its in-memory Session by sessionId alone and
|
||||
// compares no tenant on attach, so the paragraph must say exactly that —
|
||||
// an integrator who reads a tenant-keyed coalescing claim leaves tenant
|
||||
// scoping out of their own gateway on the recovery redrive, which is not
|
||||
// fenced. The Harness writer generation is the one identity that IS
|
||||
// enforced — the contract middleware answers a stale boot id with 409
|
||||
// hosted_harness_generation_mismatch and the attach handler rejects a
|
||||
// store descriptor whose writerId differs before any coalescing — so the
|
||||
// paragraph must name that rejection: an integrator reading "not
|
||||
// rejected" for the generation omits the 409 path and every attach fails
|
||||
// after a Harness restart with no documented way out. Each fence pin
|
||||
// below matches a polarity phrase, not the bare name: the name alone
|
||||
// stays green when the paragraph says the generation is not rejected or
|
||||
// the store fence is shipped behavior.
|
||||
const readme = read('packages/sdk-java/managed-agent-server/README.md');
|
||||
const anchor = readme.indexOf('The Java connector caches an attachment');
|
||||
expect(anchor).toBeGreaterThan(-1);
|
||||
const end = readme.indexOf('\n\n', anchor);
|
||||
expect(end).toBeGreaterThan(anchor);
|
||||
const paragraph = readme.slice(anchor, end).replace(/\s+/g, ' ');
|
||||
expect(paragraph).toContain(
|
||||
'keys its in-memory Session by `sessionId` alone',
|
||||
);
|
||||
expect(paragraph).toContain(
|
||||
'fails closed with `409 hosted_harness_generation_mismatch`',
|
||||
);
|
||||
expect(paragraph).toContain(
|
||||
'`managed_session_store_conflict` fence is target design',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user