@staix/agent-hub 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +4 -4
- package/docs/agent-notes/README.md +6 -0
- package/docs/agent-notes/adapters.md +10 -8
- package/docs/agent-notes/daemon.md +5 -2
- package/docs/agent-notes/local-worker.md +2 -1
- package/docs/agent-notes/tasks.md +4 -1
- package/docs/agent-notes/tests.md +6 -3
- package/docs/dogfood.md +9 -3
- package/docs/memory.md +1 -1
- package/docs/operations.md +66 -28
- package/docs/quickstart.md +9 -5
- package/docs/security.md +22 -13
- package/docs/smoke.md +3 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +16 -5
- package/docs/verification/2026-10-25-remote-docker-transport.md +10 -0
- package/docs/verification/2026-10-26-agy-1.4.7.md +69 -0
- package/docs/verification/2026-10-26-claude-implementer-2.1.310.md +69 -0
- package/docs/verification/2026-10-26-headless-claude-shell-context.md +62 -0
- package/docs/verification/2026-10-26-jcode-0.99.1.md +49 -0
- package/docs/verification/2026-10-26-kimi-232-reviewer-compat.md +53 -0
- package/docs/verification/2026-10-26-pi-140-compat.md +41 -0
- package/docs/verification/2026-10-26-qwen-0261-acp.md +60 -0
- package/docs/verification/2026-10-27-0.19.0.md +86 -0
- package/docs/verification/claude-permission-transport-native.py +187 -0
- package/docs/verification/claude-shell-wrapper-host.ts +20 -0
- package/docs/verification/claude-shell-wrapper-native.py +145 -0
- package/docs/verification/pi-140-model-free.ts +57 -0
- package/docs/verification/pi-140-native.mjs +21 -0
- package/docs/verified.json +14 -14
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +273 -68
- package/src/adapters/agy/entry.ts +1 -1
- package/src/adapters/claude-channel.ts +7 -3
- package/src/adapters/claude-implementer.ts +5 -3
- package/src/adapters/compat.ts +32 -18
- package/src/adapters/conformance/fakes/pi-rpc.ts +1 -0
- package/src/adapters/definitions/builtin-metadata.ts +2 -2
- package/src/adapters/definitions/native-factories.ts +7 -2
- package/src/adapters/definitions/reviewer-variants.ts +2 -2
- package/src/adapters/pi.ts +39 -3
- package/src/adapters/qwen-dual.ts +25 -0
- package/src/adapters/registry-types.ts +6 -2
- package/src/adapters/registry.ts +1 -0
- package/src/cli/agents.ts +69 -7
- package/src/cli/claude-shell-wrapper.ts +54 -0
- package/src/cli/main.ts +30 -24
- package/src/cli/output.ts +4 -0
- package/src/hub/agent-drift.ts +61 -0
- package/src/hub/board.ts +14 -2
- package/src/hub/check-runner.ts +148 -18
- package/src/hub/claude-shell-wrapper.ts +349 -0
- package/src/hub/config-edit.ts +2 -2
- package/src/hub/config-tier.ts +2 -2
- package/src/hub/daemon.ts +197 -35
- package/src/hub/host-executable.ts +20 -1
- package/src/hub/instances.ts +18 -2
- package/src/hub/lifecycle.ts +1 -1
- package/src/hub/operational-authority.ts +7 -0
- package/src/hub/operational-view.ts +6 -10
- package/src/hub/read-only-shell-proof.ts +7 -6
- package/src/hub/review-scope.ts +13 -0
- package/src/hub/sdlc-metrics.ts +6 -4
- package/src/hub/task-shell-command.ts +85 -0
- package/src/hub/task-shell-default.ts +78 -0
- package/src/hub/task-shell-proof.ts +171 -0
- package/src/hub/task-shell-sandbox.ts +61 -0
- package/src/hub/tasks.ts +139 -13
- package/src/local/deny.ts +91 -11
- package/src/local/sandbox.ts +67 -28
- package/src/local/shell-environment.ts +10 -0
- package/src/ui/index.html +1 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Model-free native receipt. Usage: python3 this.py OUTPUT --bun /absolute/bun --claude /absolute/claude [--claude ...]
|
|
2
|
+
import argparse, hashlib, http.server, json, os, pathlib, select, shlex, signal, subprocess, sys, threading
|
|
3
|
+
parser=argparse.ArgumentParser(); parser.add_argument('output'); parser.add_argument('--bun',required=True); parser.add_argument('--claude',action='append',required=True)
|
|
4
|
+
args=parser.parse_args(); ROOT=pathlib.Path(args.output).resolve(); ROOT.mkdir(mode=0o700,parents=False,exist_ok=False)
|
|
5
|
+
HOST=pathlib.Path(__file__).with_name('claude-shell-wrapper-host.ts').resolve()
|
|
6
|
+
MCP="import sys,json,os,time\nfor line in sys.stdin:\n try: q=json.loads(line)\n except: continue\n method=q.get('method'); rid=q.get('id')\n if rid is None: continue\n if method=='initialize': r={'protocolVersion':'2024-11-05','capabilities':{'tools':{}},'serverInfo':{'name':'fixture','version':'1'}}\n elif method=='tools/list': r={'tools':[{'name':'approve','description':'Person approval fixture','inputSchema':{'type':'object','properties':{'tool_name':{'type':'string'},'input':{'type':'object'},'tool_use_id':{'type':'string'}},'required':['tool_name','input','tool_use_id'],'additionalProperties':False}}]}\n elif method=='tools/call':\n mode=os.environ['CASE']; args=q.get('params',{}).get('arguments',{})\n if mode=='timeout': time.sleep(60)\n if mode=='approval-timeout': time.sleep(.1)\n if mode=='disconnect': sys.exit(1)\n if mode=='tool-error':\n print(json.dumps({'jsonrpc':'2.0','id':rid,'error':{'code':-32000,'message':'fixture hub unavailable'}}),flush=True); continue\n if mode in ('deny','hook-failure','approval-timeout'): value={'behavior':'deny','message':'no person answered' if mode=='approval-timeout' else 'fixture denied'}\n else:\n updated=args.get('input',{})\n if mode=='normalized' and updated.get('description') in ('probe1','probe2'): updated={'command':os.environ['NORMALIZED'],'description':'host-bound fixture'}\n value={'behavior':'allow','updatedInput':updated}\n with open(os.environ['RECEIPT'],'a') as f: print(json.dumps({'arguments':args,'response':value}),file=f)\n r={'content':[{'type':'text','text':json.dumps(value)}]}\n else: r={}\n print(json.dumps({'jsonrpc':'2.0','id':rid,'result':r}),flush=True)\n"
|
|
7
|
+
(ROOT/'mcp.py').write_text(MCP)
|
|
8
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
9
|
+
def log_message(self,*a): pass
|
|
10
|
+
def do_POST(self):
|
|
11
|
+
q=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
|
|
12
|
+
state=self.server.state
|
|
13
|
+
state['requests']+=1
|
|
14
|
+
if 'count_tokens' in self.path:
|
|
15
|
+
b=json.dumps({'input_tokens':100}).encode(); self.send_response(200);self.end_headers();self.wfile.write(b);return
|
|
16
|
+
msgs=q.get('messages',[])
|
|
17
|
+
with open(state['record'],'a') as f:f.write(json.dumps({'path':self.path,'keys':list(q),'tools':[x.get('name') for x in q.get('tools',[])],'messages':msgs})+'\n')
|
|
18
|
+
count=sum(1 for m in msgs if isinstance(m.get('content'),list) for x in m['content'] if isinstance(x,dict) and x.get('type')=='tool_result'); seen=count>=len(state['commands'])
|
|
19
|
+
content=[{'type':'text','text':'Fixture finished.'}] if seen else [{'type':'tool_use','id':'toolu_probe_'+str(count),'name':'Bash','input':{'command':state['commands'][count],'description':'probe'+str(count)}}]
|
|
20
|
+
result={'id':'msg_probe_'+str(count),'type':'message','role':'assistant','model':'claude-sonnet-4-5','content':content,'stop_reason':'end_turn' if seen else 'tool_use','stop_sequence':None,'usage':{'input_tokens':100,'output_tokens':10}}
|
|
21
|
+
if not q.get('stream'):
|
|
22
|
+
self.send_response(200);self.send_header('Content-Type','application/json');self.end_headers();self.wfile.write(json.dumps(result).encode());return
|
|
23
|
+
self.send_response(200);self.send_header('Content-Type','text/event-stream');self.end_headers()
|
|
24
|
+
start=dict(result);start.update(content=[],stop_reason=None,usage={'input_tokens':100,'output_tokens':0})
|
|
25
|
+
events=[('message_start',{'type':'message_start','message':start})]
|
|
26
|
+
for i,c in enumerate(content):
|
|
27
|
+
block=dict(c)
|
|
28
|
+
if c['type']=='tool_use': block['input']={}
|
|
29
|
+
else: block['text']=''
|
|
30
|
+
events.append(('content_block_start',{'type':'content_block_start','index':i,'content_block':block}))
|
|
31
|
+
delta={'type':'input_json_delta','partial_json':json.dumps(c['input'])} if c['type']=='tool_use' else {'type':'text_delta','text':c['text']}
|
|
32
|
+
events.extend([('content_block_delta',{'type':'content_block_delta','index':i,'delta':delta}),('content_block_stop',{'type':'content_block_stop','index':i})])
|
|
33
|
+
events.extend([('message_delta',{'type':'message_delta','delta':{'stop_reason':result['stop_reason'],'stop_sequence':None},'usage':{'output_tokens':10}}),('message_stop',{'type':'message_stop'})])
|
|
34
|
+
try:
|
|
35
|
+
for e,v in events:self.wfile.write(('event: '+e+'\ndata: '+json.dumps(v)+'\n\n').encode());self.wfile.flush()
|
|
36
|
+
except BrokenPipeError:pass
|
|
37
|
+
|
|
38
|
+
def stop(process):
|
|
39
|
+
if process is None: return True
|
|
40
|
+
try:
|
|
41
|
+
if process.stdin: process.stdin.close()
|
|
42
|
+
try: process.wait(timeout=25)
|
|
43
|
+
except subprocess.TimeoutExpired:
|
|
44
|
+
if process.poll() is None:
|
|
45
|
+
try: os.killpg(process.pid,signal.SIGKILL)
|
|
46
|
+
except ProcessLookupError: pass
|
|
47
|
+
process.wait(timeout=5)
|
|
48
|
+
return process.returncode==0
|
|
49
|
+
except (OSError,subprocess.TimeoutExpired):return False
|
|
50
|
+
|
|
51
|
+
def empty_group(pid):
|
|
52
|
+
try:table=subprocess.run(['/bin/ps','-axo','pgid=,stat='],env={'PATH':'/usr/bin:/bin','LC_ALL':'C','TZ':'UTC'},capture_output=True,text=True,timeout=3)
|
|
53
|
+
except (OSError,subprocess.TimeoutExpired):return False
|
|
54
|
+
if table.returncode or len(table.stdout)>1024*1024:return False
|
|
55
|
+
parsed=[line.split() for line in table.stdout.splitlines() if line.strip()]
|
|
56
|
+
return bool(parsed) and all(len(row)==2 for row in parsed) and not any(row[0]==str(pid) and not row[1].startswith('Z') for row in parsed)
|
|
57
|
+
|
|
58
|
+
def rows(file):
|
|
59
|
+
return [json.loads(line) for line in file.read_text().splitlines() if line]
|
|
60
|
+
|
|
61
|
+
def sha(file):
|
|
62
|
+
digest=hashlib.sha256()
|
|
63
|
+
with file.open('rb') as stream:
|
|
64
|
+
for block in iter(lambda:stream.read(1024*1024),b''):digest.update(block)
|
|
65
|
+
return digest.hexdigest()
|
|
66
|
+
|
|
67
|
+
results=[]
|
|
68
|
+
for raw_binary in args.claude:
|
|
69
|
+
binary=pathlib.Path(raw_binary).resolve(); identity=ROOT/('identity-'+binary.name); identity.mkdir()
|
|
70
|
+
version=subprocess.run([str(binary),'--version'],env={'HOME':str(identity),'CLAUDE_CONFIG_DIR':str(identity/'config'),'PATH':'/usr/bin:/bin'},capture_output=True,text=True,timeout=10)
|
|
71
|
+
assert version.returncode==0 and version.stdout.strip().split()[0] in ('2.1.309','2.1.310'),(version.returncode,version.stdout)
|
|
72
|
+
label=version.stdout.strip().split()[0]; digest=sha(binary)
|
|
73
|
+
for mode in ('context','normalized'):
|
|
74
|
+
d=ROOT/(label+'-'+mode); d.mkdir(); home=d/'home';home.mkdir(); work=d/"work 'quoted'";work.mkdir();(work/'sub').mkdir()
|
|
75
|
+
work=work.resolve(); host_bin=d/"host bin's";host_bin.mkdir(); executable=host_bin/'fixture';counter=d/'effects'
|
|
76
|
+
program='#!/usr/bin/python3\nimport json,os,pathlib,sys\np=pathlib.Path('+repr(str(counter))+');p.write_text(str(int(p.read_text())+1) if p.exists() else "1")\nprint(json.dumps({"marker":"HOST_EXECUTABLE","cwd":os.getcwd(),"executable":os.path.realpath(sys.argv[0]),"nativeVars":{k:os.environ.get(k) for k in ["GIT_DIR","RIPGREP_CONFIG_PATH"]},"peer":os.environ.get("AGENTHUB_PEER_ID"),"term":os.environ.get("TERM"),"home":os.environ.get("HOME"),"path":os.environ.get("PATH"),"tmpdir":os.environ.get("TMPDIR")}))\n'
|
|
77
|
+
executable.write_text(program);executable.chmod(0o700)
|
|
78
|
+
helper=None;srv=None;native=None
|
|
79
|
+
result={'version':version.stdout.strip(),'binarySha256':digest,'case':mode}
|
|
80
|
+
try:
|
|
81
|
+
rewritten=''
|
|
82
|
+
if mode=='normalized':
|
|
83
|
+
fixture=d/'host.json';fixture.write_text(json.dumps({'root':str(work),'executable':str(executable)}))
|
|
84
|
+
helper=subprocess.Popen([str(pathlib.Path(args.bun).resolve()),str(HOST),str(fixture)],env={'HOME':str(home),'PATH':'/opt/homebrew/bin:/usr/bin:/bin','LANG':'C','TERM':'dumb'},stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=open(d/'host.stderr','w'),text=True,start_new_session=True)
|
|
85
|
+
assert select.select([helper.stdout],[],[],10)[0],'host fixture handshake timeout'
|
|
86
|
+
reply=json.loads(helper.stdout.readline());rewritten=reply['command'];assert isinstance(rewritten,str) and rewritten
|
|
87
|
+
commands=['cd sub && pwd','pwd']+(['pwd'] if mode=='normalized' else [])
|
|
88
|
+
srv=http.server.ThreadingHTTPServer(('127.0.0.1',0),Handler);srv.state={'requests':0,'record':str(d/'api.jsonl'),'commands':commands}
|
|
89
|
+
threading.Thread(target=srv.serve_forever,daemon=True).start()
|
|
90
|
+
config={'mcpServers':{'fixture':{'command':'/usr/bin/python3','args':[str(ROOT/'mcp.py')],'env':{'CASE':mode,'RECEIPT':str(d/'mcp.jsonl'),'NORMALIZED':rewritten}}}}
|
|
91
|
+
(d/'mcp.json').write_text(json.dumps(config));(d/'settings.json').write_text(json.dumps({'permissions':{'ask':['Bash']}}))
|
|
92
|
+
native_env={'HOME':str(home),'CLAUDE_CONFIG_DIR':str(home/'config'),'PATH':'/usr/bin:/bin','ANTHROPIC_BASE_URL':'http://127.0.0.1:'+str(srv.server_port),'ANTHROPIC_API_KEY':'synthetic-probe-not-a-credential','CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC':'1','DISABLE_AUTOUPDATER':'1','GIT_DIR':str(d/'poison-git'),'RIPGREP_CONFIG_PATH':str(d/'poison-rg')}
|
|
93
|
+
argv=[str(binary),'-p','--output-format','stream-json','--verbose','--strict-mcp-config','--mcp-config',str(d/'mcp.json'),'--setting-sources','','--settings',str(d/'settings.json'),'--permission-mode','default','--permission-prompts','host','--permission-prompt-tool','mcp__fixture__approve','--model','claude-sonnet-4-5','Run the fixture commands.']
|
|
94
|
+
native=subprocess.Popen(argv,cwd=work,env=native_env,stdout=open(d/'stdout','w'),stderr=open(d/'stderr','w'),start_new_session=True)
|
|
95
|
+
native.wait(timeout=25);assert native.returncode==0,(native.returncode,(d/'stderr').read_text())
|
|
96
|
+
assert (d/'stderr').read_text()==''
|
|
97
|
+
calls=rows(d/'mcp.jsonl');assert len(calls)==len(commands),calls
|
|
98
|
+
frames={}
|
|
99
|
+
for frame in rows(d/'stdout'):
|
|
100
|
+
if frame.get('type')!='user':continue
|
|
101
|
+
for block in frame.get('message',{}).get('content',[]):
|
|
102
|
+
if block.get('type')=='tool_result':
|
|
103
|
+
ident=block['tool_use_id'];assert ident not in frames;frames[ident]=(frame.get('tool_use_result'),block)
|
|
104
|
+
assert set(frames)=={'toolu_probe_'+str(i) for i in range(len(commands))},frames
|
|
105
|
+
for i,call in enumerate(calls):
|
|
106
|
+
assert call['arguments']=={'tool_name':'Bash','input':{'command':commands[i],'description':'probe'+str(i)},'tool_use_id':'toolu_probe_'+str(i)},call
|
|
107
|
+
expected=call['arguments']['input'] if i==0 or mode=='context' else {'command':rewritten,'description':'host-bound fixture'}
|
|
108
|
+
assert call['response']=={'behavior':'allow','updatedInput':expected},call
|
|
109
|
+
def output(i):
|
|
110
|
+
tool,block=frames['toolu_probe_'+str(i)];assert isinstance(tool,dict) and block.get('is_error') is False and tool.get('stderr')=='' and tool.get('interrupted') is False,(tool,block)
|
|
111
|
+
assert block.get('content')==tool['stdout'];return tool['stdout']
|
|
112
|
+
assert output(0).strip()==str(work/'sub')
|
|
113
|
+
if mode=='context':
|
|
114
|
+
assert output(1).strip()==str(work/'sub');result['persistentCwd']=True
|
|
115
|
+
else:
|
|
116
|
+
observed=json.loads(output(1));assert observed['marker']=='HOST_EXECUTABLE' and observed['cwd']==str(work) and observed['executable']==str(executable.resolve()),observed
|
|
117
|
+
assert observed['nativeVars']=={'GIT_DIR':None,'RIPGREP_CONFIG_PATH':None},observed
|
|
118
|
+
assert observed['peer']=='claude-fixture' and observed['term']=='dumb' and observed['home']==str(home) and observed['path']=='/opt/homebrew/bin:/usr/bin:/bin',observed
|
|
119
|
+
assert observed['tmpdir'] and 'ahub-shell-' in observed['tmpdir'] and str(d) not in observed['tmpdir'],observed
|
|
120
|
+
replay_tool,replay=frames['toolu_probe_2'];assert replay.get('is_error') is True and 'host shell execution refused' in json.dumps(replay_tool),frames['toolu_probe_2']
|
|
121
|
+
assert counter.read_text()=='1',counter.read_text()
|
|
122
|
+
result.update(rewrite=True,priorCd=True,environmentIsolation=True,replayRefused=True,effects=1,quotedPaths=True)
|
|
123
|
+
result.update(passed=True,nativeExit=0,requests=srv.state['requests'],permissionCalls=len(calls))
|
|
124
|
+
except Exception as error:result.update(passed=False,error=repr(error))
|
|
125
|
+
finally:
|
|
126
|
+
if native is not None:
|
|
127
|
+
# Signal only while this unreaped direct child still owns its pid, never a group id after successful wait.
|
|
128
|
+
if native.poll() is None:
|
|
129
|
+
try:os.killpg(native.pid,signal.SIGKILL)
|
|
130
|
+
except ProcessLookupError:pass
|
|
131
|
+
native.wait(timeout=5)
|
|
132
|
+
import time
|
|
133
|
+
deadline=time.monotonic()+1
|
|
134
|
+
while not empty_group(native.pid) and time.monotonic()<deadline:time.sleep(.02)
|
|
135
|
+
result['nativeGroupEmpty']=empty_group(native.pid)
|
|
136
|
+
if not result['nativeGroupEmpty']:result.update(passed=False,cleanup='native group not established empty; no unrelated process signalled')
|
|
137
|
+
result['hostStopped']=stop(helper)
|
|
138
|
+
if not result['hostStopped']:result.update(passed=False,cleanup='host wrapper cleanup failed')
|
|
139
|
+
if srv:srv.shutdown()
|
|
140
|
+
result['binaryUnchanged']=sha(binary)==digest
|
|
141
|
+
if not result['binaryUnchanged']:result.update(passed=False,error='native binary changed during probe')
|
|
142
|
+
results.append(result);print(json.dumps(result),flush=True)
|
|
143
|
+
receipt={'cases':results,'certified':all(row['passed'] for row in results),'account':False,'inference':False}
|
|
144
|
+
(ROOT/'receipt.json').write_text(json.dumps(receipt,indent=2));print(json.dumps({'receipt':str(ROOT/'receipt.json'),'certified':receipt['certified']}),flush=True)
|
|
145
|
+
raise SystemExit(0 if receipt['certified'] else 1)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {mkdtempSync, mkdirSync, readFileSync} from 'node:fs';
|
|
2
|
+
import {join, resolve} from 'node:path';
|
|
3
|
+
import {homedir} from 'node:os';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
const launcher=resolve(process.argv[2] ?? join(homedir(),'.pi/agent/bin/pi'));
|
|
7
|
+
const packageRoot=resolve(process.argv[3] ?? join(homedir(),'.pi/agent/install/releases/1.4.0/node_modules/@earendil-works/pi-coding-agent'));
|
|
8
|
+
const extension=fileURLToPath(new URL('../../src/pi/extension.ts',import.meta.url));
|
|
9
|
+
const driver=fileURLToPath(new URL('./pi-140-native.mjs',import.meta.url));
|
|
10
|
+
import {PiToolReceipts} from '../../src/pi/tool-receipts.ts';
|
|
11
|
+
const home=mkdtempSync('/tmp/ahub-505-native-home.'); mkdirSync(join(home,'agent'));
|
|
12
|
+
const pkg=JSON.parse(readFileSync(join(packageRoot,'package.json'),'utf8'));
|
|
13
|
+
assert.equal(pkg.name,'@earendil-works/pi-coding-agent');assert.equal(pkg.version,'1.4.0');
|
|
14
|
+
const version=Bun.spawn([launcher,'--version'],{cwd:home,env:{HOME:home,PATH:process.env.PATH!,PI_CODING_AGENT_DIR:join(home,'agent')},stdin:'ignore',stdout:'pipe',stderr:'pipe'});
|
|
15
|
+
const versionTimer=setTimeout(()=>version.kill(),10000);
|
|
16
|
+
const [versionCode,versionOut,versionErr]=await Promise.all([version.exited,new Response(version.stdout).text(),new Response(version.stderr).text()]);clearTimeout(versionTimer);
|
|
17
|
+
assert.equal(versionCode,0);assert.equal(versionOut.trim(),'1.4.0');assert.equal(versionErr,'');
|
|
18
|
+
const ledger=new PiToolReceipts(join(home,'hub.db'));
|
|
19
|
+
let effects=0, providers=0; const events:any[]=[];
|
|
20
|
+
const bridge=Bun.serve({hostname:'127.0.0.1',port:0,async fetch(req){
|
|
21
|
+
const url=new URL(req.url); if(req.headers.get('authorization')!=='Bearer fixture')return new Response('forbidden',{status:403});
|
|
22
|
+
if(url.pathname==='/commands') {await Bun.sleep(25);return Response.json({command:null});}
|
|
23
|
+
const body=await req.json() as any;
|
|
24
|
+
if(url.pathname==='/event'){events.push(body.type);return Response.json({ok:true});}
|
|
25
|
+
if(url.pathname==='/budget')return Response.json({decisions:[]});
|
|
26
|
+
if(url.pathname==='/tool') {
|
|
27
|
+
const text=await ledger.execute(body.sessionId,body.toolCallId,body.name,body.args,async()=>{
|
|
28
|
+
if(body.args.value==='deny')return 'error: fixture permission denied';
|
|
29
|
+
effects++;return 'fixture success';
|
|
30
|
+
});
|
|
31
|
+
return Response.json({text,failed:text.startsWith('error:')});
|
|
32
|
+
}
|
|
33
|
+
providers++;return new Response('provider forbidden',{status:500});
|
|
34
|
+
}});
|
|
35
|
+
const env={HOME:home,PATH:process.env.PATH!,PI_CODING_AGENT_DIR:join(home,'agent'),HUB_EXTENSION:extension,PI_COMPAT_PACKAGE:packageRoot,
|
|
36
|
+
AGENTHUB_PI_BRIDGE_URL:`http://127.0.0.1:${bridge.port}`,AGENTHUB_PI_BRIDGE_TOKEN:'fixture',AGENTHUB_PI_OWNER_TOKEN:'fixture',
|
|
37
|
+
AGENTHUB_PI_TOOLS:JSON.stringify([{name:'compat_probe',description:'model-free fixture',parameters:{type:'object',properties:{value:{type:'string'}},required:['value']}}])};
|
|
38
|
+
const child=Bun.spawn(['node',driver],{cwd:home,env,stdin:'ignore',stdout:'pipe',stderr:'pipe'});
|
|
39
|
+
const timer=setTimeout(()=>child.kill(),10000);
|
|
40
|
+
const [code,stdout,stderr]=await Promise.all([child.exited,new Response(child.stdout).text(),new Response(child.stderr).text()]);clearTimeout(timer);
|
|
41
|
+
console.log(JSON.stringify({code,stdout,stderr,effects,providers,events,isolatedHome:home}));
|
|
42
|
+
const native=Bun.spawn([launcher,'--mode','rpc','--provider','agent-hub-local','--model','fixture','--no-builtin-tools','--no-skills','--no-prompt-templates','--no-extensions','--extension',env.HUB_EXTENSION,'--session-dir',join(home,'native-sessions')],{
|
|
43
|
+
cwd:home,env:{...env,AGENTHUB_PI_RELAY_URL:`http://127.0.0.1:${bridge.port}/v1`,AGENTHUB_PI_RELAY_TOKEN:'fixture',AGENTHUB_PI_MODELS:JSON.stringify([{id:'fixture',name:'fixture'}])},stdin:'pipe',stdout:'pipe',stderr:'pipe'});
|
|
44
|
+
native.stdin.write(JSON.stringify({id:'probe',type:'get_state'})+'\n');
|
|
45
|
+
const nativeTimer=setTimeout(()=>native.kill(),10000);
|
|
46
|
+
let wire='';for await(const chunk of native.stdout){wire+=new TextDecoder().decode(chunk);if(wire.includes('"command":"get_state"')){native.kill();break;}}
|
|
47
|
+
await native.exited;clearTimeout(nativeTimer);const nativeErr=await new Response(native.stderr).text();
|
|
48
|
+
console.log(JSON.stringify({nativeRpc:wire,nativeStderr:nativeErr,providers,events}));
|
|
49
|
+
bridge.stop(true);await ledger.close();
|
|
50
|
+
assert.equal(code,0);assert.equal(stderr,'');
|
|
51
|
+
const result=JSON.parse(stdout);
|
|
52
|
+
assert.equal(result.extension,'loaded');assert.equal(result.first.isError,false);assert.deepEqual(result.first,result.replay);assert.equal(result.denied.isError,true);
|
|
53
|
+
assert.equal(effects,1);assert.equal(providers,0);assert.equal(nativeErr,'');
|
|
54
|
+
const state=wire.trim().split('\n').map(line=>JSON.parse(line)).find(item=>item.command==='get_state');
|
|
55
|
+
assert.equal(state?.success,true);assert.equal(state.data.messageCount,0);assert.ok(state.data.sessionId);assert.ok(state.data.sessionFile);
|
|
56
|
+
assert.ok(events.includes('session_start'));assert.ok(events.includes('session_shutdown'));
|
|
57
|
+
console.log('pi 1.4.0 model-free receipt: PASS');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
const { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager } = await import(pathToFileURL(`${process.env.PI_COMPAT_PACKAGE}/dist/index.js`).href);
|
|
3
|
+
const cwd = process.env.HOME;
|
|
4
|
+
const settingsManager = SettingsManager.create(cwd, process.env.PI_CODING_AGENT_DIR);
|
|
5
|
+
const loader = new DefaultResourceLoader({cwd, agentDir:process.env.PI_CODING_AGENT_DIR, settingsManager,
|
|
6
|
+
noExtensions:true, additionalExtensionPaths:[process.env.HUB_EXTENSION], noSkills:true, noPromptTemplates:true, noThemes:true});
|
|
7
|
+
await loader.reload();
|
|
8
|
+
const {session, extensionsResult} = await createAgentSession({cwd, agentDir:process.env.PI_CODING_AGENT_DIR,
|
|
9
|
+
resourceLoader:loader, settingsManager, sessionManager:SessionManager.create(cwd, `${cwd}/sessions`), noTools:'builtin'});
|
|
10
|
+
if (extensionsResult.errors.length) throw new Error(JSON.stringify(extensionsResult.errors));
|
|
11
|
+
await session.bindExtensions({mode:'rpc'});
|
|
12
|
+
const tool = session.extensionRunner.getAllRegisteredTools().find(t=>t.definition.name==='compat_probe');
|
|
13
|
+
if (!tool) throw new Error('hub extension tool not registered');
|
|
14
|
+
const invoke = async (id, args) => tool.definition.execute(id, args, new AbortController().signal);
|
|
15
|
+
const first = await invoke('same-call', {value:'fixture'});
|
|
16
|
+
const replay = await invoke('same-call', {value:'fixture'});
|
|
17
|
+
const denied = await invoke('denied-call', {value:'deny'});
|
|
18
|
+
console.log(JSON.stringify({extension:'loaded',first,replay,denied,tools:session.getActiveToolNames()}));
|
|
19
|
+
await session.extensionRunner.emit({type:'session_shutdown'});
|
|
20
|
+
session.dispose();
|
|
21
|
+
process.exit(0);
|
package/docs/verified.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"documents": {
|
|
4
4
|
"README.md": {
|
|
5
|
-
"verifiedAgainst": "
|
|
5
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
6
6
|
"paths": [
|
|
7
7
|
"package.json",
|
|
8
8
|
"src/",
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
]
|
|
12
12
|
},
|
|
13
13
|
"docs/security.md": {
|
|
14
|
-
"verifiedAgainst": "
|
|
14
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
15
15
|
"paths": [
|
|
16
16
|
"src/",
|
|
17
17
|
"templates/"
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
20
|
"docs/operations.md": {
|
|
21
|
-
"verifiedAgainst": "
|
|
21
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
22
22
|
"paths": [
|
|
23
23
|
"package.json",
|
|
24
24
|
"src/",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
"docs/quickstart.md": {
|
|
38
|
-
"verifiedAgainst": "
|
|
38
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
39
39
|
"paths": [
|
|
40
40
|
"package.json",
|
|
41
41
|
"src/",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
"docs/agent-notes/adapters.md": {
|
|
46
|
-
"verifiedAgainst": "
|
|
46
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
47
47
|
"paths": [
|
|
48
48
|
"src/adapters/",
|
|
49
49
|
"src/pi/",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
]
|
|
79
79
|
},
|
|
80
80
|
"docs/agent-notes/benchmarks.md": {
|
|
81
|
-
"verifiedAgainst": "
|
|
81
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
82
82
|
"paths": [
|
|
83
83
|
"scripts/benchmarks/",
|
|
84
84
|
"test/benchmarks/",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
]
|
|
91
91
|
},
|
|
92
92
|
"docs/agent-notes/budget.md": {
|
|
93
|
-
"verifiedAgainst": "
|
|
93
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
94
94
|
"paths": [
|
|
95
95
|
"src/hub/budget.ts",
|
|
96
96
|
"src/cli/statusline-tee.ts",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
]
|
|
115
115
|
},
|
|
116
116
|
"docs/agent-notes/bus.md": {
|
|
117
|
-
"verifiedAgainst": "
|
|
117
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
118
118
|
"paths": [
|
|
119
119
|
"src/hub/bus.ts",
|
|
120
120
|
"src/hub/envelope.ts",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
]
|
|
130
130
|
},
|
|
131
131
|
"docs/agent-notes/daemon.md": {
|
|
132
|
-
"verifiedAgainst": "
|
|
132
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
133
133
|
"paths": [
|
|
134
134
|
"src/hub/daemon.ts",
|
|
135
135
|
"src/hub/control-client.ts",
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
]
|
|
203
203
|
},
|
|
204
204
|
"docs/agent-notes/local-worker.md": {
|
|
205
|
-
"verifiedAgainst": "
|
|
205
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
206
206
|
"paths": [
|
|
207
207
|
"src/adapters/local-worker.ts",
|
|
208
208
|
"src/local/",
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
]
|
|
223
223
|
},
|
|
224
224
|
"docs/agent-notes/tasks.md": {
|
|
225
|
-
"verifiedAgainst": "
|
|
225
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
226
226
|
"paths": [
|
|
227
227
|
"src/hub/tasks.ts",
|
|
228
228
|
"src/hub/board.ts",
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
]
|
|
261
261
|
},
|
|
262
262
|
"docs/agent-notes/tests.md": {
|
|
263
|
-
"verifiedAgainst": "
|
|
263
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
264
264
|
"paths": [
|
|
265
265
|
"test/",
|
|
266
266
|
"scripts/check.sh",
|
|
@@ -284,7 +284,7 @@
|
|
|
284
284
|
]
|
|
285
285
|
},
|
|
286
286
|
"docs/agent-notes/README.md": {
|
|
287
|
-
"verifiedAgainst": "
|
|
287
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
288
288
|
"paths": [
|
|
289
289
|
"src/cli/console-state.ts",
|
|
290
290
|
"src/cli/attention.ts",
|
|
@@ -293,7 +293,7 @@
|
|
|
293
293
|
]
|
|
294
294
|
},
|
|
295
295
|
"src/cli/help.ts": {
|
|
296
|
-
"verifiedAgainst": "
|
|
296
|
+
"verifiedAgainst": "bc8a7f25f5ca93563bf1d43aca374b0c85560356",
|
|
297
297
|
"paths": [
|
|
298
298
|
"src/cli/main.ts",
|
|
299
299
|
"src/cli/launch.ts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@staix/agent-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"bun": ">=1.4.2"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@staix/agent-mem": "0.
|
|
20
|
+
"@staix/agent-mem": "0.10.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Young Joon Lee",
|