Remove Ask/Execute modes and general product tool approvals so requests use the selected runtime, enabled capabilities, and execution account permissions. Migrate retired application metadata while preserving user content. Retire the Results page while retaining conversation images and stored artifacts. Add local and remote workspace file imports, timestamps and sorting, improve image discovery and saving, and refine workspace navigation and supervision responsiveness. Include the accompanying web-search changes, tests, probes, and product documentation. Remote imports require an updated Agent. Existing artifacts are not automatically copied into project directories. Validation evidence and outstanding failures remain documented in feature progress files; no full test suite or real-model calls were rerun for this commit. Release note: Run requests without switching work modes, import files into a selected local or remote workspace folder, sort workspace files by date, and keep using conversation images after removal of the Results page.
457 lines
13 KiB
TypeScript
457 lines
13 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { createOperationIdentity } from './agent-protocol/canonical'
|
|
import {
|
|
REMOTE_RUNTIME_LAUNCH_LIMITS,
|
|
assertDetachedRemoteRuntimeBundleDigest,
|
|
detachedSignedRemoteRuntimeBundleSchema,
|
|
digestRemoteRuntimeBundleIdentity,
|
|
digestRemoteRuntimeBundleManifest,
|
|
remoteRuntimeQueryRequestSchema,
|
|
remoteRuntimeQueryResultSchema,
|
|
remoteRuntimeStartRequestSchema,
|
|
remoteRuntimeStartResultSchema,
|
|
remoteRuntimeStopRequestSchema,
|
|
remoteRuntimeStopResultSchema
|
|
} from './remote-runtime-launch-contracts'
|
|
|
|
const digest = `sha256:${'a'.repeat(64)}`
|
|
const rawDigest = 'a'.repeat(64)
|
|
const deadlineAt = '2030-01-01T00:00:00.000Z'
|
|
|
|
const manifest = {
|
|
formatVersion: 2 as const,
|
|
product: 'GoodBuddy' as const,
|
|
runtimeId: 'runtime-1',
|
|
runtimeVersion: '1.0.0',
|
|
provider: 'opencode' as const,
|
|
platform: 'linux' as const,
|
|
architecture: 'x64' as const,
|
|
signingKeyId: 'release-1',
|
|
bundleDigest: digest,
|
|
adapterDigest: digest,
|
|
sourcePackage: {
|
|
name: 'opencode-linux-x64-baseline',
|
|
integrity:
|
|
'sha512-x4KiJk9EF7ktM18Ru5Jue4kTntxMvlhWb7tHniQGGRvY2KeoK1iIkyAFd7ri5H/fSkM22hNv/Gg1Jk6/h9IlxQ=='
|
|
},
|
|
entrypoint: {
|
|
identity: 'opencode-acp',
|
|
path: 'bin/opencode-acp',
|
|
sha256: rawDigest,
|
|
argvPrefix: ['acp']
|
|
},
|
|
files: [
|
|
{
|
|
path: 'bin/opencode-acp',
|
|
size: 100,
|
|
sha256: rawDigest,
|
|
mode: '0755' as const
|
|
},
|
|
{
|
|
path: 'licenses/runtime.txt',
|
|
size: 10,
|
|
sha256: rawDigest,
|
|
mode: '0644' as const
|
|
}
|
|
],
|
|
licenses: [
|
|
{
|
|
package: 'runtime',
|
|
version: '1.0.0',
|
|
spdx: 'MIT',
|
|
path: 'licenses/runtime.txt'
|
|
}
|
|
],
|
|
allowedEnvironmentNames: ['HOME', 'LANG'] as const,
|
|
protocol: { major: 1, minor: 0 },
|
|
acpCapabilitiesDigest: digest,
|
|
limits: {
|
|
maximumPromptRuntimeMilliseconds: 60_000,
|
|
maximumPromptInputBytes: 4096,
|
|
maximumPromptOutputBytes: 1024
|
|
}
|
|
}
|
|
|
|
const signedBundle = {
|
|
manifest,
|
|
detachedSignature: {
|
|
manifestDigest: digest,
|
|
algorithm: 'ed25519' as const,
|
|
keyId: 'release-1',
|
|
signatureBase64: `${'A'.repeat(86)}==`
|
|
}
|
|
}
|
|
|
|
const startPayload = {
|
|
bindingId: 'binding-1',
|
|
requestId: 'request-1',
|
|
runtimeId: 'runtime-1',
|
|
adapterParameters: [
|
|
{ name: 'modelProfileId' as const, value: 'model-1' }
|
|
],
|
|
deadlineAt,
|
|
budget: {
|
|
maximumInputBytes: 10,
|
|
maximumOutputBytes: 100
|
|
}
|
|
}
|
|
|
|
async function operationRequest<T>(
|
|
method: string,
|
|
operationId: string,
|
|
requestId: string,
|
|
payload: T
|
|
) {
|
|
return {
|
|
identity: await createOperationIdentity({
|
|
controllerId: 'controller-1',
|
|
operationId,
|
|
scope: {
|
|
kind: 'run',
|
|
sessionId: 'binding-1',
|
|
requestId
|
|
},
|
|
method,
|
|
payload
|
|
}),
|
|
payload
|
|
}
|
|
}
|
|
|
|
const forbiddenLaunchFields = [
|
|
{ executable: '/bin/sh' },
|
|
{ cwd: '/tmp' },
|
|
{ env: { TOKEN: 'secret' } },
|
|
{ unit: 'runtime.service' },
|
|
{ systemdProperties: ['Delegate=yes'] },
|
|
{ network: 'host' },
|
|
{ mount: '/etc:/host' },
|
|
{ mounts: ['/etc:/host'] },
|
|
{ shell: true },
|
|
{ pid: 123 },
|
|
{ signal: 'SIGKILL' }
|
|
]
|
|
|
|
describe('remote Runtime launch contracts', () => {
|
|
it.each(['deepseek-harness', 'opencode', 'continue'] as const)(
|
|
'accepts a detached-signed %s bundle without signature metadata in signed bytes',
|
|
(provider) => {
|
|
const parsed = detachedSignedRemoteRuntimeBundleSchema.parse({
|
|
...signedBundle,
|
|
manifest: { ...manifest, provider }
|
|
})
|
|
expect(parsed.manifest.provider).toBe(provider)
|
|
expect(parsed.manifest).not.toHaveProperty('manifestDigest')
|
|
expect(parsed.manifest).not.toHaveProperty('signature')
|
|
}
|
|
)
|
|
|
|
it('binds detached keys, entrypoint files, licenses, and unique environment names', () => {
|
|
expect(() =>
|
|
detachedSignedRemoteRuntimeBundleSchema.parse({
|
|
...signedBundle,
|
|
detachedSignature: {
|
|
...signedBundle.detachedSignature,
|
|
keyId: 'other-key'
|
|
}
|
|
})
|
|
).toThrow(/key/iu)
|
|
expect(() =>
|
|
detachedSignedRemoteRuntimeBundleSchema.parse({
|
|
...signedBundle,
|
|
manifest: {
|
|
...manifest,
|
|
entrypoint: { ...manifest.entrypoint, sha256: 'b'.repeat(64) }
|
|
}
|
|
})
|
|
).toThrow(/entrypoint/iu)
|
|
expect(() =>
|
|
detachedSignedRemoteRuntimeBundleSchema.parse({
|
|
...signedBundle,
|
|
manifest: {
|
|
...manifest,
|
|
licenses: [
|
|
{ ...manifest.licenses[0], path: 'licenses/missing.txt' }
|
|
]
|
|
}
|
|
})
|
|
).toThrow(/licenses/iu)
|
|
expect(() =>
|
|
detachedSignedRemoteRuntimeBundleSchema.parse({
|
|
...signedBundle,
|
|
manifest: {
|
|
...manifest,
|
|
allowedEnvironmentNames: ['HOME', 'HOME']
|
|
}
|
|
})
|
|
).toThrow(/unique/iu)
|
|
})
|
|
|
|
it('bounds prompt limits and fixed argv', () => {
|
|
expect(
|
|
detachedSignedRemoteRuntimeBundleSchema.parse(signedBundle)
|
|
).toEqual(signedBundle)
|
|
expect(() =>
|
|
detachedSignedRemoteRuntimeBundleSchema.parse({
|
|
...signedBundle,
|
|
manifest: {
|
|
...manifest,
|
|
limits: {
|
|
...manifest.limits,
|
|
maximumPromptInputBytes:
|
|
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumPromptInputBytes + 1
|
|
}
|
|
}
|
|
})
|
|
).toThrow()
|
|
expect(() =>
|
|
detachedSignedRemoteRuntimeBundleSchema.parse({
|
|
...signedBundle,
|
|
manifest: {
|
|
...manifest,
|
|
entrypoint: {
|
|
...manifest.entrypoint,
|
|
argvPrefix: Array(
|
|
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumFixedArgumentCount + 1
|
|
).fill('x')
|
|
}
|
|
}
|
|
})
|
|
).toThrow()
|
|
})
|
|
|
|
it('keeps one current prompt-limits manifest contract', () => {
|
|
const parsed = detachedSignedRemoteRuntimeBundleSchema.parse({
|
|
...signedBundle,
|
|
manifest
|
|
})
|
|
expect(parsed.manifest.formatVersion).toBe(2)
|
|
expect(parsed.manifest).toHaveProperty('limits')
|
|
expect(Object.keys(parsed.manifest)).not.toContain('quotas')
|
|
})
|
|
|
|
it('verifies the detached digest over manifest bytes only', async () => {
|
|
const bundleDigest =
|
|
await digestRemoteRuntimeBundleIdentity(manifest)
|
|
const digestBoundManifest = { ...manifest, bundleDigest }
|
|
const manifestDigest =
|
|
await digestRemoteRuntimeBundleManifest(digestBoundManifest)
|
|
const validBundle = {
|
|
...signedBundle,
|
|
manifest: digestBoundManifest,
|
|
detachedSignature: {
|
|
...signedBundle.detachedSignature,
|
|
manifestDigest
|
|
}
|
|
}
|
|
await expect(
|
|
assertDetachedRemoteRuntimeBundleDigest(validBundle)
|
|
).resolves.toEqual(validBundle)
|
|
await expect(
|
|
assertDetachedRemoteRuntimeBundleDigest(signedBundle)
|
|
).rejects.toThrow(/digest does not match/iu)
|
|
})
|
|
|
|
it('validates start requests without adapter flags, paths or process-control fields', async () => {
|
|
const valid = await operationRequest(
|
|
'runtime/startPrompt',
|
|
'start-operation-1',
|
|
'request-1',
|
|
startPayload
|
|
)
|
|
expect(remoteRuntimeStartRequestSchema.parse(valid)).toEqual(valid)
|
|
for (const value of [
|
|
'--flag',
|
|
'/tmp/model',
|
|
'models/model',
|
|
'models\\model',
|
|
'..'
|
|
]) {
|
|
expect(() =>
|
|
remoteRuntimeStartRequestSchema.parse({
|
|
...valid,
|
|
payload: {
|
|
...startPayload,
|
|
adapterParameters: [{ name: 'modelProfileId', value }]
|
|
}
|
|
})
|
|
).toThrow()
|
|
}
|
|
for (const forbidden of forbiddenLaunchFields) {
|
|
expect(() =>
|
|
remoteRuntimeStartRequestSchema.parse({
|
|
...valid,
|
|
payload: { ...startPayload, ...forbidden }
|
|
})
|
|
).toThrow()
|
|
}
|
|
})
|
|
|
|
it('operation-binds start results and canonical stop/query requests', async () => {
|
|
const startResult = {
|
|
launchId: 'launch-1',
|
|
processId: 'process-opaque-1',
|
|
startOperationId: 'start-operation-1',
|
|
bindingId: 'binding-1',
|
|
runtimeId: 'runtime-1',
|
|
supervisorIdentityDigest: digest,
|
|
state: 'running' as const,
|
|
acceptedAt: deadlineAt
|
|
}
|
|
expect(remoteRuntimeStartResultSchema.parse(startResult)).toEqual(
|
|
startResult
|
|
)
|
|
const stopPayload = {
|
|
launchId: 'launch-1',
|
|
bindingId: 'binding-1',
|
|
runtimeId: 'runtime-1',
|
|
startOperationId: 'start-operation-1',
|
|
requestId: 'stop-request-1',
|
|
reason: 'user-cancelled' as const,
|
|
deadlineAt
|
|
}
|
|
const stop = await operationRequest(
|
|
'runtime/stopPrompt', 'stop-1', stopPayload.requestId, stopPayload
|
|
)
|
|
expect(remoteRuntimeStopRequestSchema.parse(stop)).toEqual(stop)
|
|
const query = await operationRequest(
|
|
'runtime/queryPrompt', 'query-1', 'query-request-1', {
|
|
launchId: 'launch-1',
|
|
bindingId: 'binding-1',
|
|
runtimeId: 'runtime-1',
|
|
startOperationId: 'start-operation-1',
|
|
requestId: 'query-request-1'
|
|
}
|
|
)
|
|
expect(remoteRuntimeQueryRequestSchema.parse(query)).toEqual(query)
|
|
expect(() =>
|
|
remoteRuntimeStopRequestSchema.parse({
|
|
...stop,
|
|
identity: { ...stop.identity, method: 'runtime/startPrompt' }
|
|
})
|
|
).toThrow(/method/iu)
|
|
expect(() =>
|
|
remoteRuntimeQueryRequestSchema.parse({
|
|
...query,
|
|
payload: { ...query.payload, bindingId: 'other-binding' }
|
|
})
|
|
).toThrow(/scope/iu)
|
|
for (const forbidden of forbiddenLaunchFields) {
|
|
expect(() =>
|
|
remoteRuntimeStopRequestSchema.parse({
|
|
...stop,
|
|
payload: { ...stop.payload, ...forbidden }
|
|
})
|
|
).toThrow()
|
|
expect(() =>
|
|
remoteRuntimeQueryRequestSchema.parse({
|
|
...query,
|
|
payload: { ...query.payload, ...forbidden }
|
|
})
|
|
).toThrow()
|
|
}
|
|
})
|
|
|
|
it('requires ownership identity in stop results and finite reconciliation state', () => {
|
|
const stopResult = {
|
|
launchId: 'launch-1',
|
|
operationId: 'stop-1',
|
|
startOperationId: 'start-operation-1',
|
|
bindingId: 'binding-1',
|
|
runtimeId: 'runtime-1',
|
|
supervisorIdentityDigest: digest,
|
|
state: 'stopped' as const,
|
|
acceptedAt: deadlineAt
|
|
}
|
|
expect(remoteRuntimeStopResultSchema.parse(stopResult)).toEqual(
|
|
stopResult
|
|
)
|
|
const baseQueryResult = {
|
|
launchId: 'launch-1',
|
|
queryOperationId: 'query-1',
|
|
startOperationId: 'start-operation-1',
|
|
bindingId: 'binding-1',
|
|
runtimeId: 'runtime-1',
|
|
supervisorIdentityDigest: digest,
|
|
output: {
|
|
effectiveCapturedOutputLimitBytes: 1024,
|
|
stdoutBytes: 10,
|
|
stderrBytes: 2,
|
|
truncated: false
|
|
}
|
|
}
|
|
expect(
|
|
remoteRuntimeQueryResultSchema.parse({
|
|
...baseQueryResult,
|
|
state: 'interrupted',
|
|
processTreeState: 'orphaned',
|
|
errorCode: 'process-tree-orphaned'
|
|
}).state
|
|
).toBe('interrupted')
|
|
expect(
|
|
remoteRuntimeQueryResultSchema.parse({
|
|
...baseQueryResult,
|
|
state: 'outcome-unknown',
|
|
processTreeState: 'outcome-unknown',
|
|
errorCode: 'supervisor-unavailable'
|
|
}).state
|
|
).toBe('outcome-unknown')
|
|
expect(() =>
|
|
remoteRuntimeQueryResultSchema.parse({
|
|
...baseQueryResult,
|
|
state: 'identity-conflict',
|
|
processTreeState: 'reconciled',
|
|
errorCode: 'identity-mismatch'
|
|
})
|
|
).toThrow(/inconsistent/iu)
|
|
for (const invalid of [
|
|
{ state: 'running', processTreeState: 'terminated' },
|
|
{ state: 'stopped', processTreeState: 'pending' },
|
|
{ state: 'stopped', processTreeState: 'orphaned' }
|
|
] as const) {
|
|
expect(() =>
|
|
remoteRuntimeQueryResultSchema.parse({
|
|
...baseQueryResult,
|
|
...invalid
|
|
})
|
|
).toThrow(/inconsistent/iu)
|
|
}
|
|
expect(() =>
|
|
remoteRuntimeQueryResultSchema.parse({
|
|
...baseQueryResult,
|
|
state: 'running',
|
|
processTreeState: 'reconciled',
|
|
output: {
|
|
effectiveCapturedOutputLimitBytes: 10,
|
|
stdoutBytes: 8,
|
|
stderrBytes: 3,
|
|
truncated: true
|
|
}
|
|
})
|
|
).toThrow(/aggregate limit/iu)
|
|
expect(() =>
|
|
remoteRuntimeQueryResultSchema.parse({
|
|
...baseQueryResult,
|
|
state: 'running',
|
|
processTreeState: 'reconciled',
|
|
output: {
|
|
effectiveCapturedOutputLimitBytes:
|
|
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumCapturedOutputBytes,
|
|
stdoutBytes:
|
|
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumCapturedOutputBytes,
|
|
stderrBytes: 1,
|
|
truncated: true
|
|
}
|
|
})
|
|
).toThrow(/aggregate limit/iu)
|
|
expect(() =>
|
|
remoteRuntimeQueryResultSchema.parse({
|
|
...baseQueryResult,
|
|
state: 'running',
|
|
processTreeState: 'reconciled',
|
|
diagnostic: 'arbitrary process text'
|
|
})
|
|
).toThrow()
|
|
})
|
|
})
|