From fc1410a233bd97378e69b4fd79b7466a5273e116 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 31 Oct 2026 18:50:51 +0800 Subject: [PATCH 1/4] fix(release): ship the standard incubation disclaimer in Rust packages Generated-by: Codex --- .github/workflows/website.yml | 10 ++----- DISCLAIMER | 7 +++++ DISCLAIMER-WIP | 20 -------------- LICENSE | 27 +++++++++++++++++++ NOTICE | 6 +++++ README.md | 2 +- README.zh-CN.md | 2 +- docs/code-origin-audit.md | 9 +++++++ packages/cli/package.json | 1 + products/maka/cli/src/distribution.rs | 14 ++++++++++ products/maka/cli/src/distribution/archive.rs | 1 + products/maka/cli/src/distribution/target.rs | 4 +++ scripts/asf-license-headers.mjs | 2 +- scripts/asf-source-release.mjs | 8 +++--- scripts/asf-source-release.test.mjs | 4 +-- scripts/rust/pack-cli.mjs | 26 ++++++++++++++++-- scripts/rust/pack-cli.test.mjs | 8 ++++-- scripts/rust/pack-launcher.mjs | 8 ++++-- scripts/rust/release-cli.test.mjs | 10 +++++-- scripts/rust/verify-release.mjs | 10 ++++++- website/test/site.test.mjs | 4 +-- 21 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 DISCLAIMER delete mode 100644 DISCLAIMER-WIP diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 2e0020e0d87..5a459ba63f5 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -24,13 +24,7 @@ name: Website on: pull_request: paths: - [ - "website/**", - "assets/logo.png", - "README*", - "DISCLAIMER-WIP", - ".github/workflows/website.yml", - ] + ["website/**", "assets/logo.png", "README*", "DISCLAIMER", ".github/workflows/website.yml"] push: branches: [main] tags: ["v*"] @@ -38,7 +32,7 @@ on: - "website/**" - "assets/logo.png" - "README*" - - "DISCLAIMER-WIP" + - "DISCLAIMER" - "LICENSE" - "NOTICE" - ".asf.yaml" diff --git a/DISCLAIMER b/DISCLAIMER new file mode 100644 index 00000000000..53717808017 --- /dev/null +++ b/DISCLAIMER @@ -0,0 +1,7 @@ +Apache Maka is an effort undergoing incubation at The Apache Software Foundation +(ASF), sponsored by the Apache Incubator PMC. Incubation is required of all +newly accepted projects until a further review indicates that the +infrastructure, communications, and decision-making process have stabilized in a +manner consistent with other successful ASF projects. While incubation status is +not necessarily a reflection of the completeness or stability of the code, it +does indicate that the project has yet to be fully endorsed by the ASF. diff --git a/DISCLAIMER-WIP b/DISCLAIMER-WIP deleted file mode 100644 index 40e8fe403c6..00000000000 --- a/DISCLAIMER-WIP +++ /dev/null @@ -1,20 +0,0 @@ -Apache Maka is an effort undergoing incubation at The Apache Software Foundation -(ASF), sponsored by the Apache Incubator PMC. Incubation is required of all -newly accepted projects until a further review indicates that the -infrastructure, communications, and decision-making process have stabilized in a -manner consistent with other successful ASF projects. While incubation status is -not necessarily a reflection of the completeness or stability of the code, it -does indicate that the project has yet to be fully endorsed by the ASF. - -Some of the incubating project's releases may not be fully compliant with ASF -policy. For example, releases may have incomplete or un-reviewed licensing -conditions. What follows is a list of issues the project is currently aware of -(this list is likely to be incomplete): - - * The software grant and committer ICLAs are not yet complete. - -If you are planning to incorporate this work into your product/project, please -be aware that you will need to conduct a thorough licensing review to determine -the overall implications of including this work. For the current status of this -project through the Apache Incubator, visit: -https://incubator.apache.org/projects/maka.html diff --git a/LICENSE b/LICENSE index 8e4b6c89775..a6e7e57ef35 100644 --- a/LICENSE +++ b/LICENSE @@ -203,6 +203,33 @@ THIRD-PARTY COMPONENTS +Playwright browser injected script and keyboard data (Apache License 2.0) + +third-party/playwright contains the injected script and US keyboard data from +playwright-core 1.58.2, revision ce480a952553175eae75342aad2c5e86cdf2cbba. +Source: https://github.com/microsoft/playwright +Copyright (c) Microsoft Corporation +Original LICENSE, NOTICE and ThirdPartyNotices.txt are preserved beside the +source. The published archive, extraction method and file hashes are recorded +in third-party/playwright/upstream.json. + +reqwest HTTP client source (adapted, Apache License 2.0 OR MIT) + +third-party/reqwest contains reqwest 0.13.5 from +https://github.com/seanmonstar/reqwest at revision +de55373434f07f42926599dbb5a88550d8e55112. +Copyright (c) 2016-2026 Sean McArthur +Original license texts, source hashes and local changes are recorded in +third-party/reqwest/{LICENSE-APACHE,LICENSE-MIT,upstream.json,SOURCE.md}. + +image-webp decoder source (adapted, Apache License 2.0 OR MIT) + +third-party/image-webp contains image-webp 0.2.4 from +https://github.com/image-rs/image-webp at revision +08b57d473c90148dd4b37a075df365afc3dfcc2b. +Original license texts, source hashes and local changes are recorded in +third-party/image-webp/{LICENSE-APACHE,LICENSE-MIT,upstream.json,SOURCE.md}. + Zod dependency patch Source: https://www.npmjs.com/package/zod/v/4.6.7 diff --git a/NOTICE b/NOTICE index 2d925c66640..3a1a7198f45 100644 --- a/NOTICE +++ b/NOTICE @@ -9,3 +9,9 @@ Copyright 2025 OpenAI This product includes portions of GPUI Fast / GPUI. Copyright 2022 - 2025 Zed Industries, Inc. + +Playwright +Copyright (c) Microsoft Corporation + +This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer), +available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE). diff --git a/README.md b/README.md index cdcf09067db..0b219b88070 100644 --- a/README.md +++ b/README.md @@ -61,4 +61,4 @@ Use `just --list` for development commands. See [Contributing](CONTRIBUTING.md), The npm launcher selects a matching native package; it contains no agent runtime. Native preview artifacts are built and verified from one source archive. Packaging and publication are documented in [packages/cli](packages/cli/README.md). -Apache Maka is undergoing incubation at the Apache Software Foundation. See [DISCLAIMER-WIP](DISCLAIMER-WIP), [LICENSE](LICENSE) and [NOTICE](NOTICE). +Apache Maka is undergoing incubation at the Apache Software Foundation. See [DISCLAIMER](DISCLAIMER), [LICENSE](LICENSE) and [NOTICE](NOTICE). diff --git a/README.zh-CN.md b/README.zh-CN.md index 65949b1b4f5..c1009cbc9c0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -61,4 +61,4 @@ just run npm 启动器选择匹配的原生平台包,自身不包含 Agent 运行时。原生预览包从同一份源码归档构建并验证,流程见 [packages/cli](packages/cli/README.zh-CN.md)。 -Apache Maka 正在 Apache 软件基金会孵化。参见 [DISCLAIMER-WIP](DISCLAIMER-WIP)、[LICENSE](LICENSE) 和 [NOTICE](NOTICE)。 +Apache Maka 正在 Apache 软件基金会孵化。参见 [DISCLAIMER](DISCLAIMER)、[LICENSE](LICENSE) 和 [NOTICE](NOTICE)。 diff --git a/docs/code-origin-audit.md b/docs/code-origin-audit.md index a6245758266..bf8c6c8b259 100644 --- a/docs/code-origin-audit.md +++ b/docs/code-origin-audit.md @@ -21,6 +21,14 @@ Third-party source and dependency patch attribution is recorded in [LICENSE](../LICENSE). Apply-patch adaptations have a detailed record in [plugins/workspace/workspace-files/patch/SOURCE.md](../plugins/workspace/workspace-files/patch/SOURCE.md); native Computer Use notices live in [platform/computer-use/THIRD_PARTY_NOTICES](../platform/computer-use/THIRD_PARTY_NOTICES). +### Release licensing scope + +The historical incoming-code review used the individual-contributor ICLA route submitted in the [0.2.0 RC2 vote](https://lists.apache.org/thread/743v94gvldqs4pbwwg59l22pojb44v4k). The questioned contributor ICLAs were [publicly confirmed](https://lists.apache.org/thread/47jpk47vykyqr4gxy85szpq25zw0qh9f), after which Justin [withdrew his binding -1](https://lists.apache.org/thread/yno6g7kzfc9dgntt0n572zk4vnj4jr2x). The standard [DISCLAIMER](../DISCLAIMER) retains the required incubation notice without repeating the resolved ICLA concern. + +This branch's source archive contains first-party Rust and TypeScript source, adapted source, pinned vendored source and the assets listed below. Vendored components preserve their own licenses and source records; a language rewrite does not remove those obligations. `scripts/asf-source-release.mjs` verifies the actual archive contents, including headers, manifest licenses and the finite non-text inventory. + +Native npm packages include the root legal documents and the reviewed Rust, embedded JavaScript, V8 and Rust standard-library notices. The launcher carries the same disclaimer, and packaged macOS application resources also include the legal documents and native notices. Dependency and notice inventories are checked by `scripts/rust/dependencies.mjs` and `scripts/rust/notices.mjs`. MPL dependencies permitted for binary distribution remain external dependencies of the source archive and have license texts and exact source references in the native notices. + ### Native model protocol validation `plugins/agent/provider-protocol` implements Responses, Chat Completions compatible and Anthropic Messages with protocol-owned Rust input types, wire decoders and stream state. Chat Completions and Anthropic behavior was checked against the relevant unchanged Vercel AI SDK tests at revision `e623e8c88b55066dde769614a16ae51d424974aa` (openai-compatible 3.0.77 and anthropic 4.0.87). The upstream tests and temporary validation bindings are external validation tools and are not distributed with Maka. The product uses the same Rust protocol core through Host-scoped transport in `plugins/agent/model/src/adapters`. @@ -28,6 +36,7 @@ Third-party source and dependency patch attribution is recorded in [LICENSE](../ ### Source archive non-text inventory - `assets/logo.png`: Product artwork reproduced by `scripts/generate-logo.py` from its vector geometry; Apache-2.0. +- `products/maka/desktop-shell/maka.ico`: Windows multi-resolution icon derived from `assets/logo.png` in [#6446](https://github.com/apache/maka/pull/6446); seven PNG images at 16–256 pixels, embedded by `windows.rc`; Apache-2.0. - `products/maka/workbench/app/assets/maka.png`: Byte-identical copy of `assets/logo.png`, bundled with the workbench application's composer; Apache-2.0. - `website/src/assets/incubator.png`: Unmodified Apache Incubator trademark artwork from https://www.apache.org/logos/res/incubator/default.png. - `website/src/assets/social.en.png`: English website preview rendered by `website/scripts/social-preview.mjs`; includes the product mark and self-hosted OFL-1.1 fonts. diff --git a/packages/cli/package.json b/packages/cli/package.json index 31f4bd3bd00..e72f516a3dd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -13,6 +13,7 @@ }, "files": [ "bin", + "DISCLAIMER", "LICENSE", "NOTICE", "README.md", diff --git a/products/maka/cli/src/distribution.rs b/products/maka/cli/src/distribution.rs index 0a1ec0f3efb..848f8e99138 100644 --- a/products/maka/cli/src/distribution.rs +++ b/products/maka/cli/src/distribution.rs @@ -537,6 +537,7 @@ mod tests { }; append("package.json", &serde_json::to_vec(&manifest).unwrap()); for name in [ + "DISCLAIMER", "LICENSE", "NOTICE", "THIRD_PARTY_NOTICES.txt", @@ -590,6 +591,12 @@ mod tests { b"\n", ); append("bin/Maka.app/Contents/Resources/Maka.icns", b"icns"); + for name in ["DISCLAIMER", "LICENSE", "NOTICE", "THIRD_PARTY_NOTICES.txt"] { + append( + &format!("bin/Maka.app/Contents/Resources/{name}"), + b"test license", + ); + } } if let Some(name) = extra { append( @@ -877,6 +884,7 @@ mod tests { (linux, None, true, None), (linux, Some("bin/maka"), false, None), (linux, Some("bin/extra"), false, None), + (linux, None, false, Some("DISCLAIMER")), (linux, None, false, Some("bin/maka-python-worker")), ( Target::Win32X64, @@ -891,6 +899,12 @@ mod tests { false, Some("bin/Maka.app/Contents/Resources/Maka.icns"), ), + ( + macos, + None, + false, + Some("bin/Maka.app/Contents/Resources/DISCLAIMER"), + ), // A package has no optional files. ( macos, diff --git a/products/maka/cli/src/distribution/archive.rs b/products/maka/cli/src/distribution/archive.rs index 8b6eecbcb45..ca16e673e91 100644 --- a/products/maka/cli/src/distribution/archive.rs +++ b/products/maka/cli/src/distribution/archive.rs @@ -64,6 +64,7 @@ fn allowed(target: Target, name: &str) -> bool { fn required(target: Target) -> impl Iterator { [ "package.json", + "DISCLAIMER", "LICENSE", "NOTICE", "THIRD_PARTY_NOTICES.txt", diff --git a/products/maka/cli/src/distribution/target.rs b/products/maka/cli/src/distribution/target.rs index 21b1c4cc698..d3f66f7bf19 100644 --- a/products/maka/cli/src/distribution/target.rs +++ b/products/maka/cli/src/distribution/target.rs @@ -110,6 +110,10 @@ impl Target { Self::DarwinArm64 | Self::DarwinX64 => &[ "bin/Maka.app/Contents/Info.plist", "bin/Maka.app/Contents/Resources/Maka.icns", + "bin/Maka.app/Contents/Resources/DISCLAIMER", + "bin/Maka.app/Contents/Resources/LICENSE", + "bin/Maka.app/Contents/Resources/NOTICE", + "bin/Maka.app/Contents/Resources/THIRD_PARTY_NOTICES.txt", ], _ => &[], } diff --git a/scripts/asf-license-headers.mjs b/scripts/asf-license-headers.mjs index 5fec6dfd03f..a8bf8ffbad4 100644 --- a/scripts/asf-license-headers.mjs +++ b/scripts/asf-license-headers.mjs @@ -214,7 +214,7 @@ export const exclusionRules = [ id: 'asf-release-documents', justification: 'These files are the license, notice, and incubation disclosure themselves. ASF policy fixes their contents, so a header inside them would be circular.', - matches: isOneOf('DISCLAIMER-WIP', 'LICENSE', 'NOTICE'), + matches: isOneOf('DISCLAIMER', 'LICENSE', 'NOTICE'), }, { id: 'third-party-license-texts', diff --git a/scripts/asf-source-release.mjs b/scripts/asf-source-release.mjs index 1f13c1072ae..1039e2a29b2 100644 --- a/scripts/asf-source-release.mjs +++ b/scripts/asf-source-release.mjs @@ -42,7 +42,7 @@ import { fileURLToPath } from 'node:url'; const defaultRepoRoot = resolve(import.meta.dirname, '..'); const archivePattern = /^apache-maka-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)-incubating-src\.tar\.gz$/; const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/; -const requiredReleaseDocuments = ['DISCLAIMER-WIP', 'LICENSE', 'NOTICE']; +const requiredReleaseDocuments = ['DISCLAIMER', 'LICENSE', 'NOTICE']; const requiredRootFiles = [...requiredReleaseDocuments, 'package.json', 'package-lock.json']; const forbiddenSegments = new Set(['.agents', '.claude', '.git', '.maka-shots', 'node_modules']); const knownNonCategoryXLicenses = new Set([ @@ -127,7 +127,7 @@ const textSourceBasenames = new Set([ '.mailmap', 'Cargo.toml.orig', 'CNAME', - 'DISCLAIMER-WIP', + 'DISCLAIMER', 'Dockerfile', 'LICENSE', 'LICENSE-APACHE', @@ -571,10 +571,10 @@ function validateArchiveContents(archivePath, identity, entries) { const contents = readFileSync(join(candidateRoot, requiredFile), 'utf8'); if (!contents.trim()) throw new Error(`${requiredFile} is empty`); if ( - requiredFile === 'DISCLAIMER-WIP' && + requiredFile === 'DISCLAIMER' && (!contents.includes('Apache Maka') || !contents.includes('incubation')) ) { - throw new Error('DISCLAIMER-WIP does not identify Apache Maka as an incubating project'); + throw new Error('DISCLAIMER does not identify Apache Maka as an incubating project'); } } diff --git a/scripts/asf-source-release.test.mjs b/scripts/asf-source-release.test.mjs index 4ce8abee311..805dc4feb83 100644 --- a/scripts/asf-source-release.test.mjs +++ b/scripts/asf-source-release.test.mjs @@ -373,7 +373,7 @@ describe('ASF source release verification', () => { 'add', 'package.json', 'package-lock.json', - 'DISCLAIMER-WIP', + 'DISCLAIMER', 'LICENSE', 'NOTICE', '.gitignore', @@ -641,7 +641,7 @@ function writeReleaseContents(root, { includeAttributes = false } = {}) { `${JSON.stringify({ lockfileVersion: 3, name: 'maka', packages: { '': { name: 'maka', version: '0.1.12' } }, version: '0.1.12' }, null, 2)}\n`, ); writeFileSync( - join(root, 'DISCLAIMER-WIP'), + join(root, 'DISCLAIMER'), 'Apache Maka is undergoing incubation at The Apache Software Foundation.\n', ); writeFileSync(join(root, 'LICENSE'), 'Apache License, Version 2.0\n'); diff --git a/scripts/rust/pack-cli.mjs b/scripts/rust/pack-cli.mjs index 73840403171..4bff8828931 100644 --- a/scripts/rust/pack-cli.mjs +++ b/scripts/rust/pack-cli.mjs @@ -20,7 +20,17 @@ import { execFile } from 'node:child_process'; import { createHash } from 'node:crypto'; import { constants, createReadStream } from 'node:fs'; -import { chmod, copyFile, lstat, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { + appendFile, + chmod, + copyFile, + lstat, + mkdir, + mkdtemp, + readFile, + rm, + writeFile, +} from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; @@ -94,15 +104,17 @@ export async function packNativeCli({ ); await chmod(join(directory, 'bin', 'maka-service.exe'), 0o755); } - for (const name of ['LICENSE', 'NOTICE']) { + for (const name of ['DISCLAIMER', 'LICENSE', 'NOTICE']) { await copyRegular(join(repositoryRoot, name), join(directory, name), 16 * 1024 * 1024); } + const disclaimer = await readFile(join(directory, 'DISCLAIMER'), 'utf8'); for (const name of ['README.md', 'README.zh-CN.md']) { await copyRegular( join(repositoryRoot, 'packages/cli', name), join(directory, name), 1024 * 1024, ); + await appendFile(join(directory, name), '\n\n' + disclaimer); } // Release owners supply the reviewed Rust/V8/embedded-JS notice closure. // The old Node CLI notice file alone is not sufficient for this binary. @@ -111,6 +123,15 @@ export async function packNativeCli({ join(directory, 'THIRD_PARTY_NOTICES.txt'), 16 * 1024 * 1024, ); + if (platform.os === 'darwin') { + for (const name of ['DISCLAIMER', 'LICENSE', 'NOTICE', 'THIRD_PARTY_NOTICES.txt']) { + await copyRegular( + join(directory, name), + join(directory, 'bin', 'Maka.app', 'Contents', 'Resources', name), + 16 * 1024 * 1024, + ); + } + } const name = `@maka-agent/cli-${target}`; await writeFile( join(directory, 'package.json'), @@ -128,6 +149,7 @@ export async function packNativeCli({ bin: { maka: `bin/${executable}` }, files: [ 'bin', + 'DISCLAIMER', 'LICENSE', 'NOTICE', 'THIRD_PARTY_NOTICES.txt', diff --git a/scripts/rust/pack-cli.test.mjs b/scripts/rust/pack-cli.test.mjs index 70d26846d33..af938b7d1e9 100644 --- a/scripts/rust/pack-cli.test.mjs +++ b/scripts/rust/pack-cli.test.mjs @@ -38,7 +38,7 @@ test('native archives carry the CLI and desktop shell without a terminal sidecar await mkdir(join(root, 'assets')); await writeFile(join(root, 'assets/logo.png'), png); await mkdir(join(root, 'packages/cli'), { recursive: true }); - for (const name of ['LICENSE', 'NOTICE', 'THIRD_PARTY_NOTICES.txt']) + for (const name of ['DISCLAIMER', 'LICENSE', 'NOTICE', 'THIRD_PARTY_NOTICES.txt']) await writeFile(join(root, name), 'reviewed ' + name); for (const name of ['README.md', 'README.zh-CN.md']) await writeFile(join(root, 'packages/cli', name), 'package documentation'); @@ -86,7 +86,11 @@ test('native archives carry the CLI and desktop shell without a terminal sidecar assert.match(plist, /CFBundleShortVersionString<\/key>\n 0.2.0<\/string>/); assert.equal(execFileSync('tar', ['-xOf', archive, bundle + 'Resources/Maka.icns']).subarray(0, 4).toString(), 'icns'); } - assert.equal(execFileSync('tar', ['-xOf', archive, 'package/THIRD_PARTY_NOTICES.txt'], {encoding:'utf8'}), 'reviewed THIRD_PARTY_NOTICES.txt'); + for (const name of ['DISCLAIMER', 'LICENSE', 'NOTICE', 'THIRD_PARTY_NOTICES.txt']) { + assert.equal(execFileSync('tar', ['-xOf', archive, 'package/' + name], {encoding:'utf8'}), 'reviewed ' + name); + if (macos) assert.equal(execFileSync('tar', ['-xOf', archive, bundle + 'Resources/' + name], {encoding:'utf8'}), 'reviewed ' + name); + } + for (const name of ['README.md', 'README.zh-CN.md']) assert.ok(execFileSync('tar', ['-xOf', archive, 'package/' + name], {encoding:'utf8'}).includes('reviewed DISCLAIMER')); `, ); process.chdir(root); diff --git a/scripts/rust/pack-launcher.mjs b/scripts/rust/pack-launcher.mjs index 7bab154e113..322a9d80288 100644 --- a/scripts/rust/pack-launcher.mjs +++ b/scripts/rust/pack-launcher.mjs @@ -18,7 +18,7 @@ */ import { execFile } from 'node:child_process'; -import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { appendFile, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -48,6 +48,7 @@ export async function packLauncher(directory) { '-C', stage, `${candidate.rootDirectory}/packages/cli`, + `${candidate.rootDirectory}/DISCLAIMER`, `${candidate.rootDirectory}/LICENSE`, `${candidate.rootDirectory}/NOTICE`, ], @@ -80,8 +81,11 @@ export async function packLauncher(directory) { join(packageDirectory, 'package.json'), JSON.stringify(manifest, null, 2) + '\n', ); - for (const name of ['LICENSE', 'NOTICE']) + for (const name of ['DISCLAIMER', 'LICENSE', 'NOTICE']) await copyFile(join(root, name), join(packageDirectory, name)); + const disclaimer = await readFile(join(root, 'DISCLAIMER'), 'utf8'); + for (const name of ['README.md', 'README.zh-CN.md']) + await appendFile(join(packageDirectory, name), '\n\n' + disclaimer); const { stdout } = await run( 'npm', ['pack', '--json', '--ignore-scripts', '--offline'], diff --git a/scripts/rust/release-cli.test.mjs b/scripts/rust/release-cli.test.mjs index 0bcd09946e0..1de405a2acd 100644 --- a/scripts/rust/release-cli.test.mjs +++ b/scripts/rust/release-cli.test.mjs @@ -56,7 +56,7 @@ test('prepared release binds its source and recovers partial publication and mis }), LICENSE: 'Apache License, Version 2.0\n', NOTICE: 'Apache Maka\n', - 'DISCLAIMER-WIP': 'Apache Maka is undergoing incubation.\n', + DISCLAIMER: 'Apache Maka is undergoing incubation.\n', })) await writeFile(join(sourceRoot, file), content); const archive = root + '-src.tar.gz'; @@ -105,6 +105,7 @@ test('prepared release binds its source and recovers partial publication and mis assert.deepEqual( files, [ + 'package/DISCLAIMER', 'package/LICENSE', 'package/NOTICE', 'package/README.md', @@ -113,6 +114,10 @@ test('prepared release binds its source and recovers partial publication and mis 'package/package.json', ].sort(), ); + assert.equal( + execFileSync('tar', ['-xOf', packed, 'package/DISCLAIMER'], { encoding: 'utf8' }), + await readFile(join(sourceRoot, 'DISCLAIMER'), 'utf8'), + ); assert.equal( execFileSync('tar', ['-xOf', packed, 'package/bin/maka.mjs'], { encoding: 'utf8' }), await readFile(join(sourceRoot, 'packages/cli/bin/maka.mjs'), 'utf8'), @@ -126,6 +131,7 @@ test('prepared release binds its source and recovers partial publication and mis ); assert.match(manifest.description, /Unofficial experimental.*not an ASF release/); const readme = execFileSync('tar', ['-xOf', packed, 'package/README.md'], { encoding: 'utf8' }); + assert.ok(readme.includes(await readFile(join(sourceRoot, 'DISCLAIMER'), 'utf8'))); assert.match( readme, /not an official Apache Software Foundation release and is not approved, endorsed or supported by the ASF/, @@ -304,7 +310,7 @@ test('source identity failures cannot reach dependency installation or compilati const files = { LICENSE: 'Apache-2.0\n', NOTICE: 'Apache Maka\n', - 'DISCLAIMER-WIP': 'Apache Maka is undergoing incubation.\n', + DISCLAIMER: 'Apache Maka is undergoing incubation.\n', 'package.json': JSON.stringify({ name: 'maka', version: '1.2.3', license: 'Apache-2.0' }), 'package-lock.json': JSON.stringify({ version: '1.2.3', diff --git a/scripts/rust/verify-release.mjs b/scripts/rust/verify-release.mjs index 54db5fa15f0..db9ecd18a42 100644 --- a/scripts/rust/verify-release.mjs +++ b/scripts/rust/verify-release.mjs @@ -18,7 +18,7 @@ */ import { execFile } from 'node:child_process'; -import { copyFile, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { copyFile, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -57,6 +57,14 @@ export async function verifyRelease(directory) { ], npmSpawnOptions({ cwd: stage, timeout: 180_000, maxBuffer: 1024 * 1024 }), ); + const installed = join(stage, 'node_modules'); + const disclaimer = await readFile(join(installed, native.name, 'DISCLAIMER'), 'utf8'); + if ( + !disclaimer.includes('Apache Maka') || + !disclaimer.includes('incubation') || + disclaimer !== (await readFile(join(installed, launcher.name, 'DISCLAIMER'), 'utf8')) + ) + throw new Error('Installed native package and launcher disclaimers differ or are invalid'); const command = join(stage, 'node_modules/@maka-agent/cli/bin/maka.mjs'); const options = { cwd: stage, timeout: 30_000, maxBuffer: 1024 * 1024, encoding: 'utf8' }; const { stdout } = await run(process.execPath, [command, '--version'], options); diff --git a/website/test/site.test.mjs b/website/test/site.test.mjs index 485315f6bea..f6831b64be9 100644 --- a/website/test/site.test.mjs +++ b/website/test/site.test.mjs @@ -32,9 +32,9 @@ const page = (path) => readFileSync(new URL(path, dist), 'utf8'); const locales = ['en', 'zh-CN']; const pages = ['index.html', 'downloads/index.html']; -// DISCLAIMER-WIP is the project's copy of the Incubator disclaimer, so the +// DISCLAIMER is the project's copy of the Incubator disclaimer, so the // site has to carry that paragraph rather than a wording of its own. -const disclaimer = readFileSync(new URL('../../DISCLAIMER-WIP', import.meta.url), 'utf8') +const disclaimer = readFileSync(new URL('../../DISCLAIMER', import.meta.url), 'utf8') .split('\n\n')[0] .replace(/\s+/gu, ' ') .trim(); From acdc72f1fb62ee124e3b690d2992d18338189766 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 31 Oct 2026 19:35:20 +0800 Subject: [PATCH 2/4] fix(computer-use): collapse browser handoff condition for CI Generated-by: Codex --- platform/computer-use/src/browser.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/platform/computer-use/src/browser.rs b/platform/computer-use/src/browser.rs index 8b61fb7ad42..1d4d404ad18 100644 --- a/platform/computer-use/src/browser.rs +++ b/platform/computer-use/src/browser.rs @@ -786,12 +786,12 @@ impl Browsers { extension_instance_id: None, }), }; - if let Some(options) = options { - if self.acquire_handoff(&options, cancellation).await? { - let discovered = provider::discover(&self.driver, self.owner).await; - self.extension_providers = discovered.providers; - self.extension_errors = discovered.errors; - } + if let Some(options) = options + && self.acquire_handoff(&options, cancellation).await? + { + let discovered = provider::discover(&self.driver, self.owner).await; + self.extension_providers = discovered.providers; + self.extension_errors = discovered.errors; } self.reconcile_extension_command(&command).await?; match command { From 487fe02777330edb7e5f483b1e81f8d09c8d4fea Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sat, 31 Oct 2026 20:25:28 +0800 Subject: [PATCH 3/4] fix(tests): gate desktop-only model reply helper Generated-by: Codex --- products/maka/terminal-shell/tests/real_host/support.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/products/maka/terminal-shell/tests/real_host/support.rs b/products/maka/terminal-shell/tests/real_host/support.rs index 37515efd76f..82df3ab1d2e 100644 --- a/products/maka/terminal-shell/tests/real_host/support.rs +++ b/products/maka/terminal-shell/tests/real_host/support.rs @@ -538,6 +538,7 @@ impl ModelRequest { self.finish_with_reason("stop").await; } /// Ends the reply with one call to `name`. + #[cfg(feature = "desktop-tests")] pub async fn tool_call(mut self, name: &str, arguments: Value) { self.packet(json!({"id":"nsh-real-model","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"nsh-real-call","type":"function","function":{"name":name,"arguments":arguments.to_string()}}]},"finish_reason":null}]})).await; self.finish_with_reason("tool_calls").await; From cfb19672cfcbddb8f8e2c3c6c2cf04569c8ee30e Mon Sep 17 00:00:00 2001 From: liugddx Date: Sat, 31 Oct 2026 22:11:48 +0800 Subject: [PATCH 4/4] feat(agent): give the Bot a sleep tool that waits for events sleep takes no arguments and has no duration. It ends the Turn through the engine's existing FinishTurn semantics, and the Bot is woken again by a user message, a WorkHub result or a scheduled event, each of which already starts its own Turn. It is refused only while a queued message is already bound for the running Invocation's next step, so a promoted message is read first. Like SendUser it is granted by Bot identity, always visible, hidden from other Sessions and rejected from a requested tool list. The standing instructions tell the model to call it on its own, not in the same step as SendUser, which FinishTurn would reject. Co-Authored-By: Claude Opus 5.5 --- plugins/agent/native/README.md | 2 +- plugins/agent/native/README.zh-CN.md | 2 +- plugins/agent/native/src/owner.rs | 16 ++- plugins/agent/native/src/owner/bot.rs | 110 ++++++++++++--- .../agent/native/src/owner/tests/bot_tests.rs | 127 ++++++++++++++++++ 5 files changed, 230 insertions(+), 27 deletions(-) diff --git a/plugins/agent/native/README.md b/plugins/agent/native/README.md index 87e689a3f6e..9e58874b288 100644 --- a/plugins/agent/native/README.md +++ b/plugins/agent/native/README.md @@ -59,7 +59,7 @@ Shell tools accept an optional model-authored `title`, enabled by default. Assis The default Maka composition enables `todo`: `todo_read` and `todo_write` maintain a persistent checklist for the current session, including in Code Mode and read-only workspaces. Writes replace the whole list with revision checks. The UI shows reported progress and a read-only Checklist panel, with bounded pages covering up to 200 items. Checklist completion is model-reported progress, not proof that an operation succeeded. -The default Maka composition enables `bot`, the Bot mode. `openBot` returns the installation's single Bot Session, creating it in the given workspace on first use or after the previous one was archived; the record is claimed before the Session is created, so concurrent and retried opens converge on one Session. That registered record is the only source of Bot identity: tool grants, recovery and `SendUser` all consult it, so forks and Sessions derived from the Bot are ordinary Sessions. The Bot Session is created with agent-native coordination tools (scheduled tasks, goals, recall, checklist, WorkHub delegation and skills), and `SendUser` is granted by identity rather than by that list; ordinary creation cannot request it. A call records its messages in Host-owned storage kept apart from Run state, one record per call and per message: the same `messageId` with the same content is a duplicate and is not recorded again, the same id with different content is refused, and sending nothing is valid. Calls are never evicted, so a reused id is always recognized. `botOutbox` pages messages oldest first after a sequence cursor, and `watchBotOutbox` signals head changes. Queued wakes of the Bot Session, including scheduled and WorkHub-origin messages, continue after an unknown tool outcome with the model told rather than the tool replayed; ordinary Sessions keep the stricter rule. +The default Maka composition enables `bot`, the Bot mode. `openBot` returns the installation's single Bot Session, creating it in the given workspace on first use or after the previous one was archived; the record is claimed before the Session is created, so concurrent and retried opens converge on one Session. That registered record is the only source of Bot identity: tool grants, recovery and `SendUser` all consult it, so forks and Sessions derived from the Bot are ordinary Sessions. The Bot Session is created with agent-native coordination tools (scheduled tasks, goals, recall, checklist, WorkHub delegation and skills), and `SendUser` and `sleep` are granted by identity rather than by that list; ordinary creation cannot request either. `sleep` takes no arguments and has no duration: it ends the Turn and the Bot is woken by a user message, a WorkHub result or a scheduled event, each of which starts its own Turn. It is refused only while a queued message is already bound for the running Invocation's next step, and it must be called on its own, not in the same step as `SendUser`. A call records its messages in Host-owned storage kept apart from Run state, one record per call and per message: the same `messageId` with the same content is a duplicate and is not recorded again, the same id with different content is refused, and sending nothing is valid. Calls are never evicted, so a reused id is always recognized. `botOutbox` pages messages oldest first after a sequence cursor, and `watchBotOutbox` signals head changes. Queued wakes of the Bot Session, including scheduled and WorkHub-origin messages, continue after an unknown tool outcome with the model told rather than the tool replayed; ordinary Sessions keep the stricter rule. The default Maka composition enables `automaticTitles`. Conversations created with `automaticTitle: true` receive one title attempt after their first admitted message, using the accepted model with a bounded, separately accounted request. Explicit titles, manual renames (including the unchanged placeholder), archived and managed sessions retain their names. Retries and Host restarts do not repeat an admitted title attempt; Host shutdown waits for its physical cleanup. diff --git a/plugins/agent/native/README.zh-CN.md b/plugins/agent/native/README.zh-CN.md index 5dfafbf1f5e..65ed99c9411 100644 --- a/plugins/agent/native/README.zh-CN.md +++ b/plugins/agent/native/README.zh-CN.md @@ -59,7 +59,7 @@ Shell 工具默认接受模型生成的可选 `title`。助手偏好可关闭后 Maka 默认组合启用 `todo`:`todo_read` 和 `todo_write` 维护当前会话的持久清单,也可用于 Code Mode 和只读工作区。写入会校验版本并替换整个列表。UI 显示已报告的进度和只读「清单」面板,以有界分页浏览最多 200 项。清单完成状态是模型报告的进度,不代表操作已成功的执行证据。 -Maka 默认组合启用 `bot`,即 Bot 模式。`openBot` 返回本安装唯一的 Bot 会话:首次使用或上一个已归档时,在指定工作区中创建。创建会话之前先认领登记记录,所以并发和重试的打开请求都会收敛到同一个会话。这条登记记录是判断 Bot 身份的唯一依据:工具授予、恢复和 `SendUser` 都以它为准,因此 fork 出来的会话和从 Bot 派生的会话都是普通会话。Bot 会话创建时只带 Agent 自带的协调类工具(定时任务、目标、历史检索、清单、WorkHub 委派和技能),`SendUser` 由身份授予,不依赖这份工具列表;普通创建不能申请它。每次调用都把消息写入由 Host 持有、与 Run 状态分开的存储,每次调用、每条消息各一条记录:同一 `messageId` 且内容相同视为重复,不会再次记录;同一 id 但内容不同会被拒绝;什么都不发也是合法结果。调用记录不会被淘汰,所以复用的 id 总能被识别。`botOutbox` 按序号游标从旧到新分页读取消息,`watchBotOutbox` 在消息头变化时发出通知。Bot 会话排队的唤醒,包括定时和 WorkHub 来源的消息,在工具结果未知后仍会继续:告知模型,而不是重放工具;普通会话保持更严格的规则。 +Maka 默认组合启用 `bot`,即 Bot 模式。`openBot` 返回本安装唯一的 Bot 会话:首次使用或上一个已归档时,在指定工作区中创建。创建会话之前先认领登记记录,所以并发和重试的打开请求都会收敛到同一个会话。这条登记记录是判断 Bot 身份的唯一依据:工具授予、恢复和 `SendUser` 都以它为准,因此 fork 出来的会话和从 Bot 派生的会话都是普通会话。Bot 会话创建时只带 Agent 自带的协调类工具(定时任务、目标、历史检索、清单、WorkHub 委派和技能),`SendUser` 和 `sleep` 由身份授予,不依赖这份工具列表;普通创建不能申请它们。`sleep` 没有参数也没有时长:它结束本轮,之后由用户消息、WorkHub 结果或到点的定时事件各自开启新一轮来唤醒 Bot。只有当排队消息已经注定在运行中调用的下一步送达时才会拒绝休眠,并且必须单独调用,不能和 `SendUser` 放在同一步。每次调用都把消息写入由 Host 持有、与 Run 状态分开的存储,每次调用、每条消息各一条记录:同一 `messageId` 且内容相同视为重复,不会再次记录;同一 id 但内容不同会被拒绝;什么都不发也是合法结果。调用记录不会被淘汰,所以复用的 id 总能被识别。`botOutbox` 按序号游标从旧到新分页读取消息,`watchBotOutbox` 在消息头变化时发出通知。Bot 会话排队的唤醒,包括定时和 WorkHub 来源的消息,在工具结果未知后仍会继续:告知模型,而不是重放工具;普通会话保持更严格的规则。 Maka 默认组合启用 `automaticTitles`。通过 `automaticTitle: true` 创建的会话在首条消息获准后,使用该消息选定的模型发起一次有界、独立计费的标题请求。显式命名、手动改名(包括保留原占位名称)、归档及托管会话保留原名称。重试和 Host 重启不会重复已接收的标题尝试;Host 关闭会等待请求实际清理完成。 diff --git a/plugins/agent/native/src/owner.rs b/plugins/agent/native/src/owner.rs index f799b0b41e8..d0f0b9d5ba3 100644 --- a/plugins/agent/native/src/owner.rs +++ b/plugins/agent/native/src/owner.rs @@ -507,7 +507,7 @@ impl Owner { if settings.bot { catalog.tools = catalog .tools - .with_always_visible(&[bot::SEND_USER]) + .with_always_visible(&[bot::SEND_USER, bot::SLEEP]) .map_err(|error| error.to_string())?; } if settings.recall { @@ -750,10 +750,12 @@ impl Owner { }); } let mut tools = input.tools; - // SendUser follows Bot identity, never a requested tool list. - if tools - .as_ref() - .is_some_and(|names| names.iter().any(|name| name == bot::SEND_USER)) + // SendUser and sleep follow Bot identity, never a requested tool list. + if tools.as_ref().is_some_and(|names| { + names + .iter() + .any(|name| name == bot::SEND_USER || name == bot::SLEEP) + }) { return Ok(Created::Rejected { reason: Rejection::ToolsUnavailable, @@ -1078,13 +1080,13 @@ impl Owner { None => self.mcp_projects.catalog(basis.mcp_revision.as_deref())?, }; let selected = plan::select(&catalog.tools, basis, &catalog.planning); - // SendUser is granted by Bot identity, independent of the tool list. + // SendUser and sleep are granted by Bot identity, independent of the tool list. let bot = self.bot.is(session); let excluded: Vec<_> = selected .definitions() .map(|definition| definition.name.as_str()) .filter(|name| { - let speaks = *name == bot::SEND_USER; + let speaks = *name == bot::SEND_USER || *name == bot::SLEEP; (speaks && !bot) || (basis.graph.is_none() && crate::workhub::tasks::graph::NAMES.contains(name)) || (!(speaks && bot) diff --git a/plugins/agent/native/src/owner/bot.rs b/plugins/agent/native/src/owner/bot.rs index 131fe40cb18..5557997bf76 100644 --- a/plugins/agent/native/src/owner/bot.rs +++ b/plugins/agent/native/src/owner/bot.rs @@ -35,6 +35,8 @@ use sha2::{Digest, Sha256}; use std::sync::{OnceLock, RwLock, Weak}; pub(super) const SEND_USER: &str = "SendUser"; +/// Ends the Turn and waits for events; it has no duration. +pub(super) const SLEEP: &str = "sleep"; /// Agent-native coordination tools the Bot Session is created with. `SendUser` is /// granted by Bot identity rather than by this list. pub(super) const COORDINATION_TOOLS: &[&str] = &[ @@ -213,20 +215,36 @@ impl Bot { self.current.read().expect("bot identity").clone() } pub fn tools(self: &Arc) -> Vec { - vec![ToolRegistration { - definition: ToolDefinition { - provider: None, - name: SEND_USER.into(), - description: "Send messages to your user. This is the only way the user sees anything from you; ordinary replies stay internal. Send only when something is worth the user's attention, and send nothing otherwise. Reuse the same messageId when retrying the same content.".into(), - input_schema: serde_json::to_value(schemars::schema_for!(SendUserInput)) - .expect("SendUser schema"), - output_schema: None, - freeform: None, + vec![ + ToolRegistration { + definition: ToolDefinition { + provider: None, + name: SEND_USER.into(), + description: "Send messages to your user. This is the only way the user sees anything from you; ordinary replies stay internal. Send only when something is worth the user's attention, and send nothing otherwise. Reuse the same messageId when retrying the same content.".into(), + input_schema: serde_json::to_value(schemars::schema_for!(SendUserInput)) + .expect("SendUser schema"), + output_schema: None, + freeform: None, + }, + nesting: ToolNesting::DirectOnly, + semantics: ToolSemantics::ExclusiveStep, + handler: ToolHandler::Prepared(self.clone()), + }, + ToolRegistration { + definition: ToolDefinition { + provider: None, + name: SLEEP.into(), + description: "End this Turn and wait. You are woken by a user message, a finished background task or a scheduled event; there is no duration to choose, and nothing runs until one of them arrives. Call it on its own as your last action, after any SendUser, once nothing is left to do now. It is refused while a message is about to reach you in this Turn; handle that message first.".into(), + input_schema: serde_json::to_value(schemars::schema_for!(SleepInput)) + .expect("sleep schema"), + output_schema: None, + freeform: None, + }, + nesting: ToolNesting::DirectOnly, + semantics: ToolSemantics::FinishTurn, + handler: ToolHandler::Prepared(self.clone()), }, - nesting: ToolNesting::DirectOnly, - semantics: ToolSemantics::ExclusiveStep, - handler: ToolHandler::Prepared(self.clone()), - }] + ] } async fn read( &self, @@ -406,9 +424,14 @@ struct SendUserMessage { text: String, } +/// `sleep` takes no arguments: the Bot waits for events, not for a time. +#[derive(Deserialize, Serialize, schemars::JsonSchema)] +#[serde(deny_unknown_fields)] +struct SleepInput {} + impl ToolPreparer for Bot { fn names(&self) -> Vec { - vec![SEND_USER.into()] + vec![SEND_USER.into(), SLEEP.into()] } fn prepare( &self, @@ -422,10 +445,43 @@ impl ToolPreparer for Bot { if cancellation.is_cancelled() { return Err(ToolRejection::Cancelled); } - if name != SEND_USER { + if name != SEND_USER && name != SLEEP { return Err(ToolRejection::Unavailable); } let owner = owner.ok_or(ToolRejection::Unavailable)?; + if name == SLEEP { + let _: SleepInput = serde_json::from_value(input).map_err(invalid)?; + owner + .check_bot_invocation(&context) + .await + .map_err(|message| ToolRejection::PreparationFailed { message })?; + return Ok(PreparedEffect::new(move |cancellation| { + Box::pin(async move { + if cancellation.is_cancelled() { + return Err(ToolError::Failed("sleep call cancelled".into())); + } + owner + .check_bot_invocation(&context) + .await + .map_err(ToolError::Failed)?; + if owner + .has_steering_for(&context) + .await + .map_err(ToolError::Failed)? + { + return Err(ToolError::Failed( + "A message is about to reach you in this Turn. Read it and handle it before you sleep.".into(), + )); + } + Ok(ToolSuccess::projected( + ToolOutput::Json(serde_json::json!({ "sleeping": true })), + DurableToolProjection::Text { + text: "Turn ended. You will be woken by a user message, a background task result or a scheduled event.".into(), + }, + )) + }) + })); + } let input: SendUserInput = serde_json::from_value(input).map_err(invalid)?; let message_id = input.message_id.trim().to_owned(); if message_id.is_empty() { @@ -507,6 +563,24 @@ impl ToolPreparer for Bot { } impl Owner { + /// Whether a queued message is already bound for this Invocation's next step. + /// Ordinary follow-ups are not counted: they start the next Turn on their own, + /// and the model could not read them now. + async fn has_steering_for(&self, context: &ToolCallContext) -> Result { + let session_id = &context.invocation.session_id; + let queue = self + .store + .session(session_id) + .await + .map_err(|error| error.to_string())? + .message_queue(session_id) + .await + .map_err(|error| error.to_string())?; + Ok(queue.entries.iter().any(|entry| { + entry.source.disposition == maka_agent_facts::message::MessageDisposition::Steering + && *entry.steering_target() == context.invocation + })) + } async fn check_bot_invocation(&self, context: &ToolCallContext) -> Result<(), String> { let session_id = &context.invocation.session_id; if !self.settings.bot @@ -746,13 +820,13 @@ impl Owner { fn bot_prompt(language: Language) -> &'static str { match language { Language::ZhCn => { - "你是用户在 Maka 里的常驻 Bot。\n\n对话方式:用户只能看到你用 SendUser 工具发出的消息;你的普通回复和工具步骤都是内部过程,用户看不到。要回应用户,就调用 SendUser,普通文字尽量少写。不值得打扰用户的事,就什么都不发。\n\n你的真实能力只有这些工具:SendUser(对用户说话)、ScheduledTask(安排定时唤醒,需用户批准)、GoalStatus(目标)、Recall 系列(检索历史对话)、todo_read / todo_write(待办)、workhub_tasks(把具体工作委派给后台任务)、Skill / SkillSearch(查阅技能说明)。你自己不能读写文件、运行命令、使用浏览器、访问网络或调用 MCP。需要动手做的事,用 workhub_tasks 委派出去,等结果回来再告诉用户;做不到又无法委派的事,如实说明。技能只是说明文档,其中需要上述被禁止能力的步骤你不能自己执行,不要把技能清单当作你自己的能力介绍给用户。需要以后再看的事,用 ScheduledTask 安排。" + "你是用户在 Maka 里的常驻 Bot。\n\n对话方式:用户只能看到你用 SendUser 工具发出的消息;你的普通回复和工具步骤都是内部过程,用户看不到。要回应用户,就调用 SendUser,普通文字尽量少写。不值得打扰用户的事,就什么都不发。\n\n你的真实能力只有这些工具:SendUser(对用户说话)、ScheduledTask(安排定时唤醒,需用户批准)、GoalStatus(目标)、Recall 系列(检索历史对话)、todo_read / todo_write(待办)、workhub_tasks(把具体工作委派给后台任务)、Skill / SkillSearch(查阅技能说明)、sleep(结束本轮并等待)。你自己不能读写文件、运行命令、使用浏览器、访问网络或调用 MCP。需要动手做的事,用 workhub_tasks 委派出去,等结果回来再告诉用户;做不到又无法委派的事,如实说明。技能只是说明文档,其中需要上述被禁止能力的步骤你不能自己执行,不要把技能清单当作你自己的能力介绍给用户。需要以后再看的事,用 ScheduledTask 安排。\n\n结束一轮:事情都处理完、暂时没有要做的了,就单独调用一次 sleep(不要和 SendUser 放在同一步)。sleep 没有时长,之后用户的消息、后台任务的结果或到点的定时任务会再叫醒你;如果提示有消息即将到达,先处理它再 sleep。" } Language::ZhTw => { - "你是使用者在 Maka 裡的常駐 Bot。\n\n對話方式:使用者只能看到你用 SendUser 工具發出的訊息;你的一般回覆和工具步驟都是內部過程,使用者看不到。要回應使用者,就呼叫 SendUser,一般文字盡量少寫。不值得打擾使用者的事,就什麼都不發。\n\n你的真實能力只有這些工具:SendUser(對使用者說話)、ScheduledTask(安排定時喚醒,需使用者核准)、GoalStatus(目標)、Recall 系列(檢索歷史對話)、todo_read / todo_write(待辦)、workhub_tasks(把具體工作委派給背景任務)、Skill / SkillSearch(查閱技能說明)。你自己不能讀寫檔案、執行指令、使用瀏覽器、存取網路或呼叫 MCP。需要動手做的事,用 workhub_tasks 委派出去,等結果回來再告訴使用者;做不到又無法委派的事,如實說明。技能只是說明文件,其中需要上述被禁止能力的步驟你不能自己執行,不要把技能清單當作你自己的能力介紹給使用者。需要之後再看的事,用 ScheduledTask 安排。" + "你是使用者在 Maka 裡的常駐 Bot。\n\n對話方式:使用者只能看到你用 SendUser 工具發出的訊息;你的一般回覆和工具步驟都是內部過程,使用者看不到。要回應使用者,就呼叫 SendUser,一般文字盡量少寫。不值得打擾使用者的事,就什麼都不發。\n\n你的真實能力只有這些工具:SendUser(對使用者說話)、ScheduledTask(安排定時喚醒,需使用者核准)、GoalStatus(目標)、Recall 系列(檢索歷史對話)、todo_read / todo_write(待辦)、workhub_tasks(把具體工作委派給背景任務)、Skill / SkillSearch(查閱技能說明)、sleep(結束本輪並等待)。你自己不能讀寫檔案、執行指令、使用瀏覽器、存取網路或呼叫 MCP。需要動手做的事,用 workhub_tasks 委派出去,等結果回來再告訴使用者;做不到又無法委派的事,如實說明。技能只是說明文件,其中需要上述被禁止能力的步驟你不能自己執行,不要把技能清單當作你自己的能力介紹給使用者。需要之後再看的事,用 ScheduledTask 安排。\n\n結束一輪:事情都處理完、暫時沒有要做的了,就單獨呼叫一次 sleep(不要和 SendUser 放在同一步)。sleep 沒有時長,之後使用者的訊息、背景任務的結果或到點的定時任務會再叫醒你;如果提示有訊息即將到達,先處理它再 sleep。" } _ => { - "You are the user's persistent Bot in Maka.\n\nHow you talk: the user sees only the messages you send with the SendUser tool; your ordinary replies and tool steps are internal and never shown. To respond to the user, call SendUser, and keep ordinary text minimal. When something does not need the user's attention, send nothing.\n\nYour real abilities are only these tools: SendUser (speak to the user), ScheduledTask (schedule a wake-up, which the user must approve), GoalStatus (goals), the Recall tools (search past conversations), todo_read / todo_write (checklist), workhub_tasks (delegate hands-on work to background tasks) and Skill / SkillSearch (read skill instructions). You yourself cannot read or edit files, run commands, use a browser, reach the network or call MCP tools. Delegate hands-on work with workhub_tasks and tell the user when the result comes back; if something can neither be done nor delegated, say so plainly. Skills are only documents: steps in them that need the abilities you lack are not yours to run, and the skill list is not a description of what you can do, so do not present it to the user as your abilities. Use ScheduledTask to come back to something later." + "You are the user's persistent Bot in Maka.\n\nHow you talk: the user sees only the messages you send with the SendUser tool; your ordinary replies and tool steps are internal and never shown. To respond to the user, call SendUser, and keep ordinary text minimal. When something does not need the user's attention, send nothing.\n\nYour real abilities are only these tools: SendUser (speak to the user), ScheduledTask (schedule a wake-up, which the user must approve), GoalStatus (goals), the Recall tools (search past conversations), todo_read / todo_write (checklist), workhub_tasks (delegate hands-on work to background tasks) Skill / SkillSearch (read skill instructions) and sleep (end the Turn and wait). You yourself cannot read or edit files, run commands, use a browser, reach the network or call MCP tools. Delegate hands-on work with workhub_tasks and tell the user when the result comes back; if something can neither be done nor delegated, say so plainly. Skills are only documents: steps in them that need the abilities you lack are not yours to run, and the skill list is not a description of what you can do, so do not present it to the user as your abilities. Use ScheduledTask to come back to something later.\n\nEnding a Turn: when everything is handled and nothing is left to do for now, call sleep on its own, never in the same step as SendUser. sleep has no duration; a user message, a background task result or a scheduled event wakes you again. If it says a message is about to reach you, handle that message first, then sleep." } } } diff --git a/plugins/agent/native/src/owner/tests/bot_tests.rs b/plugins/agent/native/src/owner/tests/bot_tests.rs index d69bd892661..358792f92ef 100644 --- a/plugins/agent/native/src/owner/tests/bot_tests.rs +++ b/plugins/agent/native/src/owner/tests/bot_tests.rs @@ -646,3 +646,130 @@ async fn a_live_slow_creator_keeps_its_claim() { .await .unwrap(); } + +#[tokio::test] +async fn sleep_ends_the_turn_and_the_next_message_starts_a_new_one() { + tokio::time::timeout(Duration::from_secs(90), async { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let f = bot_fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; + let bot = open(&f).await["sessionId"].as_str().unwrap().to_owned(); + f.send(&bot).await.unwrap(); + let (mut socket, _) = listener.accept().await.unwrap(); + let sent = request(&mut socket).await; + assert!( + tool_names(&sent).contains(&"sleep".to_owned()), + "{:?}", + tool_names(&sent) + ); + // sleep takes no duration. + let schema = sent["tools"] + .as_array() + .unwrap() + .iter() + .find(|tool| tool["function"]["name"] == "sleep") + .unwrap()["function"]["parameters"] + .clone(); + assert!( + schema["properties"] + .as_object() + .is_none_or(|properties| properties.is_empty()), + "{schema}" + ); + service_tools::named_tool_response(&mut socket, "sleep", json!({})).await; + + // The Turn is over: no further model request follows the sleep. + idle(&f, &bot).await; + assert!( + tokio::time::timeout(Duration::from_millis(500), listener.accept()) + .await + .is_err(), + "the model must not be called again after sleep" + ); + + // A later message wakes the Bot into a new Turn that still remembers the sleep. + f.mutation( + 2, + "send", + json!({"sessionId":bot,"message":"are you there?","model":f.selection().await}), + Uuid::new_v4(), + ) + .await + .unwrap(); + let (mut socket, _) = listener.accept().await.unwrap(); + let sent = request(&mut socket).await; + assert!(sent.to_string().contains("are you there?")); + assert!( + last_tool_result(&sent).contains("Turn ended"), + "{}", + last_tool_result(&sent) + ); + response(&mut socket).await; + idle(&f, &bot).await; + f.app.shutdown().await.unwrap(); + f.config.shutdown().await.unwrap(); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn only_the_bot_has_sleep_and_ordinary_creation_cannot_request_it() { + tokio::time::timeout(Duration::from_secs(90), async { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let f = bot_fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; + let _ = open(&f).await; + let ordinary = f.create("Ordinary").await; + f.send(&ordinary).await.unwrap(); + let (mut socket, _) = listener.accept().await.unwrap(); + let sent = request(&mut socket).await; + response(&mut socket).await; + idle(&f, &ordinary).await; + assert!( + !tool_names(&sent).contains(&"sleep".to_owned()), + "{:?}", + tool_names(&sent) + ); + let mut input = f.create_input("Impostor").await; + input["tools"] = json!(["sleep"]); + assert_eq!( + f.mutation(2, "create", input, Uuid::new_v4()) + .await + .unwrap()["kind"], + "rejected" + ); + f.app.shutdown().await.unwrap(); + f.config.shutdown().await.unwrap(); + }) + .await + .unwrap(); +} + +#[tokio::test] +async fn sleep_is_refused_while_a_message_is_bound_for_the_next_step() { + tokio::time::timeout(Duration::from_secs(90), async { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let f = bot_fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; + let bot = open(&f).await["sessionId"].as_str().unwrap().to_owned(); + f.send(&bot).await.unwrap(); + let (mut socket, _) = listener.accept().await.unwrap(); + request(&mut socket).await; + // A message queued during the Turn and promoted will be delivered at the next step. + let queued = f.mutation(2, "send", json!({"sessionId":bot,"message":"urgent: change of plan","model":f.selection().await}), Uuid::new_v4()).await.unwrap(); + let queue = f.query("queue", json!({"sessionId":bot})).await.unwrap(); + let promoted = f.mutation(2, "editQueue", json!({"sessionId":bot,"expectedRevision":queue["revision"],"edit":{"kind":"promote","messageId":queued["messageId"]}}), Uuid::new_v4()).await.unwrap(); + assert_eq!(promoted["kind"], "committed", "{promoted}"); + service_tools::named_tool_response(&mut socket, "sleep", json!({})).await; + + // The refusal reaches the model, the Turn goes on, and the message arrives with it. + let (mut socket, _) = listener.accept().await.unwrap(); + let sent = request(&mut socket).await; + assert!(last_tool_result(&sent).contains("about to reach you"), "{}", last_tool_result(&sent)); + assert!(sent.to_string().contains("urgent: change of plan")); + service_tools::named_tool_response(&mut socket, "sleep", json!({})).await; + idle(&f, &bot).await; + f.app.shutdown().await.unwrap(); + f.config.shutdown().await.unwrap(); + }) + .await + .unwrap(); +}