refactor(managed-agent): Split server into Maven modules (#15871)

* refactor(managed-agent): split server into Maven modules

Separate contracts, orchestration, persistence, adapters, assembly, and verification while retaining the existing service delivery model. Move tests and build consumers together and document local evidence and pending CI acceptance.

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(managed-agent): reduce configuration and SDK boilerplate

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(managed-agent): narrow contracts and simplify test scaffolding

Co-authored-by: Qwen-Coder <[email protected]>

* fix(managed-agent): repair verification after module migration

Resolve shared fixtures and worker paths from the Maven module root, update CI packaging assertions, and align existing test fixtures with the extracted contracts. Restore restricted Workspace constructors and remove redundant test visibility changes without altering inherited MySQL coverage.

Co-authored-by: Qwen-Coder <[email protected]>

* test(managed-agent): align remaining Harness and OSS fixtures

Preserve SDK refusal messages and register the managed OSS adapter at fixture injection points. Keep the existing retry and client shutdown assertions intact.

Co-authored-by: Qwen-Coder <[email protected]>

* style(managed-agent): order Hosted cleanup Harness imports

Co-authored-by: Qwen-Coder <[email protected]>

* test(managed-agent): fix recovery smoke fixture and import order

Supply the required transcript coverage in the recovery browser fixture and wait for history before testing a complete handoff. Preserve its existing safety and submission assertions. Order the cleanup coordinator test imports for Checkstyle.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
This commit is contained in:
jinyeandQwen-Coder authored and GitHub committed 2026-10-30 14:56:35 +00:00
1 parent e3f12f54f9
commit f3af3dd857
1231 files changed
+17813 -12514

No files matched your search

+1 -1
View File
@@ -871,7 +871,7 @@ describe('trusted-PR lanes moved off the hosted queue (#13245)', () => {
assert.match(run('Build Java components'), /-Dmaven\.repo\.local=/); assert.match(run('Build Java components'), /-Dmaven\.repo\.local=/);
assert.match( assert.match(
run('Build Java components'), run('Build Java components'),
/rm -rf -- "\$\{repo\}\/com\/alibaba\/qwencode-sdk" "\$\{repo\}\/com\/alibaba\/qwen-managed-agent-server"/, /rm -rf -- "\$\{repo\}\/com\/alibaba\/qwencode-sdk" "\$\{repo\}\/com\/alibaba\/"qwen-managed-agent-\*/,
); );
assert.match( assert.match(
run('Assemble reference distribution'), run('Assemble reference distribution'),
+2 -2
View File
@@ -1196,7 +1196,7 @@ jobs:
PR_NUMBER: '${{ github.event.pull_request.number }}' PR_NUMBER: '${{ github.event.pull_request.number }}'
run: |- run: |-
set -euo pipefail set -euo pipefail
DOCUMENT='packages/managed-agent-server/src/main/resources/openapi/managed-agent-public-api.openapi.json' DOCUMENT='packages/managed-agent-server/contracts/src/main/resources/openapi/managed-agent-public-api.openapi.json'
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files?per_page=100" --paginate --jq '.[] | .filename, .previous_filename // empty' > "${RUNNER_TEMP}/contract-pr-files.txt" gh api "repos/${REPO}/pulls/${PR_NUMBER}/files?per_page=100" --paginate --jq '.[] | .filename, .previous_filename // empty' > "${RUNNER_TEMP}/contract-pr-files.txt"
if ! grep -qxF "${DOCUMENT}" "${RUNNER_TEMP}/contract-pr-files.txt"; then if ! grep -qxF "${DOCUMENT}" "${RUNNER_TEMP}/contract-pr-files.txt"; then
exit 0 exit 0
@@ -1204,7 +1204,7 @@ jobs:
BASE_DOCUMENT="${RUNNER_TEMP}/contract-base.json" BASE_DOCUMENT="${RUNNER_TEMP}/contract-base.json"
git fetch --quiet --depth=1 --filter=blob:none origin "refs/heads/${BASE_REF}" git fetch --quiet --depth=1 --filter=blob:none origin "refs/heads/${BASE_REF}"
if ! git show "FETCH_HEAD:${DOCUMENT}" > "${BASE_DOCUMENT}" 2>/dev/null; then if ! git show "FETCH_HEAD:${DOCUMENT}" > "${BASE_DOCUMENT}" 2>/dev/null; then
PREVIOUS_DOCUMENT="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files?per_page=100" --paginate --jq '.[] | select(.filename == "packages/managed-agent-server/src/main/resources/openapi/managed-agent-public-api.openapi.json") | .previous_filename // empty')" PREVIOUS_DOCUMENT="$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files?per_page=100" --paginate --jq '.[] | select(.filename == "packages/managed-agent-server/contracts/src/main/resources/openapi/managed-agent-public-api.openapi.json") | .previous_filename // empty')"
if [ -n "${PREVIOUS_DOCUMENT}" ]; then if [ -n "${PREVIOUS_DOCUMENT}" ]; then
git show "FETCH_HEAD:${PREVIOUS_DOCUMENT}" > "${BASE_DOCUMENT}" git show "FETCH_HEAD:${PREVIOUS_DOCUMENT}" > "${BASE_DOCUMENT}"
else else
+1 -1
View File
@@ -3226,7 +3226,7 @@ jobs:
uses: 'actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830' # v4.3.0 uses: 'actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830' # v4.3.0
with: with:
path: '${{ runner.temp }}/verify-maven-repo' path: '${{ runner.temp }}/verify-maven-repo'
key: "verify-maven-repo-${{ hashFiles('packages/sdk-java/*/pom.xml', 'packages/managed-agent-server/pom.xml') }}" key: "verify-maven-repo-${{ hashFiles('packages/sdk-java/*/pom.xml', 'packages/managed-agent-server/**/pom.xml') }}"
restore-keys: 'verify-maven-repo-' restore-keys: 'verify-maven-repo-'
- name: 'Install and build PR app' - name: 'Install and build PR app'
@@ -217,7 +217,7 @@ jobs:
cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}" cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}"
cache-dependency-path: |- cache-dependency-path: |-
packages/sdk-java/qwencode/pom.xml packages/sdk-java/qwencode/pom.xml
packages/managed-agent-server/pom.xml packages/managed-agent-server/**/pom.xml
settings-path: '${{ runner.temp }}/setup-java-m2' settings-path: '${{ runner.temp }}/setup-java-m2'
- name: 'Set up Maven (self-hosted)' - name: 'Set up Maven (self-hosted)'
@@ -326,7 +326,7 @@ jobs:
# PR's build must never reach the host-shared ~/.m2. # PR's build must never reach the host-shared ~/.m2.
repo="${RUNNER_TEMP}/m2-repo" repo="${RUNNER_TEMP}/m2-repo"
cp -aln "${HOME}/.m2/repository" "${repo}" 2>/dev/null || true cp -aln "${HOME}/.m2/repository" "${repo}" 2>/dev/null || true
rm -rf -- "${repo}/com/alibaba/qwencode-sdk" "${repo}/com/alibaba/qwen-managed-agent-server" rm -rf -- "${repo}/com/alibaba/qwencode-sdk" "${repo}/com/alibaba/"qwen-managed-agent-*
MAVEN_ARGS="${MAVEN_ARGS} -Dmaven.repo.local=${repo}" MAVEN_ARGS="${MAVEN_ARGS} -Dmaven.repo.local=${repo}"
fi fi
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true clean install mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true clean install
@@ -119,7 +119,7 @@ jobs:
cache: 'maven' cache: 'maven'
cache-dependency-path: | cache-dependency-path: |
packages/sdk-java/qwencode/pom.xml packages/sdk-java/qwencode/pom.xml
packages/managed-agent-server/pom.xml packages/managed-agent-server/**/pom.xml
- name: 'Set up Node.js 22' - name: 'Set up Node.js 22'
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
@@ -172,8 +172,9 @@ jobs:
- name: 'Verify the executable JAR and publication profile' - name: 'Verify the executable JAR and publication profile'
run: |- run: |-
mvn --batch-mode --no-transfer-progress -f packages/managed-agent-server/pom.xml clean verify -Pcentral-release -Dgpg.skip=true -DforkCount=1 -DreuseForks=false -Dproject.build.outputTimestamp="${BUILD_TIMESTAMP}" mvn --batch-mode --no-transfer-progress -f packages/managed-agent-server/pom.xml clean verify -Pcentral-release -Dgpg.skip=true -DforkCount=1 -DreuseForks=false -Dproject.build.outputTimestamp="${BUILD_TIMESTAMP}"
runtime_jar="packages/managed-agent-server/target/qwen-managed-agent-server-${CLI_VERSION}-exec.jar" runtime_jar="packages/managed-agent-server/server/target/qwen-managed-agent-server-${CLI_VERSION}-exec.jar"
# Older release tags shipped the executable as the main artifact. # Older release tags shipped the executable as the main artifact.
if [[ ! -f "${runtime_jar}" ]]; then runtime_jar="packages/managed-agent-server/target/qwen-managed-agent-server-${CLI_VERSION}-exec.jar"; fi
if [[ ! -f "${runtime_jar}" ]]; then runtime_jar="packages/managed-agent-server/target/qwen-managed-agent-server-${CLI_VERSION}.jar"; fi if [[ ! -f "${runtime_jar}" ]]; then runtime_jar="packages/managed-agent-server/target/qwen-managed-agent-server-${CLI_VERSION}.jar"; fi
unzip -p "${runtime_jar}" META-INF/MANIFEST.MF | tr -d '\r' | grep -Fx 'Main-Class: org.springframework.boot.loader.launch.JarLauncher' unzip -p "${runtime_jar}" META-INF/MANIFEST.MF | tr -d '\r' | grep -Fx 'Main-Class: org.springframework.boot.loader.launch.JarLauncher'
node .qwen/managed-release-tools/scripts/managed-runtime-release.js --jar "${runtime_jar}" --version "${CLI_VERSION}" --out-dir dist/managed-runtime node .qwen/managed-release-tools/scripts/managed-runtime-release.js --jar "${runtime_jar}" --version "${CLI_VERSION}" --out-dir dist/managed-runtime
@@ -21,7 +21,7 @@ on:
branches: branches:
- 'main' - 'main'
paths: paths:
- 'packages/managed-agent-server/src/main/resources/openapi/managed-agent-public-api.openapi.json' - 'packages/managed-agent-server/contracts/src/main/resources/openapi/managed-agent-public-api.openapi.json'
- 'scripts/check-contract-open-prs.js' - 'scripts/check-contract-open-prs.js'
- 'scripts/contract-version-utils.js' - 'scripts/contract-version-utils.js'
- '.github/workflows/sdk-java-contract-open-prs.yml' - '.github/workflows/sdk-java-contract-open-prs.yml'
@@ -17,9 +17,15 @@ on:
branches: branches:
- 'main' - 'main'
paths: paths:
- 'packages/sdk-java/*/persistence/src/main/resources/db/migration/**'
- 'packages/sdk-java/*/persistence/src/main/java/db/migration/**'
- 'packages/sdk-java/*/persistence/src/main/resources/db/managed-schema/**'
- 'packages/sdk-java/*/src/main/resources/db/migration/**' - 'packages/sdk-java/*/src/main/resources/db/migration/**'
- 'packages/sdk-java/*/src/main/java/db/migration/**' - 'packages/sdk-java/*/src/main/java/db/migration/**'
- 'packages/sdk-java/*/src/main/resources/db/managed-schema/**' - 'packages/sdk-java/*/src/main/resources/db/managed-schema/**'
- 'packages/managed-agent-server/persistence/src/main/resources/db/migration/**'
- 'packages/managed-agent-server/persistence/src/main/java/db/migration/**'
- 'packages/managed-agent-server/persistence/src/main/resources/db/managed-schema/**'
- 'packages/managed-agent-server/src/main/resources/db/migration/**' - 'packages/managed-agent-server/src/main/resources/db/migration/**'
- 'packages/managed-agent-server/src/main/java/db/migration/**' - 'packages/managed-agent-server/src/main/java/db/migration/**'
- 'packages/managed-agent-server/src/main/resources/db/managed-schema/**' - 'packages/managed-agent-server/src/main/resources/db/managed-schema/**'
+63 -35
View File
@@ -199,7 +199,7 @@ jobs:
cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}" cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}"
cache-dependency-path: |- cache-dependency-path: |-
packages/sdk-java/qwencode/pom.xml packages/sdk-java/qwencode/pom.xml
packages/managed-agent-server/pom.xml packages/managed-agent-server/**/pom.xml
settings-path: '${{ runner.temp }}/setup-java-m2' settings-path: '${{ runner.temp }}/setup-java-m2'
# The Broker process tests spawn the Node fixture worker. # The Broker process tests spawn the Node fixture worker.
@@ -294,7 +294,14 @@ jobs:
# Maven runs would rewrite through into the host cache. Drop them # Maven runs would rewrite through into the host cache. Drop them
# so the install writes fresh inodes. # so the install writes fresh inodes.
rm -rf -- "${RUNNER_TEMP}/m2-repo/com/alibaba/qwencode-sdk" \ rm -rf -- "${RUNNER_TEMP}/m2-repo/com/alibaba/qwencode-sdk" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" "${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-parent" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-contracts" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-control-plane" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-broker" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-persistence" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-adapters" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-verification"
# The suite resolves from the job-local copy above, so it does not # The suite resolves from the job-local copy above, so it does not
# touch host-shared state and must not hold the per-host lock. # touch host-shared state and must not hold the per-host lock.
# On 2026-10-22 run 39771949573's fast tier failed at its # On 2026-10-22 run 39771949573's fast tier failed at its
@@ -425,7 +432,7 @@ jobs:
mkdir -p "${HOME}/.m2/repository" 9>&- mkdir -p "${HOME}/.m2/repository" 9>&-
cd "${RUNNER_TEMP}/m2-repo" cd "${RUNNER_TEMP}/m2-repo"
write_back=0 write_back=0
timeout 300 find . \( -path ./com/alibaba/qwencode-sdk -o -path ./com/alibaba/qwen-managed-agent-server -o -name '*.lastUpdated' -o -name 'resolver-status.properties' -o -name '_remote.repositories' -o -name '*.tmp' \) -prune -o -type f -links 1 -exec cp -ln --parents -t "${HOME}/.m2/repository" {} + 9>&- || write_back=$? timeout 300 find . \( -path ./com/alibaba/qwencode-sdk -o -path ./com/alibaba/qwen-managed-agent-parent -o -path ./com/alibaba/qwen-managed-agent-contracts -o -path ./com/alibaba/qwen-managed-agent-control-plane -o -path ./com/alibaba/qwen-managed-agent-runtime-broker -o -path ./com/alibaba/qwen-managed-agent-persistence -o -path ./com/alibaba/qwen-managed-agent-adapters -o -path ./com/alibaba/qwen-managed-agent-server -o -path ./com/alibaba/qwen-managed-agent-verification -o -name '*.lastUpdated' -o -name 'resolver-status.properties' -o -name '_remote.repositories' -o -name '*.tmp' \) -prune -o -type f -links 1 -exec cp -ln --parents -t "${HOME}/.m2/repository" {} + 9>&- || write_back=$?
if [ "${write_back}" -eq 124 ]; then if [ "${write_back}" -eq 124 ]; then
echo "::warning::Maven cache write-back stopped after 300 s; the gate result above is unaffected" echo "::warning::Maven cache write-back stopped after 300 s; the gate result above is unaffected"
elif [ "${write_back}" -ne 0 ]; then elif [ "${write_back}" -ne 0 ]; then
@@ -466,8 +473,8 @@ jobs:
path: | path: |
packages/sdk-java/qwencode/target/surefire-reports/*.txt packages/sdk-java/qwencode/target/surefire-reports/*.txt
packages/sdk-java/qwencode/target/surefire-reports/*.dumpstream packages/sdk-java/qwencode/target/surefire-reports/*.dumpstream
packages/managed-agent-server/target/surefire-reports/*.txt packages/managed-agent-server/verification/target/surefire-reports/*.txt
packages/managed-agent-server/target/surefire-reports/*.dumpstream packages/managed-agent-server/verification/target/surefire-reports/*.dumpstream
if-no-files-found: 'ignore' if-no-files-found: 'ignore'
retention-days: 7 retention-days: 7
@@ -776,7 +783,7 @@ jobs:
cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}" cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}"
cache-dependency-path: |- cache-dependency-path: |-
packages/sdk-java/qwencode/pom.xml packages/sdk-java/qwencode/pom.xml
packages/managed-agent-server/pom.xml packages/managed-agent-server/**/pom.xml
settings-path: '${{ runner.temp }}/setup-java-m2' settings-path: '${{ runner.temp }}/setup-java-m2'
- name: 'Point Maven at the pool mirror' - name: 'Point Maven at the pool mirror'
@@ -927,7 +934,14 @@ jobs:
# Maven runs would rewrite through into the host cache. Drop them # Maven runs would rewrite through into the host cache. Drop them
# so the install writes fresh inodes. # so the install writes fresh inodes.
rm -rf -- "${RUNNER_TEMP}/m2-repo/com/alibaba/qwencode-sdk" \ rm -rf -- "${RUNNER_TEMP}/m2-repo/com/alibaba/qwencode-sdk" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" "${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-parent" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-contracts" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-control-plane" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-broker" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-persistence" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-adapters" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-verification"
# Both Maven runs resolve from the job-local copy above, so they do # Both Maven runs resolve from the job-local copy above, so they do
# not touch host-shared state and must not hold the per-host lock: # not touch host-shared state and must not hold the per-host lock:
# a MySQL verify can run for hours under pool contention, and the # a MySQL verify can run for hours under pool contention, and the
@@ -1039,7 +1053,7 @@ jobs:
mkdir -p "${HOME}/.m2/repository" 9>&- mkdir -p "${HOME}/.m2/repository" 9>&-
cd "${RUNNER_TEMP}/m2-repo" cd "${RUNNER_TEMP}/m2-repo"
write_back=0 write_back=0
timeout 300 find . \( -path ./com/alibaba/qwencode-sdk -o -path ./com/alibaba/qwen-managed-agent-server -o -name '*.lastUpdated' -o -name 'resolver-status.properties' -o -name '_remote.repositories' -o -name '*.tmp' \) -prune -o -type f -links 1 -exec cp -ln --parents -t "${HOME}/.m2/repository" {} + 9>&- || write_back=$? timeout 300 find . \( -path ./com/alibaba/qwencode-sdk -o -path ./com/alibaba/qwen-managed-agent-parent -o -path ./com/alibaba/qwen-managed-agent-contracts -o -path ./com/alibaba/qwen-managed-agent-control-plane -o -path ./com/alibaba/qwen-managed-agent-runtime-broker -o -path ./com/alibaba/qwen-managed-agent-persistence -o -path ./com/alibaba/qwen-managed-agent-adapters -o -path ./com/alibaba/qwen-managed-agent-server -o -path ./com/alibaba/qwen-managed-agent-verification -o -name '*.lastUpdated' -o -name 'resolver-status.properties' -o -name '_remote.repositories' -o -name '*.tmp' \) -prune -o -type f -links 1 -exec cp -ln --parents -t "${HOME}/.m2/repository" {} + 9>&- || write_back=$?
if [ "${write_back}" -eq 124 ]; then if [ "${write_back}" -eq 124 ]; then
echo "::warning::Maven cache write-back stopped after 300 s; the gate result above is unaffected" echo "::warning::Maven cache write-back stopped after 300 s; the gate result above is unaffected"
elif [ "${write_back}" -ne 0 ]; then elif [ "${write_back}" -ne 0 ]; then
@@ -1057,10 +1071,10 @@ jobs:
with: with:
name: 'sdk-java-test-reports-mysql-integration-java-21' name: 'sdk-java-test-reports-mysql-integration-java-21'
path: | path: |
packages/managed-agent-server/target/surefire-reports/*.txt packages/managed-agent-server/verification/target/surefire-reports/*.txt
packages/managed-agent-server/target/surefire-reports/*.dumpstream packages/managed-agent-server/verification/target/surefire-reports/*.dumpstream
packages/managed-agent-server/target/failsafe-reports/*.txt packages/managed-agent-server/verification/target/failsafe-reports/*.txt
packages/managed-agent-server/target/failsafe-reports/*.dumpstream packages/managed-agent-server/verification/target/failsafe-reports/*.dumpstream
if-no-files-found: 'ignore' if-no-files-found: 'ignore'
retention-days: 7 retention-days: 7
@@ -1188,7 +1202,7 @@ jobs:
cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}" cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}"
cache-dependency-path: |- cache-dependency-path: |-
packages/sdk-java/**/pom.xml packages/sdk-java/**/pom.xml
packages/managed-agent-server/pom.xml packages/managed-agent-server/**/pom.xml
settings-path: '${{ runner.temp }}/setup-java-m2' settings-path: '${{ runner.temp }}/setup-java-m2'
- name: 'Set up Node.js' - name: 'Set up Node.js'
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
@@ -1281,7 +1295,7 @@ jobs:
run: 'node scripts/check-failsafe-reports.js hosted packages/managed-agent-server' run: 'node scripts/check-failsafe-reports.js hosted packages/managed-agent-server'
- name: 'Check Hosted latency measurements' - name: 'Check Hosted latency measurements'
run: |- run: |-
test -s packages/managed-agent-server/target/hosted-latency-baseline.json test -s packages/managed-agent-server/verification/target/hosted-latency-baseline.json
cd integration-tests cd integration-tests
npx vitest run cli/hosted-latency-baseline.test.ts npx vitest run cli/hosted-latency-baseline.test.ts
- name: 'Run Runtime Broker fault gates' - name: 'Run Runtime Broker fault gates'
@@ -1302,16 +1316,16 @@ jobs:
with: with:
name: 'hosted-harness-mysql-reports' name: 'hosted-harness-mysql-reports'
path: | path: |
packages/managed-agent-server/target/failsafe-reports/ packages/managed-agent-server/verification/target/failsafe-reports/
packages/managed-agent-server/target/hosted-latency-baseline.json packages/managed-agent-server/verification/target/hosted-latency-baseline.json
packages/managed-agent-server/target/fg7-cases/ packages/managed-agent-server/verification/target/fg7-cases/
packages/managed-agent-server/target/fg8-cases/ packages/managed-agent-server/verification/target/fg8-cases/
packages/managed-agent-server/target/fg9-cases/ packages/managed-agent-server/verification/target/fg9-cases/
packages/managed-agent-server/target/fg10-cases/ packages/managed-agent-server/verification/target/fg10-cases/
packages/managed-agent-server/target/fg11-cases/ packages/managed-agent-server/verification/target/fg11-cases/
packages/managed-agent-server/target/fg12-cases/ packages/managed-agent-server/verification/target/fg12-cases/
packages/managed-agent-server/target/fg13-cases/ packages/managed-agent-server/verification/target/fg13-cases/
packages/managed-agent-server/target/l4-shell-lifecycle/ packages/managed-agent-server/verification/target/l4-shell-lifecycle/
retention-days: 7 retention-days: 7
- name: 'Verify Hosted admission gates' - name: 'Verify Hosted admission gates'
@@ -1362,7 +1376,7 @@ jobs:
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1 uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
with: with:
name: 'o4-mysql-gate-reports' name: 'o4-mysql-gate-reports'
path: 'packages/managed-agent-server/target/failsafe-reports/' path: 'packages/managed-agent-server/verification/target/failsafe-reports/'
retention-days: 7 retention-days: 7
hosted-failover-e2e: hosted-failover-e2e:
@@ -1391,7 +1405,7 @@ jobs:
cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}" cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}"
cache-dependency-path: |- cache-dependency-path: |-
packages/sdk-java/**/pom.xml packages/sdk-java/**/pom.xml
packages/managed-agent-server/pom.xml packages/managed-agent-server/**/pom.xml
settings-path: '${{ runner.temp }}/setup-java-m2' settings-path: '${{ runner.temp }}/setup-java-m2'
- name: 'Set up Node.js' - name: 'Set up Node.js'
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
@@ -1710,7 +1724,7 @@ jobs:
cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}" cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}"
cache-dependency-path: |- cache-dependency-path: |-
packages/sdk-java/**/pom.xml packages/sdk-java/**/pom.xml
packages/managed-agent-server/pom.xml packages/managed-agent-server/**/pom.xml
settings-path: '${{ runner.temp }}/setup-java-m2' settings-path: '${{ runner.temp }}/setup-java-m2'
- name: 'Point Maven at the pool mirror' - name: 'Point Maven at the pool mirror'
if: "${{ runner.environment == 'self-hosted' }}" if: "${{ runner.environment == 'self-hosted' }}"
@@ -1773,7 +1787,14 @@ jobs:
# Maven runs would rewrite through into the host cache. Drop them # Maven runs would rewrite through into the host cache. Drop them
# so the install writes fresh inodes. # so the install writes fresh inodes.
rm -rf -- "${RUNNER_TEMP}/m2-repo/com/alibaba/qwencode-sdk" \ rm -rf -- "${RUNNER_TEMP}/m2-repo/com/alibaba/qwencode-sdk" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" "${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-parent" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-contracts" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-control-plane" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-broker" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-persistence" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-adapters" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-verification"
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true -Dmaven.javadoc.skip=true install mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true -Dmaven.javadoc.skip=true install
- name: 'Cache the RocketMQ distribution' - name: 'Cache the RocketMQ distribution'
uses: 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' # v6.1.0 uses: 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' # v6.1.0
@@ -1816,9 +1837,9 @@ jobs:
with: with:
name: 'event-transport-broker-gate-reports' name: 'event-transport-broker-gate-reports'
path: |- path: |-
packages/managed-agent-server/target/failsafe-reports/ packages/managed-agent-server/verification/target/failsafe-reports/
packages/managed-agent-server/target/event-transport-latency*.json packages/managed-agent-server/verification/target/event-transport-latency*.json
packages/managed-agent-server/target/event-transport-latency.jfr packages/managed-agent-server/verification/target/event-transport-latency.jfr
${{ runner.temp }}/rocketmq/logs/ ${{ runner.temp }}/rocketmq/logs/
${{ runner.temp }}/rocketmq/home/logs/ ${{ runner.temp }}/rocketmq/home/logs/
retention-days: 7 retention-days: 7
@@ -1970,7 +1991,7 @@ jobs:
cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}" cache: "${{ runner.environment == 'github-hosted' && 'maven' || '' }}"
cache-dependency-path: |- cache-dependency-path: |-
packages/sdk-java/**/pom.xml packages/sdk-java/**/pom.xml
packages/managed-agent-server/pom.xml packages/managed-agent-server/**/pom.xml
settings-path: '${{ runner.temp }}/setup-java-m2' settings-path: '${{ runner.temp }}/setup-java-m2'
- name: 'Point Maven at the pool mirror' - name: 'Point Maven at the pool mirror'
if: "${{ runner.environment == 'self-hosted' }}" if: "${{ runner.environment == 'self-hosted' }}"
@@ -2055,7 +2076,14 @@ jobs:
# Maven runs would rewrite through into the host cache. Drop them # Maven runs would rewrite through into the host cache. Drop them
# so the install writes fresh inodes. # so the install writes fresh inodes.
rm -rf -- "${RUNNER_TEMP}/m2-repo/com/alibaba/qwencode-sdk" \ rm -rf -- "${RUNNER_TEMP}/m2-repo/com/alibaba/qwencode-sdk" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" "${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-parent" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-contracts" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-control-plane" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-runtime-broker" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-persistence" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-adapters" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-server" \
"${RUNNER_TEMP}/m2-repo/com/alibaba/qwen-managed-agent-verification"
mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true -Dmaven.javadoc.skip=true install mvn --batch-mode --no-transfer-progress -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true -Dmaven.javadoc.skip=true install
- name: 'Install libfaketime' - name: 'Install libfaketime'
# The clock-skew row moves one node's whole wall clock. A pool host # The clock-skew row moves one node's whole wall clock. A pool host
@@ -2152,8 +2180,8 @@ jobs:
with: with:
name: 'event-transport-multi-node-gate-reports' name: 'event-transport-multi-node-gate-reports'
path: |- path: |-
packages/managed-agent-server/target/failsafe-reports/ packages/managed-agent-server/verification/target/failsafe-reports/
packages/managed-agent-server/target/mq3-multi-node/ packages/managed-agent-server/verification/target/mq3-multi-node/
${{ runner.temp }}/rocketmq/logs/ ${{ runner.temp }}/rocketmq/logs/
${{ runner.temp }}/rocketmq/home/logs/ ${{ runner.temp }}/rocketmq/home/logs/
retention-days: 7 retention-days: 7
+9 -2
View File
@@ -12,7 +12,7 @@ on:
branches: ['main'] branches: ['main']
paths: paths:
- 'packages/sdk-java/*/pom.xml' - 'packages/sdk-java/*/pom.xml'
- 'packages/managed-agent-server/pom.xml' - 'packages/managed-agent-server/**/pom.xml'
- '.github/workflows/verify-maven-repo.yml' - '.github/workflows/verify-maven-repo.yml'
workflow_dispatch: workflow_dispatch:
@@ -107,7 +107,14 @@ jobs:
repo="${RUNNER_TEMP:?}/verify-maven-repo" repo="${RUNNER_TEMP:?}/verify-maven-repo"
STRIP_ARTIFACTS=( STRIP_ARTIFACTS=(
qwencode-sdk qwencode-sdk
qwen-managed-agent-parent
qwen-managed-agent-contracts
qwen-managed-agent-control-plane
qwen-managed-agent-runtime-broker
qwen-managed-agent-persistence
qwen-managed-agent-adapters
qwen-managed-agent-server qwen-managed-agent-server
qwen-managed-agent-verification
acp-sdk acp-sdk
) )
for artifact in "${STRIP_ARTIFACTS[@]}"; do for artifact in "${STRIP_ARTIFACTS[@]}"; do
@@ -118,4 +125,4 @@ jobs:
uses: 'actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830' # v4.3.0 uses: 'actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830' # v4.3.0
with: with:
path: '${{ runner.temp }}/verify-maven-repo' path: '${{ runner.temp }}/verify-maven-repo'
key: "verify-maven-repo-${{ hashFiles('packages/sdk-java/*/pom.xml', 'packages/managed-agent-server/pom.xml') }}" key: "verify-maven-repo-${{ hashFiles('packages/sdk-java/*/pom.xml', 'packages/managed-agent-server/**/pom.xml') }}"
+1
View File
@@ -42,3 +42,4 @@ packages/core/src/skills/bundled/browser-use/runtime/
# next sync. eslint.config.js already ignores packages/mobile-mcp/** for the # next sync. eslint.config.js already ignores packages/mobile-mcp/** for the
# same reason; .prettierignore had cua-driver but not its sibling. # same reason; .prettierignore had cua-driver but not its sibling.
packages/mobile-mcp/ packages/mobile-mcp/
packages/managed-agent-server/**/target/
+6 -6
View File
@@ -59,11 +59,11 @@ bin/qwen-managed-agent-admin csi-protection \
/absolute/path/registration.json resident-protection resident-binding /absolute/path/registration.json resident-protection resident-binding
``` ```
From a module build, run the same entry from the executable server jar: From a module build, run the same entry from the repository root:
```bash ```bash
java -Dloader.main=com.alibaba.qwen.code.managedagent.workspace.runtime.WorkspaceCsiProtectionMain \ java -Dloader.main=com.alibaba.qwen.code.managedagent.maintenance.WorkspaceCsiProtectionMain \
-cp target/qwen-managed-agent-server-<version>-exec.jar \ -cp packages/managed-agent-server/server/target/qwen-managed-agent-server-<version>-exec.jar \
org.springframework.boot.loader.launch.PropertiesLauncher \ org.springframework.boot.loader.launch.PropertiesLauncher \
/absolute/path/registration.json resident-protection resident-binding /absolute/path/registration.json resident-protection resident-binding
``` ```
@@ -121,11 +121,11 @@ bin/qwen-managed-agent-admin resident-retirement \
--spring.config.additional-location=file:/absolute/path/application.yaml --spring.config.additional-location=file:/absolute/path/application.yaml
``` ```
From a module build, use the executable server jar and the same arguments: From a module build, run from the repository root with the same arguments:
```bash ```bash
java -Dloader.main=com.alibaba.qwen.code.managedagent.persistence.ResidentWorkspaceRetirementMain \ java -Dloader.main=com.alibaba.qwen.code.managedagent.maintenance.ResidentWorkspaceRetirementMain \
-cp target/qwen-managed-agent-server-<version>-exec.jar \ -cp packages/managed-agent-server/server/target/qwen-managed-agent-server-<version>-exec.jar \
org.springframework.boot.loader.launch.PropertiesLauncher \ org.springframework.boot.loader.launch.PropertiesLauncher \
inspect /absolute/path/retirement.json \ inspect /absolute/path/retirement.json \
--spring.config.additional-location=file:/absolute/path/application.yaml --spring.config.additional-location=file:/absolute/path/application.yaml
+6 -6
View File
@@ -46,11 +46,11 @@ bin/qwen-managed-agent-admin csi-protection \
/absolute/path/registration.json resident-protection resident-binding /absolute/path/registration.json resident-protection resident-binding
``` ```
在模块构建中,从可执行的 server jar 运行同一个入口: 使用模块构建产物时,在仓库根目录运行同一个入口:
```bash ```bash
java -Dloader.main=com.alibaba.qwen.code.managedagent.workspace.runtime.WorkspaceCsiProtectionMain \ java -Dloader.main=com.alibaba.qwen.code.managedagent.maintenance.WorkspaceCsiProtectionMain \
-cp target/qwen-managed-agent-server-<version>-exec.jar \ -cp packages/managed-agent-server/server/target/qwen-managed-agent-server-<version>-exec.jar \
org.springframework.boot.loader.launch.PropertiesLauncher \ org.springframework.boot.loader.launch.PropertiesLauncher \
/absolute/path/registration.json resident-protection resident-binding /absolute/path/registration.json resident-protection resident-binding
``` ```
@@ -103,11 +103,11 @@ bin/qwen-managed-agent-admin resident-retirement \
--spring.config.additional-location=file:/absolute/path/application.yaml --spring.config.additional-location=file:/absolute/path/application.yaml
``` ```
在模块构建中,使用可执行的 server jar 和相同的参数: 使用模块构建产物时,在仓库根目录运行,并使用相同的参数:
```bash ```bash
java -Dloader.main=com.alibaba.qwen.code.managedagent.persistence.ResidentWorkspaceRetirementMain \ java -Dloader.main=com.alibaba.qwen.code.managedagent.maintenance.ResidentWorkspaceRetirementMain \
-cp target/qwen-managed-agent-server-<version>-exec.jar \ -cp packages/managed-agent-server/server/target/qwen-managed-agent-server-<version>-exec.jar \
org.springframework.boot.loader.launch.PropertiesLauncher \ org.springframework.boot.loader.launch.PropertiesLauncher \
inspect /absolute/path/retirement.json \ inspect /absolute/path/retirement.json \
--spring.config.additional-location=file:/absolute/path/application.yaml --spring.config.additional-location=file:/absolute/path/application.yaml
@@ -2,6 +2,8 @@
[English](2026-10-15-managed-agent-server-layout.md) | [简体中文](2026-10-15-managed-agent-server-layout.zh-CN.md) [English](2026-10-15-managed-agent-server-layout.md) | [简体中文](2026-10-15-managed-agent-server-layout.zh-CN.md)
Successor: [Maven module boundaries](2026-10-29-managed-agent-maven-modules.md). This document retains its historical decisions and validation record; the successor defines the current build layout.
Status: implemented and validated locally. Remote CI and real MySQL/Linux deployment acceptance have not been run for this change. Status: implemented and validated locally. Remote CI and real MySQL/Linux deployment acceptance have not been run for this change.
## Problem and current state ## Problem and current state
@@ -2,6 +2,8 @@
[English](2026-10-15-managed-agent-server-layout.md) | [简体中文](2026-10-15-managed-agent-server-layout.zh-CN.md) [English](2026-10-15-managed-agent-server-layout.md) | [简体中文](2026-10-15-managed-agent-server-layout.zh-CN.md)
后继设计:[Maven 多模块边界](2026-10-29-managed-agent-maven-modules.zh-CN.md)。本文保留当时的决策与验证记录;当前构建结构以多模块设计为准。
状态:已实现并完成本地验证。本次尚未运行远端 CI 或真实 MySQL/Linux 部署验收。 状态:已实现并完成本地验证。本次尚未运行远端 CI 或真实 MySQL/Linux 部署验收。
## 问题与现状 ## 问题与现状
@@ -2,6 +2,8 @@
[English](2026-10-19-managed-agent-package-boundaries.md) | [简体中文](2026-10-19-managed-agent-package-boundaries.zh-CN.md) [English](2026-10-19-managed-agent-package-boundaries.md) | [简体中文](2026-10-19-managed-agent-package-boundaries.zh-CN.md)
Successor: [Maven module boundaries](2026-10-29-managed-agent-maven-modules.md). This document retains its historical decisions and validation record; the successor defines the current build layout.
Status: implemented and verified locally on main base `f37e003d9450`. Remote CI and formal review are tracked on [PR #14710](https://github.com/QwenLM/qwen-code/pull/14710). The optional MySQL, Hosted and Linux fault profiles were not run locally for this package reorganization. Status: implemented and verified locally on main base `f37e003d9450`. Remote CI and formal review are tracked on [PR #14710](https://github.com/QwenLM/qwen-code/pull/14710). The optional MySQL, Hosted and Linux fault profiles were not run locally for this package reorganization.
## Problem and current state ## Problem and current state
@@ -2,6 +2,8 @@
[English](2026-10-19-managed-agent-package-boundaries.md) | [简体中文](2026-10-19-managed-agent-package-boundaries.zh-CN.md) [English](2026-10-19-managed-agent-package-boundaries.md) | [简体中文](2026-10-19-managed-agent-package-boundaries.zh-CN.md)
后继设计:[Maven 多模块边界](2026-10-29-managed-agent-maven-modules.zh-CN.md)。本文保留当时的决策与验证记录;当前构建结构以多模块设计为准。
状态:已在主干基点 `f37e003d9450` 上实现并完成本地验证。远端 CI 与正式评审状态见 [PR #14710](https://github.com/QwenLM/qwen-code/pull/14710)。本次包调整未在本地运行可选的 MySQL、Hosted 和 Linux 故障专项 profile。 状态:已在主干基点 `f37e003d9450` 上实现并完成本地验证。远端 CI 与正式评审状态见 [PR #14710](https://github.com/QwenLM/qwen-code/pull/14710)。本次包调整未在本地运行可选的 MySQL、Hosted 和 Linux 故障专项 profile。
## 问题与现状 ## 问题与现状
@@ -36,18 +36,18 @@ That form does not work for an operator who installed the reference distribution
| Command | Entry point | | Command | Entry point |
| ------------------------ | ---------------------------------------------- | | ------------------------ | ---------------------------------------------- |
| `storage-registration` | `persistence.WorkspaceStorageRegistrationMain` | | `storage-registration` | `maintenance.WorkspaceStorageRegistrationMain` |
| `workspace-recovery` | `persistence.WorkspaceRecoveryMain` | | `workspace-recovery` | `maintenance.WorkspaceRecoveryMain` |
| `workspace-migration` | `workspace.runtime.WorkspaceMigrationMain` | | `workspace-migration` | `maintenance.WorkspaceMigrationMain` |
| `maintenance-recovery` | `persistence.WorkspaceMaintenanceRecoveryMain` | | `maintenance-recovery` | `maintenance.WorkspaceMaintenanceRecoveryMain` |
| `operator-recovery` | `workspace.runtime.WorkspaceRecoveryCommand` | | `operator-recovery` | `maintenance.WorkspaceRecoveryCommand` |
| `csi-registration` | `persistence.WorkspaceCsiRegistrationMain` | | `csi-registration` | `maintenance.WorkspaceCsiRegistrationMain` |
| `csi-protection` | `workspace.runtime.WorkspaceCsiProtectionMain` | | `csi-protection` | `maintenance.WorkspaceCsiProtectionMain` |
| `csi-session` | `persistence.WorkspaceCsiSessionMain` | | `csi-session` | `maintenance.WorkspaceCsiSessionMain` |
| `csi-runtime` | `persistence.WorkspaceCsiRuntimeMain` | | `csi-runtime` | `maintenance.WorkspaceCsiRuntimeMain` |
| `csi-retirement` | `persistence.WorkspaceCsiRetirementMain` | | `csi-retirement` | `maintenance.WorkspaceCsiRetirementMain` |
| `csi-worker-ack` | `persistence.WorkspaceCsiWorkerAckMain` | | `csi-worker-ack` | `maintenance.WorkspaceCsiWorkerAckMain` |
| `resident-retirement` | `persistence.ResidentWorkspaceRetirementMain` | | `resident-retirement` | `maintenance.ResidentWorkspaceRetirementMain` |
| `resolved-configuration` | `preflight.ResolvedConfiguration` | | `resolved-configuration` | `preflight.ResolvedConfiguration` |
All classes are under `com.alibaba.qwen.code.managedagent`. All classes are under `com.alibaba.qwen.code.managedagent`.
@@ -36,18 +36,18 @@ server 有十二个离线维护入口,包括 Workspace storage 注册、Worksp
| 命令 | 入口 | | 命令 | 入口 |
| ------------------------ | ---------------------------------------------- | | ------------------------ | ---------------------------------------------- |
| `storage-registration` | `persistence.WorkspaceStorageRegistrationMain` | | `storage-registration` | `maintenance.WorkspaceStorageRegistrationMain` |
| `workspace-recovery` | `persistence.WorkspaceRecoveryMain` | | `workspace-recovery` | `maintenance.WorkspaceRecoveryMain` |
| `workspace-migration` | `workspace.runtime.WorkspaceMigrationMain` | | `workspace-migration` | `maintenance.WorkspaceMigrationMain` |
| `maintenance-recovery` | `persistence.WorkspaceMaintenanceRecoveryMain` | | `maintenance-recovery` | `maintenance.WorkspaceMaintenanceRecoveryMain` |
| `operator-recovery` | `workspace.runtime.WorkspaceRecoveryCommand` | | `operator-recovery` | `maintenance.WorkspaceRecoveryCommand` |
| `csi-registration` | `persistence.WorkspaceCsiRegistrationMain` | | `csi-registration` | `maintenance.WorkspaceCsiRegistrationMain` |
| `csi-protection` | `workspace.runtime.WorkspaceCsiProtectionMain` | | `csi-protection` | `maintenance.WorkspaceCsiProtectionMain` |
| `csi-session` | `persistence.WorkspaceCsiSessionMain` | | `csi-session` | `maintenance.WorkspaceCsiSessionMain` |
| `csi-runtime` | `persistence.WorkspaceCsiRuntimeMain` | | `csi-runtime` | `maintenance.WorkspaceCsiRuntimeMain` |
| `csi-retirement` | `persistence.WorkspaceCsiRetirementMain` | | `csi-retirement` | `maintenance.WorkspaceCsiRetirementMain` |
| `csi-worker-ack` | `persistence.WorkspaceCsiWorkerAckMain` | | `csi-worker-ack` | `maintenance.WorkspaceCsiWorkerAckMain` |
| `resident-retirement` | `persistence.ResidentWorkspaceRetirementMain` | | `resident-retirement` | `maintenance.ResidentWorkspaceRetirementMain` |
| `resolved-configuration` | `preflight.ResolvedConfiguration` | | `resolved-configuration` | `preflight.ResolvedConfiguration` |
所有类都位于 `com.alibaba.qwen.code.managedagent` 包下。 所有类都位于 `com.alibaba.qwen.code.managedagent` 包下。
@@ -0,0 +1,281 @@
# Managed Agent Server Maven modules
[English](2026-10-29-managed-agent-maven-modules.md) | [简体中文](2026-10-29-managed-agent-maven-modules.zh-CN.md)
Status: implemented in the working tree; final acceptance is pending CI. The
investigation used `523be857db`. This design supersedes the single-module
boundary in the [source layout](2026-10-15-managed-agent-server-layout.md) and
[Java package design](2026-10-19-managed-agent-package-boundaries.md).
## Problem and constraints
The baseline has 391 production Java sources, 614 test Java sources and 18 Maven
profiles. Lexical dependency analysis found cycles spanning 83 and 35 production
sources. Models import stores, orchestration recognizes concrete JDBC classes,
services contain SQL, and repositories call business services and external tools.
Configuration, test fixtures, launchers and CI also assume one source tree.
The change establishes internal compile-time boundaries in one coordinated
migration. Modules share one version and release. Java source compatibility may
change; repository consumers migrate together. HTTP routes, JSON, configuration
keys and defaults, database bytes, transaction boundaries, identity, fencing and
recovery behavior remain unchanged. There is no database migration, service split,
new product feature, independent module versioning or unrelated large-class
decomposition. Product-specific integrations remain outside this application.
## Modules and dependencies
The parent is `com.alibaba:qwen-managed-agent-parent`, with `pom` packaging.
Production children use `qwen-managed-agent-<directory>`; `server` preserves
`com.alibaba:qwen-managed-agent-server`. All children inherit the parent version.
| Directory | Responsibility | Internal production dependencies |
| ---------------- | ------------------------------------------------------------------------------------------ | -------------------------------- |
| `contracts` | Interfaces, values, errors, read-only settings and pure protocol rules | None |
| `control-plane` | Session, Workspace, task, lifecycle, automation, Channel and extension orchestration | `contracts` |
| `runtime-broker` | Runtime bindings, execution, leases, cancellation, draining and recovery | `contracts` |
| `persistence` | Control-plane and Broker JDBC/memory stores, schema and transactions | `contracts` |
| `adapters` | Harness SDK, physical providers, HTTP clients, OSS, MQ, Git and filesystem implementations | `contracts` |
| `server` | HTTP servers, Spring composition, startup, preflight and operator entry points | All other production modules |
| `verification` | Existing tests, fixtures, architectural checks and artifact acceptance; never published | All production modules |
The four implementation modules do not depend on one another. The server injects
their implementations through contracts. A production Java package belongs to one
artifact. No sibling source directories, aggregate compilation classpath, shading,
generic service registry or catch-all common module bypasses these boundaries.
Contracts may use the existing JSON, validation annotation and Commons libraries.
They must not reference Spring, JDBC, the Java SDK or cloud clients, or perform
database, network or process operations. Control-plane code may retain Spring
components, scheduling and transaction support, but not JDBC or concrete external
clients. Broker orchestration remains independent of Spring.
Separate Maven artifacts make forbidden implementation imports fail during compilation: sibling implementations are absent from each module's compile classpath. Dependency rules and ArchUnit checks supplement that boundary; a single artifact would leave all implementations on the same classpath. The coordinated package migration gives each package one artifact owner without preserving cross-JAR package-private access. This is the agreed internal-isolation scope, with shared versioning and one service delivery.
Source counts do not determine module ownership. Broker requests, identities, records and deterministic wire validators belong to contracts when multiple modules consume them without I/O. Runtime scheduling and recovery decisions belong to the Broker implementation. A small Broker implementation module is therefore expected; moving its shared protocol rules back would introduce implementation dependencies in consumers.
## Contract and implementation changes
Move existing storage, Harness, Runtime, event and object-store interfaces and
their shared values into contracts. Extract only interfaces needed at actual
module boundaries. Move externally used nested store records with their value
semantics; shared models no longer depend on store implementations. Represent
application HTTP status as an integer and translate it in the HTTP layer without
changing codes, details or causes. Preserve protocol strings, JSON names, nulls,
ordering, encodings and digest inputs. Move pure request-identity calculations and
provider-kind constants out of JDBC/provider classes.
`ManagedAgentSettings` supplies read-only views needed outside server. Authentication,
internal listeners, the service account, trusted headers and credential-loading
state remain server settings without shared contract declarations.
`ManagedAgentProperties` remains server-owned and implements those views, retaining
binding annotations, duration units, environment access, credential-file reads and
startup checks. Consumers receive views or existing scalar values. This introduces
neither configuration snapshots nor dynamic refresh.
The seven transactional default entry points on `AgentStateStore` receive explicit
overrides on the JDBC implementation with their existing transaction semantics.
Removing the interface annotations must not lose proxy interception on delegating
overloads. Existing Spring and native JDBC transaction mechanisms stay distinct.
Same-connection operations and package-private locking helpers remain together in
persistence; `Connection` does not escape into ordinary repository contracts.
Move each existing transaction body as a whole, never as independently committed
CRUD operations.
`NativeCsiExecutionRepository` carries the existing private CSI preparation and
batch-read capabilities. The original JDBC binding store implements it; memory
stores do not. The Broker consumes the capability instead of concrete JDBC types.
Capability discovery never substitutes for transactional admission, original
DataSource identity, writer/generation checks or locking. Missing capabilities keep
the existing refusal behavior. A neutral store failure preserves genuine-outage
classification without exposing JDBC exception types. A Broker operations
interface includes the operations consumed outside its implementation module.
Convenience overloads without those consumers remain on the implementation only.
Move recovery queries and SQL-backed evidence into persistence. Inject the
existing Channel repository interfaces rather than constructing JDBC repositories
inside the service. Separate extension transactions, archive access and business
coordination without changing their order. Extract user-message authorization and
input policy without moving authorization outside its original transaction or
introducing a policy-to-message-store dependency. Git and SSH work cross explicit
ports at the same call sites as before.
Only adapters depend directly on the Java SDK and cloud clients. Map Harness
results and synchronous, asynchronous and callback errors into contract values.
Keep HTTP, transport, protocol, generation, capability and refusal distinctions,
including retry inputs, status, reason and cause. Delegate SDK payload-digest and
permission-rule algorithms through narrow ports instead of duplicating them.
Workspace scope and authorization wrappers stay in the control plane. Physical
provisioning and communication belong to adapters; construction and listeners
belong to server composition.
CSI maintenance retains its durable sequence: lock and save the original attempt,
perform the bounded Kubernetes operation, then fence and save the receipt or
unknown outcome. Preserve UID/resourceVersion, request bytes and receipt digests.
A lost response remains UNKNOWN and does not authorize blind replay. Operator
entry points retain their isolated application context and do not start the public
server or business schedulers.
## Build, resources and verification
Keep JDK 21, Spring Boot 3.5.16 and existing build-plugin versions. The parent
manages versions; children declare their own libraries. The reactor builds all
modules, with Boot repackage only in `server`. Preserve the executable and operator
classifiers and published server coordinate; outputs move to `server/target`.
The SDK retains its existing separately installed build prerequisite. Publish the
parent and every production dependency at the same version; never publish the
verification module.
Server composition preserves non-servlet embedding, explicitly enabled internal
callbacks and standalone serving. Keep named managed data sources, transaction
managers, mappers, executors and schedulers separate from host defaults. Restrict
component scans and maintain a single owner for closing each resource.
| Resource | Owner |
| --------------------------------------------- | -------------- |
| OpenAPI and shared protocol schemas | `contracts` |
| CSI native-tool protocol declarations | `contracts` |
| V1 database schema | `persistence` |
| Application YAML and Spring registration | `server` |
| Test fixtures and JUnit service registrations | `verification` |
Classpath resource names stay unchanged and each resource has one artifact owner.
The TypeScript tool-result schema remains authoritative. Update launchers, Docker,
distribution/version checks, WebShell contract reads, historical-path fallbacks
and CI consumers together. Resource moves do not increment protocol/schema versions.
Move all existing tests and fixtures to verification, preserving their execution
families and all 18 profiles. Artifact tests run after server packaging and read
the server output directory. Keep MySQL fork routing, JUnit extensions, fault-test
environment and real-process parameters. Report completeness checks examine the
complete relocated test tree. Checkstyle and SpotBugs still cover every production
module. Aggregate coverage includes production classes and source, not only test
execution data. Verification directly depends on production modules at compile
scope for that aggregation, without becoming a published artifact.
Tests keep `verification` as their working directory so local `src/test/resources` paths remain valid. Surefire and Failsafe pass `managed.module.root` for shared repository fixtures and `qwen.cli.entry` for the built CLI, including profile overrides. Repository paths are resolved from the module root instead of assuming the old process working directory.
Use test-scoped `com.tngtech.archunit:archunit:1.5.1` under existing JUnit to enforce
the module matrix, forbidden dependencies, package ownership and nonempty scans.
No additional test engine is introduced. Framework/client types must not leak
through signatures, inheritance, annotations or exception references.
## Implementation and acceptance
Implement as one change: record source/test/resource/entry-point inventories;
extract contracts and remove cycles; move sources and define POMs; migrate
composition, tests and delivery tooling; verify and audit the complete result.
Maintain this design in both languages and update the development instructions.
Acceptance covers independent module compilation; contract purity; unchanged
configuration and host isolation; rollback through all seven transaction entry
points; native CSI refusal and admission; restart, takeover and lost-response
recovery; SDK conversion and digest parity; HTTP/tenant/actor/Workspace security;
all ordinary/executable/operator artifacts and unique nested-JAR resources; and
unchanged CI test-family completeness.
Follow the module AGENTS.md shared ten-minute local Java verification budget.
Prioritize architectural, transactional and composition regressions and affected
real integration paths. CI retains complete Java, MySQL, Hosted, fault and MQ/OSS
gates. Cross-language edits also require repository build, typecheck and affected
script/meta-tests. Do not convert skipped or unavailable integrations into passes.
Audit the full diff and evidence until two consecutive clean passes. Report local,
CI, skipped and pending results separately. Rollback replaces application and
distribution artifacts and does not rewrite persistent data.
## Implementation notes and local evidence
The seven-module reactor, contract extraction, composition changes, relocated
tests/resources and delivery-script updates are implemented together. All 614
original test source files and all 18 profile names are retained. The six original
production resources retain their bytes and each has one production owner. New
regressions cover compiled module boundaries, transactional default-method
overrides, SDK failure conversion and single-instance Harness composition.
The Harness SDK boundary wraps both single-instance and pooled connectors. The
single-instance factory explicitly supplies skills, extension records and the
managed mapper to the wrapped connector; Spring cannot inject fields into a
delegate hidden behind a returned wrapper. The settings views preserve existing
collection references and read timing. Existing filesystem identity/admission
checks remain at their original business/transaction positions; physical Git,
SSH, provider and storage-identity operations cross the extracted ports.
Local evidence was collected on macOS with JDK 21, Maven 3.9.14, an isolated Maven
repository, Node 26.12.1 and Bash 5 for workflow shell fixtures. It does not establish
Linux, remote-cloud or production behavior.
| Area | Observed local result | Limit |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Production reactor | All six production modules compiled with their declared dependencies; production Checkstyle and SpotBugs passed; ordinary, executable and six operator JARs were built | These complete quality/package results precede the last composition and collection-reference fixes |
| Architecture and transactions | Bytecode architecture check passed; all seven former default transaction entries committed and rolled back through a real Spring proxy and dedicated H2 transaction manager; 44 lock-order cases passed | H2 and proxy evidence does not establish MySQL concurrency behavior |
| Configuration and embedding | 31 property-binding cases, non-servlet embedding and explicit internal-callback tests passed | The independent Maven embedding-host fixture remains unrun |
| Broker and lifecycle | 199 Broker cases, 33 resident-retirement cases and 47 cancellation/command/automation cases passed | Full restart, Hosted, failure and MySQL profiles remain pending |
| SDK, HTTP and packaging | SDK conversion, 23 API-contract cases and three packaging cases passed; inspected executable archives contain the five internal dependency JARs with no duplicate production classes/resources | The packaged artifacts predate the final audit fixes |
| Final composition fix | The two changed production classes and the new Harness wiring regression compiled; the regression method ran successfully | This focused check was not a full Maven/Surefire run; the subsequent collection-reference-only fix still needs compilation on CI |
| TypeScript and scripts | Repository build and typecheck passed; affected CLI tests passed 288 cases and integration-helper tests passed 22; focused ESLint and affected script/workflow rechecks passed | The broad script run initially failed under macOS Bash 3.2; Bash 5 rechecks resolved those failures, while one unchanged 600 ms timing case passed only in isolation |
The shared local Java ledger totals about 598 seconds. An earlier quality run
stopped on verification-module formatting, which was corrected and rechecked.
The configuration failures in a later selected run were corrected and all 31
cases reran successfully. The final `mvn -DskipTests verify` attempt reached server
SpotBugs but hit its bounded timeout; it is **not** a passing full-reactor result.
No further Java runs were started after the final focused composition check.
Acceptance still requires the final source revision to pass the unfiltered Java
reactor, aggregate coverage generation, independent embedding-host fixture and
the existing MySQL, Hosted, fault, MQ and OSS CI gates. New Java test formatting
and the final collection-reference fix also await that run. CI was not dispatched,
Central publication was not attempted, and no deployment or database migration
was performed. Rebuild the deliverables from the final revision before using or
publishing them; local intermediate JARs are verification artifacts only.
## PR integration baseline
PR preparation incorporates `main@68f181b885` on top of the original investigation.
The newer bounded-history queries and models remain in the repository contracts
and persistence implementation. The upstream cleanup retry coordinator depends on
a cleanup-debt repository contract and neutral Harness failures; its SQL scan and
unchanged keyset/backoff behavior remain intact. The resolved-configuration entry
point and upgrade packaging stay in server composition. All 619 current-main test
sources plus the two embedding-fixture sources are retained, including the five
tests added since the investigation. All six production resources are byte-identical
to this newer main revision; the refactor adds no further protocol/schema change.
After integration, repository build and typecheck passed again. Reference packaging
tests passed 25 cases with one existing skip, the five affected script suites
passed 196 cases, CLI consumers passed 288 cases, and selected Maven/workflow checks
passed 22 cases. The earlier Java results above predate this integration. The shared
598-second budget was not reset: compilation, style checks and Java regressions for
the integrated revision remain pending CI, including the new repository boundary
and migrated upstream tests. Final acceptance requires those checks to pass.
## Boilerplate reduction
The server configuration classes use Lombok `@Getter` and `@Setter` for 349 simple
accessors. Keep normalization, defensive copies, fallback values and the
`EnvironmentAware` callback explicit. Internal credential-loading state remains
excluded from generated accessors. Existing records, field defaults and
`@DurationUnit` annotations remain unchanged; `@Data` is not used. Lombok is an
optional provided dependency of `server`, with its version inherited from Spring
Boot and an explicit compiler annotation processor. Exclude it from every Boot
executable and operator archive; other production modules do not require it.
The SDK wrapper retains explicit interface implementations and delegates repeated
runtime-exception conversion to two private helpers for value and void calls.
Its size falls from 693 to 422 lines without reflection or a proxy framework.
Context-compression submission stays deferred, and stream epoch reads, event
reads, recovery checks and close operations still convert SDK failures after
stream creation. Preserve result identity, arguments, call order, unrelated
exceptions and `Error` propagation.
Regression coverage adds nested configuration binding, hidden internal state,
normalization and collection semantics, immediate and deferred SDK failures, both
stream entry points and archive exclusion. Standalone Harness composition is checked
through session creation, resource publication and the resulting SDK request, rather
than connector private fields. Unused test-factory overloads and private service
forwarders are removed; existing behavioral test cases are retained.
Source comparison confirms unchanged JavaBean signatures and SDK delegate call
sites. These new Java regressions have not
run locally: the shared budget remains exhausted, and earlier Java passes do not
validate this increment. Final compilation, quality checks and regressions remain
pending CI.
@@ -0,0 +1,207 @@
# Managed Agent Server Maven 多模块
[English](2026-10-29-managed-agent-maven-modules.md) | [简体中文](2026-10-29-managed-agent-maven-modules.zh-CN.md)
状态:已在工作树实现,最终验收待 CI。调研基线为 `523be857db`。本设计取代
[源码布局](2026-10-15-managed-agent-server-layout.zh-CN.md)及
[Java 包设计](2026-10-19-managed-agent-package-boundaries.zh-CN.md)中的单模块边界。
## 问题与约束
基线包含 391 个生产 Java 源文件、614 个测试 Java 源文件和 18 个 Maven profile。
词法依赖分析发现分别涉及 83 和 35 个生产文件的循环。模型引用仓储,编排识别具体 JDBC
类,服务包含 SQL,仓储调用业务服务和外部工具。配置、测试夹具、启动器和 CI 也假定
只有一个源码目录。
本次通过一次完整迁移建立工程内部的编译边界。模块统一版本和发布。允许调整 Java
源码接口,仓库内使用方同步迁移。HTTP 路由、JSON、配置键及默认值、数据库字节、事务
边界、身份、隔离栅栏和恢复行为保持不变。不迁移数据库、不拆服务、不增加产品功能、
不独立维护模块版本,也不附带拆解大类。产品专用集成继续放在本应用之外。
## 模块与依赖
父工程为 `com.alibaba:qwen-managed-agent-parent`,使用 `pom` packaging。生产子模块
使用 `qwen-managed-agent-<目录名>`;`server` 保留
`com.alibaba:qwen-managed-agent-server`。所有子模块继承父工程版本。
| 目录 | 职责 | 内部生产依赖 |
| ---------------- | ----------------------------------------------------------------- | ---------------- |
| `contracts` | 接口、值类型、异常、只读配置与纯协议规则 | 无 |
| `control-plane` | Session、Workspace、任务、生命周期、自动化、Channel 和扩展编排 | `contracts` |
| `runtime-broker` | Runtime 绑定、执行、租约、取消、排空及恢复 | `contracts` |
| `persistence` | 控制面和 Broker 的 JDBC/内存仓储、schema 与事务 | `contracts` |
| `adapters` | Harness SDK、物理供应器、HTTP 客户端、OSS、MQ、Git 和文件系统实现 | `contracts` |
| `server` | HTTP 服务端、Spring 装配、启动、预检与运维入口 | 其他所有生产模块 |
| `verification` | 现有测试、夹具、架构检查和产物验收;不发布 | 所有生产模块 |
四个实现模块不相互依赖,由 server 通过契约注入实现。一个生产 Java package 只属于
一个产物。不通过兄弟源码目录、聚合编译 classpath、shading、通用服务注册框架或
大而全的 common 模块绕过边界。
契约可以使用现有 JSON、校验注解和 Commons 库,不能引用 Spring、JDBC、Java SDK 或
云产品客户端,也不能执行数据库、网络或进程操作。控制面可以保留 Spring 组件、调度
和事务支持,但不能引用 JDBC 或具体外部客户端。Broker 编排继续独立于 Spring。
独立 Maven 产物让违规实现引用在编译阶段失败:各模块的编译 classpath 中不存在兄弟实现模块。依赖规则和 ArchUnit 检查补充这一边界;单一产物仍会把所有实现放在同一个 classpath 上。同步迁移包名让每个包只有一个产物所有者,不保留跨 JAR 的包内访问。这是已确认的内部隔离范围,继续统一版本并作为一个服务交付。
模块归属不按源码数量决定。多个模块共同使用、且不执行 I/O 的 Broker 请求、身份、记录和确定性协议校验归契约;运行时调度与恢复决策归 Broker 实现。因此 Broker 实现模块较小是预期结果,把共享协议规则移回实现模块反而会让使用方依赖实现。
## 契约与实现调整
将现有存储、Harness、Runtime、事件和对象存储接口及共享值类型迁入契约。只在真实
模块边界提取接口。跨模块使用的仓储嵌套记录类型连同值语义迁移,共享模型不再依赖
仓储实现。应用 HTTP 状态改用整数,在 HTTP 层转换,保持错误码、详情和 cause。
保留协议字符串、JSON 名称、空值、顺序、编码和摘要输入。将纯请求身份计算和 provider
kind 常量移出 JDBC/provider 类。
`ManagedAgentSettings` 只为 server 外的使用方提供只读视图。认证、内部监听、服务账号、
可信请求头和凭据加载状态保留为 server 配置,不声明在共享契约中。`ManagedAgentProperties` 仍归 server
并实现这些视图,保留绑定注解、时间单位、环境访问、凭据文件读取和启动检查。使用方
接收视图或原有标量,不引入配置快照或动态刷新。
在 JDBC 实现中显式覆盖 `AgentStateStore` 的七个默认事务入口,保持原事务语义。
移除接口注解不能丢失委托重载入口的代理拦截。现有 Spring 与原生 JDBC 事务机制分别
保留。同连接操作和包内锁辅助方法留在 persistence,普通仓储契约不暴露 `Connection`。
每个原事务体整体迁移,不拆成分别提交的 CRUD 操作。
`NativeCsiExecutionRepository` 承接现有私有 CSI prepare 和 batch read 能力。原 JDBC
binding 仓储实现它,内存仓储不实现。Broker 使用能力接口而非具体 JDBC 类型。能力
发现不替代事务 admission、原 DataSource 身份、writer/generation 校验及锁。缺失能力
保持原拒绝行为。中立存储异常保留真实存储故障分类,不暴露 JDBC 异常类型。Broker
操作接口只包含实现模块外的实际消费操作;没有此类使用方的便利重载仅保留在实现类中。
恢复查询和 SQL 证据读取进入 persistence。Channel 服务注入现有仓储接口,不自行
创建 JDBC 仓储。分离扩展事务、归档访问和业务协调,保持原顺序。提取用户消息授权及
输入策略,不将授权移出原事务,也不引入策略对消息仓储的依赖。Git 和 SSH 在原调用
位置通过明确的接口访问。
仅 adapters 直接依赖 Java SDK 和云产品客户端。将 Harness 结果以及同步、异步和回调
异常映射为契约值。保留 HTTP、传输、协议、generation、能力不匹配和拒绝的区别,包含
重试所需输入、状态、原因和 cause。SDK payload digest 与 permission rules 算法通过
小接口委托复用,不复制实现。Workspace 范围和授权 wrapper 留在控制面,物理供应及
通信进入适配器,实例创建和监听器归 server 装配。
CSI 维护保留持久化顺序:锁定并保存原操作尝试,执行限定的 Kubernetes 操作,然后
通过栅栏校验保存回执或未知结果。保留 UID/resourceVersion、请求字节及回执摘要。
响应丢失保持 UNKNOWN,不允许盲目重放。运维入口保留独立应用上下文,不启动公共服务
或业务调度器。
## 构建、资源与验证
保留 JDK 21、Spring Boot 3.5.16 和既有构建插件版本。父工程管理版本,各子模块声明
自身依赖。reactor 构建全部模块,仅 `server` 执行 Boot repackage。保留可执行及运维
classifier 和服务发布坐标,产物移至 `server/target`。SDK 保留现有单独安装的构建前提。
父 POM 与全部生产依赖同版本发布,verification 不发布。
server 装配保留无 Servlet 嵌入、显式内部回调和独立服务三种方式。专用数据源、事务
管理器、mapper、执行器和调度器保留名称,与宿主默认 Bean 隔离。限定组件扫描,并让
每项资源只有一个关闭责任方。
| 资源 | 归属 |
| ------------------------- | -------------- |
| OpenAPI 和共享协议 schema | `contracts` |
| CSI 原生工具协议声明 | `contracts` |
| V1 数据库 schema | `persistence` |
| 应用 YAML 与 Spring 注册 | `server` |
| 测试夹具与 JUnit 服务注册 | `verification` |
classpath 资源名保持不变,每份资源只有一个产物所有者。TypeScript 工具结果 schema
仍为权威来源。同步更新启动器、Docker、分发和版本检查、WebShell 契约读取、历史路径
回退及 CI 使用方。资源移动不增加协议/schema 版本。
将全部现有测试和夹具迁至 verification,保留执行族和全部 18 个 profile。产物测试
在 server 打包之后读取其输出目录。保留 MySQL fork 路由、JUnit 扩展、故障测试环境及
真实进程参数。报告完整性检查核对完整的新测试树。Checkstyle 与 SpotBugs 继续覆盖
每个生产模块。聚合覆盖率包含生产类和源码,而不只包含测试执行数据。verification
为此以 compile scope 直接依赖生产模块,自身仍不发布。
测试继续以 `verification` 为工作目录,使本地 `src/test/resources` 路径保持有效。Surefire 和 Failsafe 传入用于共享仓库夹具的 `managed.module.root` 以及指向已构建 CLI 的 `qwen.cli.entry`,包括覆盖插件配置的 profile。仓库路径从模块根目录解析,不再假定旧进程工作目录。
使用测试范围的 `com.tngtech.archunit:archunit:1.5.1`,在现有 JUnit 下验证模块矩阵、
禁止依赖、包归属及非空扫描,不增加测试引擎。框架/客户端类型不得从签名、继承、注解
或异常引用中泄漏。
## 实施与验收
一次整体交付:记录源码、测试、资源和入口清单;提取契约、解除循环;移动源码并建立
POM;迁移装配、测试和交付工具;验证并自审完整结果。同步维护双语设计与开发说明。
验收覆盖独立模块编译、契约纯度、配置及宿主隔离、七个事务入口回滚、原生 CSI 拒绝和
admission、重启/接管/响应丢失恢复、SDK 转换和摘要一致性、HTTP/tenant/actor/Workspace
安全、全部普通/可执行/运维产物与唯一嵌套 JAR 资源,以及原 CI 测试族完整性。
本地 Java 验证遵守模块 AGENTS.md 共享十分钟预算,优先架构、事务和装配回归及受影响
的真实集成路径。完整 Java、MySQL、Hosted、故障和 MQ/OSS 门禁由 CI 承担。跨语言
修改还需仓库 build、typecheck 及相关脚本/meta-test。跳过或不可用的集成不能记为通过。
完整 diff 和证据需连续两遍干净自审。分别报告本地、CI、跳过和待验证结果。回退替换
应用及分发产物,不重写持久化数据。
## 实现说明与本地证据
七模块 reactor、契约提取、装配调整、测试/资源迁移及交付脚本更新作为同一批次实现。
原有 614 个测试源文件和 18 个 profile 名称全部保留。六份原有生产资源保持字节不变,
各有唯一生产模块归属。新增回归覆盖编译后模块边界、默认事务方法的实现覆盖、SDK
异常转换及单实例 Harness 装配。
Harness SDK 边界同时包装单实例和池化 connector。单实例工厂显式向被包装的 connector
提供 skills、扩展记录和专用 mapper;Spring 无法向返回 wrapper 后隐藏的 delegate
注入字段。配置视图保留原有集合引用和读取时机。现有文件系统身份/admission 检查留在
原业务/事务位置;物理 Git、SSH、provider 和存储身份操作通过提取的接口调用。
本地证据来自 macOS、JDK 21、Maven 3.9.14、隔离的 Maven 仓库、Node 26.12.1,以及
用于工作流 Shell 夹具的 Bash 5,不能证明 Linux、远端云环境或生产行为。
| 范围 | 本地观测结果 | 限制 |
| ----------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| 生产 reactor | 六个生产模块均按声明依赖编译;生产 Checkstyle 与 SpotBugs 通过;普通、可执行及六个运维 JAR 已构建 | 这组完整质量/打包结果早于最后的装配与集合引用修正 |
| 架构与事务 | 字节码架构检查通过;七个原默认事务入口经真实 Spring 代理和专用 H2 事务管理器完成提交及回滚;44 个锁顺序用例通过 | H2 与代理证据不能证明 MySQL 并发行为 |
| 配置与嵌入 | 31 个属性绑定用例、无 Servlet 嵌入及显式内部回调测试通过 | 独立 Maven embedding-host 夹具尚未运行 |
| Broker 与生命周期 | 199 个 Broker、33 个驻留 Workspace 退休,以及 47 个取消/命令/自动化用例通过 | 完整重启、Hosted、故障及 MySQL profile 待验收 |
| SDK、HTTP 与打包 | SDK 转换、23 个 API 契约及三个打包用例通过;检查过的可执行包包含五个内部依赖 JAR,无重复生产类/资源 | 这些产物早于最终自审修正 |
| 最后装配修正 | 两个改动的生产类和新增 Harness 装配回归完成编译,回归方法执行成功 | 该定向检查不是完整 Maven/Surefire 执行;随后仅涉及集合引用的修正仍需 CI 编译 |
| TypeScript 与脚本 | 仓库 build 和 typecheck 通过;受影响 CLI 测试 288 项、集成辅助测试 22 项通过;定向 ESLint 及相关脚本/工作流复查通过 | 首次较广脚本执行因 macOS Bash 3.2 失败,Bash 5 复查解决了这些失败;其中一个未修改的 600 ms 时序用例仅在隔离执行时通过 |
共享本地 Java 账本累计约 598 秒。较早的质量检查在验证模块格式处停止,修正后已复查。
后续定向执行中的配置失败已修正,31 个用例全部重跑通过。最后一次
`mvn -DskipTests verify` 到达 server SpotBugs 后触发限定超时,**不能记为完整 reactor
通过**。最后的定向装配检查后未再启动 Java 验证。
验收仍要求最终源码版本通过未过滤的 Java reactor、聚合覆盖率生成、独立
embedding-host 夹具及现有 MySQL、Hosted、故障、MQ 和 OSS CI 门禁。新增 Java 测试
格式和最后的集合引用修正也等待该次执行。尚未派发 CI、尝试 Central 发布、部署服务或
迁移数据库。使用或发布前须从最终版本重新构建交付物;本地中间 JAR 仅是验证产物。
## PR 整合基线
准备 PR 时,在原调研基础上接入了 `main@68f181b885`。较新的有界历史查询及模型保留在
仓储契约和持久化实现中。主干新增的清理重试协调器依赖清理债务仓储契约及中立 Harness
异常;SQL 扫描和原有 keyset/退避行为保持不变。配置解析入口和升级打包留在 server
装配层。当前主干的 619 个测试源文件及两个嵌入夹具源文件全部保留,包括调研后新增的
五个测试。六份生产资源与这一较新的主干版本字节一致,重构不再附加协议/schema 变更。
整合后,仓库 build 和 typecheck 再次通过。参考包测试通过 25 项、保留一个既有跳过项,
五组相关脚本测试通过 196 项、CLI 使用方测试通过 288 项、定向 Maven/工作流检查通过
22 项。上文的 Java 证据早于本次整合。共享 598 秒预算没有重置:
整合版本的编译、格式检查及 Java 回归仍待 CI,包括新增仓储边界和迁入的主干测试。
最终验收要求这些检查通过。
## 样板代码精简
server 配置类使用 Lombok `@Getter` 和 `@Setter` 替代 349 个简单访问方法。
规范化、防御性复制、默认值回退及 `EnvironmentAware` 回调保持显式实现。凭据加载
内部状态排除在生成访问方法之外。现有 record、字段默认值和 `@DurationUnit` 注解
不变,不使用 `@Data`。Lombok 仅作为 `server` 的 optional provided 依赖,版本继承
Spring Boot,并显式配置编译器注解处理器。全部 Boot 可执行包和运维包排除 Lombok;
其他生产模块无需依赖它。
SDK 包装保留显式接口实现,将重复的运行时异常转换集中到两个私有辅助方法,分别处理
有返回值和无返回值的调用。文件从 693 行缩减至 422 行,不引入反射或代理框架。
上下文压缩提交仍延迟执行;流创建后的 epoch 读取、事件读取、恢复检查及关闭操作
继续转换 SDK 异常。保留返回对象身份、参数、调用顺序、无关异常和 `Error` 传播。
回归覆盖新增嵌套配置绑定、内部状态不可绑定、规范化与集合语义、同步和延迟 SDK
异常、两个流入口及产物排除。单实例 Harness 装配通过创建会话、资源发布和最终 SDK
请求检查,不再检查连接器私有字段。删除未使用的测试工厂重载和业务服务私有转发,
保留现有行为测试用例。源码比对确认 JavaBean 方法签名及 SDK 委托调用点保持不变。
这些新增 Java 回归尚未在本地运行:共享预算已用尽,较早的 Java 通过记录不能证明
本次增量通过。最终编译、质量检查及回归仍待 CI。
+4 -4
View File
@@ -174,7 +174,7 @@ mvn -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true install
mvn -f packages/managed-agent-server/pom.xml verify mvn -f packages/managed-agent-server/pom.xml verify
npm run package:managed-runtime -- \ npm run package:managed-runtime -- \
--target darwin-arm64 \ --target darwin-arm64 \
--server-jar packages/managed-agent-server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \ --server-jar packages/managed-agent-server/server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \
--jre-home /verified/java-21-home \ --jre-home /verified/java-21-home \
--out-dir /output/managed-runtime --out-dir /output/managed-runtime
``` ```
@@ -182,7 +182,7 @@ npm run package:managed-runtime -- \
The runtime builder only needs the matching server JAR, Java runtime and CLI The runtime builder only needs the matching server JAR, Java runtime and CLI
version from this checkout. It does not require built CLI assets or a Node version from this checkout. It does not require built CLI assets or a Node
archive. By default it writes to archive. By default it writes to
`packages/managed-agent-server/target/managed-runtime`, outside the CLI's `packages/managed-agent-server/server/target/managed-runtime`, outside the CLI's
`dist/` assets. It emits the runtime archive and a per-archive `.sha256` file. The `dist/` assets. It emits the runtime archive and a per-archive `.sha256` file. The
normal CLI is built/released separately. normal CLI is built/released separately.
@@ -192,7 +192,7 @@ instead of `--jre-home`:
```sh ```sh
npm run package:managed-runtime -- \ npm run package:managed-runtime -- \
--target darwin-arm64 \ --target darwin-arm64 \
--server-jar packages/managed-agent-server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \ --server-jar packages/managed-agent-server/server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \
--without-jre \ --without-jre \
--out-dir /output/managed-runtime --out-dir /output/managed-runtime
``` ```
@@ -204,7 +204,7 @@ The lightweight builder needs no Java input. For an optional complete distributi
npm run package:standalone -- \ npm run package:standalone -- \
--target darwin-arm64 \ --target darwin-arm64 \
--node-archive /verified/node-v22-darwin-arm64.tar.gz \ --node-archive /verified/node-v22-darwin-arm64.tar.gz \
--managed-server-jar packages/managed-agent-server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \ --managed-server-jar packages/managed-agent-server/server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \
--managed-jre-home /verified/java-21-home \ --managed-jre-home /verified/java-21-home \
--out-dir /output/managed-local --out-dir /output/managed-local
``` ```
+4 -4
View File
@@ -147,14 +147,14 @@ mvn -f packages/sdk-java/qwencode/pom.xml -DskipTests -Dgpg.skip=true install
mvn -f packages/managed-agent-server/pom.xml verify mvn -f packages/managed-agent-server/pom.xml verify
npm run package:managed-runtime -- \ npm run package:managed-runtime -- \
--target darwin-arm64 \ --target darwin-arm64 \
--server-jar packages/managed-agent-server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \ --server-jar packages/managed-agent-server/server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \
--jre-home /verified/java-21-home \ --jre-home /verified/java-21-home \
--out-dir /output/managed-runtime --out-dir /output/managed-runtime
``` ```
runtime 构建器只要求匹配的服务 JAR、Java runtime 和本 checkout 中的 CLI 版本, runtime 构建器只要求匹配的服务 JAR、Java runtime 和本 checkout 中的 CLI 版本,
不依赖编译好的 CLI 资源或 Node archive。默认输出到 不依赖编译好的 CLI 资源或 Node archive。默认输出到
`packages/managed-agent-server/target/managed-runtime`,位于 CLI 的 `dist/` 资源树 `packages/managed-agent-server/server/target/managed-runtime`,位于 CLI 的 `dist/` 资源树
之外。输出 runtime archive 及单独的 `.sha256` 之外。输出 runtime archive 及单独的 `.sha256`
文件。普通 CLI 独立构建和发行。 文件。普通 CLI 独立构建和发行。
@@ -163,7 +163,7 @@ runtime 构建器只要求匹配的服务 JAR、Java runtime 和本 checkout 中
```sh ```sh
npm run package:managed-runtime -- \ npm run package:managed-runtime -- \
--target darwin-arm64 \ --target darwin-arm64 \
--server-jar packages/managed-agent-server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \ --server-jar packages/managed-agent-server/server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \
--without-jre \ --without-jre \
--out-dir /output/managed-runtime --out-dir /output/managed-runtime
``` ```
@@ -175,7 +175,7 @@ npm run package:managed-runtime -- \
npm run package:standalone -- \ npm run package:standalone -- \
--target darwin-arm64 \ --target darwin-arm64 \
--node-archive /verified/node-v22-darwin-arm64.tar.gz \ --node-archive /verified/node-v22-darwin-arm64.tar.gz \
--managed-server-jar packages/managed-agent-server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \ --managed-server-jar packages/managed-agent-server/server/target/qwen-managed-agent-server-0.1.0-alpha-exec.jar \
--managed-jre-home /verified/java-21-home \ --managed-jre-home /verified/java-21-home \
--out-dir /output/managed-local --out-dir /output/managed-local
``` ```
+2 -1
View File
@@ -78,6 +78,7 @@ export default tseslint.config(
'packages/desktop/runtime/**', 'packages/desktop/runtime/**',
'packages/core/src/skills/bundled/browser-use/runtime/**', 'packages/core/src/skills/bundled/browser-use/runtime/**',
'packages/desktop/src-tauri/target/**', 'packages/desktop/src-tauri/target/**',
'packages/managed-agent-server/**/target/**',
'packages/live-host/**', // standalone Electron app with its own Node test conventions 'packages/live-host/**', // standalone Electron app with its own Node test conventions
'packages/cua-driver/**', // vendored trycua/cua driver (Rust + scripts); not qwen-code TS 'packages/cua-driver/**', // vendored trycua/cua driver (Rust + scripts); not qwen-code TS
'packages/mobile-mcp/**', // vendored mobile-next/mobile-mcp; has own eslint config 'packages/mobile-mcp/**', // vendored mobile-next/mobile-mcp; has own eslint config
@@ -506,7 +507,7 @@ export default tseslint.config(
'packages/*/scripts/**/*.js', 'packages/*/scripts/**/*.js',
'packages/*/scripts/**/*.mjs', 'packages/*/scripts/**/*.mjs',
'packages/*/build.mjs', 'packages/*/build.mjs',
'packages/managed-agent-server/src/test/resources/*.mjs', 'packages/managed-agent-server/verification/src/test/resources/**/*.mjs',
// web-templates' export-html template build scripts also run with `node`. // web-templates' export-html template build scripts also run with `node`.
'packages/*/src/export-html/*.mjs', 'packages/*/src/export-html/*.mjs',
// Verification reproducer scripts under docs/ also run with `node`. // Verification reproducer scripts under docs/ also run with `node`.
@@ -45,7 +45,7 @@ const baselinePath = path.resolve(
'integration-tests/baselines/hosted-latency.json', 'integration-tests/baselines/hosted-latency.json',
); );
const reportPath = path.resolve( const reportPath = path.resolve(
'packages/managed-agent-server/target/hosted-latency-baseline.json', 'packages/managed-agent-server/verification/target/hosted-latency-baseline.json',
); );
const samples: HostedLatencySample[] = []; const samples: HostedLatencySample[] = [];
let sample: HostedLatencySample; let sample: HostedLatencySample;
@@ -366,7 +366,7 @@ try {
'integration-tests/helpers/hosted-harness-process.ts', 'integration-tests/helpers/hosted-harness-process.ts',
'integration-tests/helpers/hosted-relay-headers.ts', 'integration-tests/helpers/hosted-relay-headers.ts',
'integration-tests/fake-openai-server.ts', 'integration-tests/fake-openai-server.ts',
'packages/managed-agent-server/src/test/java/com/alibaba/qwen/code/managedagent/HostedWorkspaceToolTurnIT.java', 'packages/managed-agent-server/verification/src/test/java/com/alibaba/qwen/code/managedagent/HostedWorkspaceToolTurnIT.java',
]; ];
const sourceHashes = Object.fromEntries( const sourceHashes = Object.fromEntries(
await Promise.all( await Promise.all(
@@ -34,7 +34,7 @@ import {
const REPO = resolve(import.meta.dirname, '../../../..'); const REPO = resolve(import.meta.dirname, '../../../..');
const MANAGED_SURFACE_FILE = resolve( const MANAGED_SURFACE_FILE = resolve(
REPO, REPO,
'packages/managed-agent-server/src/test/resources/capability-parity/managed-surface.txt', 'packages/managed-agent-server/verification/src/test/resources/capability-parity/managed-surface.txt',
); );
const ROUTE_METHODS: ReadonlySet<string> = new Set([ const ROUTE_METHODS: ReadonlySet<string> = new Set([
'GET', 'GET',
@@ -551,7 +551,7 @@ it('fixes full Parts, original IDs, exact input bytes and final definitions befo
const pinned = JSON.parse( const pinned = JSON.parse(
await readFile( await readFile(
new URL( new URL(
'../../../managed-agent-server/src/main/resources/csi-native-file-declarations.json', '../../../managed-agent-server/contracts/src/main/resources/csi-native-file-declarations.json',
import.meta.url, import.meta.url,
), ),
'utf8', 'utf8',
+1 -1
View File
@@ -30,7 +30,7 @@ describe('Hosted immutable attachment input', () => {
const spec = JSON.parse( const spec = JSON.parse(
readFileSync( readFileSync(
new URL( new URL(
'../../../managed-agent-server/src/main/resources/openapi/managed-agent-public-api.openapi.json', '../../../managed-agent-server/contracts/src/main/resources/openapi/managed-agent-public-api.openapi.json',
import.meta.url, import.meta.url,
), ),
'utf8', 'utf8',
+1 -1
View File
@@ -1 +1 @@
/target/ **/target/
+13 -8
View File
@@ -86,7 +86,14 @@ cross-language changes.
### Maven and MySQL checks ### Maven and MySQL checks
Use JDK 21 and Maven 3.8.9 or newer; run Maven from this directory. Use JDK 21 and Maven 3.8.9 or newer; run Maven from this directory. This is the
reactor parent. Production modules compile independently; all tests and fixtures
live in `verification`. Preserve the dependency matrix in the
[Maven modules design](../../docs/design/2026-10-29-managed-agent-maven-modules.md).
Checkstyle and SpotBugs apply to every production module. Test reports and
aggregate coverage live under `verification/target`; executable artifacts live
under `server/target`. The verification module must never be installed or
published.
Before testing, check the toolchain and that the installed sibling SDK matches Before testing, check the toolchain and that the installed sibling SDK matches
the source being tested. Reuse a valid build; otherwise install it once into the source being tested. Reuse a valid build; otherwise install it once into
@@ -144,11 +151,9 @@ work follows this module's budget; CI retains the full cross-language gates.
Documentation-only changes need formatting and reference checks and do not Documentation-only changes need formatting and reference checks and do not
invalidate unchanged Java test results. invalidate unchanged Java test results.
Maven report output under `target/` (javadoc `reports/apidocs`, JaCoCo Maven output under every module's `target/` is ignored by Git, ESLint and
`site/jacoco`) contains generated JavaScript that the repository-root ESLint Prettier. Keep these exclusions when adding modules so generated reports and
sweep does not ignore, so a root `npm run lint` fails while those directories packaged fixtures are not linted or committed as source.
exist. Run `mvn clean` or delete the `target/` directories before running root
checks in the same checkout.
## Code Review ## Code Review
@@ -160,7 +165,7 @@ checks in the same checkout.
idempotency and writer lease/generation checks across restarts: a stale owner idempotency and writer lease/generation checks across restarts: a stale owner
must not commit durable state or release a newer owner's resources. must not commit durable state or release a newer owner's resources.
- Server and Broker use the - Server and Broker use the
[shared schema baseline](src/main/resources/db/managed-schema/V1__managed_schema.sql). [shared schema baseline](persistence/src/main/resources/db/managed-schema/V1__managed_schema.sql).
Until the first formal Managed Agent release, edit this V1 baseline in place; Until the first formal Managed Agent release, edit this V1 baseline in place;
do not add V2+ migrations or increment the database schema version. Recreate do not add V2+ migrations or increment the database schema version. Recreate
development catalogs after baseline changes rather than maintaining upgrades. development catalogs after baseline changes rather than maintaining upgrades.
@@ -169,7 +174,7 @@ checks in the same checkout.
migrations or use Flyway repair/history edits to bypass that refusal. Follow migrations or use Flyway repair/history edits to bypass that refusal. Follow
the README's current schema policy rather than assuming an upgrade path. the README's current schema policy rather than assuming an upgrade path.
- Public and WebShell API changes must keep the - Public and WebShell API changes must keep the
[OpenAPI contract](src/main/resources/openapi/managed-agent-public-api.openapi.json), [OpenAPI contract](contracts/src/main/resources/openapi/managed-agent-public-api.openapi.json),
mapped routes, `SurfaceRegistry`, and contract tests consistent. Run mapped routes, `SurfaceRegistry`, and contract tests consistent. Run
`ManagedAgentApiContractTest` and `SurfaceRegistryGateTest` for those changes. `ManagedAgentApiContractTest` and `SurfaceRegistryGateTest` for those changes.
Regenerate WebShell types from the repository root with Regenerate WebShell types from the repository root with
+1 -1
View File
@@ -17,7 +17,7 @@ RUN mvn --batch-mode --no-transfer-progress \
# exists. sh has no nullglob, so a zero-match glob yields the pattern # exists. sh has no nullglob, so a zero-match glob yields the pattern
# itself — break on it so the count check below still fails, and name # itself — break on it so the count check below still fails, and name
# the expectation loudly instead of dying at cp. # the expectation loudly instead of dying at cp.
&& cd packages/managed-agent-server/target \ && cd packages/managed-agent-server/server/target \
&& main="" && count=0 \ && main="" && count=0 \
&& for f in qwen-managed-agent-server-*-exec.jar; do \ && for f in qwen-managed-agent-server-*-exec.jar; do \
[ -e "$f" ] || break; \ [ -e "$f" ] || break; \
+2 -2
View File
@@ -17,7 +17,7 @@ alive. The Broker reconciles and adopts bindings through recovery-capable
provisioners. The embedding service owns those provisioners, reprovisioning provisioners. The embedding service owns those provisioners, reprovisioning
policy, and Session rebind policy. policy, and Session rebind policy.
Broker and Server sources, resources and tests share this Maven module. Keep their Java packages separate. The server depends on the Java SDK at `../sdk-java/qwencode`; install that SDK before building. The parent builds six production Maven modules: `contracts`, `control-plane`, `runtime-broker`, `persistence`, `adapters`, and `server`. Tests live in `verification`. Keep each production Java package in one module. Only `adapters` depends on the Java SDK at `../sdk-java/qwencode`; install that SDK before building. Follow [AGENTS.md](AGENTS.md) for focused verification and the shared local budget.
Use JDK 21 or later to build and run this module. Use JDK 21 or later to build and run this module.
@@ -25,7 +25,7 @@ Build and test with:
```bash ```bash
mvn test mvn test
mvn checkstyle:check checkstyle:check@runtime-broker-checkstyle mvn checkstyle:check
mvn verify mvn verify
``` ```
+38 -23
View File
@@ -1,6 +1,6 @@
# Qwen Managed Agent Server # Qwen Managed Agent Server
The application lives in `packages/managed-agent-server`, alongside the Java SDK. Runtime Broker sources, resources and tests are part of this Maven module; see [Runtime Broker internals](docs/runtime-broker.md). Install the Java SDK before building this application; no separate Broker artifact is needed. The application lives in `packages/managed-agent-server`, alongside the Java SDK. It builds as one Maven reactor with six production modules and a verification module. The executable remains one Spring Boot service. Install the Java SDK before building the reactor; see [Runtime Broker internals](docs/runtime-broker.md).
Standalone Spring Boot control plane for the Qwen Code Hosted Harness. It has Standalone Spring Boot control plane for the Qwen Code Hosted Harness. It has
no DataWorks dependency. The default loopback listen address keeps the no DataWorks dependency. The default loopback listen address keeps the
@@ -91,20 +91,32 @@ adoption only. Linux operators may explicitly enable
trusted-host contract. Missing or conflicting physical identity markers trusted-host contract. Missing or conflicting physical identity markers
remain closed to execution; startup does not re-register or repair them. remain closed to execution; startup does not re-register or repair them.
## Java package layout ## Maven modules and Java packages
Business components live in `managedagent.session`, `workspace`, `artifact`, | Module | Responsibility | Internal dependencies |
`channel`, `automation`, `catalog` and `event`. Controllers and HTTP filters | ---------------- | --------------------------------------------------------------------------------------- | -------------------------- |
remain in `managedagent.api`; shared contracts and application errors live in | `contracts` | Service and repository interfaces, shared values, settings views and protocol rules | None |
`managedagent.model` and `exception`. The shared SQL aggregate remains in | `control-plane` | Session, Workspace, catalog, lifecycle, Channel, automation and extension orchestration | `contracts` |
`managedagent.persistence` so its transaction and locking helpers stay | `runtime-broker` | Runtime execution, binding, leases, cancellation and recovery coordination | `contracts` |
package-private. Broker providers, HTTP adapters and credential protection live | `persistence` | JDBC/memory repositories, schema and atomic database operations | `contracts` |
in `runtimebroker.provisioning`, `transport` and `security` respectively. The | `adapters` | Harness SDK, Runtime providers, HTTP clients, OSS, MQ, Git and filesystem operations | `contracts` |
separate `maintenance` configuration remains outside the server's component scan. | `server` | HTTP endpoints, Spring assembly, startup and maintenance entry points | All five modules |
| `verification` | Unit, integration, architecture and packaging tests; not published | All six production modules |
These are packages in one Maven module. See the package-boundary design: The parent coordinate is `com.alibaba:qwen-managed-agent-parent`. All modules share its version; the server keeps `com.alibaba:qwen-managed-agent-server`. Each production Java package belongs to one module. Keep implementation modules independent of one another: their collaboration uses contracts injected by server configuration. SQL helpers sharing a connection remain in `managedagent.persistence` or `runtimebroker.persistence`. The Broker service lives in `runtimebroker.service`; physical clients and providers live in the adapter module. Maintenance applications retain their separate component scan.
[English](../../docs/design/2026-10-19-managed-agent-package-boundaries.md) |
[简体中文](../../docs/design/2026-10-19-managed-agent-package-boundaries.zh-CN.md). Only `server` creates executable JARs. Its ordinary library JAR and the existing `exec` and maintenance classifiers are under `server/target/`. Dependencies are ordinary JARs nested under `BOOT-INF/lib` in executable artifacts. Install the reactor before using the server POM directly or embedding the library in another Maven project:
```bash
mvn -pl server -am -DskipTests install
mvn -f server/pom.xml spring-boot:run
```
Embedding imports `ManagedAgentConfiguration`; internal callbacks additionally import `ManagedAgentInternalConfiguration`. The standalone application also enables the public web configuration. Add the ordinary server artifact as the dependency; Maven resolves the other five modules transitively. The verification module is never installed or deployed. The standalone application YAML is excluded from the ordinary library JAR.
Server configuration accessors use Lombok through the server POM's annotation processor; enable annotation processing when compiling through an IDE. Keep accessors with normalization, copying or fallback logic explicit. Lombok is excluded from executable and maintenance JARs and is not a runtime dependency of embedding applications.
See the complete Maven boundary design: [English](../../docs/design/2026-10-29-managed-agent-maven-modules.md) | [简体中文](../../docs/design/2026-10-29-managed-agent-maven-modules.zh-CN.md).
## Integration status ## Integration status
@@ -119,10 +131,10 @@ results below do not establish completion of later slices.
## API contract ## API contract
`src/main/resources/openapi/managed-agent-public-api.openapi.json` is the `contracts/src/main/resources/openapi/managed-agent-public-api.openapi.json` is the
single source for the public and WebShell routes. `ManagedAgentApiContractTest` single source for the public and WebShell routes. `ManagedAgentApiContractTest`
compares the mapped routes, the `ApiModels` records and real responses with it; compares the mapped routes, the `ApiModels` records and real responses with it;
`src/test/resources/openapi/contract-known-gaps.txt` lists the differences that `verification/src/test/resources/openapi/contract-known-gaps.txt` lists the differences that
a later slice still has to close; none remain after D4. The WebShell client types are generated from the a later slice still has to close; none remain after D4. The WebShell client types are generated from the
same file by `npm run generate:managed-agent-api` in `packages/web-shell`. same file by `npm run generate:managed-agent-api` in `packages/web-shell`.
The test-tree `SurfaceRegistry` names every mounted route, internal ones The test-tree `SurfaceRegistry` names every mounted route, internal ones
@@ -335,7 +347,8 @@ export QWEN_MANAGED_AGENT_HARNESS_TOKEN='replace-me'
export QWEN_MANAGED_AGENT_CAPABILITY_DIGEST='sha256:replace-with-64-hex-characters' export QWEN_MANAGED_AGENT_CAPABILITY_DIGEST='sha256:replace-with-64-hex-characters'
export QWEN_MANAGED_AGENT_WORKSPACE_ID='local-dev-workspace' export QWEN_MANAGED_AGENT_WORKSPACE_ID='local-dev-workspace'
mvn spring-boot:run mvn -pl server -am -DskipTests install
mvn -f server/pom.xml spring-boot:run
``` ```
Instead of the token and digest, `QWEN_MANAGED_AGENT_HARNESS_CREDENTIALS_FILE` Instead of the token and digest, `QWEN_MANAGED_AGENT_HARNESS_CREDENTIALS_FILE`
@@ -719,7 +732,8 @@ source <printed spring.env path>
export SPRING_DATASOURCE_URL='jdbc:mysql://127.0.0.1:3306/qwen_managed_agent' export SPRING_DATASOURCE_URL='jdbc:mysql://127.0.0.1:3306/qwen_managed_agent'
export SPRING_DATASOURCE_USERNAME='qwen' export SPRING_DATASOURCE_USERNAME='qwen'
export SPRING_DATASOURCE_PASSWORD='replace-me' export SPRING_DATASOURCE_PASSWORD='replace-me'
mvn -f packages/managed-agent-server/pom.xml spring-boot:run mvn -f packages/managed-agent-server/pom.xml -pl server -am -DskipTests install
mvn -f packages/managed-agent-server/server/pom.xml spring-boot:run
``` ```
The daemon, Harness and Java URLs print at startup with the `spring.env` The daemon, Harness and Java URLs print at startup with the `spring.env`
@@ -1268,7 +1282,7 @@ Sessions. Persisted bound Sessions use the private Workspace execution path
below. below.
Managed Server and Runtime Broker share the single 38-table baseline in Managed Server and Runtime Broker share the single 38-table baseline in
[`src/main/resources/db/managed-schema/V1__managed_schema.sql`](src/main/resources/db/managed-schema/V1__managed_schema.sql). [`persistence/src/main/resources/db/managed-schema/V1__managed_schema.sql`](persistence/src/main/resources/db/managed-schema/V1__managed_schema.sql).
Until the first formal Managed Agent release, schema changes update this V1 baseline Until the first formal Managed Agent release, schema changes update this V1 baseline
in place. Do not add V2+ migrations or increment the database schema version before in place. Do not add V2+ migrations or increment the database schema version before
that release. Recreate development catalogs when the baseline changes; pre-release that release. Recreate development catalogs when the baseline changes; pre-release
@@ -1431,8 +1445,9 @@ crash. Database credentials come from `W1_JDBC_URL`, `W1_JDBC_USER` and
`W1_JDBC_PASSWORD` environment variables: `W1_JDBC_PASSWORD` environment variables:
```bash ```bash
mvn -q -DskipTests compile exec:java \ mvn -q -pl server -am -DskipTests -Dgpg.skip=true install
-Dexec.mainClass=com.alibaba.qwen.code.managedagent.persistence.WorkspaceStorageRegistrationMain \ mvn -q -pl server exec:java \
-Dexec.mainClass=com.alibaba.qwen.code.managedagent.maintenance.WorkspaceStorageRegistrationMain \
-Dexec.args='register tenant-a storage-a /absolute/canonical/workspace-a <operation-uuid> --offline-confirmed' -Dexec.args='register tenant-a storage-a /absolute/canonical/workspace-a <operation-uuid> --offline-confirmed'
``` ```
@@ -1443,7 +1458,7 @@ wraps. The same entry is available from a shipped Spring Boot fat jar, without t
```bash ```bash
java -cp /path/to/app.jar \ java -cp /path/to/app.jar \
-Dloader.main=com.alibaba.qwen.code.managedagent.persistence.WorkspaceStorageRegistrationMain \ -Dloader.main=com.alibaba.qwen.code.managedagent.maintenance.WorkspaceStorageRegistrationMain \
org.springframework.boot.loader.launch.PropertiesLauncher \ org.springframework.boot.loader.launch.PropertiesLauncher \
register tenant-a storage-a /absolute/canonical/workspace-a '<operation-uuid>' --offline-confirmed register tenant-a storage-a /absolute/canonical/workspace-a '<operation-uuid>' --offline-confirmed
``` ```
@@ -1542,7 +1557,7 @@ networks whose peers you trust.
## Managed Session Store verification ## Managed Session Store verification
Unit and H2 contract tests run with the normal Maven test phase. `mvn verify` Run Maven from the parent directory. Unit and H2 tests run in `verification` with the normal test phase; all original profile names and test families remain available from the parent reactor. Focused commands such as `mvn -Dtest=ModuleArchitectureTest,ManagedAgentEmbeddingTest test` build required production modules first. Test reports are under `verification/target/{surefire-reports,failsafe-reports}` and aggregate coverage under `verification/target/site/jacoco-aggregate`. Do not run the verification POM alone against stale installed same-version artifacts. `mvn clean verify` checks the compiled module boundaries, the existing test families and packaged server artifacts. The embedding-host fixture is under `verification/src/test/embedding-host`; after installing the production reactor, run its normal and `embedded` profiles to verify an independent host. `mvn verify`
additionally runs the SpotBugs high-confidence gate (Maven 3.8.9+): a new additionally runs the SpotBugs high-confidence gate (Maven 3.8.9+): a new
warning fails the build, and a false positive goes into warning fails the build, and a false positive goes into
`spotbugs-excludes.xml` with a justification in the PR. To run the static `spotbugs-excludes.xml` with a justification in the PR. To run the static
@@ -1639,7 +1654,7 @@ QWEN_MANAGED_HOOK_CGROUP_ROOT=/sys/fs/cgroup/your-delegated-root \
-Dmysql.user=root -Dmysql.password= verify -Dmysql.user=root -Dmysql.password= verify
``` ```
Missing Linux/MySQL/cgroup prerequisites are not acceptance. This selector verifies only the extension class; it does not establish that the complete Hosted profile passed. Evidence is saved before forced fixture teardown under `target/failsafe-reports/hosted-extension-evidence/` and is included in the existing CI report upload. The interrupted MCP case leaves its public Turn `RUNNING` with durable `ma_turn.error_code = hosted_runtime_recovery_blocked` and ordinary execution `UNKNOWN`, rejects new input and close with `409 turn_active`, and does not replay the physical effect; successful cleanup is established by the separate healthy-close case. This gate uses a controlled loopback model and local stdio transport, so it does not establish upstream-model, remote transport, reboot, cross-machine, SSH MCP/Hook, or production acceptance. See the complete [English design](../../docs/design/2026-10-16-managed-extension-acceptance.md) and [Chinese design](../../docs/design/2026-10-16-managed-extension-acceptance.zh-CN.md). Missing Linux/MySQL/cgroup prerequisites are not acceptance. This selector verifies only the extension class; it does not establish that the complete Hosted profile passed. Evidence is saved before forced fixture teardown under `verification/target/failsafe-reports/hosted-extension-evidence/` and is included in the existing CI report upload. The interrupted MCP case leaves its public Turn `RUNNING` with durable `ma_turn.error_code = hosted_runtime_recovery_blocked` and ordinary execution `UNKNOWN`, rejects new input and close with `409 turn_active`, and does not replay the physical effect; successful cleanup is established by the separate healthy-close case. This gate uses a controlled loopback model and local stdio transport, so it does not establish upstream-model, remote transport, reboot, cross-machine, SSH MCP/Hook, or production acceptance. See the complete [English design](../../docs/design/2026-10-16-managed-extension-acceptance.md) and [Chinese design](../../docs/design/2026-10-16-managed-extension-acceptance.zh-CN.md).
## Real-model end-to-end check ## Real-model end-to-end check
@@ -0,0 +1,87 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.alibaba</groupId>
<artifactId>qwen-managed-agent-parent</artifactId>
<version>0.1.0-alpha</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>qwen-managed-agent-adapters</artifactId>
<name>Qwen Managed Agent Adapters</name>
<properties>
<managed.module.root>${project.basedir}/..</managed.module.root>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qwen-managed-agent-contracts</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qwencode-sdk</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<failIfNoSpecifiedTests>false</failIfNoSpecifiedTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,7 +1,5 @@
package com.alibaba.qwen.code.managedagent.config; package com.alibaba.qwen.code.managedagent.adapters.artifact;
import com.alibaba.qwen.code.managedagent.artifact.AliyunToolPublicationObjectStore;
import com.alibaba.qwen.code.managedagent.artifact.ToolPublicationObjectStore;
import com.aliyun.oss.ClientBuilderConfiguration; import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.OSS; import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.OSSClientBuilder;
@@ -9,26 +7,26 @@ import com.aliyun.oss.common.auth.CredentialsProvider;
import com.aliyun.oss.common.auth.CredentialsProviderFactory; import com.aliyun.oss.common.auth.CredentialsProviderFactory;
import com.aliyun.oss.common.comm.SignVersion; import com.aliyun.oss.common.comm.SignVersion;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import java.net.URI; import java.net.URI;
public final class AliyunObjectStoreClient implements AutoCloseable {
@AutoConfiguration private final OSS client;
@ConditionalOnProperty(prefix = "qwen.managed-agent.tool-publication", public AliyunObjectStoreClient(OSS client) { this.client = client; }
name = "enabled", havingValue = "true") public static AliyunObjectStoreClient environment(com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings settings) throws Exception {
@ConditionalOnBean(ToolPublicationConfiguration.class) return new AliyunObjectStoreClient(buildOss(settings, CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider()));
@ConditionalOnMissingBean(ToolPublicationObjectStore.class)
public class AliyunToolPublicationStorageAutoConfiguration {
@Bean(destroyMethod = "shutdown")
public OSS toolPublicationOss(ManagedAgentProperties properties) throws Exception {
return buildOss(properties, CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider());
} }
public static AliyunObjectStoreClient regional(URI endpoint, String region) throws Exception {
public static OSS buildOss(ManagedAgentProperties properties, CredentialsProvider credentials) { var configuration = new ClientBuilderConfiguration();
configuration.setSignatureVersion(SignVersion.V4);
AliyunToolPublicationObjectStore.configureClientRetries(configuration);
return new AliyunObjectStoreClient(OSSClientBuilder.create().endpoint(endpoint.toString()).region(region)
.credentialsProvider(CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider())
.clientConfiguration(configuration).build());
}
public AliyunToolPublicationObjectStore objects(String bucket) { return new AliyunToolPublicationObjectStore(client, bucket); }
public void shutdown() { client.shutdown(); }
public void close() { shutdown(); }
public static OSS buildOss(com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings properties, CredentialsProvider credentials) {
var settings = properties.getToolPublication(); var settings = properties.getToolPublication();
URI endpoint = URI.create(required(settings.getOssEndpoint(), "OSS endpoint")); URI endpoint = URI.create(required(settings.getOssEndpoint(), "OSS endpoint"));
String region = required(settings.getOssRegion(), "OSS region"); String region = required(settings.getOssRegion(), "OSS region");
@@ -48,13 +46,6 @@ public class AliyunToolPublicationStorageAutoConfiguration {
.clientConfiguration(client).build(); .clientConfiguration(client).build();
} }
@Bean
public ToolPublicationObjectStore toolPublicationObjects(OSS toolPublicationOss,
ManagedAgentProperties properties) {
return new AliyunToolPublicationObjectStore(toolPublicationOss,
properties.getToolPublication().getOssBucket());
}
private static String required(String value, String label) { private static String required(String value, String label) {
if (StringUtils.isBlank(value)) { if (StringUtils.isBlank(value)) {
throw new IllegalStateException("Tool publication " + label + " is required"); throw new IllegalStateException("Tool publication " + label + " is required");
@@ -1,5 +1,7 @@
package com.alibaba.qwen.code.managedagent.artifact; package com.alibaba.qwen.code.managedagent.adapters.artifact;
import com.alibaba.qwen.code.managedagent.contracts.artifact.ToolPublicationObjectStore;
import com.alibaba.qwen.code.managedagent.contracts.artifact.ToolPublicationStorageException;
import com.aliyun.oss.ClientConfiguration; import com.aliyun.oss.ClientConfiguration;
import com.aliyun.oss.ClientException; import com.aliyun.oss.ClientException;
import com.aliyun.oss.HttpMethod; import com.aliyun.oss.HttpMethod;
@@ -1,5 +1,8 @@
package com.alibaba.qwen.code.managedagent.event; package com.alibaba.qwen.code.managedagent.adapters.event;
import com.alibaba.qwen.code.managedagent.contracts.event.JournalEventTransport;
import com.alibaba.qwen.code.managedagent.contracts.event.ManagedEventEnvelope;
import com.alibaba.qwen.code.managedagent.contracts.event.ManagedEventEnvelopes;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.rocketmq.client.apis.ClientServiceProvider; import org.apache.rocketmq.client.apis.ClientServiceProvider;
@@ -1,5 +1,8 @@
package com.alibaba.qwen.code.managedagent.event; package com.alibaba.qwen.code.managedagent.adapters.event;
import com.alibaba.qwen.code.managedagent.contracts.event.EventTransport;
import com.alibaba.qwen.code.managedagent.contracts.event.ManagedEventEnvelope;
import com.alibaba.qwen.code.managedagent.contracts.event.ManagedEventEnvelopes;
import com.alibaba.qwen.code.managedagent.model.StoreModels.EventRecord; import com.alibaba.qwen.code.managedagent.model.StoreModels.EventRecord;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.rocketmq.client.apis.ClientServiceProvider; import org.apache.rocketmq.client.apis.ClientServiceProvider;
@@ -63,9 +66,10 @@ public class LiteTopicPublicEventTransport implements EventTransport, Closeable
ClientServiceProvider provider, String topic, ObjectMapper mapper, ClientServiceProvider provider, String topic, ObjectMapper mapper,
BiFunction<String, String, Optional<String>> workspaces) { BiFunction<String, String, Optional<String>> workspaces) {
this(producer, provider, topic, mapper, workspaces, this(producer, provider, topic, mapper, workspaces,
TransportExecutors.bounded(Thread.ofPlatform().daemon() new java.util.concurrent.ThreadPoolExecutor(1, 1, 0, java.util.concurrent.TimeUnit.MILLISECONDS,
.name("event-transport-public").factory(), new java.util.concurrent.ArrayBlockingQueue<>(QUEUE_CAPACITY),
1, QUEUE_CAPACITY)); Thread.ofPlatform().daemon().name("event-transport-public").factory(),
new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()));
} }
// The executor is injectable so a test can distribute synchronously. // The executor is injectable so a test can distribute synchronously.
@@ -84,7 +88,14 @@ public class LiteTopicPublicEventTransport implements EventTransport, Closeable
/** Stops the distribution thread; the shared producer is the config's. */ /** Stops the distribution thread; the shared producer is the config's. */
@Override @Override
public void close() { public void close() {
TransportExecutors.close(executor); if (executor instanceof java.util.concurrent.ExecutorService service) {
service.shutdown();
try {
service.awaitTermination(2, java.util.concurrent.TimeUnit.SECONDS);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
}
}
} }
@Override @Override
@@ -0,0 +1,146 @@
package com.alibaba.qwen.code.managedagent.adapters.event;
import com.alibaba.qwen.code.managedagent.contracts.event.EventMessageListener;
import com.alibaba.qwen.code.managedagent.contracts.event.EventSubscriptions;
import com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.LitePushConsumer;
import org.apache.rocketmq.client.apis.producer.Producer;
public final class RocketMqEventClients {
public static final String CAPABILITY_PROBE_LITE = "capability-probe";
private final ClientServiceProvider provider;
public RocketMqEventClients() { this(ClientServiceProvider.loadService()); }
public RocketMqEventClients(ClientServiceProvider provider) { this.provider = provider; }
public static final class Publisher implements java.io.Closeable {
private final Producer producer;
private Publisher(Producer producer) { this.producer = producer; }
public void close() throws java.io.IOException { producer.close(); }
}
public LiteTopicEventTransport journal(Publisher publisher, ManagedAgentSettings properties) {
var config = properties.getEventTransport();
return new LiteTopicEventTransport(publisher.producer, provider, config.getTopic(), config.getWakeBootstrapLiteTopic());
}
public LiteTopicPublicEventTransport publicEvents(Publisher publisher, ManagedAgentSettings properties,
ObjectMapper json, java.util.function.BiFunction<String, String, java.util.Optional<String>> workspaceOf) {
return new LiteTopicPublicEventTransport(publisher.producer, provider, properties.getEventTransport().getTopic(), json, workspaceOf);
}
public Publisher producer(ManagedAgentSettings properties) throws ClientException {
var config = properties.getEventTransport();
ClientConfiguration clientConfig = clientConfiguration(config);
verifyLiteTopicCapability(provider, clientConfig, config.getTopic(),
config.getCapabilityProbeGroup());
Producer producer = provider.newProducerBuilder()
.setClientConfiguration(clientConfig)
.setTopics(config.getTopic())
.build();
try {
verifyTopicAcceptsLiteMessages(provider, producer,
config.getTopic());
} catch (RuntimeException failure) {
closeQuietly(producer, failure);
throw failure;
}
return new Publisher(producer);
}
public EventSubscriptions owner(
ManagedAgentSettings properties,
EventMessageListener listener,
java.util.function.Consumer<EventSubscriptions> bind)
throws ClientException {
var config = properties.getEventTransport();
LitePushConsumer consumer = provider.newLitePushConsumerBuilder()
.setClientConfiguration(clientConfiguration(config))
.setConsumerGroup(config.getOwnerGroup())
.bindTopic(config.getTopic())
.setMessageListener(message -> com.alibaba.qwen.code.managedagent.adapters.event.RocketMqMessages.consume(listener, message))
.build();
bind.accept(com.alibaba.qwen.code.managedagent.adapters.event.RocketMqMessages.subscriptions(consumer));
return RocketMqMessages.subscriptions(consumer);
}
public EventSubscriptions wake(ManagedAgentSettings properties, EventMessageListener listener)
throws ClientException {
var config = properties.getEventTransport();
LitePushConsumer consumer = provider.newLitePushConsumerBuilder()
.setClientConfiguration(clientConfiguration(config))
.setConsumerGroup(config.getWakeBootstrapGroup())
.bindTopic(config.getTopic())
.setMessageListener(message -> com.alibaba.qwen.code.managedagent.adapters.event.RocketMqMessages.consume(listener, message))
.build();
try {
consumer.subscribeLite(config.getWakeBootstrapLiteTopic());
} catch (ClientException | RuntimeException failure) {
closeQuietly(consumer, failure);
throw new IllegalStateException("the event-transport wake"
+ " bootstrap group " + config.getWakeBootstrapGroup()
+ " could not subscribe "
+ config.getWakeBootstrapLiteTopic() + " on topic "
+ config.getTopic() + ": pre-create it as a LITE group"
+ " bound to the topic (mqadmin updateSubGroup -c"
+ " <cluster> -g " + config.getWakeBootstrapGroup()
+ " -o true --attributes +lite.bind.topic="
+ config.getTopic() + ")", failure);
}
return RocketMqMessages.subscriptions(consumer);
}
private static ClientConfiguration clientConfiguration(
ManagedAgentSettings.EventTransport config) {
return ClientConfiguration.newBuilder()
.setEndpoints(config.getEndpoints())
.build();
}
private static void closeQuietly(java.io.Closeable closeable,
Throwable failure) {
try {
closeable.close();
} catch (Exception suppressed) {
failure.addSuppressed(suppressed);
}
}
private static void verifyLiteTopicCapability(ClientServiceProvider provider,
ClientConfiguration clientConfig, String topic, String group) {
try (LitePushConsumer probe = provider.newLitePushConsumerBuilder()
.setClientConfiguration(clientConfig)
.setConsumerGroup(group)
.bindTopic(topic)
.setMessageListener(messageView -> ConsumeResult.SUCCESS)
.build()) {
probe.subscribeLite(CAPABILITY_PROBE_LITE);
probe.unsubscribeLite(CAPABILITY_PROBE_LITE);
} catch (Exception failure) {
throw new IllegalStateException("the event-transport LiteTopic"
+ " capability probe failed on topic " + topic + ": the"
+ " broker must serve LiteTopics (RocketMQ 5.5.0+ with"
+ " enableLmq) and the probe group " + group
+ " must be pre-created as a LITE group bound to the topic"
+ " (mqadmin updateSubGroup -c <cluster> -g " + group
+ " -o true --attributes +lite.bind.topic=" + topic
+ "); check qwen.managed-agent.event-transport.endpoints",
failure);
}
}
private static void verifyTopicAcceptsLiteMessages(
ClientServiceProvider provider, Producer producer, String topic) {
try {
producer.send(provider.newMessageBuilder()
.setTopic(topic)
.setLiteTopic(CAPABILITY_PROBE_LITE)
.setKeys(CAPABILITY_PROBE_LITE)
.setBody("{}".getBytes(java.nio.charset.StandardCharsets.UTF_8))
.build());
} catch (ClientException | RuntimeException failure) {
throw new IllegalStateException("the event-transport topic "
+ topic + " refused a LiteTopic message: create it as a"
+ " LITE topic (mqadmin updateTopic -c <cluster> -t "
+ topic + " -a +message.type=LITE)", failure);
}
}
}
@@ -0,0 +1,28 @@
package com.alibaba.qwen.code.managedagent.adapters.event;
import com.alibaba.qwen.code.managedagent.contracts.event.EventMessageListener;
import com.alibaba.qwen.code.managedagent.contracts.event.EventSubscriptions;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.LitePushConsumer;
import org.apache.rocketmq.client.apis.message.MessageView;
import java.io.IOException;
import java.util.Set;
public final class RocketMqMessages {
private RocketMqMessages() { }
public static ConsumeResult consume(EventMessageListener listener, MessageView message) {
var buffer = message.getBody().duplicate();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
listener.consume(bytes, message.getMessageId().toString());
return ConsumeResult.SUCCESS;
}
public static EventSubscriptions subscriptions(LitePushConsumer consumer) {
return new EventSubscriptions() {
public Set<String> getLiteTopicSet() { return consumer.getLiteTopicSet(); }
public void subscribeLite(String topic) throws Exception { consumer.subscribeLite(topic); }
public void unsubscribeLite(String topic) throws Exception { consumer.unsubscribeLite(topic); }
public void close() throws IOException { consumer.close(); }
};
}
}
@@ -1,11 +1,11 @@
package com.alibaba.qwen.code.managedagent.extensions; package com.alibaba.qwen.code.managedagent.adapters.extensions;
import com.alibaba.qwen.code.managedagent.config.ManagedAgentProperties; import com.alibaba.qwen.code.managedagent.contracts.extensions.ManagedExtensionPackages;
import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException; import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException;
import com.alibaba.qwen.code.runtimebroker.managedschema.ManagedSchemaKeys; import com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings;
import com.alibaba.qwen.code.runtimebroker.schema.ManagedSchemaKeys;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
@@ -19,10 +19,10 @@ import java.nio.file.StandardOpenOption;
import java.util.UUID; import java.util.UUID;
@Component @Component
public class ManagedExtensionArchiveStore { public class ManagedExtensionArchiveStore implements com.alibaba.qwen.code.managedagent.contracts.extensions.ExtensionArchives {
private final ManagedAgentProperties properties; private final ManagedAgentSettings properties;
public ManagedExtensionArchiveStore(ManagedAgentProperties properties) { public ManagedExtensionArchiveStore(ManagedAgentSettings properties) {
this.properties = properties; this.properties = properties;
} }
@@ -44,7 +44,7 @@ public class ManagedExtensionArchiveStore {
while ((count = source.read(buffer)) != -1) { while ((count = source.read(buffer)) != -1) {
length += count; length += count;
if (length > ManagedExtensionPackages.MAX_ARCHIVE_BYTES) { if (length > ManagedExtensionPackages.MAX_ARCHIVE_BYTES) {
throw new ManagedAgentException(HttpStatus.PAYLOAD_TOO_LARGE, throw new ManagedAgentException(413,
"extension_package_too_large", "Extension archive exceeds 100 MiB."); "extension_package_too_large", "Extension archive exceeds 100 MiB.");
} }
hash.update(buffer, 0, count); hash.update(buffer, 0, count);
@@ -52,7 +52,7 @@ public class ManagedExtensionArchiveStore {
} }
} }
if (length == 0 || !digest.equals(org.apache.commons.codec.binary.Hex.encodeHexString(hash.digest()))) { if (length == 0 || !digest.equals(org.apache.commons.codec.binary.Hex.encodeHexString(hash.digest()))) {
throw new ManagedAgentException(HttpStatus.BAD_REQUEST, throw new ManagedAgentException(400,
"extension_package_digest_mismatch", "Extension archive digest does not match its contents."); "extension_package_digest_mismatch", "Extension archive digest does not match its contents.");
} }
try (FileChannel file = FileChannel.open(temporary, StandardOpenOption.WRITE)) { try (FileChannel file = FileChannel.open(temporary, StandardOpenOption.WRITE)) {
@@ -90,7 +90,7 @@ public class ManagedExtensionArchiveStore {
throw unavailable(); throw unavailable();
} }
if (digest == null || !digest.matches("[a-f0-9]{64}")) { if (digest == null || !digest.matches("[a-f0-9]{64}")) {
throw new ManagedAgentException(HttpStatus.BAD_REQUEST, "invalid_request", "Invalid extension digest."); throw new ManagedAgentException(400, "invalid_request", "Invalid extension digest.");
} }
Path root = Path.of(properties.getExtensions().getArtifactDirectory()).toAbsolutePath().normalize(); Path root = Path.of(properties.getExtensions().getArtifactDirectory()).toAbsolutePath().normalize();
if (Files.exists(root, LinkOption.NOFOLLOW_LINKS) && Files.isSymbolicLink(root)) { if (Files.exists(root, LinkOption.NOFOLLOW_LINKS) && Files.isSymbolicLink(root)) {
@@ -115,7 +115,7 @@ public class ManagedExtensionArchiveStore {
} }
private static ManagedAgentException unavailable() { private static ManagedAgentException unavailable() {
return new ManagedAgentException(HttpStatus.SERVICE_UNAVAILABLE, return new ManagedAgentException(503,
"extension_package_unavailable", "The saved extension archive is unavailable."); "extension_package_unavailable", "The saved extension archive is unavailable.");
} }
} }
@@ -1,12 +1,11 @@
package com.alibaba.qwen.code.managedagent.workspace.runtime; package com.alibaba.qwen.code.managedagent.adapters.runtime;
import com.alibaba.qwen.code.managedagent.config.ManagedAgentProperties; import com.alibaba.qwen.code.managedagent.contracts.workspace.ResidentWorkspaceCatalog;
import com.alibaba.qwen.code.managedagent.config.ManagedAgentProperties.RuntimeBroker.ResidentWorkspace; import com.alibaba.qwen.code.managedagent.repository.WorkspaceCsiReservations;
import com.alibaba.qwen.code.managedagent.persistence.ResidentWorkspaces; import com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings;
import com.alibaba.qwen.code.managedagent.persistence.WorkspaceCsiReservationStore; import com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings.RuntimeBroker.ResidentWorkspace;
import com.alibaba.qwen.code.managedagent.persistence.WorkspaceExecutionStore;
import com.alibaba.qwen.code.runtimebroker.JdbcRuntimeBindingRepository;
import com.alibaba.qwen.code.runtimebroker.RuntimeBindingRecord; import com.alibaba.qwen.code.runtimebroker.RuntimeBindingRecord;
import com.alibaba.qwen.code.runtimebroker.RuntimeBindingRepository;
import com.alibaba.qwen.code.runtimebroker.RuntimeLease; import com.alibaba.qwen.code.runtimebroker.RuntimeLease;
import com.alibaba.qwen.code.runtimebroker.RuntimeObservation; import com.alibaba.qwen.code.runtimebroker.RuntimeObservation;
import com.alibaba.qwen.code.runtimebroker.RuntimeProvisionRequest; import com.alibaba.qwen.code.runtimebroker.RuntimeProvisionRequest;
@@ -15,8 +14,6 @@ import com.alibaba.qwen.code.runtimebroker.RuntimeProvisioner;
import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle; import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle;
import com.alibaba.qwen.code.runtimebroker.RuntimeScope; import com.alibaba.qwen.code.runtimebroker.RuntimeScope;
import com.alibaba.qwen.code.runtimebroker.provisioning.KubernetesHttpRuntimeClient; import com.alibaba.qwen.code.runtimebroker.provisioning.KubernetesHttpRuntimeClient;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import java.net.URI; import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
@@ -25,27 +22,26 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletionStage; import java.util.concurrent.CompletionStage;
final class ResidentWorkspaceProvisioner implements RuntimeProvisioner { public final class ResidentWorkspaceProvisioner implements RuntimeProvisioner {
private final ResidentWorkspaces residents; private final ResidentWorkspaceCatalog residents;
private final Map<ResidentWorkspace, WorkspaceCsiRuntimeProvisioner> providers = new LinkedHashMap<>(); private final Map<ResidentWorkspace, WorkspaceCsiRuntimeProvisioner> providers = new LinkedHashMap<>();
ResidentWorkspaceProvisioner(ManagedAgentProperties properties, JdbcTemplate jdbc, public ResidentWorkspaceProvisioner(ManagedAgentSettings properties, WorkspaceCsiReservations storage,
PlatformTransactionManager manager, JdbcRuntimeBindingRepository bindings) { RuntimeBindingRepository bindings) {
var config = properties.getRuntimeBroker(); var config = properties.getRuntimeBroker();
residents = new ResidentWorkspaces(config.getResidentWorkspaces()); residents = new ResidentWorkspaceCatalog(config.getResidentWorkspaces());
if (config.getResidentWorkspaces().isEmpty() || !"workspace".equals(config.getIsolationClass()) if (config.getResidentWorkspaces().isEmpty() || !"workspace".equals(config.getIsolationClass())
|| config.isTrustedLocalRebootRecovery() || config.isVerifiedWorkspaceRecoveryEnabled() || config.isTrustedLocalRebootRecovery() || config.isVerifiedWorkspaceRecoveryEnabled()
|| config.isChildWorkspacesEnabled() || !config.getWorkspaceMounts().isEmpty() || config.isChildWorkspacesEnabled() || !config.getWorkspaceMounts().isEmpty()
|| !properties.getHarness().isWorkspaceFilesEnabled() || properties.getHarness().isWorkspaceShellEnabled() || !properties.getHarness().isWorkspaceFilesEnabled() || properties.getHarness().isWorkspaceShellEnabled()
|| config.getResidentKubernetesApiUrl().isBlank() || config.getResidentKubernetesTokenFile().isBlank() || config.getResidentKubernetesApiUrl().isBlank() || config.getResidentKubernetesTokenFile().isBlank()
|| config.getResidentKubernetesCaFile().isBlank() || !bindings.usesDataSource(jdbc.getDataSource())) { || config.getResidentKubernetesCaFile().isBlank() || !storage.usesSameDataSource(bindings)) {
throw new IllegalStateException("Resident Workspace requires exclusive CSI/files configuration"); throw new IllegalStateException("Resident Workspace requires exclusive CSI/files configuration");
} }
var api = new KubernetesHttpRuntimeClient(URI.create(config.getResidentKubernetesApiUrl()), var api = new KubernetesHttpRuntimeClient(URI.create(config.getResidentKubernetesApiUrl()),
Path.of(config.getResidentKubernetesTokenFile()), Path.of(config.getResidentKubernetesCaFile())); Path.of(config.getResidentKubernetesTokenFile()), Path.of(config.getResidentKubernetesCaFile()));
var storage = new WorkspaceCsiReservationStore(jdbc, manager, new com.fasterxml.jackson.databind.ObjectMapper());
for (var selected : config.getResidentWorkspaces()) { for (var selected : config.getResidentWorkspaces()) {
ResidentWorkspaces.requireRegistration(jdbc, selected); storage.requireRegistration(selected);
var guard = new WorkspaceCsiResourceGuard(api, selected.registration().clusterDomain(), var guard = new WorkspaceCsiResourceGuard(api, selected.registration().clusterDomain(),
selected.registration(), selected.protection()); selected.registration(), selected.protection());
providers.put(selected, new WorkspaceCsiRuntimeProvisioner(storage, bindings, selected.registration(), providers.put(selected, new WorkspaceCsiRuntimeProvisioner(storage, bindings, selected.registration(),
@@ -57,19 +53,19 @@ final class ResidentWorkspaceProvisioner implements RuntimeProvisioner {
var mapping = residents.require(request.getScope()); var mapping = residents.require(request.getScope());
if (request.getIsolationKey() != null || !mapping.registration().storageId().equals(request.getStorageId()) if (request.getIsolationKey() != null || !mapping.registration().storageId().equals(request.getStorageId())
|| !kind().equals(request.getProvisionerKind())) { || !kind().equals(request.getProvisionerKind())) {
throw WorkspaceExecutionStore.unavailable(); throw com.alibaba.qwen.code.managedagent.repository.WorkspaceExecutionStore.unavailable();
} }
return providers.get(mapping); return providers.get(mapping);
} }
@Override @Override
public String kind() { return WorkspaceCsiReservationStore.PROVISIONER_KIND; } public String kind() { return WorkspaceCsiReservations.PROVISIONER_KIND; }
@Override @Override
public RuntimeProvisionRequest createRequest(RuntimeScope scope, String isolationKey) { public RuntimeProvisionRequest createRequest(RuntimeScope scope, String isolationKey) {
var mapping = residents.require(scope); var mapping = residents.require(scope);
if (isolationKey != null) { if (isolationKey != null) {
throw WorkspaceExecutionStore.unavailable(); throw com.alibaba.qwen.code.managedagent.repository.WorkspaceExecutionStore.unavailable();
} }
return new RuntimeProvisionRequest(scope, null, kind(), mapping.registration().storageId()); return new RuntimeProvisionRequest(scope, null, kind(), mapping.registration().storageId());
} }
@@ -106,7 +102,7 @@ final class ResidentWorkspaceProvisioner implements RuntimeProvisioner {
@Override @Override
public CompletionStage<Void> release(RuntimeProvisionRequest request, RuntimeLease lease) { public CompletionStage<Void> release(RuntimeProvisionRequest request, RuntimeLease lease) {
return java.util.concurrent.CompletableFuture.failedFuture(WorkspaceExecutionStore.unavailable()); return java.util.concurrent.CompletableFuture.failedFuture(com.alibaba.qwen.code.managedagent.repository.WorkspaceExecutionStore.unavailable());
} }
@Override @Override
@@ -1,37 +1,30 @@
package com.alibaba.qwen.code.managedagent.persistence; package com.alibaba.qwen.code.managedagent.adapters.runtime;
import com.alibaba.qwen.code.managedagent.config.ManagedAgentProperties.RuntimeBroker.ResidentWorkspace; import com.alibaba.qwen.code.managedagent.contracts.workspace.ResidentReclamation.*;
import com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings.RuntimeBroker.ResidentWorkspace;
import com.alibaba.qwen.code.runtimebroker.BrokerValues; import com.alibaba.qwen.code.runtimebroker.BrokerValues;
import com.alibaba.qwen.code.runtimebroker.RuntimeBindingRecord; import com.alibaba.qwen.code.runtimebroker.RuntimeBindingRecord;
import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException; import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException;
import com.alibaba.qwen.code.runtimebroker.provisioning.KubernetesHttpRuntimeClient; import com.alibaba.qwen.code.runtimebroker.spi.KubernetesMaintenanceClient;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static com.alibaba.qwen.code.managedagent.contracts.workspace.ResidentReclamation.DATA_POLICY;
import static com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity.digest; import static com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity.digest;
import static com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity.map; import static com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity.map;
import static com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity.text; import static com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity.text;
/** Normal platform cleanup inside an exclusive Managed namespace; never an exceptional fence. */ /** Normal platform cleanup inside an exclusive Managed namespace; never an exceptional fence. */
final class ResidentWorkspaceReclamation { public final class ResidentWorkspaceReclamation {
static final String DATA_POLICY = "retain-same-tenant-admin-handoff/1";
record Baseline(String finalizer, String bootId, String nodeResourceVersion, String nodeHeartbeat,
String leaseUid, String leaseResourceVersion, String leaseRenewTime, String attachmentUid) { }
record Receipt(String resourceIdentity, String baselineDigest, String podResourceVersion,
String deletionTimestamp, String finishedAt, String nodeResourceVersion, String nodeHeartbeat,
String leaseResourceVersion, String leaseRenewTime, String observedAt, String dataPolicy) { }
private record Node(Map<String, Object> value, String bootId, String resourceVersion, Instant heartbeat) { } private record Node(Map<String, Object> value, String bootId, String resourceVersion, Instant heartbeat) { }
private record Lease(String uid, String resourceVersion, Instant renewed) { } private record Lease(String uid, String resourceVersion, Instant renewed) { }
private ResidentWorkspaceReclamation() { } private ResidentWorkspaceReclamation() { }
static Baseline prepare(RuntimeBindingRecord original, ResidentWorkspace selected, public static Baseline prepare(RuntimeBindingRecord original, ResidentWorkspace selected,
String finalizer, KubernetesHttpRuntimeClient api, Instant now) { String finalizer, KubernetesMaintenanceClient api, Instant now) {
var placement = map(original.getResourceHandle().getValue().get("placement")); var placement = map(original.getResourceHandle().getValue().get("placement"));
var node = node(placement, api, now, false); var node = node(placement, api, now, false);
var lease = lease(placement, api, now); var lease = lease(placement, api, now);
@@ -48,8 +41,8 @@ final class ResidentWorkspaceReclamation {
lease.uid(), lease.resourceVersion(), lease.renewed().toString(), text(metadata, "uid", 128)); lease.uid(), lease.resourceVersion(), lease.renewed().toString(), text(metadata, "uid", 128));
} }
static Receipt complete(RuntimeBindingRecord original, ResidentWorkspace selected, Baseline baseline, public static Receipt complete(RuntimeBindingRecord original, ResidentWorkspace selected, Baseline baseline,
Map<String, Object> pod, KubernetesHttpRuntimeClient api, Instant now) { Map<String, Object> pod, KubernetesMaintenanceClient api, Instant now) {
var handle = original.getResourceHandle().getValue(); var handle = original.getResourceHandle().getValue();
var placement = map(handle.get("placement")); var placement = map(handle.get("placement"));
require(pod != null); require(pod != null);
@@ -92,15 +85,7 @@ final class ResidentWorkspaceReclamation {
lease.resourceVersion(), lease.renewed().toString(), now.toString(), DATA_POLICY); lease.resourceVersion(), lease.renewed().toString(), now.toString(), DATA_POLICY);
} }
static void verifyReceipt(RuntimeBindingRecord original, Baseline baseline, Receipt receipt) { private static Node node(Map<String, Object> placement, KubernetesMaintenanceClient api, Instant now, boolean fresh) {
require(receipt != null && receipt.resourceIdentity().equals(original.getResourceHandle().getValue().get("identity"))
&& receipt.baselineDigest().equals(digest(baseline)) && DATA_POLICY.equals(receipt.dataPolicy())
&& receipt.observedAt() != null && receipt.finishedAt() != null && receipt.deletionTimestamp() != null
&& !Instant.parse(receipt.observedAt()).isBefore(Instant.parse(receipt.finishedAt())));
Instant.parse(receipt.deletionTimestamp());
}
private static Node node(Map<String, Object> placement, KubernetesHttpRuntimeClient api, Instant now, boolean fresh) {
var node = api.getCluster("nodes", (String) placement.get("nodeName")).toCompletableFuture().join(); var node = api.getCluster("nodes", (String) placement.get("nodeName")).toCompletableFuture().join();
require(node != null); require(node != null);
var metadata = map(node.get("metadata")); var metadata = map(node.get("metadata"));
@@ -122,7 +107,7 @@ final class ResidentWorkspaceReclamation {
text(metadata, "resourceVersion", 256), heartbeat); text(metadata, "resourceVersion", 256), heartbeat);
} }
private static Lease lease(Map<String, Object> placement, KubernetesHttpRuntimeClient api, Instant now) { private static Lease lease(Map<String, Object> placement, KubernetesMaintenanceClient api, Instant now) {
var lease = api.getNodeLease((String) placement.get("nodeName")).toCompletableFuture().join(); var lease = api.getNodeLease((String) placement.get("nodeName")).toCompletableFuture().join();
require(lease != null); require(lease != null);
var metadata = map(lease.get("metadata")); var metadata = map(lease.get("metadata"));
@@ -140,7 +125,7 @@ final class ResidentWorkspaceReclamation {
return new Lease(text(metadata, "uid", 128), text(metadata, "resourceVersion", 256), renewed); return new Lease(text(metadata, "uid", 128), text(metadata, "resourceVersion", 256), renewed);
} }
private static void exclusive(RuntimeBindingRecord original, ResidentWorkspace selected, KubernetesHttpRuntimeClient api) { private static void exclusive(RuntimeBindingRecord original, ResidentWorkspace selected, KubernetesMaintenanceClient api) {
var registration = selected.registration(); var registration = selected.registration();
var physical = api.listPersistentVolumes().toCompletableFuture().join().stream().filter(pv -> { var physical = api.listPersistentVolumes().toCompletableFuture().join().stream().filter(pv -> {
var spec = map(pv.get("spec")); var spec = map(pv.get("spec"));
@@ -159,7 +144,7 @@ final class ResidentWorkspaceReclamation {
&& placement.get("podName").equals(map(consumers.getFirst().get("metadata")).get("name"))); && placement.get("podName").equals(map(consumers.getFirst().get("metadata")).get("name")));
} }
private static List<Map<String, Object>> attachments(ResidentWorkspace selected, KubernetesHttpRuntimeClient api) { private static List<Map<String, Object>> attachments(ResidentWorkspace selected, KubernetesMaintenanceClient api) {
return api.listVolumeAttachments().toCompletableFuture().join().stream().filter(value -> { return api.listVolumeAttachments().toCompletableFuture().join().stream().filter(value -> {
var spec = map(value.get("spec")); var spec = map(value.get("spec"));
if (!selected.registration().pvName().equals(map(spec.get("source")).get("persistentVolumeName"))) { if (!selected.registration().pvName().equals(map(spec.get("source")).get("persistentVolumeName"))) {
@@ -1,8 +1,10 @@
package com.alibaba.qwen.code.managedagent.workspace.runtime; package com.alibaba.qwen.code.managedagent.adapters.runtime;
import com.alibaba.qwen.code.managedagent.persistence.WorkspaceCsiRegistration; import com.alibaba.qwen.code.managedagent.contracts.persistence.WorkspaceCsiRegistration;
import com.alibaba.qwen.code.managedagent.contracts.runtime.ProtectionIdentity;
import com.alibaba.qwen.code.managedagent.contracts.runtime.Receipt;
import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException; import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException;
import com.alibaba.qwen.code.runtimebroker.provisioning.KubernetesRuntimeClient; import com.alibaba.qwen.code.runtimebroker.spi.KubernetesRuntimeClient;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -20,24 +22,6 @@ public final class WorkspaceCsiResourceGuard {
private final WorkspaceCsiRegistration registration; private final WorkspaceCsiRegistration registration;
private final ProtectionIdentity protection; private final ProtectionIdentity protection;
public record ProtectionIdentity(String namespaceUid, String policyName, String policyUid,
long policyGeneration, String bindingName, String bindingUid, long bindingGeneration) {
public ProtectionIdentity {
for (String value : List.of(namespaceUid, policyName, policyUid, bindingName, bindingUid)) {
if (value.isBlank() || value.length() > 253 || value.codePoints().anyMatch(point -> point < 33)) {
throw new IllegalArgumentException("Invalid CSI protection identity");
}
}
if (policyGeneration < 1 || bindingGeneration < 1) {
throw new IllegalArgumentException("Invalid CSI protection generation");
}
}
}
public record Receipt(String physicalKey, long registrationRevision, String namespaceUid,
String pvcResourceVersion, String pvResourceVersion, ProtectionIdentity protection) {
}
public WorkspaceCsiResourceGuard(KubernetesRuntimeClient api, String trustedClusterDomain, public WorkspaceCsiResourceGuard(KubernetesRuntimeClient api, String trustedClusterDomain,
WorkspaceCsiRegistration registration, ProtectionIdentity protection) { WorkspaceCsiRegistration registration, ProtectionIdentity protection) {
this.api = Objects.requireNonNull(api); this.api = Objects.requireNonNull(api);
@@ -1,9 +1,10 @@
package com.alibaba.qwen.code.managedagent.workspace.runtime; package com.alibaba.qwen.code.managedagent.adapters.runtime;
import com.alibaba.qwen.code.managedagent.persistence.WorkspaceCsiRegistration; import com.alibaba.qwen.code.managedagent.contracts.persistence.WorkspaceCsiRegistration;
import com.alibaba.qwen.code.managedagent.persistence.WorkspaceCsiReservationStore; import com.alibaba.qwen.code.managedagent.contracts.runtime.WorkerArtifact;
import com.alibaba.qwen.code.runtimebroker.JdbcRuntimeBindingRepository; import com.alibaba.qwen.code.managedagent.repository.WorkspaceCsiReservations;
import com.alibaba.qwen.code.runtimebroker.RuntimeBindingRecord; import com.alibaba.qwen.code.runtimebroker.RuntimeBindingRecord;
import com.alibaba.qwen.code.runtimebroker.RuntimeBindingRepository;
import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException; import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException;
import com.alibaba.qwen.code.runtimebroker.RuntimeLease; import com.alibaba.qwen.code.runtimebroker.RuntimeLease;
import com.alibaba.qwen.code.runtimebroker.RuntimeObservation; import com.alibaba.qwen.code.runtimebroker.RuntimeObservation;
@@ -14,13 +15,11 @@ import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle;
import com.alibaba.qwen.code.runtimebroker.RuntimeScope; import com.alibaba.qwen.code.runtimebroker.RuntimeScope;
import com.alibaba.qwen.code.runtimebroker.RuntimeTransport; import com.alibaba.qwen.code.runtimebroker.RuntimeTransport;
import com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity; import com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity;
import com.alibaba.qwen.code.runtimebroker.provisioning.KubernetesRuntimeClient; import com.alibaba.qwen.code.runtimebroker.adapter.HttpRuntimeTransport;
import com.alibaba.qwen.code.runtimebroker.transport.HttpRuntimeTransport; import com.alibaba.qwen.code.runtimebroker.spi.KubernetesRuntimeClient;
import com.alibaba.qwen.code.runtimebroker.transport.ManagedCsiFilesProtocol; import com.alibaba.qwen.code.runtimebroker.transport.ManagedCsiFilesProtocol;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.DataAccessException;
import org.springframework.transaction.TransactionException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
@@ -61,8 +60,8 @@ import static com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity.uu
/** Private CSI placement; ambiguous creation never authorizes adoption or replacement. */ /** Private CSI placement; ambiguous creation never authorizes adoption or replacement. */
public final class WorkspaceCsiRuntimeProvisioner implements RuntimeProvisioner { public final class WorkspaceCsiRuntimeProvisioner implements RuntimeProvisioner {
private static final String BOOT_PATH = "/var/run/qwen-runtime/boot.json"; private static final String BOOT_PATH = "/var/run/qwen-runtime/boot.json";
private final WorkspaceCsiReservationStore storage; private final WorkspaceCsiReservations storage;
private final JdbcRuntimeBindingRepository bindings; private final RuntimeBindingRepository bindings;
private final WorkspaceCsiRegistration registration; private final WorkspaceCsiRegistration registration;
private final KubernetesRuntimeClient api; private final KubernetesRuntimeClient api;
private final WorkspaceCsiResourceGuard guard; private final WorkspaceCsiResourceGuard guard;
@@ -77,49 +76,41 @@ public final class WorkspaceCsiRuntimeProvisioner implements RuntimeProvisioner
private final Map<RuntimeProvisionSeed, RuntimeBindingRecord> claims = new ConcurrentHashMap<>(); private final Map<RuntimeProvisionSeed, RuntimeBindingRecord> claims = new ConcurrentHashMap<>();
private final Map<RuntimeProvisionSeed, Placement> placements = new ConcurrentHashMap<>(); private final Map<RuntimeProvisionSeed, Placement> placements = new ConcurrentHashMap<>();
public record WorkerArtifact(String configMapName, String uid, String sha256) { public WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservations storage,
public WorkerArtifact { RuntimeBindingRepository bindings, WorkspaceCsiRegistration registration) {
dns(Objects.requireNonNull(configMapName));
uuid(Objects.requireNonNull(uid));
require(sha256 != null && sha256.matches("[0-9a-f]{64}"));
}
}
public WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservationStore storage,
JdbcRuntimeBindingRepository bindings, WorkspaceCsiRegistration registration) {
this(storage, bindings, registration, null, null, null, List.of(), Duration.ofSeconds(30), List.of(), new HttpRuntimeTransport()); this(storage, bindings, registration, null, null, null, List.of(), Duration.ofSeconds(30), List.of(), new HttpRuntimeTransport());
} }
public WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservationStore storage, public WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservations storage,
JdbcRuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api, RuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api,
WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout) { WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout) {
this(storage, bindings, registration, api, guard, image, command, timeout, List.of()); this(storage, bindings, registration, api, guard, image, command, timeout, List.of());
} }
public WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservationStore storage, public WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservations storage,
JdbcRuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api, RuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api,
WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout, List<WorkerArtifact> artifacts) { WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout, List<WorkerArtifact> artifacts) {
this(storage, bindings, registration, Objects.requireNonNull(api), Objects.requireNonNull(guard), this(storage, bindings, registration, Objects.requireNonNull(api), Objects.requireNonNull(guard),
image, command, timeout, artifacts, new HttpRuntimeTransport()); image, command, timeout, artifacts, new HttpRuntimeTransport());
} }
public WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservationStore storage, public WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservations storage,
JdbcRuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api, RuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api,
WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout, WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout,
List<WorkerArtifact> artifacts, String authorityOrigin) { List<WorkerArtifact> artifacts, String authorityOrigin) {
this(storage, bindings, registration, Objects.requireNonNull(api), Objects.requireNonNull(guard), image, command, this(storage, bindings, registration, Objects.requireNonNull(api), Objects.requireNonNull(guard), image, command,
timeout, artifacts, new HttpRuntimeTransport(), ManagedCsiFilesProtocol.authority(authorityOrigin)); timeout, artifacts, new HttpRuntimeTransport(), ManagedCsiFilesProtocol.authority(authorityOrigin));
} }
WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservationStore storage, WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservations storage,
JdbcRuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api, RuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api,
WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout, WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout,
List<WorkerArtifact> artifacts, RuntimeTransport transport) { List<WorkerArtifact> artifacts, RuntimeTransport transport) {
this(storage, bindings, registration, api, guard, image, command, timeout, artifacts, transport, null); this(storage, bindings, registration, api, guard, image, command, timeout, artifacts, transport, null);
} }
WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservationStore storage, WorkspaceCsiRuntimeProvisioner(WorkspaceCsiReservations storage,
JdbcRuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api, RuntimeBindingRepository bindings, WorkspaceCsiRegistration registration, KubernetesRuntimeClient api,
WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout, WorkspaceCsiResourceGuard guard, String image, List<String> command, Duration timeout,
List<WorkerArtifact> artifacts, RuntimeTransport transport, Map<String, Object> authority) { List<WorkerArtifact> artifacts, RuntimeTransport transport, Map<String, Object> authority) {
this.storage = Objects.requireNonNull(storage); this.storage = Objects.requireNonNull(storage);
@@ -145,7 +136,7 @@ public final class WorkspaceCsiRuntimeProvisioner implements RuntimeProvisioner
@Override @Override
public String kind() { public String kind() {
return WorkspaceCsiReservationStore.PROVISIONER_KIND; return WorkspaceCsiReservations.PROVISIONER_KIND;
} }
@Override @Override
@@ -264,7 +255,7 @@ public final class WorkspaceCsiRuntimeProvisioner implements RuntimeProvisioner
} catch (RuntimeException error) { } catch (RuntimeException error) {
forgetPlacement(request, seed, handle); forgetPlacement(request, seed, handle);
return error instanceof RuntimeBrokerException failure && failure.isRetryable() return error instanceof RuntimeBrokerException failure && failure.isRetryable()
|| error instanceof DataAccessException || error instanceof TransactionException || error instanceof com.alibaba.qwen.code.runtimebroker.StoreFailureException
? RuntimeObservation.unknown(handle) : RuntimeObservation.conflict(handle); ? RuntimeObservation.unknown(handle) : RuntimeObservation.conflict(handle);
} }
}); });
@@ -523,7 +514,7 @@ public final class WorkspaceCsiRuntimeProvisioner implements RuntimeProvisioner
} }
} }
private Map<String, Object> storageTuple(WorkspaceCsiReservationStore.Reservation reservation) { private Map<String, Object> storageTuple(WorkspaceCsiReservations.Reservation reservation) {
var result = new LinkedHashMap<String, Object>(); var result = new LinkedHashMap<String, Object>();
result.putAll(Map.of("clusterDomain", registration.clusterDomain(), "namespace", registration.namespace(), result.putAll(Map.of("clusterDomain", registration.clusterDomain(), "namespace", registration.namespace(),
"pvcUid", registration.pvcUid(), "pvUid", registration.pvUid(), "driver", registration.driver(), "pvcUid", registration.pvcUid(), "pvUid", registration.pvUid(), "driver", registration.driver(),
@@ -534,7 +525,7 @@ public final class WorkspaceCsiRuntimeProvisioner implements RuntimeProvisioner
return result; return result;
} }
private Map<String, Object> protection(WorkspaceCsiResourceGuard.Receipt receipt) { private Map<String, Object> protection(com.alibaba.qwen.code.managedagent.contracts.runtime.Receipt receipt) {
require(registration.physicalKey().equals(receipt.physicalKey()) && registration.revision() == receipt.registrationRevision()); require(registration.physicalKey().equals(receipt.physicalKey()) && registration.revision() == receipt.registrationRevision());
var pin = receipt.protection(); var pin = receipt.protection();
return Map.of("namespaceUid", pin.namespaceUid(), "policyName", pin.policyName(), "policyUid", pin.policyUid(), return Map.of("namespaceUid", pin.namespaceUid(), "policyName", pin.policyName(), "policyUid", pin.policyUid(),
@@ -1,5 +1,6 @@
package com.alibaba.qwen.code.managedagent.workspace; package com.alibaba.qwen.code.managedagent.adapters.workspace;
import com.alibaba.qwen.code.managedagent.contracts.workspace.ChildWorkspaceException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.IOException; import java.io.IOException;
@@ -41,11 +42,9 @@ import java.util.regex.Pattern;
* repository's own config was read as data and found free of keys that * repository's own config was read as data and found free of keys that
* name a program (docs/design/2026-10-09-managed-child-workspace.md). * name a program (docs/design/2026-10-09-managed-child-workspace.md).
*/ */
public final class ChildWorktreeGit { public final class ChildWorktreeGit implements com.alibaba.qwen.code.managedagent.contracts.workspace.ChildWorktreeGit {
/** The reserved directory below the storage root that holds every child Workspace. */ /** The reserved directory below the storage root that holds every child Workspace. */
public static final String CONTAINER = ".qwen-child-workspaces";
static final String PIN_PREFIX = "refs/qwen/child-workspaces/";
static final int MAX_CONFLICT_PATHS = 100;
private static final int MIN_MAJOR = 2; private static final int MIN_MAJOR = 2;
private static final int MIN_MINOR = 40; private static final int MIN_MINOR = 40;
private static final String GITLINK = "160000"; private static final String GITLINK = "160000";
@@ -102,19 +101,10 @@ public final class ChildWorktreeGit {
} }
/** The located repository a step works in, every path real and inside the storage root. */ /** The located repository a step works in, every path real and inside the storage root. */
public record Repository(Path top, Path gitDir, Path commonDir) {
}
/** Where the parent's repository lies, relative to the storage root and to its own top. */ /** Where the parent's repository lies, relative to the storage root and to its own top. */
public record Layout(String repositoryRelative, String offset) {
}
/** The three-way merge of decision 5: a clean tree, or the conflicting paths. */ /** The three-way merge of decision 5: a clean tree, or the conflicting paths. */
public record Merge(String parentTree, String mergedTree, List<String> conflicts) {
public boolean clean() {
return conflicts.isEmpty();
}
}
/** /**
* The Git version, refused below 2.40, the first release whose * The Git version, refused below 2.40, the first release whose
@@ -175,15 +165,8 @@ public final class ChildWorktreeGit {
} }
/** The child Workspace's directory, relative to the storage root. */ /** The child Workspace's directory, relative to the storage root. */
public static String childDirectory(String childWorkspaceId) {
return CONTAINER + "/" + childWorkspaceId;
}
/** The child Session's working directory, relative to the storage root. */ /** The child Session's working directory, relative to the storage root. */
public static String childCwd(String childWorkspaceId, String offset) {
return ".".equals(offset) ? childDirectory(childWorkspaceId)
: childDirectory(childWorkspaceId) + "/" + offset;
}
/** /**
* Locates the repository of the parent's working directory and checks * Locates the repository of the parent's working directory and checks
@@ -1558,4 +1541,8 @@ public final class ChildWorktreeGit {
// Scratch only. // Scratch only.
} }
} }
@Override
public <T> T runWhileLive(BooleanSupplier live, Supplier<T> work) {
return whileLive(live, work);
}
} }
@@ -0,0 +1,84 @@
package com.alibaba.qwen.code.managedagent.adapters.workspace;
import com.alibaba.qwen.code.managedagent.contracts.workspace.RecoveryWorker;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceRecoveryProtocol.check;
import static com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceRecoveryProtocol.text;
public final class LocalRecoveryWorker implements RecoveryWorker {
private static final com.fasterxml.jackson.databind.ObjectMapper JSON = new com.fasterxml.jackson.databind.ObjectMapper();
public void execute(JsonNode request, Session store,
java.util.function.BiFunction<String, JsonNode, JsonNode> calls) throws Exception {
var command = new ProcessBuilder(text(request, "nodeExecutable"), text(request, "cliEntry"),
"--workspace-recovery-worker").redirectError(ProcessBuilder.Redirect.INHERIT);
command.environment().keySet().removeIf(key -> key.startsWith("W1_") || key.startsWith("OSS_"));
Process worker;
try {
worker = command.start();
} catch (java.io.IOException error) {
store.workerFailed();
throw error;
}
try (var output = new BufferedInputStream(worker.getInputStream());
var replies = new BufferedWriter(new OutputStreamWriter(worker.getOutputStream(), StandardCharsets.UTF_8))) {
long lastId = 0;
byte[] line;
while ((line = message(output)) != null) {
JsonNode call = com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceRecoveryProtocol.parse(line);
long callId = com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceRecoveryProtocol.positive(call, "id");
check(callId == lastId + 1, "worker_protocol_error");
lastId = callId;
ObjectNode response = JSON.createObjectNode().put("id", callId);
try {
response.set("result", calls.apply(text(call, "method"), call.path("params")));
} catch (RuntimeException error) {
String code = error instanceof com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceRecoveryProtocol.RecoveryFailure failure
? failure.code : "recovery_read_failed";
if (!(error instanceof com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceRecoveryProtocol.RecoveryFailure)) {
store.workerFailed();
}
response.putObject("error").put("code", code).put("message", "Workspace recovery: " + code);
}
replies.write(response.toString());
replies.newLine();
replies.flush();
}
check(worker.waitFor(30, TimeUnit.SECONDS) && worker.exitValue() == 0, "worker_failed");
JsonNode receipt = store.inspect();
check(Set.of("SEALED", "VERIFIED").contains(receipt.path("state").asText()), "worker_incomplete");
} catch (Exception error) {
store.workerFailed();
throw error;
} finally {
if (worker.isAlive()) {
worker.destroyForcibly();
}
}
}
private static byte[] message(InputStream input) throws java.io.IOException {
var bytes = new ByteArrayOutputStream();
int next;
while ((next = input.read()) != -1 && next != '\n') {
check(bytes.size() < 16 * 1024 * 1024, "worker_message_too_large");
bytes.write(next);
}
check(next != -1 || bytes.size() == 0, "worker_protocol_error");
return next == -1 ? null : bytes.toByteArray();
}
public void validateStateDirectory(java.nio.file.Path directory) throws java.io.IOException {
com.alibaba.qwen.code.runtimebroker.provisioning.LocalProcessRuntimeProvisioner.validateStateDirectory(directory);
}
}
@@ -0,0 +1,11 @@
package com.alibaba.qwen.code.managedagent.adapters.workspace;
import com.alibaba.qwen.code.managedagent.contracts.workspace.WorktreeFiles;
import com.alibaba.qwen.code.managedagent.repository.SessionWorktreeStore.Plan;
import java.nio.file.Path;
public final class LocalWorktreeFiles implements WorktreeFiles {
public String verify(Plan plan, String savedIdentity) { return SessionWorktreeGit.verify(plan, savedIdentity); }
public void requireRoot(Plan plan) { SessionWorktreeGit.requireRoot(plan); }
public String currentBranch(String verifiedIdentity) { return SessionWorktreeGit.currentBranch(verifiedIdentity); }
public String identity(Path path) { return SessionWorktreeGit.identity(path); }
}
@@ -1,9 +1,9 @@
package com.alibaba.qwen.code.managedagent.workspace; package com.alibaba.qwen.code.managedagent.adapters.workspace;
import com.alibaba.qwen.code.managedagent.contracts.workspace.ChildWorkspaceException;
import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException; import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException;
import com.alibaba.qwen.code.managedagent.persistence.SessionWorktreeStore.Plan; import com.alibaba.qwen.code.managedagent.repository.SessionWorktreeStore.Plan;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -16,7 +16,7 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public final class SessionWorktreeGit implements AutoCloseable { public final class SessionWorktreeGit implements com.alibaba.qwen.code.managedagent.contracts.workspace.SessionWorktreeOperations {
public static final String CONTAINER = ".qwen-managed-worktrees"; public static final String CONTAINER = ".qwen-managed-worktrees";
private static final ObjectMapper JSON = new ObjectMapper() private static final ObjectMapper JSON = new ObjectMapper()
.enable(com.fasterxml.jackson.databind.DeserializationFeature.USE_LONG_FOR_INTS); .enable(com.fasterxml.jackson.databind.DeserializationFeature.USE_LONG_FOR_INTS);
@@ -27,9 +27,6 @@ public final class SessionWorktreeGit implements AutoCloseable {
git.requireSupportedVersion(); git.requireSupportedVersion();
} }
public record Source(String repositoryRelative, String offset, String commonDirectory,
String commonIdentity, String commit, List<String> branches) { }
public Source source(Path root, String cwd, String baseRef) { public Source source(Path root, String cwd, String baseRef) {
if (cwd.split("/")[0].equals(CONTAINER)) { if (cwd.split("/")[0].equals(CONTAINER)) {
throw error("worktree_source_private", "Select the original project directory."); throw error("worktree_source_private", "Select the original project directory.");
@@ -318,11 +315,12 @@ public final class SessionWorktreeGit implements AutoCloseable {
} }
public static ManagedAgentException error(String code, String message) { public static ManagedAgentException error(String code, String message) {
return new ManagedAgentException(HttpStatus.CONFLICT, code, message); return new ManagedAgentException(409, code, message);
} }
@Override @Override
public void close() { public void close() {
git.close(); git.close();
} }
public <T> T runWhileLive(java.util.function.BooleanSupplier live, java.util.function.Supplier<T> work) { return git.runWhileLive(live, work); }
} }
@@ -0,0 +1,33 @@
package com.alibaba.qwen.code.managedagent.harness;
import com.alibaba.qwen.code.daemon.HostedHarnessClient;
import com.alibaba.qwen.code.managedagent.harness.contracts.HarnessCapabilities;
import java.net.URI;
import java.time.Duration;
public final class HarnessNegotiation implements AutoCloseable {
private final HostedHarnessClient client;
public HarnessNegotiation(URI uri, String token, String digest) {
this(uri, token, digest, null);
}
public HarnessNegotiation(URI uri, String token, String digest, Duration timeout) {
try {
var builder = HostedHarnessClient.builder().baseUri(uri).bearerToken(token).capabilityDigest(digest);
if (timeout != null) {
builder.connectTimeout(timeout).requestTimeout(timeout);
}
client = builder.build();
} catch (RuntimeException failure) {
throw HarnessSdkValues.runtime(failure);
}
}
public HarnessCapabilities capabilities() {
try {
var value = client.capabilities();
return new HarnessCapabilities(value.getCurrentProtocolVersion(), value.getSupportedProtocolVersions(), value.getBootId(), value.getInstanceId(), value.getCapabilityDigest(), value.getLifecycleProtocolVersion(), value.getFeatures(), value.getTitleProtocolVersion(), value.getLifecycleV2Profiles());
} catch (RuntimeException failure) {
throw HarnessSdkValues.runtime(failure);
}
}
public void close() { client.close(); }
}
@@ -6,16 +6,16 @@ import com.alibaba.qwen.code.daemon.HostedHarnessClient;
import com.alibaba.qwen.code.daemon.HostedHarnessGenerationException; import com.alibaba.qwen.code.daemon.HostedHarnessGenerationException;
import com.alibaba.qwen.code.daemon.HostedHarnessPlacement; import com.alibaba.qwen.code.daemon.HostedHarnessPlacement;
import com.alibaba.qwen.code.daemon.MutationOutcomeUnknownException; import com.alibaba.qwen.code.daemon.MutationOutcomeUnknownException;
import com.alibaba.qwen.code.managedagent.config.ManagedAgentProperties; import com.alibaba.qwen.code.managedagent.contracts.harness.HarnessConnector;
import com.alibaba.qwen.code.managedagent.contracts.security.WriterCredentialPolicy;
import com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceToolProfiles;
import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException; import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException;
import com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord; import com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord;
import com.alibaba.qwen.code.managedagent.persistence.HarnessPlacementStore; import com.alibaba.qwen.code.managedagent.repository.HarnessPlacementStore;
import com.alibaba.qwen.code.managedagent.persistence.HarnessPlacementStore.Placement; import com.alibaba.qwen.code.managedagent.repository.HarnessPlacementStore.Placement;
import com.alibaba.qwen.code.managedagent.persistence.ManagedSessionSkills; import com.alibaba.qwen.code.managedagent.repository.ManagedSessionSkills;
import com.alibaba.qwen.code.managedagent.security.WriterCredentialPolicy; import com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings;
import com.alibaba.qwen.code.managedagent.workspace.WorkspaceToolProfiles;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.http.HttpStatus;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
@@ -33,7 +33,7 @@ import java.util.function.Function;
/** Sticky routing over independently fenced Hosted process incarnations. */ /** Sticky routing over independently fenced Hosted process incarnations. */
public class HarnessPoolConnector implements HarnessConnector { public class HarnessPoolConnector implements HarnessConnector {
private final ManagedAgentProperties properties; private final ManagedAgentSettings properties;
private final HarnessPlacementStore placements; private final HarnessPlacementStore placements;
private final WriterCredentialPolicy credentials; private final WriterCredentialPolicy credentials;
private final HarnessDiscovery discovery; private final HarnessDiscovery discovery;
@@ -82,13 +82,13 @@ public class HarnessPoolConnector implements HarnessConnector {
private record Selected(Placement placement, Member member, QwenHostedHarnessConnector.PoolCall call) { private record Selected(Placement placement, Member member, QwenHostedHarnessConnector.PoolCall call) {
} }
public HarnessPoolConnector(ManagedAgentProperties properties, HarnessPlacementStore placements, public HarnessPoolConnector(ManagedAgentSettings properties, HarnessPlacementStore placements,
HarnessDiscovery discovery, Function<HarnessDiscovery.Endpoint, QwenHostedHarnessConnector> connectors, HarnessDiscovery discovery, Function<HarnessDiscovery.Endpoint, QwenHostedHarnessConnector> connectors,
ManagedSessionSkills skills) { ManagedSessionSkills skills) {
this(properties, placements, discovery, connectors, skills, HarnessTls.poolContext(properties.getHarness().getPool()), null); this(properties, placements, discovery, connectors, skills, HarnessTls.poolContext(properties.getHarness().getPool()), null);
} }
HarnessPoolConnector(ManagedAgentProperties properties, HarnessPlacementStore placements, HarnessPoolConnector(ManagedAgentSettings properties, HarnessPlacementStore placements,
HarnessDiscovery discovery, Function<HarnessDiscovery.Endpoint, QwenHostedHarnessConnector> connectors, HarnessDiscovery discovery, Function<HarnessDiscovery.Endpoint, QwenHostedHarnessConnector> connectors,
ManagedSessionSkills skills, SSLContext tls, ManagedSessionSkills skills, SSLContext tls,
Function<HarnessDiscovery.Endpoint, HostedHarnessClient> clients) { Function<HarnessDiscovery.Endpoint, HostedHarnessClient> clients) {
@@ -270,7 +270,7 @@ public class HarnessPoolConnector implements HarnessConnector {
: placements.takeoverForLifecycle(operation, placement, candidate.endpoint.instanceId(), candidate.owner().bootId()); : placements.takeoverForLifecycle(operation, placement, candidate.endpoint.instanceId(), candidate.owner().bootId());
} }
} catch (ManagedAgentException refusal) { } catch (ManagedAgentException refusal) {
if (!quiescingOwner || !member.reachable || refusal.getStatus() != HttpStatus.CONFLICT if (!quiescingOwner || !member.reachable || refusal.getStatus() != 409
|| !"harness_owner_still_live".equals(refusal.getCode())) { || !"harness_owner_still_live".equals(refusal.getCode())) {
throw refusal; throw refusal;
} }
@@ -294,7 +294,7 @@ public class HarnessPoolConnector implements HarnessConnector {
} }
private static ManagedAgentException unavailable() { private static ManagedAgentException unavailable() {
return new ManagedAgentException(HttpStatus.SERVICE_UNAVAILABLE, "harness_pool_unavailable", "No compatible Harness owner is available"); return new ManagedAgentException(503, "harness_pool_unavailable", "No compatible Harness owner is available");
} }
private <T> T call(String tenant, String session, Function<QwenHostedHarnessConnector, T> action) { private <T> T call(String tenant, String session, Function<QwenHostedHarnessConnector, T> action) {
@@ -0,0 +1,106 @@
package com.alibaba.qwen.code.managedagent.harness;
import com.alibaba.qwen.code.managedagent.harness.contracts.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.CompletionException;
final class HarnessSdkValues {
private static final ObjectMapper JSON = new ObjectMapper();
private HarnessSdkValues() { }
static HarnessRuntimeRecovery recovery(com.alibaba.qwen.code.daemon.HarnessRuntimeRecovery value) {
return value == null ? null : new HarnessRuntimeRecovery(value.getPhase(), value.getCheckpointId(),
value.getActivationId(), value.getExecutions().stream().map(execution ->
new HarnessRuntimeRecovery.Execution(execution.getFunctionCallId(), execution.getToolName(),
execution.getExecutionCallId(), execution.getRuntimeSessionId(), execution.getProgressCursor(),
execution.getOutcome(), execution.getStatus())).toList(),
value.hasUnknownOutcome(), value.isContinuationReady(), value.isCancellationReady());
}
static Throwable failure(Throwable error) {
if (error == null) {
return null;
}
Throwable cause = failure(error.getCause());
Throwable[] suppressed = java.util.Arrays.stream(error.getSuppressed())
.map(HarnessSdkValues::failure).toArray(Throwable[]::new);
boolean changed = cause != error.getCause()
|| !java.util.Arrays.equals(suppressed, error.getSuppressed());
Throwable mapped = mapFailure(error, cause);
if (mapped == error && changed) {
mapped = error instanceof CompletionException
? new CompletionException(error.getMessage(), cause)
: error instanceof java.util.concurrent.ExecutionException
? new java.util.concurrent.ExecutionException(error.getMessage(), cause)
: new HarnessFailure(error.getMessage(), cause);
}
if (mapped != error) {
mapped.setStackTrace(error.getStackTrace());
for (Throwable failure : suppressed) {
mapped.addSuppressed(failure);
}
}
return mapped;
}
private static Throwable mapFailure(Throwable error, Throwable cause) {
if (error instanceof com.alibaba.qwen.code.daemon.DaemonHttpException value) {
return new HarnessHttpFailure(value.getMessage(), cause, value.getStatusCode(), value.getResponseBody(), bodyFields(value));
}
if (error instanceof com.alibaba.qwen.code.daemon.DaemonProtocolException value) {
return new HarnessProtocolFailure(value.getMessage(), cause);
}
if (error instanceof com.alibaba.qwen.code.daemon.DaemonTransportException value) {
return new HarnessTransportFailure(value.getMessage(), cause);
}
if (error instanceof com.alibaba.qwen.code.daemon.HostedHarnessGenerationException value) {
return new HarnessGenerationFailure(value.getMessage(), cause, value.getExpectedBootId(), value.getActualBootId(), value.getCode());
}
if (error instanceof com.alibaba.qwen.code.daemon.HarnessSessionRefusedException value) {
return new HarnessSessionRefusedFailure(value.getMessage(), cause, value.getStatusCode(), value.getCode());
}
if (error instanceof com.alibaba.qwen.code.daemon.HostedHarnessCapabilityMismatchException value) {
return new HarnessCapabilityFailure(value.getMessage(), cause, value.getExpectedDigest(), value.getActualDigest(), value.getCode());
}
if (error instanceof com.alibaba.qwen.code.daemon.DaemonClientCapacityException value) {
return new HarnessCapacityFailure(value.getMessage(), cause);
}
if (error instanceof com.alibaba.qwen.code.daemon.MutationOutcomeUnknownException value) {
return new HarnessMutationUnknownFailure(value.getMessage(), cause, value.getOperation());
}
if (error instanceof com.alibaba.qwen.code.daemon.HostedHarnessSettleAnsweredException value) {
return new HarnessSettleAnsweredFailure(value.getMessage(), cause);
}
if (error instanceof com.alibaba.qwen.code.daemon.DaemonException value) {
return new HarnessFailure(value.getMessage(), cause);
}
return error;
}
static RuntimeException runtime(RuntimeException error) {
return (RuntimeException) failure(error);
}
private static Map<String, String> bodyFields(com.alibaba.qwen.code.daemon.DaemonHttpException error) {
Map<String, String> fields = new LinkedHashMap<>();
try {
var body = JSON.readTree(error.getResponseBody());
if (body != null && body.isObject()) {
body.fieldNames()
.forEachRemaining(
name -> {
String value = error.getBodyField(name);
if (value != null) {
fields.put(name, value);
}
});
}
} catch (java.io.IOException | RuntimeException malformed) {
return Map.of();
}
return fields;
}
}
@@ -1,6 +1,6 @@
package com.alibaba.qwen.code.managedagent.harness; package com.alibaba.qwen.code.managedagent.harness;
import com.alibaba.qwen.code.managedagent.config.ManagedAgentProperties; import com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings;
import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
@@ -18,7 +18,7 @@ public final class HarnessTls {
private HarnessTls() { private HarnessTls() {
} }
public static SSLContext poolContext(ManagedAgentProperties.HarnessPool properties) { public static SSLContext poolContext(ManagedAgentSettings.HarnessPool properties) {
if (properties.getKeyStoreFile().isBlank() && properties.getTrustStoreFile().isBlank()) { if (properties.getKeyStoreFile().isBlank() && properties.getTrustStoreFile().isBlank()) {
return null; return null;
} }
@@ -1,7 +1,7 @@
package com.alibaba.qwen.code.managedagent.harness; package com.alibaba.qwen.code.managedagent.harness;
import com.alibaba.qwen.code.managedagent.config.ManagedAgentProperties; import com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings;
import com.alibaba.qwen.code.runtimebroker.transport.HttpRuntimeTransport; import com.alibaba.qwen.code.runtimebroker.adapter.HttpRuntimeTransport;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicReference;
/** A bounded namespace-scoped list/watch; uncertainty never creates placement candidates. */ /** A bounded namespace-scoped list/watch; uncertainty never creates placement candidates. */
public final class KubernetesHarnessDiscovery implements HarnessDiscovery { public final class KubernetesHarnessDiscovery implements HarnessDiscovery {
private static final int RESPONSE_LIMIT = 2 * 1024 * 1024; private static final int RESPONSE_LIMIT = 2 * 1024 * 1024;
private final ManagedAgentProperties.HarnessPool properties; private final ManagedAgentSettings.HarnessPool properties;
private final ObjectMapper json; private final ObjectMapper json;
private final URI origin; private final URI origin;
private final HttpClient client; private final HttpClient client;
@@ -43,13 +43,13 @@ public final class KubernetesHarnessDiscovery implements HarnessDiscovery {
private volatile Snapshot snapshot = new Snapshot(List.of(), false); private volatile Snapshot snapshot = new Snapshot(List.of(), false);
private volatile boolean closed; private volatile boolean closed;
public KubernetesHarnessDiscovery(ManagedAgentProperties.HarnessPool properties, ObjectMapper json) { public KubernetesHarnessDiscovery(ManagedAgentSettings.HarnessPool properties, ObjectMapper json) {
this(properties, json, secureOrigin(properties.getKubernetesApiUrl()), this(properties, json, secureOrigin(properties.getKubernetesApiUrl()),
HttpClient.newBuilder().sslContext(HarnessTls.kubernetesContext(Path.of(properties.getKubernetesCaFile()))) HttpClient.newBuilder().sslContext(HarnessTls.kubernetesContext(Path.of(properties.getKubernetesCaFile())))
.connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build()); .connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build());
} }
KubernetesHarnessDiscovery(ManagedAgentProperties.HarnessPool properties, ObjectMapper json, URI origin, HttpClient client) { KubernetesHarnessDiscovery(ManagedAgentSettings.HarnessPool properties, ObjectMapper json, URI origin, HttpClient client) {
label(properties.getNamespace()); label(properties.getNamespace());
label(properties.getServiceName()); label(properties.getServiceName());
label(properties.getPortName()); label(properties.getPortName());
@@ -192,7 +192,7 @@ public final class KubernetesHarnessDiscovery implements HarnessDiscovery {
} }
} }
static List<Endpoint> endpoints(ManagedAgentProperties.HarnessPool properties, Iterable<JsonNode> slices) throws IOException { static List<Endpoint> endpoints(ManagedAgentSettings.HarnessPool properties, Iterable<JsonNode> slices) throws IOException {
Map<String, Endpoint> result = new LinkedHashMap<>(); Map<String, Endpoint> result = new LinkedHashMap<>();
for (JsonNode slice : slices) { for (JsonNode slice : slices) {
JsonNode metadata = slice.path("metadata"); JsonNode metadata = slice.path("metadata");
@@ -17,19 +17,21 @@ import com.alibaba.qwen.code.daemon.PromptReceipt;
import com.alibaba.qwen.code.daemon.SessionCreationOutcomeUnknownException; import com.alibaba.qwen.code.daemon.SessionCreationOutcomeUnknownException;
import com.alibaba.qwen.code.daemon.StreamHarnessEvents; import com.alibaba.qwen.code.daemon.StreamHarnessEvents;
import com.alibaba.qwen.code.daemon.SubmitHarnessTurn; import com.alibaba.qwen.code.daemon.SubmitHarnessTurn;
import com.alibaba.qwen.code.managedagent.catalog.AgentDefinitionExtensions; import com.alibaba.qwen.code.managedagent.contracts.catalog.AgentDefinitionExtensions;
import com.alibaba.qwen.code.managedagent.config.ManagedAgentProperties; import com.alibaba.qwen.code.managedagent.contracts.harness.HarnessConnector;
import com.alibaba.qwen.code.managedagent.contracts.harness.HostedHarnessRecoveryDeclinedException;
import com.alibaba.qwen.code.managedagent.contracts.security.WriterCredentialPolicy;
import com.alibaba.qwen.code.managedagent.contracts.session.AgentStateStore;
import com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceToolProfiles;
import com.alibaba.qwen.code.managedagent.model.StoreModels; import com.alibaba.qwen.code.managedagent.model.StoreModels;
import com.alibaba.qwen.code.managedagent.model.StoreModels.SessionRecord; import com.alibaba.qwen.code.managedagent.model.StoreModels.SessionRecord;
import com.alibaba.qwen.code.managedagent.persistence.ManagedActionStore; import com.alibaba.qwen.code.managedagent.repository.ManagedActionStore;
import com.alibaba.qwen.code.managedagent.persistence.ManagedAgentDefinitionStore; import com.alibaba.qwen.code.managedagent.repository.ManagedAgentDefinitionStore;
import com.alibaba.qwen.code.managedagent.persistence.ManagedSessionSkills; import com.alibaba.qwen.code.managedagent.repository.ManagedSessionSkills;
import com.alibaba.qwen.code.managedagent.persistence.ManagedSessionStore; import com.alibaba.qwen.code.managedagent.repository.ManagedSessionStore;
import com.alibaba.qwen.code.managedagent.persistence.SshWorkspaceStore; import com.alibaba.qwen.code.managedagent.repository.SshWorkspaceStore;
import com.alibaba.qwen.code.managedagent.persistence.WorkspaceExecutionStore; import com.alibaba.qwen.code.managedagent.repository.WorkspaceExecutionStore;
import com.alibaba.qwen.code.managedagent.security.WriterCredentialPolicy; import com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings;
import com.alibaba.qwen.code.managedagent.session.AgentStateStore;
import com.alibaba.qwen.code.managedagent.workspace.WorkspaceToolProfiles;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -48,10 +50,10 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier; import java.util.function.Supplier;
public class QwenHostedHarnessConnector implements HarnessConnector { public class QwenHostedHarnessConnector implements HarnessConnector {
private final ManagedAgentProperties settings; private final ManagedAgentSettings settings;
private final ManagedAgentProperties.Harness properties; private final ManagedAgentSettings.Harness properties;
private final ManagedAgentProperties.SessionStore sessionStore; private final ManagedAgentSettings.SessionStore sessionStore;
private final ManagedAgentProperties.ToolPublication toolPublication; private final ManagedAgentSettings.ToolPublication toolPublication;
private final String workspaceId; private final String workspaceId;
private final AgentStateStore sessions; private final AgentStateStore sessions;
private final WorkspaceExecutionStore workspaceExecution; private final WorkspaceExecutionStore workspaceExecution;
@@ -64,7 +66,7 @@ public class QwenHostedHarnessConnector implements HarnessConnector {
@org.springframework.beans.factory.annotation.Autowired @org.springframework.beans.factory.annotation.Autowired
private com.fasterxml.jackson.databind.ObjectMapper extensionJson; private com.fasterxml.jackson.databind.ObjectMapper extensionJson;
@org.springframework.beans.factory.annotation.Autowired @org.springframework.beans.factory.annotation.Autowired
private com.alibaba.qwen.code.managedagent.persistence.ManagedExtensionRecordStore extensions; private com.alibaba.qwen.code.managedagent.repository.ManagedExtensionRecordStore extensions;
private final WriterCredentialPolicy credentials; private final WriterCredentialPolicy credentials;
private final SshWorkspaceStore ssh; private final SshWorkspaceStore ssh;
/** #13753 I2: this control plane serves child Workspaces (startup checks the shape). */ /** #13753 I2: this control plane serves child Workspaces (startup checks the shape). */
@@ -88,12 +90,16 @@ public class QwenHostedHarnessConnector implements HarnessConnector {
this.client = negotiated; this.client = negotiated;
} }
public void configureExtensionResources(com.alibaba.qwen.code.managedagent.persistence.ManagedExtensionRecordStore records, public void configureExtensionResources(com.alibaba.qwen.code.managedagent.repository.ManagedExtensionRecordStore records,
com.fasterxml.jackson.databind.ObjectMapper json) { com.fasterxml.jackson.databind.ObjectMapper json) {
this.extensions = records; this.extensions = records;
this.extensionJson = json; this.extensionJson = json;
} }
public void configureSessionSkills(ManagedSessionSkills skills) {
this.sessionSkills = skills;
}
<T> T withPlacement(PoolCall call, Supplier<T> action) { <T> T withPlacement(PoolCall call, Supplier<T> action) {
PoolCall previous = poolCall.get(); PoolCall previous = poolCall.get();
poolCall.set(call); poolCall.set(call);
@@ -150,27 +156,27 @@ public class QwenHostedHarnessConnector implements HarnessConnector {
private final Map<AttachmentKey, AttachmentLock> attachmentLocks = private final Map<AttachmentKey, AttachmentLock> attachmentLocks =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
public QwenHostedHarnessConnector(ManagedAgentProperties properties, public QwenHostedHarnessConnector(ManagedAgentSettings properties,
AgentStateStore sessions, WorkspaceExecutionStore workspaceExecution) { AgentStateStore sessions, WorkspaceExecutionStore workspaceExecution) {
this(properties, sessions, workspaceExecution, null); this(properties, sessions, workspaceExecution, null);
} }
public QwenHostedHarnessConnector( public QwenHostedHarnessConnector(
ManagedAgentProperties properties, ManagedAgentSettings properties,
AgentStateStore sessions, AgentStateStore sessions,
WorkspaceExecutionStore workspaceExecution, WorkspaceExecutionStore workspaceExecution,
ManagedActionStore actions) { ManagedActionStore actions) {
this(properties, sessions, workspaceExecution, actions, null, null); this(properties, sessions, workspaceExecution, actions, null, null);
} }
public QwenHostedHarnessConnector(ManagedAgentProperties properties, public QwenHostedHarnessConnector(ManagedAgentSettings properties,
AgentStateStore sessions, WorkspaceExecutionStore workspaceExecution, AgentStateStore sessions, WorkspaceExecutionStore workspaceExecution,
ManagedActionStore actions, ManagedAgentDefinitionStore definitions, ManagedActionStore actions, ManagedAgentDefinitionStore definitions,
ManagedSessionStore resources) { ManagedSessionStore resources) {
this(properties, sessions, workspaceExecution, actions, definitions, resources, null); this(properties, sessions, workspaceExecution, actions, definitions, resources, null);
} }
public QwenHostedHarnessConnector(ManagedAgentProperties properties, public QwenHostedHarnessConnector(ManagedAgentSettings properties,
AgentStateStore sessions, WorkspaceExecutionStore workspaceExecution, AgentStateStore sessions, WorkspaceExecutionStore workspaceExecution,
ManagedActionStore actions, ManagedAgentDefinitionStore definitions, ManagedActionStore actions, ManagedAgentDefinitionStore definitions,
ManagedSessionStore resources, SshWorkspaceStore ssh) { ManagedSessionStore resources, SshWorkspaceStore ssh) {
@@ -589,7 +595,7 @@ public class QwenHostedHarnessConnector implements HarnessConnector {
throw new IllegalStateException("Hosted Harness generation moved during passive load"); throw new IllegalStateException("Hosted Harness generation moved during passive load");
} }
return new Attachment(attached.getHarnessBootId(), return new Attachment(attached.getHarnessBootId(),
attached.getRuntimeRecovery(), HarnessSdkValues.recovery(attached.getRuntimeRecovery()),
attached.getHarnessLastEventId(), attached.getHarnessLastEventId(),
attached.getHarnessEventEpoch()); attached.getHarnessEventEpoch());
} }
@@ -815,7 +821,7 @@ public class QwenHostedHarnessConnector implements HarnessConnector {
var preparedClient = client(); var preparedClient = client();
if (!preparedClient.capabilities().getFeatures().contains("context-compression/1")) { if (!preparedClient.capabilities().getFeatures().contains("context-compression/1")) {
throw new com.alibaba.qwen.code.managedagent.exception.ManagedAgentException( throw new com.alibaba.qwen.code.managedagent.exception.ManagedAgentException(
org.springframework.http.HttpStatus.CONFLICT, "unsupported_feature", 409, "unsupported_feature",
"Hosted context compression is unavailable."); "Hosted context compression is unavailable.");
} }
return () -> { return () -> {
@@ -1846,7 +1852,7 @@ public class QwenHostedHarnessConnector implements HarnessConnector {
return attached; return attached;
}); });
return new Attachment(attached.getHarnessBootId(), return new Attachment(attached.getHarnessBootId(),
pendingRecovery.contains(key) ? attached.getRuntimeRecovery() pendingRecovery.contains(key) ? HarnessSdkValues.recovery(attached.getRuntimeRecovery())
: null, : null,
attached.getHarnessLastEventId(), attached.getHarnessLastEventId(),
attached.getHarnessEventEpoch()); attached.getHarnessEventEpoch());
@@ -1932,7 +1938,7 @@ public class QwenHostedHarnessConnector implements HarnessConnector {
// handed out again only until its continue or cancel has been // handed out again only until its continue or cancel has been
// admitted; after that a re-entered Turn just resumes its stream. // admitted; after that a re-entered Turn just resumes its stream.
return new Attachment(cached.getHarnessBootId(), return new Attachment(cached.getHarnessBootId(),
pendingRecovery.contains(key) ? cached.getRuntimeRecovery() pendingRecovery.contains(key) ? HarnessSdkValues.recovery(cached.getRuntimeRecovery())
: null, : null,
cached.getHarnessLastEventId(), cached.getHarnessLastEventId(),
cached.getHarnessEventEpoch()); cached.getHarnessEventEpoch());
@@ -0,0 +1,422 @@
package com.alibaba.qwen.code.managedagent.harness;
import com.alibaba.qwen.code.managedagent.contracts.harness.HarnessConnector;
import com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
/** Converts SDK failures at the adapter boundary, including deferred stream reads. */
public final class SdkHarnessBoundary implements HarnessConnector {
private final HarnessConnector delegate;
public SdkHarnessBoundary(HarnessConnector delegate) {
this.delegate = delegate;
}
@Override
public boolean isAvailable() {
return call(delegate::isAvailable);
}
@Override
public void recoverForWake(String tenantId, String sessionId) {
run(() -> delegate.recoverForWake(tenantId, sessionId));
}
@Override
public boolean supportsPublicChildAgents() {
return call(delegate::supportsPublicChildAgents);
}
@Override
public String closeSession(OperationRecord operation) {
return call(() -> delegate.closeSession(operation));
}
@Override
public void runLifecycleChildOperation(OperationRecord operation, Map<String, Object> body) {
run(() -> delegate.runLifecycleChildOperation(operation, body));
}
@Override
public Map<String, Object> drainInstance(String instanceId, String operationId) {
return call(() -> delegate.drainInstance(instanceId, operationId));
}
@Override
public Map<String, Object> releaseOwnership(String tenantId, String sessionId,
String operationId, long expectedPlacementEpoch) {
return call(() -> delegate.releaseOwnership(tenantId, sessionId, operationId, expectedPlacementEpoch));
}
@Override
public boolean supportsApprovalSelection() {
return call(delegate::supportsApprovalSelection);
}
@Override
public boolean supportsLiveApprovalSelection() {
return call(delegate::supportsLiveApprovalSelection);
}
@Override
public boolean supportsAutoApproval() {
return call(delegate::supportsAutoApproval);
}
@Override
public boolean supportsPermissionRules() {
return call(delegate::supportsPermissionRules);
}
@Override
public boolean supportsAgentDefinitionApprovalSelection() {
return call(delegate::supportsAgentDefinitionApprovalSelection);
}
@Override
public boolean supportsModelSelection() {
return call(delegate::supportsModelSelection);
}
@Override
public boolean supportsUserMessageQueue() {
return call(delegate::supportsUserMessageQueue);
}
@Override
public boolean supportsContextCompression() {
return call(delegate::supportsContextCompression);
}
@Override
public ContextCompressionSubmission prepareContextCompression(String tenantId, String sessionId,
Map<String, Object> body) {
return call(() -> {
var prepared = delegate.prepareContextCompression(tenantId, sessionId, body);
return () -> call(prepared::submit);
});
}
@Override
public Map<String, Object> runContextCompression(String tenantId, String sessionId,
Map<String, Object> body) {
return call(() -> delegate.runContextCompression(tenantId, sessionId, body));
}
@Override
public Map<String, Object> readContextCompression(String tenantId, String sessionId,
String operationId) {
return call(() -> delegate.readContextCompression(tenantId, sessionId, operationId));
}
@Override
public boolean supportsPlanMode() {
return call(delegate::supportsPlanMode);
}
@Override
public boolean supportsInputAttachments() {
return call(delegate::supportsInputAttachments);
}
@Override
public boolean supportsSessionSkills() {
return call(delegate::supportsSessionSkills);
}
@Override
public boolean supportsMcpManagement() {
return call(delegate::supportsMcpManagement);
}
@Override
public Map<String, Object> mcpManagement(String tenantId, String sessionId) {
return call(() -> delegate.mcpManagement(tenantId, sessionId));
}
@Override
public Map<String, Object> manageMcp(String tenantId, String sessionId, Map<String, Object> command) {
return call(() -> delegate.manageMcp(tenantId, sessionId, command));
}
@Override
public Map<String, Object> mcpManagementOperation(String tenantId, String sessionId, String operationId) {
return call(() -> delegate.mcpManagementOperation(tenantId, sessionId, operationId));
}
@Override
public boolean supportsSessionExtensions() {
return call(delegate::supportsSessionExtensions);
}
@Override
public Map<String, Object> applyExtensions(String tenantId, String sessionId, Map<String, Object> command) {
return call(() -> delegate.applyExtensions(tenantId, sessionId, command));
}
@Override
public Map<String, Object> continueExtensionOperation(String tenantId, String sessionId, String operationId, boolean recoverPrevious) {
return call(() -> delegate.continueExtensionOperation(tenantId, sessionId, operationId, recoverPrevious));
}
@Override
public boolean isWorkspaceFilesAvailable() {
return call(delegate::isWorkspaceFilesAvailable);
}
@Override
public boolean supportsLifecycle() {
return call(delegate::supportsLifecycle);
}
@Override
public boolean supportsLifecycle(String profile) {
return call(() -> delegate.supportsLifecycle(profile));
}
@Override
public void prepareLifecycleResources(OperationRecord operation) {
run(() -> delegate.prepareLifecycleResources(operation));
}
@Override
public void settleLifecycleResources(OperationRecord operation) {
run(() -> delegate.settleLifecycleResources(operation));
}
@Override
public boolean supportsFencedTitles() {
return call(delegate::supportsFencedTitles);
}
@Override
public JsonNode settleLifecycle(OperationRecord operation) {
return call(() -> delegate.settleLifecycle(operation));
}
@Override
public void detachLifecycle(OperationRecord operation) {
run(() -> delegate.detachLifecycle(operation));
}
@Override
public Attachment createOrLoad(String tenantId, String sessionId,
boolean loadExisting) {
return call(() -> delegate.createOrLoad(tenantId, sessionId, loadExisting));
}
@Override
public Attachment createOrLoad(String tenantId, String sessionId,
boolean loadExisting, boolean passiveManagedRuntimeRecovery) {
return call(() -> delegate.createOrLoad(tenantId, sessionId, loadExisting, passiveManagedRuntimeRecovery));
}
@Override
public Attachment recoverManagedRuntime(String tenantId, String sessionId,
boolean cancellation) {
return call(() -> delegate.recoverManagedRuntime(tenantId, sessionId, cancellation));
}
@Override
public Attachment recoverManagedRuntime(String tenantId, String sessionId,
boolean cancellation, String promptId) {
return call(() -> delegate.recoverManagedRuntime(tenantId, sessionId, cancellation, promptId));
}
@Override
public Attachment recoverManagedCancellation(String tenantId,
String sessionId) {
return call(() -> delegate.recoverManagedCancellation(tenantId, sessionId));
}
@Override
public Attachment recoverManagedCancellation(String tenantId,
String sessionId, String promptId) {
return call(() -> delegate.recoverManagedCancellation(tenantId, sessionId, promptId));
}
@Override
public Attachment recoverManagedFailureCancellation(String tenantId,
String sessionId, String turnId, String dispatchOwner) {
return call(() -> delegate.recoverManagedFailureCancellation(tenantId, sessionId, turnId, dispatchOwner));
}
@Override
public Attachment recoverManagedFailureCancellation(String tenantId,
String sessionId, String turnId, String dispatchOwner, String promptId) {
return call(() -> delegate.recoverManagedFailureCancellation(tenantId, sessionId, turnId, dispatchOwner, promptId));
}
@Override
public void discardManagedRecovery(String tenantId, String sessionId) {
run(() -> delegate.discardManagedRecovery(tenantId, sessionId));
}
@Override
public RecoveryStatus inspectRecovery(String tenantId, String sessionId, String promptId) {
return call(() -> delegate.inspectRecovery(tenantId, sessionId, promptId));
}
@Override
public Attachment cachedRecoveryAttachment(String tenantId, String sessionId) {
return call(() -> delegate.cachedRecoveryAttachment(tenantId, sessionId));
}
@Override
public boolean recheckRecoveryCleanup(String tenantId, String sessionId, String promptId) {
return call(() -> delegate.recheckRecoveryCleanup(tenantId, sessionId, promptId));
}
@Override
public Admission submit(String tenantId, String sessionId, String promptId,
List<Map<String, Object>> input, String payloadDigest) {
return call(() -> delegate.submit(tenantId, sessionId, promptId, input, payloadDigest));
}
@Override
public Admission continueManagedRuntime(String tenantId,
String sessionId, String promptId, String checkpointId,
String activationId) {
return call(() -> delegate.continueManagedRuntime(tenantId, sessionId, promptId, checkpointId, activationId));
}
@Override
public Admission cancelManagedRuntime(String tenantId, String sessionId,
String promptId, String checkpointId, String activationId) {
return call(() -> delegate.cancelManagedRuntime(tenantId, sessionId, promptId, checkpointId, activationId));
}
@Override
public SourceStream stream(String tenantId, String sessionId, long lastEventId,
String eventEpoch) {
return call(() -> streamBoundary(delegate.stream(tenantId, sessionId, lastEventId, eventEpoch)));
}
@Override
public SourceStream observeStream(String tenantId, String sessionId, String bootId,
long lastEventId, String eventEpoch) {
return call(() -> streamBoundary(delegate.observeStream(tenantId, sessionId, bootId, lastEventId, eventEpoch)));
}
@Override
public void resolveAction(
String tenantId,
String sessionId,
String actionId,
JsonNode response) {
run(() -> delegate.resolveAction(tenantId, sessionId, actionId, response));
}
@Override
public void cancel(String tenantId, String sessionId) {
run(() -> delegate.cancel(tenantId, sessionId));
}
@Override
public void cancelOriginalTurn(String tenantId, String sessionId, String promptId) {
run(() -> delegate.cancelOriginalTurn(tenantId, sessionId, promptId));
}
@Override
public Map<String, Object> runAutomationOperation(String tenantId,
String sessionId, Map<String, Object> body) {
return call(() -> delegate.runAutomationOperation(tenantId, sessionId, body));
}
@Override
public void runChildOperation(String tenantId, String sessionId,
Map<String, Object> body) {
run(() -> delegate.runChildOperation(tenantId, sessionId, body));
}
@Override
public Map<String, Object> runMessageOperation(String tenantId,
String sessionId, Map<String, Object> body) {
return call(() -> delegate.runMessageOperation(tenantId, sessionId, body));
}
@Override
public Map<String, Object> runChannelOperation(String tenantId,
String sessionId, Map<String, Object> body) {
return call(() -> delegate.runChannelOperation(tenantId, sessionId, body));
}
@Override
public void rename(String tenantId, String sessionId, String title) {
run(() -> delegate.rename(tenantId, sessionId, title));
}
@Override
public void rename(String tenantId, String sessionId, String title, long revision) {
run(() -> delegate.rename(tenantId, sessionId, title, revision));
}
@Override
public String closeSession(String tenantId, String sessionId) {
return call(() -> delegate.closeSession(tenantId, sessionId));
}
@Override
public List<AttachedSession> attachedSessions() {
return call(delegate::attachedSessions);
}
@Override
public String writerIdentity() {
return call(delegate::writerIdentity);
}
@Override
public boolean activateForWake(String tenantId, String sessionId) {
return call(() -> delegate.activateForWake(tenantId, sessionId));
}
@Override
public void close() {
run(delegate::close);
}
private static <T> T call(Supplier<T> operation) {
try {
return operation.get();
} catch (RuntimeException error) {
throw HarnessSdkValues.runtime(error);
}
}
private static void run(Runnable operation) {
try {
operation.run();
} catch (RuntimeException error) {
throw HarnessSdkValues.runtime(error);
}
}
private static SourceStream streamBoundary(SourceStream stream) {
return new SourceStream() {
@Override
public String eventEpoch() {
return call(stream::eventEpoch);
}
@Override
public SourceEvent next() {
return call(stream::next);
}
@Override
public boolean recoveryBlocked(long afterEventId) {
return call(() -> stream.recoveryBlocked(afterEventId));
}
@Override
public void close() {
run(stream::close);
}
};
}
}
@@ -0,0 +1,25 @@
package com.alibaba.qwen.code.managedagent.harness;
import com.alibaba.qwen.code.daemon.HostedPermissionRules;
import com.alibaba.qwen.code.daemon.SubmitHarnessTurn;
import com.alibaba.qwen.code.managedagent.harness.contracts.HarnessRules;
import java.util.List;
import java.util.Map;
public final class SdkHarnessRules implements HarnessRules {
@Override
public String payloadDigest(List<Map<String, Object>> input) {
return SubmitHarnessTurn.computePayloadDigest(input);
}
@Override
public Map<String, List<String>> permissionRules(Map<?, ?> rules) {
return HostedPermissionRules.copyOf(rules);
}
@Override
public Map<String, List<String>> nonEmptyPermissionRules(Map<String, List<String>> rules) {
return HostedPermissionRules.nonEmpty(rules);
}
}
@@ -1,5 +1,8 @@
package com.alibaba.qwen.code.managedagent.harness; package com.alibaba.qwen.code.managedagent.harness;
import com.alibaba.qwen.code.managedagent.contracts.harness.HarnessConnector;
import com.alibaba.qwen.code.managedagent.contracts.harness.HarnessDisabledException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.runtimebroker.transport; package com.alibaba.qwen.code.runtimebroker.adapter;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader; import com.alibaba.fastjson2.JSONReader;
@@ -18,11 +18,21 @@ import com.alibaba.qwen.code.runtimebroker.RuntimePublicationGrant;
import com.alibaba.qwen.code.runtimebroker.RuntimeScope; import com.alibaba.qwen.code.runtimebroker.RuntimeScope;
import com.alibaba.qwen.code.runtimebroker.RuntimeSession; import com.alibaba.qwen.code.runtimebroker.RuntimeSession;
import com.alibaba.qwen.code.runtimebroker.RuntimeSessionRecord; import com.alibaba.qwen.code.runtimebroker.RuntimeSessionRecord;
import com.alibaba.qwen.code.runtimebroker.RuntimeTransport;
import com.alibaba.qwen.code.runtimebroker.WorkspaceExecutionProfile; import com.alibaba.qwen.code.runtimebroker.WorkspaceExecutionProfile;
import com.alibaba.qwen.code.runtimebroker.managedworkspace.ContextBinding; import com.alibaba.qwen.code.runtimebroker.managedworkspace.ContextBinding;
import com.alibaba.qwen.code.runtimebroker.provisioning.ManagedContextProtocol; import com.alibaba.qwen.code.runtimebroker.protocol.ManagedContextProtocol;
import com.alibaba.qwen.code.runtimebroker.provisioning.ProviderRuntimeProtocol; import com.alibaba.qwen.code.runtimebroker.protocol.ProviderRuntimeProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.CsiFileFinalizeProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.CsiFileHistoryProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.CsiNativeReadbackProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.ManagedCsiFilesProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.ManagedCsiProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.ManagedExtensionCommandProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.ManagedExtensionProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.ManagedHookProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.ManagedMcpProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.ManagedMonitorProtocol;
import com.alibaba.qwen.code.runtimebroker.transport.ManagedShellProtocol;
import java.io.EOFException; import java.io.EOFException;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -54,7 +64,7 @@ import java.util.function.Function;
* their separate Session control protocol. MCP controls have their own * their separate Session control protocol. MCP controls have their own
* bounded envelope. * bounded envelope.
*/ */
public final class HttpRuntimeTransport implements RuntimeTransport { public final class HttpRuntimeTransport implements com.alibaba.qwen.code.runtimebroker.WorkspacePhysicalTransport {
static final int BODY_LIMIT_BYTES = 16 * 1024; static final int BODY_LIMIT_BYTES = 16 * 1024;
static final int TOOL_REQUEST_LIMIT_BYTES = 256 * 1024; static final int TOOL_REQUEST_LIMIT_BYTES = 256 * 1024;
static final int TOOL_RESULT_LIMIT_BYTES = 1024 * 1024; static final int TOOL_RESULT_LIMIT_BYTES = 1024 * 1024;
@@ -126,12 +136,6 @@ public final class HttpRuntimeTransport implements RuntimeTransport {
managedExtensionCacheDirectory, managedExtensionCgroupRoot) : null); managedExtensionCacheDirectory, managedExtensionCgroupRoot) : null);
} }
@FunctionalInterface
public interface ExtensionPreparation {
CompletionStage<Map<String, Object>> prepare(RuntimeBindingRecord runtime, String harnessSessionId,
Map<String, Object> operation);
}
public void configureExtensionPreparation(ExtensionPreparation preparation) { public void configureExtensionPreparation(ExtensionPreparation preparation) {
this.extensionPreparation = java.util.Objects.requireNonNull(preparation, "preparation"); this.extensionPreparation = java.util.Objects.requireNonNull(preparation, "preparation");
} }
@@ -1,8 +1,10 @@
package com.alibaba.qwen.code.runtimebroker.security; package com.alibaba.qwen.code.runtimebroker.adapter.security;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter; import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.qwen.code.runtimebroker.BrokerValues; import com.alibaba.qwen.code.runtimebroker.BrokerValues;
import com.alibaba.qwen.code.runtimebroker.security.ProtectedSecret;
import com.alibaba.qwen.code.runtimebroker.security.SecretProtector;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -2,7 +2,8 @@ package com.alibaba.qwen.code.runtimebroker.provisioning;
import com.alibaba.qwen.code.runtimebroker.JsonCodec; import com.alibaba.qwen.code.runtimebroker.JsonCodec;
import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException; import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException;
import com.alibaba.qwen.code.runtimebroker.transport.HttpRuntimeTransport; import com.alibaba.qwen.code.runtimebroker.adapter.HttpRuntimeTransport;
import com.alibaba.qwen.code.runtimebroker.spi.ExternalRuntimeClient;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
@@ -11,8 +11,8 @@ import com.alibaba.qwen.code.runtimebroker.RuntimeProvisionSeed;
import com.alibaba.qwen.code.runtimebroker.RuntimeProvisioner; import com.alibaba.qwen.code.runtimebroker.RuntimeProvisioner;
import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle; import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle;
import com.alibaba.qwen.code.runtimebroker.RuntimeScope; import com.alibaba.qwen.code.runtimebroker.RuntimeScope;
import com.alibaba.qwen.code.runtimebroker.transport.HttpRuntimeTransport; import com.alibaba.qwen.code.runtimebroker.adapter.HttpRuntimeTransport;
import com.alibaba.qwen.code.runtimebroker.spi.ExternalRuntimeClient;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import java.net.URI; import java.net.URI;
@@ -7,6 +7,9 @@ import com.alibaba.qwen.code.runtimebroker.RuntimeProvisionSeed;
import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle; import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle;
import com.alibaba.qwen.code.runtimebroker.RuntimeScope; import com.alibaba.qwen.code.runtimebroker.RuntimeScope;
import com.alibaba.qwen.code.runtimebroker.WorkspaceExecutionProfile; import com.alibaba.qwen.code.runtimebroker.WorkspaceExecutionProfile;
import com.alibaba.qwen.code.runtimebroker.protocol.ManagedContextProtocol;
import com.alibaba.qwen.code.runtimebroker.spi.ExternalRuntimeClient;
import com.alibaba.qwen.code.runtimebroker.spi.ExternalStorageRegistry;
import java.time.Duration; import java.time.Duration;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@@ -2,6 +2,7 @@ package com.alibaba.qwen.code.runtimebroker.provisioning;
import com.alibaba.qwen.code.runtimebroker.BrokerValues; import com.alibaba.qwen.code.runtimebroker.BrokerValues;
import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException; import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException;
import com.alibaba.qwen.code.runtimebroker.spi.KubernetesRuntimeClient;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
@@ -23,8 +24,8 @@ public final class KubernetesCsiLogReader {
public record Source(String podName, String podUid, String nodeName, String nodeUid, public record Source(String podName, String podUid, String nodeName, String nodeUid,
String daemonSetUid, String containerId, String imageId) { String daemonSetUid, String containerId, String imageId) {
public Source { public Source {
KubernetesHttpRuntimeClient.dnsSubdomain(podName); com.alibaba.qwen.code.runtimebroker.protocol.KubernetesNames.dnsSubdomain(podName);
KubernetesHttpRuntimeClient.dnsSubdomain(nodeName); com.alibaba.qwen.code.runtimebroker.protocol.KubernetesNames.dnsSubdomain(nodeName);
if (!podName.startsWith("csi-plugin-") || !uuid(podUid) || !uuid(nodeUid) || !uuid(daemonSetUid) if (!podName.startsWith("csi-plugin-") || !uuid(podUid) || !uuid(nodeUid) || !uuid(daemonSetUid)
|| containerId == null || !containerId.matches("containerd://[0-9a-f]{64}") || containerId == null || !containerId.matches("containerd://[0-9a-f]{64}")
|| imageId == null || imageId.length() > 512 || !imageId.matches("[^\\s]*sha256:[0-9a-f]{64}")) { || imageId == null || imageId.length() > 512 || !imageId.matches("[^\\s]*sha256:[0-9a-f]{64}")) {
@@ -4,7 +4,7 @@ import com.alibaba.qwen.code.runtimebroker.BrokerValues;
import com.alibaba.qwen.code.runtimebroker.JsonCodec; import com.alibaba.qwen.code.runtimebroker.JsonCodec;
import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException; import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException;
import com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity; import com.alibaba.qwen.code.runtimebroker.WorkspaceCsiRuntimeIdentity;
import com.alibaba.qwen.code.runtimebroker.transport.HttpRuntimeTransport; import com.alibaba.qwen.code.runtimebroker.adapter.HttpRuntimeTransport;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
@@ -24,17 +24,17 @@ import java.security.GeneralSecurityException;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.time.Duration; import java.time.Duration;
import java.util.Map;
import java.util.List;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage; import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** Bounded core-v1 API calls with rotating service-account tokens and explicit CA trust. */ /** Bounded core-v1 API calls with rotating service-account tokens and explicit CA trust. */
public final class KubernetesHttpRuntimeClient implements KubernetesRuntimeClient { public final class KubernetesHttpRuntimeClient implements com.alibaba.qwen.code.runtimebroker.spi.KubernetesMaintenanceClient {
private static final int RESPONSE_LIMIT = 1024 * 1024; private static final int RESPONSE_LIMIT = 1024 * 1024;
private static final int CONFIG_MAP_RESPONSE_LIMIT = RESPONSE_LIMIT + 64 * 1024; private static final int CONFIG_MAP_RESPONSE_LIMIT = RESPONSE_LIMIT + 64 * 1024;
private static final int DELETE_REQUEST_LIMIT = 16 * 1024; private static final int DELETE_REQUEST_LIMIT = 16 * 1024;
@@ -188,8 +188,6 @@ public final class KubernetesHttpRuntimeClient implements KubernetesRuntimeClien
return result; return result;
} }
public record DeleteResponse(int status, byte[] body, boolean complete) { }
@Override @Override
public CompletionStage<String> readCsiPodLog(String podName) { public CompletionStage<String> readCsiPodLog(String podName) {
dnsSubdomain(podName); dnsSubdomain(podName);
@@ -335,4 +333,20 @@ public final class KubernetesHttpRuntimeClient implements KubernetesRuntimeClien
return new RuntimeBrokerException(status >= 400 && status <= 599 ? status : 502, return new RuntimeBrokerException(status >= 400 && status <= 599 ? status : 502,
"runtime_kubernetes_api_failed", "Kubernetes API request failed.", retryable); "runtime_kubernetes_api_failed", "Kubernetes API request failed.", retryable);
} }
public java.util.concurrent.CompletionStage<Void> verifyProtection(
com.alibaba.qwen.code.managedagent.contracts.persistence.WorkspaceCsiRegistration registration,
com.alibaba.qwen.code.managedagent.contracts.runtime.ProtectionIdentity protection) {
return new com.alibaba.qwen.code.managedagent.adapters.runtime.WorkspaceCsiResourceGuard(
this, registration.clusterDomain(), registration, protection).verify().thenApply(receipt -> null);
}
public com.alibaba.qwen.code.managedagent.contracts.workspace.ResidentReclamation.Baseline reclamationBaseline(
com.alibaba.qwen.code.runtimebroker.RuntimeBindingRecord original,
com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings.RuntimeBroker.ResidentWorkspace selected,
String finalizer, java.time.Instant now) { return com.alibaba.qwen.code.managedagent.adapters.runtime.ResidentWorkspaceReclamation.prepare(original, selected, finalizer, this, now); }
public com.alibaba.qwen.code.managedagent.contracts.workspace.ResidentReclamation.Receipt reclamationReceipt(
com.alibaba.qwen.code.runtimebroker.RuntimeBindingRecord original,
com.alibaba.qwen.code.managedagent.settings.ManagedAgentSettings.RuntimeBroker.ResidentWorkspace selected,
com.alibaba.qwen.code.managedagent.contracts.workspace.ResidentReclamation.Baseline baseline,
Map<String, Object> pod, java.time.Instant now) { return com.alibaba.qwen.code.managedagent.adapters.runtime.ResidentWorkspaceReclamation.complete(original, selected, baseline, pod, this, now); }
} }
@@ -11,7 +11,8 @@ import com.alibaba.qwen.code.runtimebroker.RuntimeProvisionSeed;
import com.alibaba.qwen.code.runtimebroker.RuntimeProvisioner; import com.alibaba.qwen.code.runtimebroker.RuntimeProvisioner;
import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle; import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle;
import com.alibaba.qwen.code.runtimebroker.RuntimeScope; import com.alibaba.qwen.code.runtimebroker.RuntimeScope;
import com.alibaba.qwen.code.runtimebroker.transport.HttpRuntimeTransport; import com.alibaba.qwen.code.runtimebroker.adapter.HttpRuntimeTransport;
import com.alibaba.qwen.code.runtimebroker.spi.KubernetesRuntimeClient;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.URI; import java.net.URI;
@@ -65,7 +66,7 @@ public final class KubernetesRuntimeProvisioner implements RuntimeProvisioner {
|| startupTimeout == null || startupTimeout.isNegative() || startupTimeout.isZero()) { || startupTimeout == null || startupTimeout.isNegative() || startupTimeout.isZero()) {
throw new IllegalArgumentException("Client, pinned image, worker command and positive timeout are required"); throw new IllegalArgumentException("Client, pinned image, worker command and positive timeout are required");
} }
KubernetesHttpRuntimeClient.dnsLabel(namespace); com.alibaba.qwen.code.runtimebroker.protocol.KubernetesNames.dnsLabel(namespace);
this.client = client; this.client = client;
this.cluster = BrokerValues.requireId(cluster, "cluster"); this.cluster = BrokerValues.requireId(cluster, "cluster");
this.namespace = namespace; this.namespace = namespace;
@@ -17,7 +17,8 @@ import com.alibaba.qwen.code.runtimebroker.RuntimeRecoveryEvidence;
import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle; import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle;
import com.alibaba.qwen.code.runtimebroker.RuntimeScope; import com.alibaba.qwen.code.runtimebroker.RuntimeScope;
import com.alibaba.qwen.code.runtimebroker.WorkspaceExecutionProfile; import com.alibaba.qwen.code.runtimebroker.WorkspaceExecutionProfile;
import com.alibaba.qwen.code.runtimebroker.transport.HttpRuntimeTransport; import com.alibaba.qwen.code.runtimebroker.adapter.HttpRuntimeTransport;
import com.alibaba.qwen.code.runtimebroker.protocol.ManagedContextProtocol;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
@@ -587,7 +588,7 @@ public final class LocalProcessRuntimeProvisioner
// A closed provisioner is not a managed-context startup // A closed provisioner is not a managed-context startup
// failure, and recovery is not blocked by it: keep the // failure, and recovery is not blocked by it: keep the
// refusal's own message and retryable flag for callers that // refusal's own message and retryable flag for callers that
// provision directly. Through RuntimeBrokerService a // provision directly. Through RuntimeBrokerOperations a
// managed-context provision failure blocks recovery either // managed-context provision failure blocks recovery either
// way, so this changes the surfaced error, not that outcome. // way, so this changes the surfaced error, not that outcome.
throw closedRefusal; throw closedRefusal;
@@ -229,7 +229,7 @@ final class LocalRuntimeStore {
} }
static boolean supported(RuntimeResourceHandle handle) { static boolean supported(RuntimeResourceHandle handle) {
return handle != null && LocalProcessRuntimeProvisioner.KIND.equals(handle.getKind()) return handle != null && com.alibaba.qwen.code.runtimebroker.protocol.RuntimeProviderKinds.LOCAL_PROCESS.equals(handle.getKind())
&& handle.getVersion() == 2; && handle.getVersion() == 2;
} }
@@ -350,7 +350,7 @@ final class LocalRuntimeStore {
|| !Objects.equals(value.get("version"), 1)) { || !Objects.equals(value.get("version"), 1)) {
throw blocked(); throw blocked();
} }
return new Registration(RuntimeResourceHandle.fromJson(LocalProcessRuntimeProvisioner.KIND, 2, return new Registration(RuntimeResourceHandle.fromJson(com.alibaba.qwen.code.runtimebroker.protocol.RuntimeProviderKinds.LOCAL_PROCESS, 2,
(String) value.get("handle")), State.valueOf((String) value.get("state")), (String) value.get("handle")), State.valueOf((String) value.get("state")),
((Number) value.get("pid")).longValue(), ((Number) value.get("pid")).longValue(),
"".equals(value.get("started")) ? null : (String) value.get("started"), "".equals(value.get("started")) ? null : (String) value.get("started"),
@@ -440,7 +440,7 @@ final class LocalRuntimeStore {
scope.getTenantId(), scope.getWorkspaceId(), scope.getWorkspaceGeneration(), scope.getCanonicalCwd(), scope.getTenantId(), scope.getWorkspaceId(), scope.getWorkspaceGeneration(), scope.getCanonicalCwd(),
scope.getCapabilityDigest(), scope.getIsolationClass(), request.getProvisionerKind(), scope.getCapabilityDigest(), scope.getIsolationClass(), request.getProvisionerKind(),
Objects.toString(request.getIsolationKey(), ""), Objects.toString(request.getStorageId(), ""))))); Objects.toString(request.getIsolationKey(), ""), Objects.toString(request.getStorageId(), "")))));
return new RuntimeResourceHandle(LocalProcessRuntimeProvisioner.KIND, 2, values); return new RuntimeResourceHandle(com.alibaba.qwen.code.runtimebroker.protocol.RuntimeProviderKinds.LOCAL_PROCESS, 2, values);
} }
private static String digest(byte[] bytes) { private static String digest(byte[] bytes) {
@@ -0,0 +1,120 @@
package com.alibaba.qwen.code.runtimebroker.provisioning;
import com.alibaba.qwen.code.managedagent.contracts.workspace.StorageIdentityReader;
import com.alibaba.qwen.code.managedagent.contracts.workspace.StorageIdentityReader.Identity;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.security.GeneralSecurityException;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public final class NativeStorageIdentityReader implements StorageIdentityReader {
private static final DateTimeFormatter LINUX_BIRTH_TIME_FORMAT = DateTimeFormatter
.ofPattern("uuuu-MM-dd HH:mm:ss.SSSSSSSSS xx", Locale.ROOT)
.withResolverStyle(ResolverStyle.STRICT);
public Identity read(Path root) throws IOException {
String os = System.getProperty("os.name");
if (!List.of("Linux", "Mac OS X").contains(os)) {
throw new IOException("Unsupported mount identity provider");
}
Map<String, Object> attributes = Files.readAttributes(root,
"unix:isDirectory,dev,ino,creationTime,lastModifiedTime", LinkOption.NOFOLLOW_LINKS);
if (!Boolean.TRUE.equals(attributes.get("isDirectory")) || !root.isAbsolute()
|| !root.equals(root.toRealPath())) {
throw new IOException("Workspace root is not canonical");
}
String device = Long.toUnsignedString(((Number) attributes.get("dev")).longValue());
String inode = Long.toUnsignedString(((Number) attributes.get("ino")).longValue());
String birthTime = "Mac OS X".equals(os) ? MacOsHostIdentity.birthTime(root, device, inode)
: linuxBirthTime(root, device, inode, (FileTime) attributes.get("creationTime"),
(FileTime) attributes.get("lastModifiedTime"));
String machineId = "Mac OS X".equals(os) ? MacOsHostIdentity.machineId()
: Files.readString(Path.of("/etc/machine-id")).strip();
if (machineId.isEmpty() || machineId.length() > 256) {
throw new IOException("Host identity is unavailable");
}
String host;
try {
host = new HmacUtils(HmacAlgorithms.HMAC_SHA_256,
machineId.getBytes(StandardCharsets.UTF_8)).hmacHex(
"Qwen-Code/verified-workspace/v2".getBytes(StandardCharsets.UTF_8));
} catch (IllegalArgumentException error) {
if (error.getCause() instanceof GeneralSecurityException cause) {
throw new IOException("Host identity is unavailable", cause);
}
throw error;
}
return new Identity(root.toString(), host, device, inode, birthTime);
}
private static String linuxBirthTime(Path root, String device, String inode,
FileTime creation, FileTime modified) throws IOException {
if (!creation.equals(modified) || creation.compareTo(FileTime.fromMillis(0)) <= 0) {
return verifiedBirthTime(creation, modified);
}
// A marker write can share the birth clock tick. Confirm the exact root,
// rather than treating the JDK's possible mtime fallback as birth evidence.
ProcessBuilder builder = new ProcessBuilder("/usr/bin/stat", "-c", "%d|%i|%w", "--", root.toString())
.redirectError(ProcessBuilder.Redirect.DISCARD);
builder.environment().put("LC_ALL", "C");
builder.environment().put("TZ", "UTC");
Process process = builder.start();
try {
process.getOutputStream().close();
if (!process.waitFor(5, TimeUnit.SECONDS) || process.exitValue() != 0) {
throw new IOException("Workspace birth time probe failed");
}
byte[] output = process.getInputStream().readNBytes(257);
if (output.length > 256) {
throw new IOException("Workspace birth time probe exceeds its size limit");
}
return verifiedNativeBirthTime(new String(output, StandardCharsets.UTF_8).strip(),
device, inode, creation);
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
throw new IOException("Workspace birth time probe interrupted", error);
} finally {
process.destroyForcibly();
process.getInputStream().close();
process.getErrorStream().close();
}
}
public static String verifiedNativeBirthTime(String output, String device, String inode,
FileTime creation) throws IOException {
String[] fields = output.split("\\|", -1);
if (fields.length != 3 || !device.equals(fields[0]) || !inode.equals(fields[1])) {
throw new IOException("Workspace storage identity changed or is unavailable");
}
try {
Instant birth = OffsetDateTime.parse(fields[2], LINUX_BIRTH_TIME_FORMAT).toInstant();
if (!birth.isAfter(Instant.EPOCH) || !birth.equals(creation.toInstant())) {
throw new IOException("Workspace birth time is unavailable or ambiguous");
}
return creation.toInstant().toString();
} catch (DateTimeException error) {
throw new IOException("Workspace native birth time is invalid", error);
}
}
public static String verifiedBirthTime(FileTime creation, FileTime modified) throws IOException {
// OpenJDK 21 substitutes mtime or epoch when Linux does not provide birth time.
if (creation.compareTo(FileTime.fromMillis(0)) <= 0 || creation.equals(modified)) {
throw new IOException("Workspace birth time is unavailable or ambiguous");
}
return creation.toInstant().toString();
}
}
@@ -4,6 +4,7 @@ import com.alibaba.qwen.code.runtimebroker.BrokerValues;
import com.alibaba.qwen.code.runtimebroker.JsonCodec; import com.alibaba.qwen.code.runtimebroker.JsonCodec;
import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException; import com.alibaba.qwen.code.runtimebroker.RuntimeBrokerException;
import com.alibaba.qwen.code.runtimebroker.RuntimeLease; import com.alibaba.qwen.code.runtimebroker.RuntimeLease;
import com.alibaba.qwen.code.runtimebroker.spi.SshRuntimeNode;
import java.io.IOException; import java.io.IOException;
import java.net.ConnectException; import java.net.ConnectException;
@@ -38,7 +39,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
/** Owns SSH control sockets and ephemeral forwarding addresses, never remote workers. */ /** Owns SSH control sockets and ephemeral forwarding addresses, never remote workers. */
public final class SshConnectionManager implements AutoCloseable { public final class SshConnectionManager implements com.alibaba.qwen.code.runtimebroker.spi.SshConnections {
private static final int RECORD_LIMIT = 64 * 1024; private static final int RECORD_LIMIT = 64 * 1024;
private static final Duration COMMAND_TIMEOUT = Duration.ofSeconds(60); private static final Duration COMMAND_TIMEOUT = Duration.ofSeconds(60);
private final URI publicationEndpoint; private final URI publicationEndpoint;
@@ -12,7 +12,11 @@ import com.alibaba.qwen.code.runtimebroker.RuntimeProvisioner;
import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle; import com.alibaba.qwen.code.runtimebroker.RuntimeResourceHandle;
import com.alibaba.qwen.code.runtimebroker.RuntimeScope; import com.alibaba.qwen.code.runtimebroker.RuntimeScope;
import com.alibaba.qwen.code.runtimebroker.WorkspaceExecutionProfile; import com.alibaba.qwen.code.runtimebroker.WorkspaceExecutionProfile;
import com.alibaba.qwen.code.runtimebroker.transport.HttpRuntimeTransport; import com.alibaba.qwen.code.runtimebroker.adapter.HttpRuntimeTransport;
import com.alibaba.qwen.code.runtimebroker.protocol.ManagedContextProtocol;
import com.alibaba.qwen.code.runtimebroker.spi.SshConnections;
import com.alibaba.qwen.code.runtimebroker.spi.SshRuntimeNode;
import com.alibaba.qwen.code.runtimebroker.spi.SshRuntimeStorage;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -43,7 +47,7 @@ public final class SshRuntimeProvisioner implements RuntimeProvisioner {
"provisionRequestId", "runtimeInstanceId", "runtimeIncarnation", "leaseId", "epoch", "retirementId", "retiredAt"); "provisionRequestId", "runtimeInstanceId", "runtimeIncarnation", "leaseId", "epoch", "retirementId", "retiredAt");
private final Function<String, SshRuntimeNode> nodes; private final Function<String, SshRuntimeNode> nodes;
private final Function<RuntimeScope, SshRuntimeStorage> storages; private final Function<RuntimeScope, SshRuntimeStorage> storages;
private final SshConnectionManager connections; private final SshConnections connections;
private final HttpRuntimeTransport transport; private final HttpRuntimeTransport transport;
private final ExecutorService executor = new ThreadPoolExecutor(4, 4, 0, TimeUnit.SECONDS, private final ExecutorService executor = new ThreadPoolExecutor(4, 4, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(64), Thread.ofPlatform().daemon().name("runtime-ssh-provisioner-", 0).factory()); new ArrayBlockingQueue<>(64), Thread.ofPlatform().daemon().name("runtime-ssh-provisioner-", 0).factory());
@@ -53,7 +57,7 @@ public final class SshRuntimeProvisioner implements RuntimeProvisioner {
private String managedExtensionCgroupRoot; private String managedExtensionCgroupRoot;
public SshRuntimeProvisioner(Function<String, SshRuntimeNode> nodes, public SshRuntimeProvisioner(Function<String, SshRuntimeNode> nodes,
Function<RuntimeScope, SshRuntimeStorage> storages, SshConnectionManager connections, Function<RuntimeScope, SshRuntimeStorage> storages, SshConnections connections,
HttpRuntimeTransport transport) { HttpRuntimeTransport transport) {
this.nodes = java.util.Objects.requireNonNull(nodes, "nodes"); this.nodes = java.util.Objects.requireNonNull(nodes, "nodes");
this.storages = java.util.Objects.requireNonNull(storages, "storages"); this.storages = java.util.Objects.requireNonNull(storages, "storages");
@@ -257,7 +261,6 @@ public final class SshRuntimeProvisioner implements RuntimeProvisioner {
// Let queued control futures run their closed checks instead of abandoning them. // Let queued control futures run their closed checks instead of abandoning them.
executor.shutdown(); executor.shutdown();
owned.clear(); owned.clear();
connections.close();
} }
private <T> CompletionStage<T> submit(Supplier<T> operation) { private <T> CompletionStage<T> submit(Supplier<T> operation) {
@@ -9,10 +9,10 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** Bounded Workspace file exchanges using the configured local CLI. */ /** Bounded Workspace file exchanges using the configured local CLI. */
public final class WorkspaceFileProcess { public final class WorkspaceFileProcess implements com.alibaba.qwen.code.managedagent.contracts.workspace.LocalWorkspaceFiles {
public static final int MAX_UPLOAD_BYTES = 8 * 1024 * 1024; public static final int MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
public static final int MAX_UPLOAD_REQUEST_BYTES = 12 * 1024 * 1024; public static final int MAX_UPLOAD_REQUEST_BYTES = 12 * 1024 * 1024;
private WorkspaceFileProcess() { public WorkspaceFileProcess() {
} }
public static Map<String, Object> read(String node, String entry, Map<String, Object> request) { public static Map<String, Object> read(String node, String entry, Map<String, Object> request) {
@@ -86,4 +86,6 @@ public final class WorkspaceFileProcess {
throw unsupported(); throw unsupported();
} }
} }
public Map<String, Object> readFiles(String node, String entry, Map<String, Object> request) { return read(node, entry, request); }
public Map<String, Object> uploadFiles(String node, String entry, Map<String, Object> request) { return upload(node, entry, request); }
} }
@@ -0,0 +1,79 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.alibaba</groupId>
<artifactId>qwen-managed-agent-parent</artifactId>
<version>0.1.0-alpha</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>qwen-managed-agent-contracts</artifactId>
<name>Qwen Managed Agent Contracts</name>
<properties>
<managed.module.root>${project.basedir}/..</managed.module.root>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>${managed.module.root}/../core/src/managed-runtime/contracts</directory>
<includes>
<include>managed-tool-result-v1.schema.json</include>
</includes>
<targetPath>contracts</targetPath>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<failIfNoSpecifiedTests>false</failIfNoSpecifiedTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package com.alibaba.qwen.code.managedagent.contracts.artifact;
import com.fasterxml.jackson.databind.JsonNode;
public interface ArtifactReadLease extends AutoCloseable {
public void requireScope(JsonNode key);
public void requireScope(String tenant, String session);
public void check();
public void checkReadView();
@Override
public void close();
}
@@ -0,0 +1,15 @@
package com.alibaba.qwen.code.managedagent.contracts.artifact;
public interface ManagedArtifactPolicy {
/** A stable configuration fingerprint that changes whenever a policy decision changes. */
public String version();
public boolean publishOriginal(String tenantId, String workspaceId,
String sessionId);
public boolean publishPreview(String tenantId, String workspaceId,
String sessionId);
public boolean readOriginal(String tenantId, String actorId, String workspaceId,
String sessionId);
}
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.artifact; package com.alibaba.qwen.code.managedagent.contracts.artifact;
import java.io.InputStream; import java.io.InputStream;
@@ -12,22 +12,22 @@ public interface ToolPublicationObjectStore {
* PUT attempt. An existing key must never be overwritten. Once the complete object is readable, * PUT attempt. An existing key must never be overwritten. Once the complete object is readable,
* this invocation must not create it again, including after a later deletion. * this invocation must not create it again, including after a later deletion.
*/ */
void putIfAbsent(String key, byte[] bytes); public void putIfAbsent(String key, byte[] bytes);
/** The caller closes the stream after verifying its complete contents. */ /** The caller closes the stream after verifying its complete contents. */
InputStream open(String key); public InputStream open(String key);
/** Check authorization before each new storage request, including implementation retries. */ /** Check authorization before each new storage request, including implementation retries. */
default InputStream open(String key, Runnable guard) { public default InputStream open(String key, Runnable guard) {
guard.run(); guard.run();
return open(key); return open(key);
} }
/** Physically delete the exact owned key; an absent object is already deleted. */ /** Physically delete the exact owned key; an absent object is already deleted. */
default void deleteIfPresent(String key) { public default void deleteIfPresent(String key) {
throw new UnsupportedOperationException("Output deletion is not supported by this adapter"); throw new UnsupportedOperationException("Output deletion is not supported by this adapter");
} }
/** Validate private, durable, immutable storage and exact-key deletion guarantees. */ /** Validate private, durable, immutable storage and exact-key deletion guarantees. */
void validateConfiguration(); public void validateConfiguration();
} }
@@ -0,0 +1,8 @@
package com.alibaba.qwen.code.managedagent.contracts.artifact;
import java.time.Duration;
import java.util.List;
public interface ToolPublicationRetention {
public record Candidate(String scope, String publication, String tenant, String session,
long bytes, String blocker) { }
public List<Candidate> observe(Duration grace);
}
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.artifact; package com.alibaba.qwen.code.managedagent.contracts.artifact;
import java.util.Objects; import java.util.Objects;
@@ -1,11 +1,10 @@
package com.alibaba.qwen.code.managedagent.catalog; package com.alibaba.qwen.code.managedagent.contracts.catalog;
import com.alibaba.qwen.code.managedagent.contracts.persistence.ManagedSessionStoreModels;
import com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceToolProfiles;
import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException; import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException;
import com.alibaba.qwen.code.managedagent.persistence.ManagedSessionStoreModels;
import com.alibaba.qwen.code.managedagent.workspace.WorkspaceToolProfiles;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
@@ -173,7 +172,7 @@ public final class AgentDefinitionExtensions {
} }
private static ManagedAgentException unsupported(String field) { private static ManagedAgentException unsupported(String field) {
return new ManagedAgentException(HttpStatus.CONFLICT, "agent_definition_unsupported", return new ManagedAgentException(409, "agent_definition_unsupported",
"The agent definition field cannot take effect yet.", Map.of("field", field)); "The agent definition field cannot take effect yet.", Map.of("field", field));
} }
} }
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.channel; package com.alibaba.qwen.code.managedagent.contracts.channel;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.event; package com.alibaba.qwen.code.managedagent.contracts.event;
import com.alibaba.qwen.code.managedagent.model.StoreModels.EventRecord; import com.alibaba.qwen.code.managedagent.model.StoreModels.EventRecord;
@@ -11,5 +11,5 @@ import java.util.List;
* change to the publication contract must be made in both files. * change to the publication contract must be made in both files.
*/ */
public interface CommittedEventPublisher { public interface CommittedEventPublisher {
void publish(List<EventRecord> events); public void publish(List<EventRecord> events);
} }
@@ -0,0 +1,6 @@
package com.alibaba.qwen.code.managedagent.contracts.event;
@FunctionalInterface
public interface EventMessageListener {
void consume(byte[] body, String messageId);
}
@@ -0,0 +1,8 @@
package com.alibaba.qwen.code.managedagent.contracts.event;
import java.util.Set;
public interface EventSubscriptions extends java.io.Closeable {
Set<String> getLiteTopicSet();
void subscribeLite(String topic) throws Exception;
void unsubscribeLite(String topic) throws Exception;
}
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.event; package com.alibaba.qwen.code.managedagent.contracts.event;
import com.alibaba.qwen.code.managedagent.model.StoreModels.EventRecord; import com.alibaba.qwen.code.managedagent.model.StoreModels.EventRecord;
@@ -27,5 +27,5 @@ import java.util.List;
* change to the publication contract must be made in both files. * change to the publication contract must be made in both files.
*/ */
public interface EventTransport { public interface EventTransport {
void publish(List<EventRecord> events); public void publish(List<EventRecord> events);
} }
@@ -1,8 +1,8 @@
package com.alibaba.qwen.code.managedagent.event; package com.alibaba.qwen.code.managedagent.contracts.event;
/** /**
* The authoritative-journal wire (design 2026-10-04-managed-agent-event-transport, * The authoritative-journal wire (design 2026-10-04-managed-agent-event-transport,
* phase MQ2b). An after-commit publication point on {@link com.alibaba.qwen.code.managedagent.persistence.ManagedSessionStore#commit} * phase MQ2b). An after-commit publication point on {@link com.alibaba.qwen.code.managedagent.repository.ManagedSessionStore#commit}
* hands the committed journal record (the raw record bytes) to this port. It is * hands the committed journal record (the raw record bytes) to this port. It is
* distinct from the {@link EventTransport} hub seam, which carries the public * distinct from the {@link EventTransport} hub seam, which carries the public
* event stream that MQ2c wires as the {@code public_event} envelope extension. * event stream that MQ2c wires as the {@code public_event} envelope extension.
@@ -14,5 +14,5 @@ package com.alibaba.qwen.code.managedagent.event;
* treat the broker as recovery evidence (the #12380 constraint). * treat the broker as recovery evidence (the #12380 constraint).
*/ */
public interface JournalEventTransport { public interface JournalEventTransport {
void publishCommitted(byte[] recordBytes); public void publishCommitted(byte[] recordBytes);
} }
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.event; package com.alibaba.qwen.code.managedagent.contracts.event;
/** /**
* The managed-event-envelope/1 contract mirrored to Java: the distribution * The managed-event-envelope/1 contract mirrored to Java: the distribution
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.event; package com.alibaba.qwen.code.managedagent.contracts.event;
import com.alibaba.qwen.code.managedagent.model.StoreModels.EventRecord; import com.alibaba.qwen.code.managedagent.model.StoreModels.EventRecord;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@@ -29,32 +29,32 @@ import java.util.regex.Pattern;
* consumers parse both. * consumers parse both.
*/ */
public final class ManagedEventEnvelopes { public final class ManagedEventEnvelopes {
static final int FORMAT_VERSION = 1; public static final int FORMAT_VERSION = 1;
public static final String STREAM_AUTHORITATIVE_JOURNAL = public static final String STREAM_AUTHORITATIVE_JOURNAL =
"authoritative_journal"; "authoritative_journal";
public static final String STREAM_PUBLIC_EVENT = "public_event"; public static final String STREAM_PUBLIC_EVENT = "public_event";
// Ordered (the failure message joins it), as the TypeScript constant. // Ordered (the failure message joins it), as the TypeScript constant.
static final List<String> STREAMS = List.of( public static final List<String> STREAMS = List.of(
STREAM_AUTHORITATIVE_JOURNAL, STREAM_PUBLIC_EVENT); STREAM_AUTHORITATIVE_JOURNAL, STREAM_PUBLIC_EVENT);
static final int MAX_ID_BYTES = 512; public static final int MAX_ID_BYTES = 512;
// A public kind is the public event type: managed_agent_event.event_type // A public kind is the public event type: managed_agent_event.event_type
// and the public API both cap it at 128, and the grammar is ASCII. // and the public API both cap it at 128, and the grammar is ASCII.
static final int MAX_PUBLIC_KIND_BYTES = 128; public static final int MAX_PUBLIC_KIND_BYTES = 128;
static final Pattern PUBLIC_KIND = Pattern.compile( public static final Pattern PUBLIC_KIND = Pattern.compile(
"^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$"); "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$");
static final long MAX_TIME_MS = 8640000000000000L; public static final long MAX_TIME_MS = 8640000000000000L;
// MANAGED_SESSION_LIMITS.maxJsonDepth: the digest refuses a value nested // MANAGED_SESSION_LIMITS.maxJsonDepth: the digest refuses a value nested
// deeper, counting from the top-level list at depth 0. // deeper, counting from the top-level list at depth 0.
static final int MAX_JSON_DEPTH = 64; public static final int MAX_JSON_DEPTH = 64;
// Number.MAX_SAFE_INTEGER - 1: the schema's sequence ceiling (the TypeScript // Number.MAX_SAFE_INTEGER - 1: the schema's sequence ceiling (the TypeScript
// assert rejects exactly MAX_SAFE_INTEGER as "cannot advance"). // assert rejects exactly MAX_SAFE_INTEGER as "cannot advance").
static final long MAX_SEQUENCE = 9007199254740990L; public static final long MAX_SEQUENCE = 9007199254740990L;
private static final Pattern DIGEST = Pattern.compile("[0-9a-f]{64}"); private static final Pattern DIGEST = Pattern.compile("[0-9a-f]{64}");
public static final String KIND_WAKE_REQUESTED = "wake.requested"; public static final String KIND_WAKE_REQUESTED = "wake.requested";
// Ordered (the failure message joins it) — the frozen contract's 20-kind // Ordered (the failure message joins it) — the frozen contract's 20-kind
// vocabulary in fixture order. // vocabulary in fixture order.
static final List<String> KINDS = List.of( public static final List<String> KINDS = List.of(
"input.accepted", KIND_WAKE_REQUESTED, "activation.changed", "input.accepted", KIND_WAKE_REQUESTED, "activation.changed",
"model.attempt", "message.committed", "tool.intent", "model.attempt", "message.committed", "tool.intent",
"action.changed", "tool.receipt", "checkpoint.committed", "action.changed", "tool.receipt", "checkpoint.committed",
@@ -63,7 +63,7 @@ public final class ManagedEventEnvelopes {
"message.delta", "message.retracted", "operation.replayed", "message.delta", "message.retracted", "operation.replayed",
"hosted.batch.planned", "hosted.cleanup"); "hosted.batch.planned", "hosted.cleanup");
static final Set<String> FORBIDDEN_FIELDS = Set.of( public static final Set<String> FORBIDDEN_FIELDS = Set.of(
"absolutePath", "localPath", "pid", "pod", "runtimeBindingId", "absolutePath", "localPath", "pid", "pod", "runtimeBindingId",
"runtimeEndpoint", "secretHandle", "sidecar"); "runtimeEndpoint", "secretHandle", "sidecar");
@@ -278,7 +278,7 @@ public final class ManagedEventEnvelopes {
// --- canonical JSON (mirrors canonicalManagedSessionJson) --- // --- canonical JSON (mirrors canonicalManagedSessionJson) ---
static String canonicalJson(JsonNode node) { public static String canonicalJson(JsonNode node) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
appendCanonicalJson(sb, node); appendCanonicalJson(sb, node);
return sb.toString(); return sb.toString();
@@ -334,7 +334,7 @@ public final class ManagedEventEnvelopes {
* the TypeScript side byte-for-byte. The thresholds are the spec's: * the TypeScript side byte-for-byte. The thresholds are the spec's:
* decimal form while 1e-6 ≤ |x| < 1e21, exponential outside it. * decimal form while 1e-6 ≤ |x| < 1e21, exponential outside it.
*/ */
static String jsNumberToString(double d) { public static String jsNumberToString(double d) {
if (d == 0) { if (d == 0) {
return "0"; return "0";
} }
@@ -0,0 +1,11 @@
package com.alibaba.qwen.code.managedagent.contracts.extensions;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
public interface ExtensionArchives {
public boolean configured();
public long put(String tenant, String workspace, String digest, InputStream archive) throws IOException;
public Path verify(String tenant, String workspace, String digest, long expectedLength) throws IOException;
}
@@ -0,0 +1,20 @@
package com.alibaba.qwen.code.managedagent.contracts.extensions;
import com.alibaba.qwen.code.managedagent.contracts.extensions.ManagedExtensionPackages.Package;
import com.alibaba.qwen.code.managedagent.contracts.extensions.ManagedExtensionPackages.Pin;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public interface ExtensionPackages {
public void requireSelection(String tenant, String actor, String workspace, List<Pin> pins);
public ResolvedPackage resolvePin(String tenant, String workspace, Pin pin) throws IOException;
public record ResolvedPackage(Package descriptor, Path archive, Map<String, String> settings) {
@Override
public String toString() {
return "ResolvedPackage[descriptor=" + descriptor + ", settings=redacted]";
}
}
}
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.extensions; package com.alibaba.qwen.code.managedagent.contracts.extensions;
import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
@@ -1,6 +1,6 @@
package com.alibaba.qwen.code.managedagent.harness; package com.alibaba.qwen.code.managedagent.contracts.harness;
import com.alibaba.qwen.code.daemon.HarnessRuntimeRecovery; import com.alibaba.qwen.code.managedagent.harness.contracts.HarnessRuntimeRecovery;
import com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord; import com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@@ -8,130 +8,130 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public interface HarnessConnector extends AutoCloseable { public interface HarnessConnector extends AutoCloseable {
boolean isAvailable(); public boolean isAvailable();
/** Reattaches a SQL-proven unowned Session without trusting the local attachment cache. */ /** Reattaches a SQL-proven unowned Session without trusting the local attachment cache. */
default void recoverForWake(String tenantId, String sessionId) { public default void recoverForWake(String tenantId, String sessionId) {
throw new UnsupportedOperationException("Hosted wake recovery is unavailable"); throw new UnsupportedOperationException("Hosted wake recovery is unavailable");
} }
default boolean supportsPublicChildAgents() { return false; } public default boolean supportsPublicChildAgents() { return false; }
default String closeSession(OperationRecord operation) { public default String closeSession(OperationRecord operation) {
return closeSession(operation.tenantId(), operation.sessionId()); return closeSession(operation.tenantId(), operation.sessionId());
} }
default void runLifecycleChildOperation(OperationRecord operation, Map<String, Object> body) { public default void runLifecycleChildOperation(OperationRecord operation, Map<String, Object> body) {
runChildOperation(operation.tenantId(), operation.sessionId(), body); runChildOperation(operation.tenantId(), operation.sessionId(), body);
} }
default Map<String, Object> drainInstance(String instanceId, String operationId) { public default Map<String, Object> drainInstance(String instanceId, String operationId) {
throw new UnsupportedOperationException("Harness drain is unavailable"); throw new UnsupportedOperationException("Harness drain is unavailable");
} }
default Map<String, Object> releaseOwnership(String tenantId, String sessionId, public default Map<String, Object> releaseOwnership(String tenantId, String sessionId,
String operationId, long expectedPlacementEpoch) { String operationId, long expectedPlacementEpoch) {
throw new UnsupportedOperationException("Harness ownership release is unavailable"); throw new UnsupportedOperationException("Harness ownership release is unavailable");
} }
default boolean supportsApprovalSelection() { return false; } public default boolean supportsApprovalSelection() { return false; }
default boolean supportsLiveApprovalSelection() { return false; } public default boolean supportsLiveApprovalSelection() { return false; }
default boolean supportsAutoApproval() { return false; } public default boolean supportsAutoApproval() { return false; }
default boolean supportsPermissionRules() { return false; } public default boolean supportsPermissionRules() { return false; }
default boolean supportsAgentDefinitionApprovalSelection() { return false; } public default boolean supportsAgentDefinitionApprovalSelection() { return false; }
default boolean supportsModelSelection() { return false; } public default boolean supportsModelSelection() { return false; }
default boolean supportsUserMessageQueue() { return false; } public default boolean supportsUserMessageQueue() { return false; }
default boolean supportsContextCompression() { return false; } public default boolean supportsContextCompression() { return false; }
@FunctionalInterface @FunctionalInterface
interface ContextCompressionSubmission { public interface ContextCompressionSubmission {
Map<String, Object> submit(); public Map<String, Object> submit();
} }
default ContextCompressionSubmission prepareContextCompression(String tenantId, String sessionId, public default ContextCompressionSubmission prepareContextCompression(String tenantId, String sessionId,
Map<String, Object> body) { Map<String, Object> body) {
return () -> runContextCompression(tenantId, sessionId, body); return () -> runContextCompression(tenantId, sessionId, body);
} }
default Map<String, Object> runContextCompression(String tenantId, String sessionId, public default Map<String, Object> runContextCompression(String tenantId, String sessionId,
Map<String, Object> body) { Map<String, Object> body) {
throw new UnsupportedOperationException("Context compression is unavailable"); throw new UnsupportedOperationException("Context compression is unavailable");
} }
default Map<String, Object> readContextCompression(String tenantId, String sessionId, public default Map<String, Object> readContextCompression(String tenantId, String sessionId,
String operationId) { String operationId) {
throw new UnsupportedOperationException("Context compression is unavailable"); throw new UnsupportedOperationException("Context compression is unavailable");
} }
default boolean supportsPlanMode() { return false; } public default boolean supportsPlanMode() { return false; }
default boolean supportsInputAttachments() { return false; } public default boolean supportsInputAttachments() { return false; }
default boolean supportsSessionSkills() { return false; } public default boolean supportsSessionSkills() { return false; }
default boolean supportsMcpManagement() { return false; } public default boolean supportsMcpManagement() { return false; }
default Map<String, Object> mcpManagement(String tenantId, String sessionId) { public default Map<String, Object> mcpManagement(String tenantId, String sessionId) {
throw new UnsupportedOperationException("MCP management is unavailable"); throw new UnsupportedOperationException("MCP management is unavailable");
} }
default Map<String, Object> manageMcp(String tenantId, String sessionId, Map<String, Object> command) { public default Map<String, Object> manageMcp(String tenantId, String sessionId, Map<String, Object> command) {
throw new UnsupportedOperationException("MCP management is unavailable"); throw new UnsupportedOperationException("MCP management is unavailable");
} }
default Map<String, Object> mcpManagementOperation(String tenantId, String sessionId, String operationId) { public default Map<String, Object> mcpManagementOperation(String tenantId, String sessionId, String operationId) {
throw new UnsupportedOperationException("MCP management is unavailable"); throw new UnsupportedOperationException("MCP management is unavailable");
} }
default boolean supportsSessionExtensions() { return false; } public default boolean supportsSessionExtensions() { return false; }
default Map<String, Object> applyExtensions(String tenantId, String sessionId, Map<String, Object> command) { public default Map<String, Object> applyExtensions(String tenantId, String sessionId, Map<String, Object> command) {
throw new UnsupportedOperationException("Extension management is unavailable"); throw new UnsupportedOperationException("Extension management is unavailable");
} }
default Map<String, Object> continueExtensionOperation(String tenantId, String sessionId, String operationId, boolean recoverPrevious) { public default Map<String, Object> continueExtensionOperation(String tenantId, String sessionId, String operationId, boolean recoverPrevious) {
throw new UnsupportedOperationException("Extension management is unavailable"); throw new UnsupportedOperationException("Extension management is unavailable");
} }
default boolean isWorkspaceFilesAvailable() { public default boolean isWorkspaceFilesAvailable() {
return false; return false;
} }
default boolean supportsLifecycle() { return false; } public default boolean supportsLifecycle() { return false; }
default boolean supportsLifecycle(String profile) { public default boolean supportsLifecycle(String profile) {
return com.alibaba.qwen.code.managedagent.workspace.WorkspaceToolProfiles.FILES.equals(profile) && supportsLifecycle(); return com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceToolProfiles.FILES.equals(profile) && supportsLifecycle();
} }
default void prepareLifecycleResources(com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord operation) { public default void prepareLifecycleResources(com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord operation) {
throw new UnsupportedOperationException("Hosted resource lifecycle is unavailable"); throw new UnsupportedOperationException("Hosted resource lifecycle is unavailable");
} }
default void settleLifecycleResources(com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord operation) { public default void settleLifecycleResources(com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord operation) {
throw new UnsupportedOperationException("Hosted resource lifecycle is unavailable"); throw new UnsupportedOperationException("Hosted resource lifecycle is unavailable");
} }
default boolean supportsFencedTitles() { return false; } public default boolean supportsFencedTitles() { return false; }
default JsonNode settleLifecycle(com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord operation) { public default JsonNode settleLifecycle(com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord operation) {
throw new UnsupportedOperationException("Hosted lifecycle is unavailable"); throw new UnsupportedOperationException("Hosted lifecycle is unavailable");
} }
default void detachLifecycle(com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord operation) { public default void detachLifecycle(com.alibaba.qwen.code.managedagent.model.StoreModels.OperationRecord operation) {
throw new UnsupportedOperationException("Hosted lifecycle is unavailable"); throw new UnsupportedOperationException("Hosted lifecycle is unavailable");
} }
Attachment createOrLoad(String tenantId, String sessionId, public Attachment createOrLoad(String tenantId, String sessionId,
boolean loadExisting); boolean loadExisting);
default Attachment createOrLoad(String tenantId, String sessionId, public default Attachment createOrLoad(String tenantId, String sessionId,
boolean loadExisting, boolean passiveManagedRuntimeRecovery) { boolean loadExisting, boolean passiveManagedRuntimeRecovery) {
return createOrLoad(tenantId, sessionId, loadExisting); return createOrLoad(tenantId, sessionId, loadExisting);
} }
@@ -141,13 +141,13 @@ public interface HarnessConnector extends AutoCloseable {
* reporting its parked Runtime executions. A plain cold load must stay * reporting its parked Runtime executions. A plain cold load must stay
* inert, so only this path may touch the Broker for a parked Turn. * inert, so only this path may touch the Broker for a parked Turn.
*/ */
default Attachment recoverManagedRuntime(String tenantId, String sessionId, public default Attachment recoverManagedRuntime(String tenantId, String sessionId,
boolean cancellation) { boolean cancellation) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Managed Runtime recovery is unavailable"); "Managed Runtime recovery is unavailable");
} }
default Attachment recoverManagedRuntime(String tenantId, String sessionId, public default Attachment recoverManagedRuntime(String tenantId, String sessionId,
boolean cancellation, String promptId) { boolean cancellation, String promptId) {
throw new UnsupportedOperationException("Targeted Runtime recovery is unavailable"); throw new UnsupportedOperationException("Targeted Runtime recovery is unavailable");
} }
@@ -159,24 +159,24 @@ public interface HarnessConnector extends AutoCloseable {
* while the healthy-attachment shortcut would answer from cache and * while the healthy-attachment shortcut would answer from cache and
* never tell the daemon (R9-P1-2). * never tell the daemon (R9-P1-2).
*/ */
default Attachment recoverManagedCancellation(String tenantId, public default Attachment recoverManagedCancellation(String tenantId,
String sessionId) { String sessionId) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Managed Runtime recovery is unavailable"); "Managed Runtime recovery is unavailable");
} }
default Attachment recoverManagedCancellation(String tenantId, public default Attachment recoverManagedCancellation(String tenantId,
String sessionId, String promptId) { String sessionId, String promptId) {
throw new UnsupportedOperationException("Targeted Runtime recovery is unavailable"); throw new UnsupportedOperationException("Targeted Runtime recovery is unavailable");
} }
default Attachment recoverManagedFailureCancellation(String tenantId, public default Attachment recoverManagedFailureCancellation(String tenantId,
String sessionId, String turnId, String dispatchOwner) { String sessionId, String turnId, String dispatchOwner) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Managed Runtime recovery is unavailable"); "Managed Runtime recovery is unavailable");
} }
default Attachment recoverManagedFailureCancellation(String tenantId, public default Attachment recoverManagedFailureCancellation(String tenantId,
String sessionId, String turnId, String dispatchOwner, String promptId) { String sessionId, String turnId, String dispatchOwner, String promptId) {
throw new UnsupportedOperationException("Targeted Runtime recovery is unavailable"); throw new UnsupportedOperationException("Targeted Runtime recovery is unavailable");
} }
@@ -186,53 +186,53 @@ public interface HarnessConnector extends AutoCloseable {
* recovery load re-reads the parked Runtime state instead of answering * recovery load re-reads the parked Runtime state instead of answering
* from a report already known to be stale. * from a report already known to be stale.
*/ */
default void discardManagedRecovery(String tenantId, String sessionId) { public default void discardManagedRecovery(String tenantId, String sessionId) {
} }
record RecoveryStatus(String state, String reasonCode, String promptId, public record RecoveryStatus(String state, String reasonCode, String promptId,
boolean inputSettled, boolean continuationReady, boolean nextTurnAllowed) {} boolean inputSettled, boolean continuationReady, boolean nextTurnAllowed) {}
/** Inspects an existing attachment without loading or acquiring anything. */ /** Inspects an existing attachment without loading or acquiring anything. */
default RecoveryStatus inspectRecovery(String tenantId, String sessionId, String promptId) { public default RecoveryStatus inspectRecovery(String tenantId, String sessionId, String promptId) {
return null; return null;
} }
/** Returns only cached replay identity, without loading or acquiring anything. */ /** Returns only cached replay identity, without loading or acquiring anything. */
default Attachment cachedRecoveryAttachment(String tenantId, String sessionId) { public default Attachment cachedRecoveryAttachment(String tenantId, String sessionId) {
return null; return null;
} }
/** Returns false when the original terminal cleanup cannot safely be retried. */ /** Returns false when the original terminal cleanup cannot safely be retried. */
default boolean recheckRecoveryCleanup(String tenantId, String sessionId, String promptId) { public default boolean recheckRecoveryCleanup(String tenantId, String sessionId, String promptId) {
return false; return false;
} }
Admission submit(String tenantId, String sessionId, String promptId, public Admission submit(String tenantId, String sessionId, String promptId,
List<Map<String, Object>> input, String payloadDigest); List<Map<String, Object>> input, String payloadDigest);
default Admission continueManagedRuntime(String tenantId, public default Admission continueManagedRuntime(String tenantId,
String sessionId, String promptId, String checkpointId, String sessionId, String promptId, String checkpointId,
String activationId) { String activationId) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Managed Runtime continuation is unavailable"); "Managed Runtime continuation is unavailable");
} }
default Admission cancelManagedRuntime(String tenantId, String sessionId, public default Admission cancelManagedRuntime(String tenantId, String sessionId,
String promptId, String checkpointId, String activationId) { String promptId, String checkpointId, String activationId) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Managed Runtime cancellation is unavailable"); "Managed Runtime cancellation is unavailable");
} }
SourceStream stream(String tenantId, String sessionId, long lastEventId, public SourceStream stream(String tenantId, String sessionId, long lastEventId,
String eventEpoch); String eventEpoch);
/** Reads only an existing original attachment; it must never load or take over execution. */ /** Reads only an existing original attachment; it must never load or take over execution. */
default SourceStream observeStream(String tenantId, String sessionId, String bootId, public default SourceStream observeStream(String tenantId, String sessionId, String bootId,
long lastEventId, String eventEpoch) { long lastEventId, String eventEpoch) {
throw new UnsupportedOperationException("Read-only Harness observation is unavailable"); throw new UnsupportedOperationException("Read-only Harness observation is unavailable");
} }
default void resolveAction( public default void resolveAction(
String tenantId, String tenantId,
String sessionId, String sessionId,
String actionId, String actionId,
@@ -240,9 +240,9 @@ public interface HarnessConnector extends AutoCloseable {
throw new UnsupportedOperationException("Hosted Actions are unavailable"); throw new UnsupportedOperationException("Hosted Actions are unavailable");
} }
void cancel(String tenantId, String sessionId); public void cancel(String tenantId, String sessionId);
default void cancelOriginalTurn(String tenantId, String sessionId, String promptId) { public default void cancelOriginalTurn(String tenantId, String sessionId, String promptId) {
throw new UnsupportedOperationException("Targeted cancellation is unavailable"); throw new UnsupportedOperationException("Targeted cancellation is unavailable");
} }
@@ -253,7 +253,7 @@ public interface HarnessConnector extends AutoCloseable {
* carries the committed operation result; a cancel_run may still be * carries the committed operation result; a cancel_run may still be
* awaiting its Turn's physical settlement. * awaiting its Turn's physical settlement.
*/ */
default Map<String, Object> runAutomationOperation(String tenantId, public default Map<String, Object> runAutomationOperation(String tenantId,
String sessionId, Map<String, Object> body) { String sessionId, Map<String, Object> body) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Automation operations are unavailable"); "Automation operations are unavailable");
@@ -264,7 +264,7 @@ public interface HarnessConnector extends AutoCloseable {
* control plane's relay (dispatch/attach/result/accept/cancel/ * control plane's relay (dispatch/attach/result/accept/cancel/
* close-scope). The Hosted side settles it before answering. * close-scope). The Hosted side settles it before answering.
*/ */
default void runChildOperation(String tenantId, String sessionId, public default void runChildOperation(String tenantId, String sessionId,
Map<String, Object> body) { Map<String, Object> body) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Child operations are unavailable"); "Child operations are unavailable");
@@ -281,7 +281,7 @@ public interface HarnessConnector extends AutoCloseable {
* names a command id, and a settled {@code execute} answers with the * names a command id, and a settled {@code execute} answers with the
* reply the relay copies back. * reply the relay copies back.
*/ */
default Map<String, Object> runMessageOperation(String tenantId, public default Map<String, Object> runMessageOperation(String tenantId,
String sessionId, Map<String, Object> body) { String sessionId, Map<String, Object> body) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Message operations are unavailable"); "Message operations are unavailable");
@@ -294,15 +294,15 @@ public interface HarnessConnector extends AutoCloseable {
* The Hosted side settles it before answering, and the answer carries * The Hosted side settles it before answering, and the answer carries
* the operation's result. * the operation's result.
*/ */
default Map<String, Object> runChannelOperation(String tenantId, public default Map<String, Object> runChannelOperation(String tenantId,
String sessionId, Map<String, Object> body) { String sessionId, Map<String, Object> body) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Channel operations are unavailable"); "Channel operations are unavailable");
} }
void rename(String tenantId, String sessionId, String title); public void rename(String tenantId, String sessionId, String title);
default void rename(String tenantId, String sessionId, String title, long revision) { public default void rename(String tenantId, String sessionId, String title, long revision) {
throw new UnsupportedOperationException("Fenced Session titles are unavailable"); throw new UnsupportedOperationException("Fenced Session titles are unavailable");
} }
@@ -310,7 +310,7 @@ public interface HarnessConnector extends AutoCloseable {
* Closes the Session and returns the boot ID of the Harness that * Closes the Session and returns the boot ID of the Harness that
* answered. A Harness that does not hold the Session answers too. * answered. A Harness that does not hold the Session answers too.
*/ */
String closeSession(String tenantId, String sessionId); public String closeSession(String tenantId, String sessionId);
/** /**
* The Sessions this control-plane node holds an attachment for. An * The Sessions this control-plane node holds an attachment for. An
@@ -320,7 +320,7 @@ public interface HarnessConnector extends AutoCloseable {
* narrows it by SQL to the Sessions whose journal this node's Harness * narrows it by SQL to the Sessions whose journal this node's Harness
* writes. A connector without attachments holds nothing. * writes. A connector without attachments holds nothing.
*/ */
default List<AttachedSession> attachedSessions() { public default List<AttachedSession> attachedSessions() {
return List.of(); return List.of();
} }
@@ -328,7 +328,7 @@ public interface HarnessConnector extends AutoCloseable {
* The writer id this node's Harness acquires Session journals under, or * The writer id this node's Harness acquires Session journals under, or
* null while there is none (no client yet, or the connector is closed). * null while there is none (no client yet, or the connector is closed).
*/ */
default String writerIdentity() { public default String writerIdentity() {
return null; return null;
} }
@@ -339,7 +339,7 @@ public interface HarnessConnector extends AutoCloseable {
* take. Returns false, loading nothing, when this node already holds an * take. Returns false, loading nothing, when this node already holds an
* attachment for the Session. * attachment for the Session.
*/ */
default boolean activateForWake(String tenantId, String sessionId) { public default boolean activateForWake(String tenantId, String sessionId) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
"Hosted wake activation is unavailable"); "Hosted wake activation is unavailable");
} }
@@ -348,13 +348,13 @@ public interface HarnessConnector extends AutoCloseable {
default void close() { default void close() {
} }
record AttachedSession(String tenantId, String sessionId, String writerIdentity, long placementEpoch) { public record AttachedSession(String tenantId, String sessionId, String writerIdentity, long placementEpoch) {
public AttachedSession(String tenantId, String sessionId) { public AttachedSession(String tenantId, String sessionId) {
this(tenantId, sessionId, null, 0); this(tenantId, sessionId, null, 0);
} }
} }
record Attachment(String bootId, HarnessRuntimeRecovery runtimeRecovery, public record Attachment(String bootId, HarnessRuntimeRecovery runtimeRecovery,
Long lastEventId, String eventEpoch) { Long lastEventId, String eventEpoch) {
public Attachment(String bootId) { public Attachment(String bootId) {
this(bootId, null, null, null); this(bootId, null, null, null);
@@ -366,23 +366,23 @@ public interface HarnessConnector extends AutoCloseable {
} }
} }
record Admission(long lastEventId, String eventEpoch) { public record Admission(long lastEventId, String eventEpoch) {
} }
record SourceEvent(Long id, String type, Object data, String promptId, public record SourceEvent(Long id, String type, Object data, String promptId,
Map<String, Object> metadata) { Map<String, Object> metadata) {
} }
interface SourceStream extends AutoCloseable { public interface SourceStream extends AutoCloseable {
String eventEpoch(); public String eventEpoch();
SourceEvent next(); public SourceEvent next();
default boolean recoveryBlocked(long afterEventId) { public default boolean recoveryBlocked(long afterEventId) {
return false; return false;
} }
@Override @Override
void close(); public void close();
} }
} }
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.harness; package com.alibaba.qwen.code.managedagent.contracts.harness;
// Signals a Harness disabled by configuration rather than a transient // Signals a Harness disabled by configuration rather than a transient
// failure, so the coordinator can hold the Turn instead of burning its // failure, so the coordinator can hold the Turn instead of burning its
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.harness; package com.alibaba.qwen.code.managedagent.contracts.harness;
/** /**
* The Hosted Harness refused a takeover load with a typed, terminal reason: * The Hosted Harness refused a takeover load with a typed, terminal reason:
@@ -1,11 +1,10 @@
package com.alibaba.qwen.code.managedagent.persistence; package com.alibaba.qwen.code.managedagent.contracts.persistence;
import com.alibaba.qwen.code.managedagent.catalog.AgentDefinitionExtensions; import com.alibaba.qwen.code.managedagent.contracts.catalog.AgentDefinitionExtensions;
import com.alibaba.qwen.code.managedagent.contracts.workspace.WorkspaceToolProfiles;
import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException; import com.alibaba.qwen.code.managedagent.exception.ManagedAgentException;
import com.alibaba.qwen.code.managedagent.workspace.WorkspaceToolProfiles;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
@@ -18,7 +17,7 @@ import java.util.Map;
* admission with {@code 409 agent_definition_unsupported} naming the field, * admission with {@code 409 agent_definition_unsupported} naming the field,
* instead of being dropped. * instead of being dropped.
*/ */
final class AgentDefinitionCompiler { public final class AgentDefinitionCompiler {
private static final ObjectMapper JSON = new ObjectMapper(); private static final ObjectMapper JSON = new ObjectMapper();
private static final TypeReference<Map<String, Object>> CONTENT = private static final TypeReference<Map<String, Object>> CONTENT =
new TypeReference<>() { new TypeReference<>() {
@@ -26,19 +25,19 @@ final class AgentDefinitionCompiler {
private static final List<String> APPROVAL_MODES = List.of("default", private static final List<String> APPROVAL_MODES = List.of("default",
"auto-edit", "yolo"); "auto-edit", "yolo");
record Compiled(String approvalMode, String toolProfile) { public record Compiled(String approvalMode, String toolProfile) {
} }
private AgentDefinitionCompiler() { private AgentDefinitionCompiler() {
} }
static Compiled compile(String definitionJson, boolean workspaceBound, public static Compiled compile(String definitionJson, boolean workspaceBound,
String deploymentApprovalMode, List<String> toolProfiles) { String deploymentApprovalMode, List<String> toolProfiles) {
return compile(definitionJson, workspaceBound, return compile(definitionJson, workspaceBound,
deploymentApprovalMode, toolProfiles, List.of()); deploymentApprovalMode, toolProfiles, List.of());
} }
static Compiled compile(String definitionJson, boolean workspaceBound, public static Compiled compile(String definitionJson, boolean workspaceBound,
String deploymentApprovalMode, List<String> toolProfiles, String deploymentApprovalMode, List<String> toolProfiles,
List<String> models) { List<String> models) {
Map<String, Object> definition; Map<String, Object> definition;
@@ -121,7 +120,7 @@ final class AgentDefinitionCompiler {
} }
private static ManagedAgentException unsupported(String field) { private static ManagedAgentException unsupported(String field) {
return new ManagedAgentException(HttpStatus.CONFLICT, return new ManagedAgentException(409,
"agent_definition_unsupported", "agent_definition_unsupported",
"The agent definition field cannot take effect yet.", "The agent definition field cannot take effect yet.",
Map.of("field", field)); Map.of("field", field));
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.persistence; package com.alibaba.qwen.code.managedagent.contracts.persistence;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -19,7 +19,7 @@ public interface ChannelDeliveryRepository {
"delivered", "unknown", "rejected", "cancelled"); "delivered", "unknown", "rejected", "cancelled");
/** One outbound delivery. */ /** One outbound delivery. */
record ChannelDelivery( public record ChannelDelivery(
String tenantId, String tenantId,
String channelInstanceId, String channelInstanceId,
String deliveryId, String deliveryId,
@@ -32,18 +32,18 @@ public interface ChannelDeliveryRepository {
} }
/** Exclusive page cursor: created time and delivery id of the last row. */ /** Exclusive page cursor: created time and delivery id of the last row. */
record DeliveryCursor(long createdAt, String deliveryId) { public record DeliveryCursor(long createdAt, String deliveryId) {
} }
/** One newest-first page of deliveries. */ /** One newest-first page of deliveries. */
record DeliveryPage(List<ChannelDelivery> deliveries, boolean hasMore) { public record DeliveryPage(List<ChannelDelivery> deliveries, boolean hasMore) {
} }
/** /**
* Whether the shared delivery line allows one step from {@code from} to * Whether the shared delivery line allows one step from {@code from} to
* {@code to} for a channel target. * {@code to} for a channel target.
*/ */
static boolean isLegalStep(String from, String to) { public static boolean isLegalStep(String from, String to) {
return DELIVERY_STATES.contains(to) return DELIVERY_STATES.contains(to)
&& ManagedExtensionRecords.TRANSITIONS.get("delivery") && ManagedExtensionRecords.TRANSITIONS.get("delivery")
.getOrDefault(from, List.of()).contains(to); .getOrDefault(from, List.of()).contains(to);
@@ -54,9 +54,9 @@ public interface ChannelDeliveryRepository {
* the row its deliveryId already committed. A duplicate id bound to * the row its deliveryId already committed. A duplicate id bound to
* another segment fails instead of silently joining it. * another segment fails instead of silently joining it.
*/ */
ChannelDelivery findOrCreate(ChannelDelivery candidate); public ChannelDelivery findOrCreate(ChannelDelivery candidate);
Optional<ChannelDelivery> find(String tenantId, String channelInstanceId, public Optional<ChannelDelivery> find(String tenantId, String channelInstanceId,
String deliveryId); String deliveryId);
/** /**
@@ -67,11 +67,11 @@ public interface ChannelDeliveryRepository {
* current row as the idempotent replay of this step; null when the row * current row as the idempotent replay of this step; null when the row
* is absent or holds any other state. * is absent or holds any other state.
*/ */
ChannelDelivery transition(String tenantId, String channelInstanceId, public ChannelDelivery transition(String tenantId, String channelInstanceId,
String deliveryId, String expectedState, String state, String deliveryId, String expectedState, String state,
String providerReceipt); String providerReceipt);
ChannelDelivery transition(String tenantId, String channelInstanceId, public ChannelDelivery transition(String tenantId, String channelInstanceId,
String deliveryId, String expectedState, String state, String deliveryId, String expectedState, String state,
String providerReceipt, long claimGeneration); String providerReceipt, long claimGeneration);
@@ -79,6 +79,6 @@ public interface ChannelDeliveryRepository {
* Newest first by (createdAt, deliveryId) descending; {@code before} * Newest first by (createdAt, deliveryId) descending; {@code before}
* excludes its own row. {@code limit} must be in [1, 100]. * excludes its own row. {@code limit} must be in [1, 100].
*/ */
DeliveryPage listByChannel(String tenantId, String channelInstanceId, public DeliveryPage listByChannel(String tenantId, String channelInstanceId,
DeliveryCursor before, int limit); DeliveryCursor before, int limit);
} }
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.persistence; package com.alibaba.qwen.code.managedagent.contracts.persistence;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -17,7 +17,7 @@ public interface ChannelRouteRepository {
String SEPARATOR = Character.toString((char) 0); String SEPARATOR = Character.toString((char) 0);
/** One committed route binding. */ /** One committed route binding. */
record ChannelRoute( public record ChannelRoute(
String tenantId, String tenantId,
String routeKey, String routeKey,
String channelInstanceId, String channelInstanceId,
@@ -36,15 +36,15 @@ public interface ChannelRouteRepository {
} }
/** Exclusive page cursor: created time and route key of the last row. */ /** Exclusive page cursor: created time and route key of the last row. */
record RouteCursor(long createdAt, String routeKey) { public record RouteCursor(long createdAt, String routeKey) {
} }
/** One newest-first page of route bindings. */ /** One newest-first page of route bindings. */
record RoutePage(List<ChannelRoute> routes, boolean hasMore) { public record RoutePage(List<ChannelRoute> routes, boolean hasMore) {
} }
/** The primary key beside the tenant: the binding identity digest. */ /** The primary key beside the tenant: the binding identity digest. */
static String routeKey(String tenantId, String channelInstanceId, public static String routeKey(String tenantId, String channelInstanceId,
long accountGeneration, String platformEventId, long accountGeneration, String platformEventId,
long semanticRevision) { long semanticRevision) {
if (tenantId.contains(SEPARATOR) || channelInstanceId.contains( if (tenantId.contains(SEPARATOR) || channelInstanceId.contains(
@@ -62,21 +62,21 @@ public interface ChannelRouteRepository {
* committed. A candidate whose identity matches an existing row adds * committed. A candidate whose identity matches an existing row adds
* nothing, so ingress redelivery of one platform event is one binding. * nothing, so ingress redelivery of one platform event is one binding.
*/ */
ChannelRoute findOrCreate(ChannelRoute candidate); public ChannelRoute findOrCreate(ChannelRoute candidate);
Optional<ChannelRoute> find(String tenantId, String routeKey); public Optional<ChannelRoute> find(String tenantId, String routeKey);
/** /**
* Records the input a staged route's Session admitted. Returns null when * Records the input a staged route's Session admitted. Returns null when
* the binding is absent or already admitted with another input; a replay * the binding is absent or already admitted with another input; a replay
* with the same input returns the current row. * with the same input returns the current row.
*/ */
ChannelRoute admit(String tenantId, String routeKey, String inputId); public ChannelRoute admit(String tenantId, String routeKey, String inputId);
/** /**
* Newest first by (createdAt, routeKey) descending; {@code before} * Newest first by (createdAt, routeKey) descending; {@code before}
* excludes its own row. {@code limit} must be in [1, 100]. * excludes its own row. {@code limit} must be in [1, 100].
*/ */
RoutePage listByChannel(String tenantId, String channelInstanceId, public RoutePage listByChannel(String tenantId, String channelInstanceId,
RouteCursor before, int limit); RouteCursor before, int limit);
} }
@@ -1,4 +1,4 @@
package com.alibaba.qwen.code.managedagent.persistence; package com.alibaba.qwen.code.managedagent.contracts.persistence;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -6,15 +6,15 @@ import org.apache.commons.lang3.StringUtils;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** Shared identity helpers of the H5 channel persistence. */ /** Shared identity helpers of the H5 channel persistence. */
final class ChannelStoreSupport { public final class ChannelStoreSupport {
private ChannelStoreSupport() { private ChannelStoreSupport() {
} }
static String sha256(String value) { public static String sha256(String value) {
return DigestUtils.sha256Hex(value.getBytes(StandardCharsets.UTF_8)); return DigestUtils.sha256Hex(value.getBytes(StandardCharsets.UTF_8));
} }
static String requireId(String value, String name) { public static String requireId(String value, String name) {
if (StringUtils.isBlank(value)) { if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException(name + " is required"); throw new IllegalArgumentException(name + " is required");
} }
Loaded 100 of 1231 files, more files were not shown because too many files have changed in this diff. Show more