mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* feat(workflows): recover durable workflows after Gateway restart * docs(workflows): keep recovery guidance within inherited budget * fix(workflows): retain cancellation ownership and stabilize turn-limit test * fix(workflows): log service-owned action failures
127 lines
5.2 KiB
Python
127 lines
5.2 KiB
Python
"""Isolated, capability-free JavaScript worker. Only bounded JSON crosses stdio."""
|
|
|
|
import json
|
|
import math
|
|
import sys
|
|
|
|
import quickjs
|
|
|
|
MAX_FRAME = 48 * 1024
|
|
|
|
BOOT = r"""
|
|
(() => {
|
|
// Replay must not observe wall time or host randomness. These globals are
|
|
// unavailable; callers pass timestamps and random seeds explicitly in args.
|
|
Object.defineProperty(globalThis,'Date',{value:undefined,writable:false,configurable:false});
|
|
Object.defineProperty(Math,'random',{value:()=>{throw new Error('Pass randomness in args');},writable:false,configurable:false});
|
|
const encode=JSON.stringify.bind(JSON), decode=JSON.parse.bind(JSON);
|
|
const requests=[], pending=new Map(); let serial=0, final=null;
|
|
const clone=value=>decode(encode(value));
|
|
const freeze=value=>{if(value&&typeof value==='object'){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
|
|
Object.defineProperty(globalThis,'agent',{value:Object.freeze(payload=>new Promise((resolve,reject)=>{
|
|
if(pending.size>=64){reject(new Error('Pending call limit'));return;}
|
|
const id=++serial; requests.push({type:'agent',id,payload:clone(payload)});pending.set(id,{resolve,reject});
|
|
})),writable:false,configurable:false});
|
|
for(const name of ['pause','humanCheckpoint','captureArtifact','readArtifact']) {
|
|
Object.defineProperty(globalThis,name,{value:Object.freeze(payload=>new Promise((resolve,reject)=>{
|
|
if(pending.size>=64){reject(new Error('Pending call limit'));return;}
|
|
const id=++serial; requests.push({type:name,id,payload:clone(payload)});pending.set(id,{resolve,reject});
|
|
})),writable:false,configurable:false});
|
|
}
|
|
Object.defineProperty(globalThis,'parallel',{value:Object.freeze(tasks=>{
|
|
if(!Array.isArray(tasks)||tasks.length>64) throw new Error('Parallel item limit');
|
|
return Promise.all(tasks.map(task=>task()));
|
|
}),writable:false,configurable:false});
|
|
Object.defineProperty(globalThis,'log',{value:Object.freeze(text=>{
|
|
if(requests.length>=64)throw new Error('Message limit');requests.push({type:'log',text:String(text)});
|
|
}),writable:false,configurable:false});
|
|
return {
|
|
launch:(source,args)=>{
|
|
Object.defineProperty(globalThis,'args',{value:freeze(decode(args)),writable:false,configurable:false});
|
|
const run=(0,eval)('(async()=>{"use strict";\n'+source+'\n})()');
|
|
run.then(value=>{final=encode({type:'result',value:value===undefined?null:value});},
|
|
error=>{final=encode({type:'error',message:String(error)});});
|
|
},
|
|
take:()=>encode(requests.splice(0)), result:()=>final,
|
|
deliver:raw=>{const m=decode(raw),p=pending.get(m.id);if(!p)throw new Error('Unknown response');
|
|
pending.delete(m.id);m.error?p.reject(new Error(m.error)):p.resolve(m.value);}
|
|
};
|
|
})()
|
|
"""
|
|
|
|
|
|
def read():
|
|
line = sys.stdin.buffer.readline(MAX_FRAME + 1)
|
|
if not line or len(line) > MAX_FRAME or not line.endswith(b"\n"):
|
|
raise ValueError("Invalid IPC frame")
|
|
return json.loads(line)
|
|
|
|
|
|
def write(value):
|
|
raw = json.dumps(value, ensure_ascii=True, allow_nan=False).encode() + b"\n"
|
|
if len(raw) > MAX_FRAME:
|
|
raise ValueError("IPC output limit")
|
|
sys.stdout.buffer.write(raw)
|
|
sys.stdout.buffer.flush()
|
|
|
|
|
|
def main():
|
|
request = read()
|
|
# No generated Python, callbacks, imports, environment or OS bindings are exposed.
|
|
# OS limits complement the JS engine; parent wall deadlines work on all hosts.
|
|
try:
|
|
import resource
|
|
|
|
resource.setrlimit(
|
|
resource.RLIMIT_CPU,
|
|
(math.ceil(request["cpu_seconds"]), math.ceil(request["cpu_seconds"]) + 1),
|
|
)
|
|
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
|
|
resource.setrlimit(resource.RLIMIT_FSIZE, (0, 0))
|
|
except ImportError:
|
|
pass
|
|
ctx = quickjs.Context()
|
|
ctx.set_memory_limit(request["memory_bytes"])
|
|
ctx.set_max_stack_size(256 * 1024)
|
|
ctx.set_time_limit(0.05)
|
|
ctx.eval("globalThis.__bridge = " + BOOT)
|
|
launch = ctx.eval("__bridge.launch")
|
|
take = ctx.eval("__bridge.take")
|
|
result = ctx.eval("__bridge.result")
|
|
deliver = ctx.eval("__bridge.deliver")
|
|
ctx.eval("delete globalThis.__bridge")
|
|
launch(request["script"], json.dumps(request["args"]))
|
|
while True:
|
|
# Bound each JS turn and require a parent round trip between batches.
|
|
idle = False
|
|
for _ in range(64):
|
|
if not ctx.execute_pending_job():
|
|
idle = True
|
|
break
|
|
messages = json.loads(take())
|
|
outcome = result()
|
|
# idle: the job queue drained, so only a response can make further progress.
|
|
write({"messages": messages, "outcome": json.loads(outcome) if outcome else None, "idle": idle})
|
|
if outcome:
|
|
return
|
|
response = read()
|
|
for message in response["responses"]:
|
|
deliver(json.dumps(message))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except BaseException:
|
|
# Never return a Python traceback or worker environment to generated code.
|
|
write(
|
|
{
|
|
"messages": [],
|
|
"outcome": {
|
|
"type": "error",
|
|
"message": "Script failed or exceeded worker limits",
|
|
},
|
|
}
|
|
)
|
|
sys.exit(1)
|