|
@@ -38,6 +38,7 @@ export interface ComputerState {id:string;containerId:string;running:boolean;wor
|
|
|
* concurrent readers, desktop driver input is serialized, and lifecycle
|
|
* concurrent readers, desktop driver input is serialized, and lifecycle
|
|
|
* operations (start/stop/reconfigure) drain every reader first. */
|
|
* operations (start/stop/reconfigure) drain every reader first. */
|
|
|
export type ComputerLane='exec'|'gui'
|
|
export type ComputerLane='exec'|'gui'
|
|
|
|
|
+export type ComputerUser='cua'|'root'
|
|
|
export interface ComputerProvider {
|
|
export interface ComputerProvider {
|
|
|
releaseFence?(spec:ComputerSpecification,keepRunning?:boolean):Promise<void>
|
|
releaseFence?(spec:ComputerSpecification,keepRunning?:boolean):Promise<void>
|
|
|
renewFence?(spec:ComputerSpecification):Promise<void>
|
|
renewFence?(spec:ComputerSpecification):Promise<void>
|
|
@@ -47,7 +48,7 @@ export interface ComputerProvider {
|
|
|
ensure(spec:ComputerSpecification,authorize:()=>void):Promise<ComputerState>
|
|
ensure(spec:ComputerSpecification,authorize:()=>void):Promise<ComputerState>
|
|
|
stop(spec:ComputerSpecification):Promise<void>
|
|
stop(spec:ComputerSpecification):Promise<void>
|
|
|
remove(spec:ComputerSpecification):Promise<void>
|
|
remove(spec:ComputerSpecification):Promise<void>
|
|
|
- execute(spec:ComputerSpecification,argv:string[],options:{authorize():void;signal?:AbortSignal;timeout?:number;input?:Buffer;lane?:ComputerLane;mayFence?():boolean}):Promise<CommandResult>
|
|
|
|
|
|
|
+ execute(spec:ComputerSpecification,argv:string[],options:{authorize():void;signal?:AbortSignal;timeout?:number;input?:Buffer;lane?:ComputerLane;mayFence?():boolean;user?:ComputerUser}):Promise<CommandResult>
|
|
|
health(spec:ComputerSpecification,authorize:()=>void):Promise<unknown>
|
|
health(spec:ComputerSpecification,authorize:()=>void):Promise<unknown>
|
|
|
}
|
|
}
|
|
|
class Semaphore {
|
|
class Semaphore {
|
|
@@ -153,13 +154,12 @@ export class ContainerComputerProvider implements ComputerProvider {
|
|
|
this.assertOwned(spec,detail)
|
|
this.assertOwned(spec,detail)
|
|
|
const host=detail.HostConfig??{},expectedCaps=this.runtime==='podman'?['SETGID','SETUID','SYS_CHROOT']:['SETGID','SETUID']
|
|
const host=detail.HostConfig??{},expectedCaps=this.runtime==='podman'?['SETGID','SETUID','SYS_CHROOT']:['SETGID','SETUID']
|
|
|
const nanoCPUs=host.NanoCpus??(host.CpuPeriod>0?host.CpuQuota/host.CpuPeriod*1e9:0)
|
|
const nanoCPUs=host.NanoCpus??(host.CpuPeriod>0?host.CpuQuota/host.CpuPeriod*1e9:0)
|
|
|
- const noPrivilege=(host.SecurityOpt??[]).some((value:string)=>/^no-new-privileges(?::true)?$/.test(value))
|
|
|
|
|
const ports=Object.entries(host.PortBindings??{}),mounts=detail.Mounts??[]
|
|
const ports=Object.entries(host.PortBindings??{}),mounts=detail.Mounts??[]
|
|
|
const destinations=spec.cwd===COMPUTER_WORKSPACE?[COMPUTER_WORKSPACE]:[COMPUTER_WORKSPACE,spec.cwd]
|
|
const destinations=spec.cwd===COMPUTER_WORKSPACE?[COMPUTER_WORKSPACE]:[COMPUTER_WORKSPACE,spec.cwd]
|
|
|
if(detail.Config?.Labels?.[specLabel]!==digest(spec)||detail.Image!==spec.imageId||host.Privileged!==false||host.Memory!==spec.memoryMiB*mib||host.MemorySwap!==spec.memoryMiB*mib
|
|
if(detail.Config?.Labels?.[specLabel]!==digest(spec)||detail.Image!==spec.imageId||host.Privileged!==false||host.Memory!==spec.memoryMiB*mib||host.MemorySwap!==spec.memoryMiB*mib
|
|
|
||nanoCPUs!==spec.cpus*1e9||host.PidsLimit!==spec.pids||host.IpcMode!=='private'||host.CgroupnsMode!=='private'
|
|
||nanoCPUs!==spec.cpus*1e9||host.PidsLimit!==spec.pids||host.IpcMode!=='private'||host.CgroupnsMode!=='private'
|
|
|
||host.NetworkMode!=='none'||host.PidMode==='host'||host.UsernsMode==='host'||host.UTSMode==='host'
|
|
||host.NetworkMode!=='none'||host.PidMode==='host'||host.UsernsMode==='host'||host.UTSMode==='host'
|
|
|
- ||!noPrivilege||(host.SecurityOpt??[]).some((value:string)=>/unconfined/.test(value))
|
|
|
|
|
|
|
+ ||(host.SecurityOpt??[]).some((value:string)=>/^no-new-privileges(?::true)?$/.test(value)||/unconfined/.test(value))
|
|
|
||JSON.stringify(capabilities(host.CapAdd))!==JSON.stringify(expectedCaps)||!capabilities(host.CapDrop).includes('ALL')
|
|
||JSON.stringify(capabilities(host.CapAdd))!==JSON.stringify(expectedCaps)||!capabilities(host.CapDrop).includes('ALL')
|
|
|
||(host.Devices??[]).length||(host.DeviceRequests??[]).length||host.RestartPolicy?.Name!=='no'
|
|
||(host.Devices??[]).length||(host.DeviceRequests??[]).length||host.RestartPolicy?.Name!=='no'
|
|
|
||mounts.length!==destinations.length||mounts.some((mount:any)=>mount.Type!=='bind'||mount.Source!==workspace||!destinations.includes(mount.Destination)||mount.RW!==true)||new Set(mounts.map((mount:any)=>mount.Destination)).size!==destinations.length
|
|
||mounts.length!==destinations.length||mounts.some((mount:any)=>mount.Type!=='bind'||mount.Source!==workspace||!destinations.includes(mount.Destination)||mount.RW!==true)||new Set(mounts.map((mount:any)=>mount.Destination)).size!==destinations.length
|
|
@@ -197,7 +197,7 @@ export class ContainerComputerProvider implements ComputerProvider {
|
|
|
...Object.entries(this.labels(spec)).flatMap(([key,value])=>['--label',`${key}=${value}`]),
|
|
...Object.entries(this.labels(spec)).flatMap(([key,value])=>['--label',`${key}=${value}`]),
|
|
|
'--restart','no','--network','none','--ipc','private','--cgroupns','private',
|
|
'--restart','no','--network','none','--ipc','private','--cgroupns','private',
|
|
|
'--memory',`${spec.memoryMiB}m`,'--memory-swap',`${spec.memoryMiB}m`,'--cpus',String(spec.cpus),'--pids-limit',String(spec.pids),
|
|
'--memory',`${spec.memoryMiB}m`,'--memory-swap',`${spec.memoryMiB}m`,'--cpus',String(spec.cpus),'--pids-limit',String(spec.pids),
|
|
|
- '--shm-size','512m','--security-opt','no-new-privileges:true','--cap-drop','ALL','--cap-add','SETUID','--cap-add','SETGID',
|
|
|
|
|
|
|
+ '--shm-size','512m','--cap-drop','ALL','--cap-add','SETUID','--cap-add','SETGID',
|
|
|
...(this.runtime==='podman'?['--userns','keep-id:uid=1000,gid=1000','--user','root','--cap-add','SYS_CHROOT']:[]),
|
|
...(this.runtime==='podman'?['--userns','keep-id:uid=1000,gid=1000','--user','root','--cap-add','SYS_CHROOT']:[]),
|
|
|
...[...new Set([COMPUTER_WORKSPACE,spec.cwd])].flatMap(destination=>['--mount',`type=bind,source=${workspace},target=${destination}${this.runtime==='podman'?',relabel=private':''}`]),
|
|
...[...new Set([COMPUTER_WORKSPACE,spec.cwd])].flatMap(destination=>['--mount',`type=bind,source=${workspace},target=${destination}${this.runtime==='podman'?',relabel=private':''}`]),
|
|
|
...(spec.network==='public-proxy'?['--env','HTTP_PROXY=http://127.0.0.1:3128','--env','HTTPS_PROXY=http://127.0.0.1:3128','--env','http_proxy=http://127.0.0.1:3128','--env','https_proxy=http://127.0.0.1:3128','--env','NO_PROXY=localhost,127.0.0.1,::1','--env','no_proxy=localhost,127.0.0.1,::1']:[]),
|
|
...(spec.network==='public-proxy'?['--env','HTTP_PROXY=http://127.0.0.1:3128','--env','HTTPS_PROXY=http://127.0.0.1:3128','--env','http_proxy=http://127.0.0.1:3128','--env','https_proxy=http://127.0.0.1:3128','--env','NO_PROXY=localhost,127.0.0.1,::1','--env','no_proxy=localhost,127.0.0.1,::1']:[]),
|
|
@@ -209,6 +209,11 @@ export class ContainerComputerProvider implements ComputerProvider {
|
|
|
authorize();await this.verifyRuntime();authorize()
|
|
authorize();await this.verifyRuntime();authorize()
|
|
|
const workspace=await this.workspace(spec)
|
|
const workspace=await this.workspace(spec)
|
|
|
let detail=await this.migratedContainer(spec,await this.inspectRaw(this.name(spec)),workspace)
|
|
let detail=await this.migratedContainer(spec,await this.inspectRaw(this.name(spec)),workspace)
|
|
|
|
|
+ if(detail?.HostConfig?.SecurityOpt?.some((value:string)=>/^no-new-privileges(?::true)?$/.test(value))) {
|
|
|
|
|
+ await this.stopOwned(spec,detail.Id)
|
|
|
|
|
+ await this.run(this.runtime,['rm','--',detail.Id])
|
|
|
|
|
+ detail=undefined
|
|
|
|
|
+ }
|
|
|
if(!detail) {
|
|
if(!detail) {
|
|
|
const image=JSON.parse((await this.run(this.runtime,['image','inspect',spec.imageId])).stdout)[0]
|
|
const image=JSON.parse((await this.run(this.runtime,['image','inspect',spec.imageId])).stdout)[0]
|
|
|
if(image?.Id!==spec.imageId||image.Config?.Labels?.['com.openmausbot.cua-driver']!=='0.20.0'||image.Config?.Labels?.['com.openmausbot.image-layer']!=='5')
|
|
if(image?.Id!==spec.imageId||image.Config?.Labels?.['com.openmausbot.cua-driver']!=='0.20.0'||image.Config?.Labels?.['com.openmausbot.image-layer']!=='5')
|
|
@@ -251,7 +256,7 @@ export class ContainerComputerProvider implements ComputerProvider {
|
|
|
// Persistent workspace deletion is a separate, explicit operation.
|
|
// Persistent workspace deletion is a separate, explicit operation.
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
- async execute(value:ComputerSpecification,argv:string[],options:{authorize():void;signal?:AbortSignal;timeout?:number;input?:Buffer;lane?:ComputerLane;mayFence?():boolean}):Promise<CommandResult> {
|
|
|
|
|
|
|
+ async execute(value:ComputerSpecification,argv:string[],options:{authorize():void;signal?:AbortSignal;timeout?:number;input?:Buffer;lane?:ComputerLane;mayFence?():boolean;user?:ComputerUser}):Promise<CommandResult> {
|
|
|
const spec=this.validateSpecification(value)
|
|
const spec=this.validateSpecification(value)
|
|
|
if(!argv.length||argv.length>64||argv.some(arg=>typeof arg!=='string'||arg.includes('\0'))||argv.join('').length>65536)throw new ComputerError('computer_command_invalid','电脑命令无效或过长')
|
|
if(!argv.length||argv.length>64||argv.some(arg=>typeof arg!=='string'||arg.includes('\0'))||argv.join('').length>65536)throw new ComputerError('computer_command_invalid','电脑命令无效或过长')
|
|
|
return this.lane(spec.id).read(async()=>{
|
|
return this.lane(spec.id).read(async()=>{
|
|
@@ -260,7 +265,8 @@ export class ContainerComputerProvider implements ComputerProvider {
|
|
|
if(!state?.running)throw new ComputerError('computer_not_running','电脑环境尚未运行')
|
|
if(!state?.running)throw new ComputerError('computer_not_running','电脑环境尚未运行')
|
|
|
options.authorize()
|
|
options.authorize()
|
|
|
try {
|
|
try {
|
|
|
- const result=await this.run(this.runtime,['exec',...(options.input?['-i']:[]),'--user','1000:1000','--workdir',spec.cwd,'--',state.containerId,...argv],{timeout:Math.min(60000,Math.max(1000,options.timeout??30000)),signal:options.signal,input:options.input})
|
|
|
|
|
|
|
+ const user=options.user??'cua',uid=user==='root'?'0:0':'1000:1000',home=user==='root'?'/root':'/home/cua'
|
|
|
|
|
+ const result=await this.run(this.runtime,['exec',...(options.input?['-i']:[]),'--user',uid,'--env',`HOME=${home}`,'--env',`USER=${user}`,'--workdir',spec.cwd,'--',state.containerId,...argv],{timeout:Math.min(60000,Math.max(1000,options.timeout??30000)),signal:options.signal,input:options.input})
|
|
|
options.authorize();return result
|
|
options.authorize();return result
|
|
|
}catch(error){
|
|
}catch(error){
|
|
|
// Killing a docker exec client alone cannot prove its guest process stopped.
|
|
// Killing a docker exec client alone cannot prove its guest process stopped.
|