fix: make Agent updates reliable and visible
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s

Valid Agent packages could be rejected when packaged trusted-key JSON used CRLF, and Settings could not show whether a newer compatible Agent existed. Registry formatting is now parsed independently of cryptographic trust, and Settings reads only the signed catalog to report local and latest versions.

New installations use a credential-free local Ollama-compatible profile. The v20 migration changes only untouched historical defaults, while explicit profiles and legacy environment aliases remain compatible. Magic Notes count badges no longer shift navigation rows.

Release note: 修复 Agent 下载兼容性并在设置中显示更新状态;新安装默认连接本地模型,保留显式用户配置;魔法笔记待办数字不再改变侧栏菜单高度。
This commit is contained in:
mesalogo
2026-08-26 15:43:41 +08:00
parent f9137f0463
commit de64e9768a
33 changed files with 884 additions and 188 deletions
+9
View File
@@ -1091,12 +1091,19 @@ describe('registerIpcHandlers SSH hosts', () => {
}
const agentPackageInventory = {
checkedAt: '2030-01-01T00:00:00.000Z',
catalog: {
state: 'available',
checkedAt: '2030-01-01T00:00:00.000Z',
error: null
},
entries: [
{
platform: 'linux',
architecture: 'x64',
state: 'verified',
version: '0.11.2',
latestVersion: '0.11.4',
updateAvailable: true,
remoteRuntimeVersion: '1.18.9',
agentProtocol: { major: 2, minor: 0 }
},
@@ -1105,6 +1112,8 @@ describe('registerIpcHandlers SSH hosts', () => {
architecture: 'arm64',
state: 'not-downloaded',
version: null,
latestVersion: '0.11.4',
updateAvailable: false,
remoteRuntimeVersion: null,
agentProtocol: null
}
@@ -32,6 +32,8 @@ import type {
import {
agentManifestSignaturePayload,
canonicalAgentManifestBytes,
canonicalAgentReleaseKeyRegistryBytes,
parseAgentReleaseKeyRegistryBytes,
verifyAgentBundleDirectory,
verifyAgentManifestSignature,
type VerifiedAgentBundle
@@ -280,6 +282,35 @@ afterEach(() => {
})
describe('runtime and build-time Agent verifier parity', () => {
it('accepts equivalent registry whitespace and canonicalizes validated keys', () => {
const canonical = canonicalAgentReleaseKeyRegistryBytes(registry)
const crlf = Buffer.from(
canonical.toString('utf8').replace(/\n/gu, '\r\n'),
'utf8'
)
const parsed = parseAgentReleaseKeyRegistryBytes(crlf)
expect(parsed).toEqual(registry)
expect(canonicalAgentReleaseKeyRegistryBytes(parsed)).toEqual(
canonical
)
})
it('rejects malformed or non-Ed25519 registry keys', () => {
expect(() =>
parseAgentReleaseKeyRegistryBytes(
Buffer.from(JSON.stringify({
...registry,
keys: [{
...registry.keys[0],
publicKeySpkiBase64: 'AAAA'
}]
}))
)
).toThrow('contract is invalid')
})
it('uses the exact build-time canonical bytes and signature domain', async () => {
const manifest = writeBundle()
const manifestBytes = readFileSync(
+31 -4
View File
@@ -60,11 +60,15 @@ export type VerifyBundledAgentOptions = {
export async function readAgentReleaseKeyRegistry(
filePath: string
): Promise<AgentReleaseKeyRegistry> {
const value = await readJsonMetadata(
const bytes = await readBoundedFile(
filePath,
MAXIMUM_METADATA_BYTES,
'Agent trusted key registry'
)
return parseAgentReleaseKeyRegistryBytes(
bytes,
'Agent trusted key registry'
)
return parseAgentReleaseKeyRegistry(value)
}
export async function readAgentRuntimeLock(
@@ -233,13 +237,36 @@ export function canonicalAgentManifestBytes(
export function canonicalAgentReleaseKeyRegistryBytes(
registry: AgentReleaseKeyRegistry
): Buffer {
const canonical = agentReleaseKeyRegistrySchema.parse(registry)
const canonical = parseAgentReleaseKeyRegistry(registry)
return Buffer.from(
`${JSON.stringify(canonical, null, 2)}\n`,
'utf8'
)
}
export function parseAgentReleaseKeyRegistryBytes(
bytes: Uint8Array,
description = 'Agent trusted key registry'
): AgentReleaseKeyRegistry {
let value: unknown
try {
value = JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(bytes)
) as unknown
} catch (error) {
throw new Error(`${description} is invalid JSON`, {
cause: error
})
}
try {
return parseAgentReleaseKeyRegistry(value)
} catch (error) {
throw new Error(`${description} contract is invalid`, {
cause: error
})
}
}
export function agentManifestSignaturePayload(
manifestBytes: Uint8Array
): Buffer {
@@ -451,7 +478,7 @@ function importEd25519PublicKey(key: AgentReleaseKey) {
return publicKey
}
function parseAgentReleaseKeyRegistry(
export function parseAgentReleaseKeyRegistry(
value: unknown
): AgentReleaseKeyRegistry {
const registry = parseContract(
@@ -243,7 +243,10 @@ const registry: AgentReleaseKeyRegistry = {
formatVersion: 1,
keys: [{
keyId: 'test-key',
publicKeySpkiBase64: 'AAAA',
publicKeySpkiBase64: Buffer.from(
'302a300506032b6570032100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
'hex'
).toString('base64'),
environment: 'test'
}],
revocations: []
@@ -1,7 +1,6 @@
import { createHash, randomUUID } from 'node:crypto'
import { lstat, readFile } from 'node:fs/promises'
import {
agentReleaseKeyRegistrySchema,
type AgentArchitecture,
type AgentBundleManifest,
type AgentReleaseKeyRegistry
@@ -31,6 +30,8 @@ import {
import {
assertAgentManifestMatchesRuntimeLock,
canonicalAgentReleaseKeyRegistryBytes,
parseAgentReleaseKeyRegistry,
parseAgentReleaseKeyRegistryBytes,
readAgentReleaseKeyRegistry,
readAgentRuntimeLock,
verifyAgentManifestSignature,
@@ -599,7 +600,7 @@ export class AgentInstallationManager {
)
}
const canonicalRegistry =
agentReleaseKeyRegistrySchema.parse(loaded.registry)
parseAgentReleaseKeyRegistry(loaded.registry)
return {
bundle: loaded.bundle,
registry: canonicalRegistry,
@@ -627,7 +628,7 @@ export class AgentInstallationManager {
verificationEnvironment: this.#verificationEnvironment
}
)
const canonicalRegistry = agentReleaseKeyRegistrySchema.parse(
const canonicalRegistry = parseAgentReleaseKeyRegistry(
registry
)
return {
@@ -648,14 +649,14 @@ export class AgentInstallationManager {
},
signal: AbortSignal
): Promise<AgentInstallationIdentity> {
const [canonicalRegistryBytes, runtimeLock] = await Promise.all([
readFile(this.#resourcePaths.keyRegistryPath),
const [registry, runtimeLock] = await Promise.all([
readAgentReleaseKeyRegistry(
this.#resourcePaths.keyRegistryPath
),
readAgentRuntimeLock(this.#resourcePaths.runtimeLockPath)
])
const registry = parseCanonicalAgentReleaseKeyRegistry(
canonicalRegistryBytes,
'Agent trusted key registry'
)
const canonicalRegistryBytes =
canonicalAgentReleaseKeyRegistryBytes(registry)
let metadataSftp: StagedSftp | undefined
let installationSftp: StagedSftp | undefined
@@ -706,7 +707,7 @@ export class AgentInstallationManager {
signal
)
const remoteReleaseKeyRegistry =
parseCanonicalAgentReleaseKeyRegistry(
parseAgentReleaseKeyRegistryBytes(
remoteReleaseKeyBytes,
'Installed Agent release-key registry'
)
@@ -851,21 +852,6 @@ function parseJsonBytes(
}
}
function parseCanonicalAgentReleaseKeyRegistry(
value: Buffer,
description: string
): AgentReleaseKeyRegistry {
const registry = agentReleaseKeyRegistrySchema.parse(
parseJsonBytes(value, description)
)
const canonicalBytes =
canonicalAgentReleaseKeyRegistryBytes(registry)
if (!canonicalBytes.equals(value)) {
throw new Error(`${description} is not canonical`)
}
return registry
}
function decodeDetachedSignature(
value: Buffer,
description: string
+133 -26
View File
@@ -32,7 +32,6 @@ import {
} from '../../shared/agent-protocol/contracts'
import {
agentArchitectureSchema,
agentReleaseKeyRegistrySchema,
type AgentArchitecture,
type AgentReleaseKeyRegistry
} from '../../shared/agent-installation-contracts'
@@ -49,7 +48,7 @@ import type {
AgentInstallationBundleLoader
} from './agent-installation-manager'
import {
canonicalAgentReleaseKeyRegistryBytes
readAgentReleaseKeyRegistry
} from './agent-bundle-verifier'
import type {
RemoteRuntimeInstallationBundleLoader,
@@ -84,6 +83,18 @@ type InstalledRecord = {
verified: VerifiedAgentPackage
}
type CatalogState =
| {
state: 'available'
checkedAt: string
catalog: AgentPackageCatalog
}
| {
state: 'unavailable'
checkedAt: string
error: string
}
export type AgentPackageManagerOptions = {
userDataPath: string
desktopVersion: string
@@ -111,6 +122,7 @@ export class AgentPackageManager {
>()
readonly #leaseCounts = new Map<string, number>()
readonly #pendingRemovals = new Set<string>()
#catalogState?: CatalogState
constructor(options: AgentPackageManagerOptions) {
this.#rootDirectory = resolve(
@@ -170,11 +182,17 @@ export class AgentPackageManager {
} catch {
state = 'invalid'
}
const latestVersion = this.#latestVersion(architecture)
return {
platform: 'linux' as const,
architecture,
state,
version: record?.verified.descriptor.version ?? null,
latestVersion,
updateAvailable: this.#updateAvailable(
latestVersion,
record
),
remoteRuntimeVersion:
record?.verified.descriptor.remoteRuntime.version ?? null,
agentProtocol:
@@ -184,13 +202,15 @@ export class AgentPackageManager {
)
return agentPackageInventorySchema.parse({
checkedAt: this.#now().toISOString(),
catalog: this.#publicCatalogState(),
entries
})
}
getSnapshot(
async getSnapshot(
options: { refresh?: boolean } = {}
): Promise<AgentPackageInventory> {
await this.#checkCatalog()
return this.getInventory(options)
}
@@ -229,24 +249,34 @@ export class AgentPackageManager {
totalBytes: null
})
const source = await this.#getUpdateSource()
const catalog = await this.#loadCatalog(source)
const catalog = await this.#loadCatalogAndRemember(source)
const entry = selectLatestCompatibleEntry(
catalog,
architecture,
this.#desktopVersion
)
const current = await this.#loadInstalled(architecture)
const comparison = current
? compareSemanticVersions(
entry.version,
current.verified.descriptor.version
)
: 1
if (
current &&
compareSemanticVersions(
entry.version,
current.verified.descriptor.version
) < 0
comparison < 0
) {
throw new Error(
'Agent 发布目录不能降级已安装的兼容版本'
)
}
if (
current &&
comparison === 0 &&
current.archiveSha256 === entry.sha256
) {
return this.getInventory()
}
const staging = await this.#createStaging(architecture)
const archivePath = join(staging, entry.archive)
try {
@@ -710,6 +740,90 @@ export class AgentPackageManager {
return catalog
}
async #loadCatalogAndRemember(
source: UpdateSource
): Promise<AgentPackageCatalog> {
const checkedAt = this.#now().toISOString()
try {
const catalog = await this.#loadCatalog(source)
this.#catalogState = {
state: 'available',
checkedAt,
catalog
}
return catalog
} catch (error) {
this.#catalogState = {
state: 'unavailable',
checkedAt,
error: boundedErrorMessage(error)
}
throw error
}
}
async #checkCatalog(): Promise<void> {
try {
await this.#loadCatalogAndRemember(
await this.#getUpdateSource()
)
} catch {
// The failed catalog state is already recorded. Local packages remain usable.
}
}
#latestVersion(
architecture: AgentArchitecture
): string | null {
if (this.#catalogState?.state !== 'available') {
return null
}
try {
return selectLatestCompatibleEntry(
this.#catalogState.catalog,
architecture,
this.#desktopVersion
).version
} catch {
return null
}
}
#updateAvailable(
latestVersion: string | null,
installed: InstalledRecord | undefined
): boolean {
return installed !== undefined &&
latestVersion !== null &&
compareSemanticVersions(
latestVersion,
installed.verified.descriptor.version
) > 0
}
#publicCatalogState():
AgentPackageInventory['catalog'] {
if (this.#catalogState === undefined) {
return {
state: 'not-checked',
checkedAt: null,
error: null
}
}
if (this.#catalogState.state === 'available') {
return {
state: 'available',
checkedAt: this.#catalogState.checkedAt,
error: null
}
}
return {
state: 'unavailable',
checkedAt: this.#catalogState.checkedAt,
error: this.#catalogState.error
}
}
async #fetchMirrorCatalog(): Promise<[Buffer, Buffer]> {
const pointerBytes = await this.#fetchBounded(
`${MIRROR_ROOT}latest.json`
@@ -874,7 +988,7 @@ export class AgentPackageManager {
}
#loadTrustedRegistry(): Promise<AgentReleaseKeyRegistry> {
this.#trustedRegistry ??= readCanonicalRegistry(
this.#trustedRegistry ??= readAgentReleaseKeyRegistry(
this.#keyRegistryPath
)
return this.#trustedRegistry
@@ -1010,23 +1124,6 @@ function assertCanonicalDownload(
}
}
async function readCanonicalRegistry(
filePath: string
): Promise<AgentReleaseKeyRegistry> {
const bytes = await readFile(filePath)
const registry = agentReleaseKeyRegistrySchema.parse(
JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(bytes)
)
)
if (
!canonicalAgentReleaseKeyRegistryBytes(registry).equals(bytes)
) {
throw new Error('Agent 签名公钥目录不是规范格式')
}
return registry
}
function parseCanonicalCatalog(bytes: Buffer): AgentPackageCatalog {
const catalog = agentPackageCatalogSchema.parse(
JSON.parse(
@@ -1125,6 +1222,16 @@ function verifyCatalogSignature(
}
}
function boundedErrorMessage(error: unknown): string {
const message = error instanceof Error
? error.message
: String(error)
return (
message.trim().slice(0, 2_000) ||
'Agent 发布目录检查失败'
)
}
async function copyAndHashBoundedRegularFile(
sourcePath: string,
destinationPath: string,
@@ -38,12 +38,12 @@ import {
canonicalJson
} from '../../shared/agent-protocol/canonical'
import {
agentReleaseKeyRegistrySchema,
type AgentArchitecture,
type AgentReleaseKeyRegistry
} from '../../shared/agent-installation-contracts'
import {
canonicalAgentReleaseKeyRegistryBytes,
parseAgentReleaseKeyRegistry,
parseAgentReleaseKeyRegistryBytes,
verifyAgentBundleDirectory,
type AgentVerificationEnvironment,
type VerifiedAgentBundle
@@ -156,8 +156,11 @@ export async function verifyExtractedAgentPackage(options: {
'Agent package is not compatible with this GoodBuddy version'
)
}
const registry = parseCanonicalRegistry(registryBytes)
const trustedRegistry = agentReleaseKeyRegistrySchema.parse(
const registry = parseAgentReleaseKeyRegistryBytes(
registryBytes,
'Agent package key registry'
)
const trustedRegistry = parseAgentReleaseKeyRegistry(
options.trustedRegistry
)
verifyDescriptorSignature(
@@ -545,20 +548,6 @@ function parseCanonicalDescriptor(
return descriptor
}
function parseCanonicalRegistry(bytes: Buffer) {
const registry = agentReleaseKeyRegistrySchema.parse(
JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(bytes)
)
)
if (
!canonicalAgentReleaseKeyRegistryBytes(registry).equals(bytes)
) {
throw new Error('Agent package key registry is not canonical')
}
return registry
}
function verifyDescriptorSignature(
descriptor: AgentPackageDescriptor,
descriptorBytes: Buffer,
@@ -236,7 +236,10 @@ const registry: AgentReleaseKeyRegistry = {
formatVersion: 1,
keys: [{
keyId: 'test-key',
publicKeySpkiBase64: 'AAAA',
publicKeySpkiBase64: Buffer.from(
'302a300506032b6570032100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
'hex'
).toString('base64'),
environment: 'test'
}],
revocations: []
@@ -35,6 +35,9 @@ import type {
SshConnectionPoolTarget
} from '../ssh/ssh-connection-pool'
import { settleBoundedly } from './bounded-settlement'
import {
parseAgentReleaseKeyRegistryBytes
} from './agent-bundle-verifier'
import { isMissingPathError } from './path-errors'
const MAXIMUM_INSTALLATION_FILES = 200
@@ -711,18 +714,13 @@ export class RemoteRuntimeInstallationManager {
metadataSftp.readFile(RELEASE_KEYS_PATH, signal),
metadataSftp.readFile(RUNTIME_LOCK_PATH, signal)
])
const remoteReleaseKeyRegistryBytes = canonicalJsonBytes(
remoteReleaseKeyBytes,
agentReleaseKeyRegistrySchema,
const remoteReleaseKeyRegistryBytes =
Buffer.from(remoteReleaseKeyBytes)
const remoteReleaseKeyRegistry =
parseAgentReleaseKeyRegistryBytes(
remoteReleaseKeyRegistryBytes,
'Installed Runtime release-key registry'
)
const remoteReleaseKeyRegistry =
agentReleaseKeyRegistrySchema.parse(
parseJsonBytes(
remoteReleaseKeyRegistryBytes,
'Installed Runtime release-key registry'
)
)
if (!remoteRuntimeLockFileBytes.equals(remoteRuntimeLockBytes)) {
throw new Error(
'Installed Runtime lock does not match this GoodBuddy build'
@@ -173,6 +173,23 @@ describe('remote Runtime resource loader', () => {
})
})
it('accepts equivalent key-registry formatting and returns canonical bytes', async () => {
const fixture = await createFixture()
await writeFile(
fixture.paths.keyRegistryPath,
Buffer.from(
canonical(registry).toString('utf8').replace(/\n/gu, '\r\n')
)
)
await expect(
loadRemoteRuntimeVerificationMetadata(fixture.paths)
).resolves.toMatchObject({
releaseKeyRegistry: registry,
canonicalReleaseKeyRegistryBytes: canonical(registry)
})
})
it('rejects a symlinked digest directory', async () => {
const fixture = await createFixture({ digestDirectories: 0 })
const target = join(fixture.root, 'target')
@@ -226,11 +243,6 @@ describe('remote Runtime resource loader', () => {
bytes: Buffer.from('not json\n'),
error: 'invalid JSON'
},
{
name: 'noncanonical',
bytes: Buffer.from(JSON.stringify(registry)),
error: 'not canonical'
},
{
name: 'oversize',
bytes: Buffer.alloc(1024 * 1024 + 1, 0x20),
@@ -11,7 +11,6 @@ import {
type VerifyRuntimeBundleOptions
} from '../../shared/node/runtime-bundle-verifier'
import {
agentReleaseKeyRegistrySchema,
type AgentArchitecture,
type AgentReleaseKeyRegistry
} from '../../shared/agent-installation-contracts'
@@ -30,6 +29,9 @@ import {
getBundledRemoteRuntimeRoot,
type BundledRemoteRuntimeResourcePaths
} from './bundled-remote-runtime-resources'
import {
parseAgentReleaseKeyRegistry
} from './agent-bundle-verifier'
import { isMissingPathError } from './path-errors'
const MAXIMUM_METADATA_BYTES = 1024 * 1024
@@ -106,8 +108,9 @@ export async function loadRemoteRuntimeVerificationMetadata(
const [keyRegistry, runtimeLock] = await Promise.all([
readCanonicalMetadata(
paths.keyRegistryPath,
agentReleaseKeyRegistrySchema,
'Runtime release-key registry'
{ parse: parseAgentReleaseKeyRegistry },
'Runtime release-key registry',
true
),
readCanonicalMetadata(
paths.runtimeLockPath,
@@ -139,7 +142,8 @@ function assertSafeVerificationInjection(
async function readCanonicalMetadata<T>(
filePathInput: string,
schema: { parse(value: unknown): T },
label: string
label: string,
acceptEquivalentJsonFormatting = false
): Promise<CanonicalMetadata<T>> {
const filePath = resolve(filePathInput)
const pathStat = await lstat(filePath)
@@ -212,7 +216,10 @@ async function readCanonicalMetadata<T>(
`${JSON.stringify(value, null, 2)}\n`,
'utf8'
)
if (!canonicalBytes.equals(bytes)) {
if (
!acceptEquivalentJsonFormatting &&
!canonicalBytes.equals(bytes)
) {
throw new Error(`${label} is not canonical`)
}
return { bytes: canonicalBytes, value }
+76 -14
View File
@@ -80,6 +80,63 @@ afterEach(async () => {
})
describe('RuntimeSettingsStore', () => {
it('seeds a local provider-neutral model connection', async () => {
const { store } = await createStore()
await expect(store.getPublicSettings()).resolves.toMatchObject({
modelBaseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
modelProtocol: 'openai-chat-completions',
modelAuthentication: 'none',
modelProfiles: [
expect.objectContaining({
id: '00000000-0000-4000-8000-000000000001',
baseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
authentication: 'none'
})
]
})
})
it('replaces only the untouched legacy BigToken seed', async () => {
const { filePath, store } = await createStore()
await store.update(settings())
const version19 = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
version19.version = 19
await writeFile(filePath, JSON.stringify(version19), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getPublicSettings()).resolves.toMatchObject({
modelBaseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
modelAuthentication: 'none'
})
})
it('preserves an explicitly credentialed BigToken connection', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
apiKey: { action: 'replace', value: 'explicit-key' }
})
)
const version19 = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
version19.version = 19
await writeFile(filePath, JSON.stringify(version19), 'utf8')
const migrated = new RuntimeSettingsStore(filePath, cipher, {})
await expect(migrated.getResolvedSettings()).resolves.toMatchObject({
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
apiKey: 'explicit-key'
})
})
it('restores an exact credential-bearing snapshot after a failed activation', async () => {
const { store } = await createStore()
await store.update(
@@ -382,7 +439,7 @@ describe('RuntimeSettingsStore', () => {
})
await expect(store.getResolvedSettings()).resolves.toMatchObject({
modelProtocol: 'anthropic-messages',
modelProtocol: 'openai-chat-completions',
deepseekHarnessModelProfile: {
id: 'goodbuddy-platform-harness',
name: '管理员预置模型',
@@ -398,7 +455,9 @@ describe('RuntimeSettingsStore', () => {
const publicSettings = await store.getPublicSettings()
expect(JSON.stringify(publicSettings)).not.toContain(apiKey)
expect(publicSettings.modelProtocol).toBe('anthropic-messages')
expect(publicSettings.modelProtocol).toBe(
'openai-chat-completions'
)
})
it.each([
@@ -481,7 +540,7 @@ describe('RuntimeSettingsStore', () => {
const persisted = JSON.parse(
await readFile(filePath, 'utf8')
) as Record<string, unknown>
expect(persisted.version).toBe(19)
expect(persisted.version).toBe(20)
expect(persisted).not.toHaveProperty(
'deepseekHarnessBinaryPath'
)
@@ -760,7 +819,7 @@ describe('RuntimeSettingsStore', () => {
const persisted = JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
}
expect(persisted.version).toBe(19)
expect(persisted.version).toBe(20)
})
it('migrates version 11 and removes the obsolete intranet toggle', async () => {
@@ -780,7 +839,7 @@ describe('RuntimeSettingsStore', () => {
version: number
intranetCompatibilityEnabled?: boolean
}
expect(persisted.version).toBe(19)
expect(persisted.version).toBe(20)
expect(persisted).not.toHaveProperty('intranetCompatibilityEnabled')
})
@@ -1058,7 +1117,7 @@ describe('RuntimeSettingsStore', () => {
(JSON.parse(await readFile(filePath, 'utf8')) as {
version: number
}).version
).toBe(19)
).toBe(20)
})
it('migrates version 13 with reranking disabled by default', async () => {
@@ -1565,7 +1624,7 @@ describe('RuntimeSettingsStore', () => {
version: number
modelProfiles: Array<Record<string, unknown>>
}
expect(persisted.version).toBe(19)
expect(persisted.version).toBe(20)
expect(persisted.modelProfiles).toContainEqual(
expect.objectContaining({
id: imageId,
@@ -1653,7 +1712,7 @@ describe('RuntimeSettingsStore', () => {
})
})
it('does not mix an environment key with a stored base URL', async () => {
it('keeps provider-specific environment aliases for existing deployments', async () => {
const { filePath, store } = await createStore()
await store.update(
settings({
@@ -1663,11 +1722,14 @@ describe('RuntimeSettingsStore', () => {
)
const environmentStore = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_BIGTOKEN_API_KEY: 'YOUR_API_KEY_HERE'
GOODBUDDY_BIGTOKEN_API_KEY: 'legacy-key',
GOODBUDDY_BIGTOKEN_BASE_URL: 'https://legacy.example',
GOODBUDDY_BIGTOKEN_MODEL: 'legacy-model'
})
await expect(environmentStore.getResolvedSettings()).resolves.toMatchObject({
apiKey: 'YOUR_API_KEY_HERE',
modelBaseUrl: 'https://bigtoken.ai'
apiKey: 'legacy-key',
modelBaseUrl: 'https://legacy.example',
modelName: 'legacy-model'
})
})
@@ -1724,7 +1786,7 @@ describe('RuntimeSettingsStore', () => {
})
})
it('prefers generic model environment variables over legacy fallbacks', async () => {
it('prefers generic model environment variables over legacy aliases', async () => {
const { filePath } = await createStore()
const store = new RuntimeSettingsStore(filePath, cipher, {
GOODBUDDY_MODEL_API_KEY: 'generic-key',
@@ -1864,7 +1926,7 @@ describe('RuntimeSettingsStore', () => {
unknown
>
expect(saved).toMatchObject({
version: 19,
version: 20,
provider: 'model',
continueBinaryPath: '',
continueMode: 'chat',
@@ -2143,7 +2205,7 @@ describe('RuntimeSettingsStore', () => {
version: number
modelProfiles: Array<Record<string, unknown>>
}
expect(persisted.version).toBe(19)
expect(persisted.version).toBe(20)
expect(persisted.modelProfiles[0]).not.toHaveProperty('credential')
})
+58 -16
View File
@@ -259,7 +259,7 @@ const storedEmbeddingConnectionSchema = z.discriminatedUnion('kind', [
.strict()
])
const storedSettingsSchema = version18StoredSettingsSchema
const version19StoredSettingsSchema = version18StoredSettingsSchema
.omit({ version: true })
.extend({
version: z.literal(19),
@@ -270,6 +270,12 @@ const storedSettingsSchema = version18StoredSettingsSchema
activeEmbeddingConnectionId: z.string().uuid()
})
const storedSettingsSchema = version19StoredSettingsSchema
.omit({ version: true })
.extend({
version: z.literal(20)
})
type StoredSettings = z.infer<typeof storedSettingsSchema>
export type RuntimeSettingsRollback = {
publicSettings: RuntimeSettings
@@ -281,6 +287,9 @@ type Version17StoredSettings = z.infer<
type Version18StoredSettings = z.infer<
typeof version18StoredSettingsSchema
>
type Version19StoredSettings = z.infer<
typeof version19StoredSettingsSchema
>
type Version16StoredSettings = z.infer<
typeof version16StoredSettingsSchema
>
@@ -449,7 +458,7 @@ type ResolvedModelCredential = {
}
const defaultSettings: StoredSettings = {
version: 19,
version: 20,
provider: defaultRuntimeSettings.provider,
modelProfiles: [
{
@@ -653,7 +662,7 @@ function migrateVersion17(
function migrateVersion18(
settings: Version18StoredSettings
): StoredSettings {
return {
return migrateVersion19({
...settings,
version: 19,
embeddingConnections: [
@@ -673,6 +682,36 @@ function migrateVersion18(
}
],
activeEmbeddingConnectionId: legacyEmbeddingConnectionId
})
}
function migrateVersion19(
settings: Version19StoredSettings
): StoredSettings {
return {
...settings,
version: 20,
modelProfiles: settings.modelProfiles.map((profile) =>
profile.id === defaultModelProfileId &&
profile.name === '默认模型' &&
profile.baseUrl === 'https://bigtoken.ai' &&
profile.modelName === 'sonnet-5' &&
profile.protocol === 'anthropic-messages' &&
profile.authentication === 'api-key' &&
profile.supportsImageInput === false &&
profile.imageGenerationQuality === 'auto' &&
profile.contextWindowTokens === undefined &&
profile.credential === undefined
? {
...profile,
baseUrl: defaultRuntimeSettings.modelBaseUrl,
modelName: defaultRuntimeSettings.modelName,
protocol: defaultRuntimeSettings.modelProtocol,
authentication:
defaultRuntimeSettings.modelAuthentication
}
: profile
)
}
}
@@ -995,13 +1034,17 @@ export class RuntimeSettingsStore {
const parsed: unknown = JSON.parse(contents)
assertSupportedSettingsVersion(
parsed,
19,
20,
(version) =>
`当前 GoodBuddy 不支持 Runtime 设置版本 ${version},请升级应用后重试`
)
const current = storedSettingsSchema.safeParse(parsed)
const version19 =
version19StoredSettingsSchema.safeParse(parsed)
if (current.success) {
this.settings = current.data
} else if (version19.success) {
this.settings = migrateVersion19(version19.data)
} else {
const version18 =
version18StoredSettingsSchema.safeParse(parsed)
@@ -1377,10 +1420,7 @@ export class RuntimeSettingsStore {
settings.modelProfiles.find(
(profile) => profile.id === settings.defaultModelProfileId
) ?? settings.modelProfiles[0]
const environmentApiKey =
defaultProfile?.authentication === 'api-key'
? this.getEnvironmentApiKey()
: undefined
const environmentApiKey = this.getEnvironmentApiKey()
return new Map(
settings.modelProfiles.map((profile) => {
const environmentManaged =
@@ -1401,11 +1441,11 @@ export class RuntimeSettingsStore {
profile.id,
{
activeApiKey:
profile.authentication === 'api-key'
? environmentManaged
? environmentApiKey
: storedApiKey
: undefined,
environmentManaged
? environmentApiKey
: profile.authentication === 'api-key'
? storedApiKey
: undefined,
configured: environmentManaged || Boolean(storedApiKey),
source
}
@@ -1457,7 +1497,9 @@ export class RuntimeSettingsStore {
baseUrl,
model,
protocol: profile.protocol,
authentication: profile.authentication,
authentication: environmentManaged
? 'api-key'
: profile.authentication,
supportsImageInput: profile.supportsImageInput,
contextWindowTokens: profile.contextWindowTokens,
imageGenerationQuality: profile.imageGenerationQuality,
@@ -2228,7 +2270,7 @@ export class RuntimeSettingsStore {
const next: StoredSettings = {
...current,
version: 19,
version: 20,
provider: input.provider,
modelProfiles,
defaultModelProfileId,
@@ -2289,7 +2331,7 @@ export class RuntimeSettingsStore {
const current = await this.load()
const next: StoredSettings = {
...current,
version: 19,
version: 20,
runtimeCustomization: parsed
}
await writeJsonFileAtomically(this.filePath, next)
+9
View File
@@ -6148,12 +6148,19 @@ describe('App', () => {
})),
getAgentPackageInventory: vi.fn(async () => ({
checkedAt: '2026-08-24T00:00:00.000Z',
catalog: {
state: 'available' as const,
checkedAt: '2026-08-24T00:00:00.000Z',
error: null
},
entries: [
{
platform: 'linux' as const,
architecture: 'x64' as const,
state: 'verified' as const,
version: '0.11.2',
latestVersion: '0.11.4',
updateAvailable: true,
agentProtocol: { major: 2, minor: 0 },
remoteRuntimeVersion: '1.18.9'
},
@@ -6162,6 +6169,8 @@ describe('App', () => {
architecture: 'arm64' as const,
state: 'not-downloaded' as const,
version: null,
latestVersion: '0.11.4',
updateAvailable: false,
agentProtocol: null,
remoteRuntimeVersion: null
}
@@ -930,10 +930,26 @@ export function PlatformFeaturesSettingsSection({
Linux {entry.architecture}
</strong>
<span
className={`bundled-agent-inventory__badge bundled-agent-inventory__badge--${entry.state}`}
className={`bundled-agent-inventory__badge bundled-agent-inventory__badge--${
entry.updateAvailable
? 'update-available'
: agentInventory.catalog.state ===
'available' &&
entry.state === 'verified' &&
entry.latestVersion !== null
? 'up-to-date'
: entry.state
}`}
>
{t(
`platformFeatures.remoteProjects.agentInventory.states.${entry.state}`
entry.updateAvailable
? 'platformFeatures.remoteProjects.agentInventory.states.updateAvailable'
: agentInventory.catalog.state ===
'available' &&
entry.state === 'verified' &&
entry.latestVersion !== null
? 'platformFeatures.remoteProjects.agentInventory.states.upToDate'
: `platformFeatures.remoteProjects.agentInventory.states.${entry.state}`
)}
</span>
</div>
@@ -946,6 +962,14 @@ export function PlatformFeaturesSettingsSection({
</dt>
<dd>{entry.version ?? '—'}</dd>
</div>
<div>
<dt>
{t(
'platformFeatures.remoteProjects.agentInventory.fields.latestVersion'
)}
</dt>
<dd>{entry.latestVersion ?? '—'}</dd>
</div>
<div>
<dt>
{t(
@@ -985,23 +1009,36 @@ export function PlatformFeaturesSettingsSection({
</p>
)}
<div className="capability-card__actions">
<button
className="secondary-button"
disabled={agentPackageBusy !== undefined}
onClick={() =>
void downloadAgentPackage(
entry.architecture
)
}
type="button"
>
<Download aria-hidden="true" size={13} />
{t(
entry.state === 'verified'
? 'platformFeatures.remoteProjects.agentInventory.update'
: 'platformFeatures.remoteProjects.agentInventory.download'
)}
</button>
{(
entry.updateAvailable ||
(
entry.state !== 'verified' &&
entry.latestVersion !== null
) ||
agentInventory.catalog.state !== 'available'
) && (
<button
className="secondary-button"
disabled={agentPackageBusy !== undefined}
onClick={() =>
void downloadAgentPackage(
entry.architecture
)
}
type="button"
>
<Download aria-hidden="true" size={13} />
{t(
entry.updateAvailable
? 'platformFeatures.remoteProjects.agentInventory.updateTo'
: entry.state !== 'verified' &&
entry.latestVersion !== null
? 'platformFeatures.remoteProjects.agentInventory.downloadVersion'
: 'platformFeatures.remoteProjects.agentInventory.update',
{ version: entry.latestVersion }
)}
</button>
)}
{entry.state === 'verified' && (
<button
className="secondary-button"
@@ -1024,6 +1061,21 @@ export function PlatformFeaturesSettingsSection({
))}
</div>
) : null}
{agentInventory?.catalog.state === 'available' && (
<p className="settings-notice" role="status">
{t(
'platformFeatures.remoteProjects.agentInventory.catalog.available'
)}
</p>
)}
{agentInventory?.catalog.state === 'unavailable' && (
<p className="settings-warning" role="alert">
{t(
'platformFeatures.remoteProjects.agentInventory.catalog.unavailable',
{ error: agentInventory.catalog.error }
)}
</p>
)}
</article>
</section>
+87 -7
View File
@@ -514,12 +514,19 @@ let applicationSettings: ApplicationSettings = {
}
const agentPackageInventory: AgentPackageInventory = {
checkedAt: '2026-08-24T00:00:00.000Z',
catalog: {
state: 'available',
checkedAt: '2026-08-24T00:00:00.000Z',
error: null
},
entries: [
{
platform: 'linux',
architecture: 'x64',
state: 'verified',
version: '0.11.2',
latestVersion: '0.11.4',
updateAvailable: true,
agentProtocol: { major: 2, minor: 0 },
remoteRuntimeVersion: '1.18.9'
},
@@ -528,6 +535,8 @@ const agentPackageInventory: AgentPackageInventory = {
architecture: 'arm64',
state: 'not-downloaded',
version: null,
latestVersion: '0.11.4',
updateAvailable: false,
agentProtocol: null,
remoteRuntimeVersion: null
}
@@ -1878,13 +1887,13 @@ describe('SettingsPanel runtime files', () => {
).toBeInTheDocument()
expect(screen.getByText('Linux x64')).toBeInTheDocument()
expect(screen.getByText('Linux arm64')).toBeInTheDocument()
expect(screen.getByText('已下载并验证')).toBeInTheDocument()
expect(screen.getByText('有更新')).toBeInTheDocument()
expect(screen.getByText('未下载')).toBeInTheDocument()
expect(getAgentPackageInventory).toHaveBeenCalledOnce()
expect(getAgentPackageInventory).toHaveBeenNthCalledWith(1, false)
fireEvent.click(
screen.getByRole('button', { name: '刷新本地清单' })
screen.getByRole('button', { name: '刷新包清单' })
)
await waitFor(() =>
expect(getAgentPackageInventory).toHaveBeenCalledTimes(2)
@@ -1899,14 +1908,34 @@ describe('SettingsPanel runtime files', () => {
.closest('[role="listitem"]')
expect(x64Card).not.toBeNull()
expect(arm64Card).not.toBeNull()
downloadAgentPackage.mockResolvedValueOnce({
...agentPackageInventory,
entries: agentPackageInventory.entries.map((entry) =>
entry.architecture === 'x64'
? {
...entry,
version: '0.11.4',
updateAvailable: false
}
: entry
)
})
fireEvent.click(
within(x64Card as HTMLElement).getByRole('button', {
name: '检查并更新'
name: '更新到 0.11.4'
})
)
await waitFor(() =>
expect(downloadAgentPackage).toHaveBeenCalledWith('x64')
)
expect(
await within(x64Card as HTMLElement).findByText('已是最新')
).toBeInTheDocument()
expect(
within(x64Card as HTMLElement).queryByRole('button', {
name: /更新到/u
})
).not.toBeInTheDocument()
fireEvent.click(
within(x64Card as HTMLElement).getByRole('button', {
name: '导出离线包'
@@ -1917,7 +1946,7 @@ describe('SettingsPanel runtime files', () => {
)
fireEvent.click(
within(arm64Card as HTMLElement).getByRole('button', {
name: '下载'
name: '下载 0.11.4'
})
)
await waitFor(() =>
@@ -1967,7 +1996,9 @@ describe('SettingsPanel runtime files', () => {
})
)
expect(
await screen.findByText('正在校验本地 Agent 包…')
await screen.findByText(
'正在校验本地 Agent 包并检查在线版本…'
)
).toBeInTheDocument()
await act(async () => {
@@ -1982,14 +2013,14 @@ describe('SettingsPanel runtime files', () => {
new Error('inventory unavailable')
)
fireEvent.click(
screen.getByRole('button', { name: '刷新本地清单' })
screen.getByRole('button', { name: '刷新包清单' })
)
expect(
await screen.findByText('inventory unavailable')
).toBeInTheDocument()
fireEvent.click(
screen.getByRole('button', { name: '刷新本地清单' })
screen.getByRole('button', { name: '刷新包清单' })
)
await waitFor(() =>
expect(
@@ -1999,6 +2030,55 @@ describe('SettingsPanel runtime files', () => {
expect(getAgentPackageInventory).toHaveBeenCalledTimes(3)
})
it('keeps local Agent packages usable when the online catalog is unavailable', async () => {
getAgentPackageInventory.mockResolvedValueOnce({
...agentPackageInventory,
catalog: {
state: 'unavailable',
checkedAt: '2026-08-24T00:00:00.000Z',
error: 'network unavailable'
},
entries: agentPackageInventory.entries.map((entry) => ({
...entry,
latestVersion: null,
updateAvailable: false
}))
})
render(
<SettingsPanel
{...heartbeatSettingsProps}
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(screen.getByRole('tab', { name: '平台功能' }))
fireEvent.click(
await screen.findByRole('tab', {
name: '远程项目(技术预览)'
})
)
expect(
await screen.findByText(/network unavailable/u)
).toBeInTheDocument()
const x64Card = screen
.getByText('Linux x64')
.closest('[role="listitem"]')
expect(
within(x64Card as HTMLElement).getByRole('button', {
name: '检查并更新'
})
).toBeInTheDocument()
expect(
within(x64Card as HTMLElement).getByRole('button', {
name: '导出离线包'
})
).toBeInTheDocument()
})
it('shows the SSH Hosts category only while Remote Projects is enabled', () => {
const commonProps = {
...heartbeatSettingsProps,
@@ -520,27 +520,38 @@ export const settingsSections = {
agentInventory: {
title: 'GoodBuddy Agent packages',
description:
'Agent packages are released independently and include the Agent, pinned Node, and the remote OpenCode Runtime adapted by GoodBuddy. Online actions use the source selected under About & Updates; nothing is downloaded until remote projects are used.',
loading: 'Verifying local Agent packages…',
refresh: 'Refresh local inventory',
'Agent packages are released independently and include the Agent, pinned Node, and the remote OpenCode Runtime adapted by GoodBuddy. Opening this page reads only the small signed catalog to show available updates and never downloads an Agent package automatically. Online actions use the source selected under About & Updates.',
loading: 'Verifying local Agent packages and checking online versions…',
refresh: 'Refresh package inventory',
import: 'Import offline package',
export: 'Export offline package',
download: 'Download',
update: 'Check for updates',
updateTo: 'Update to {{version}}',
downloadVersion: 'Download {{version}}',
listLabel: 'Local Agent package inventory',
summary:
'{{available}} of {{total}} Linux architectures are available.',
states: {
verified: 'Downloaded and verified',
'not-downloaded': 'Not downloaded',
invalid: 'Verification failed'
invalid: 'Verification failed',
updateAvailable: 'Update available',
upToDate: 'Up to date'
},
fields: {
agentVersion: 'Agent version',
agentVersion: 'Local Agent',
latestVersion: 'Latest online version',
architecture: 'Architecture',
runtimeVersion: 'Remote OpenCode',
protocol: 'Agent protocol'
},
catalog: {
available:
'Online versions were checked through the signed catalog. Agent packages are transferred only after you choose Download or Update.',
unavailable:
'Could not check online Agent versions: {{error}}. Verified local packages and offline imports remain available. Refresh to try again.'
},
progress: {
catalog: 'Reading the signed release catalog…',
downloading: 'Downloading the Agent package…',
@@ -556,7 +567,7 @@ export const settingsSections = {
unavailable:
'Agent package management is not available in this version.',
readFailed:
'Could not load local Agent packages. Try again.',
'Could not load Agent packages. Try again.',
downloadFailed:
'Agent package download or verification failed',
importFailed:
@@ -491,26 +491,37 @@ export const settingsSections = {
agentInventory: {
title: 'GoodBuddy Agent 包',
description:
'Agent 包独立发布,包含 Agent、固定 Node 和桌面版本适配的远端 OpenCode Runtime。在线操作使用“关于与更新”中选择的更新源;未使用远程项目时不会下载。',
loading: '正在校验本地 Agent 包…',
refresh: '刷新本地清单',
'Agent 包独立发布,包含 Agent、固定 Node 和桌面版本适配的远端 OpenCode Runtime。打开此页只读取小型签名目录并显示可用更新,不会自动下载 Agent 包;在线操作使用“关于与更新”中选择的更新源。',
loading: '正在校验本地 Agent 包并检查在线版本…',
refresh: '刷新包清单',
import: '导入离线包',
export: '导出离线包',
download: '下载',
update: '检查并更新',
updateTo: '更新到 {{version}}',
downloadVersion: '下载 {{version}}',
listLabel: '本地 Agent 包清单',
summary: '当前 {{available}} / {{total}} 个 Linux 架构可用。',
states: {
verified: '已下载并验证',
'not-downloaded': '未下载',
invalid: '校验失败'
invalid: '校验失败',
updateAvailable: '有更新',
upToDate: '已是最新'
},
fields: {
agentVersion: 'Agent 版本',
agentVersion: '本地 Agent',
latestVersion: '在线最新版本',
architecture: '架构',
runtimeVersion: '远端 OpenCode',
protocol: 'Agent 协议'
},
catalog: {
available:
'在线版本已通过签名目录检查;只有点击下载或更新后才会传输 Agent 包。',
unavailable:
'未能检查在线 Agent 版本:{{error}}。本地已验证包和离线导入仍可使用,可刷新重试。'
},
progress: {
catalog: '正在读取签名发布目录…',
downloading: '正在下载 Agent 包…',
@@ -524,7 +535,7 @@ export const settingsSections = {
},
errors: {
unavailable: '当前版本未提供 Agent 包管理服务',
readFailed: '读取本地 Agent 包清单失败,请重试',
readFailed: '读取 Agent 包清单失败,请重试',
downloadFailed: 'Agent 包下载或校验失败',
importFailed: 'Agent 离线包导入或校验失败',
exportFailed: 'Agent 离线包导出失败'
+14 -3
View File
@@ -3084,14 +3084,15 @@ textarea:focus-visible {
.nav-item__badge {
display: grid;
min-width: 19px;
height: 19px;
padding: 0 5px;
min-width: 17px;
height: 17px;
padding: 0 4px;
border-radius: 999px;
background: var(--danger-solid);
color: var(--text-on-accent);
font-size: 10px;
font-weight: 700;
line-height: 1;
place-items: center;
}
@@ -6630,6 +6631,16 @@ button > svg {
color: var(--success);
}
.bundled-agent-inventory__badge--up-to-date {
background: var(--success-subtle);
color: var(--success);
}
.bundled-agent-inventory__badge--update-available {
background: var(--accent-subtle);
color: var(--accent);
}
.bundled-agent-inventory__badge--not-downloaded,
.bundled-agent-inventory__badge--invalid {
background: var(--warning-subtle);
@@ -133,6 +133,20 @@ describe('Agent installation contracts', () => {
).toThrow()
})
it('rejects Base64 encodings with non-zero padding bits', () => {
expect(() =>
agentReleaseKeyRegistrySchema.parse({
formatVersion: 1,
keys: [{
keyId: 'test-key',
publicKeySpkiBase64: 'ZE==',
environment: 'test'
}],
revocations: []
})
).toThrow('canonical Base64')
})
it('requires both locked Linux targets and the fixed Node patch', () => {
const target = {
archive: 'node.tar.gz',
+18 -1
View File
@@ -6,6 +6,19 @@ const sha256Pattern = /^[a-f0-9]{64}$/u
const keyIdPattern = /^[a-z0-9][a-z0-9._-]{0,63}$/u
const canonicalBase64Pattern =
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u
const canonicalDoublePaddingCharacterPattern = /[AQgw]==$/u
const canonicalSinglePaddingCharacterPattern =
/[AEIMQUYcgkosw048]=$/u
function hasCanonicalBase64Padding(value: string): boolean {
if (value.endsWith('==')) {
return canonicalDoublePaddingCharacterPattern.test(value)
}
if (value.endsWith('=')) {
return canonicalSinglePaddingCharacterPattern.test(value)
}
return true
}
export const agentArchitectureSchema = z.enum(['x64', 'arm64'])
@@ -151,7 +164,11 @@ export const agentReleaseKeySchema = z
.string()
.min(4)
.max(4_096)
.regex(canonicalBase64Pattern),
.regex(canonicalBase64Pattern)
.refine(
hasCanonicalBase64Padding,
'Agent public key must use canonical Base64'
),
environment: z.enum(['production', 'test'])
})
.strict()
@@ -67,12 +67,19 @@ describe('Agent package contracts', () => {
platform: 'linux',
state: 'not-downloaded',
version: null,
latestVersion: null,
updateAvailable: false,
remoteRuntimeVersion: null,
agentProtocol: null
} as const
expect(
agentPackageInventorySchema.parse({
checkedAt: '2026-08-25T00:00:00.000Z',
catalog: {
state: 'not-checked',
checkedAt: null,
error: null
},
entries: [
{ ...entry, architecture: 'x64' },
{ ...entry, architecture: 'arm64' }
@@ -82,6 +89,11 @@ describe('Agent package contracts', () => {
expect(() =>
agentPackageInventorySchema.parse({
checkedAt: '2026-08-25T00:00:00.000Z',
catalog: {
state: 'not-checked',
checkedAt: null,
error: null
},
entries: [
{ ...entry, architecture: 'x64' },
{ ...entry, architecture: 'x64' }
+28
View File
@@ -209,14 +209,42 @@ export const agentPackageInventoryEntrySchema = z
architecture: agentArchitectureSchema,
state: agentPackageStateSchema,
version: semanticVersionSchema.nullable(),
latestVersion: semanticVersionSchema.nullable(),
updateAvailable: z.boolean(),
remoteRuntimeVersion: semanticVersionSchema.nullable(),
agentProtocol: agentProtocolVersionSchema.nullable()
})
.strict()
export const agentPackageCatalogStatusSchema = z
.discriminatedUnion('state', [
z
.object({
state: z.literal('not-checked'),
checkedAt: z.null(),
error: z.null()
})
.strict(),
z
.object({
state: z.literal('available'),
checkedAt: z.string().datetime(),
error: z.null()
})
.strict(),
z
.object({
state: z.literal('unavailable'),
checkedAt: z.string().datetime(),
error: z.string().trim().min(1).max(2_000)
})
.strict()
])
export const agentPackageInventorySchema = z
.object({
checkedAt: z.string().datetime(),
catalog: agentPackageCatalogStatusSchema,
entries: z
.array(agentPackageInventoryEntrySchema)
.length(agentArchitectureSchema.options.length)
+4 -4
View File
@@ -477,10 +477,10 @@ export const defaultContextCompressionSettings = {
export const defaultRuntimeSettings = {
provider: 'model',
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
modelProtocol: 'anthropic-messages',
modelAuthentication: 'api-key',
modelBaseUrl: 'http://127.0.0.1:11434/v1',
modelName: 'qwen3',
modelProtocol: 'openai-chat-completions',
modelAuthentication: 'none',
supportsImageInput: false,
imageGenerationQuality: 'auto',
opencodeBaseUrl: '',