@botbuddy/cli 1.63.13 → 1.63.15
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/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +4 -4
- package/daemon-release/linux-x64.json +4 -4
- package/daemon-release/manifest.json +5 -5
- package/package.json +1 -1
- package/src/daemon/callback-release.mjs +50 -0
- package/src/daemon/installer.mjs +30 -0
- package/src/fixtures/bot2775-callback-contract.json +32 -0
- package/src/fixtures/bot2779-handler-status-2026-10-27.json +47 -0
- package/src/stack-callback-gateway.mjs +44 -0
- package/src/stack-identified-admission.mjs +17 -0
- package/src/stack-isolation.mjs +57 -3
- package/src/stack-native-callback.mjs +126 -0
- package/src/stack-requester-origin-config.mjs +21 -0
- package/src/stack-requester-origin.mjs +62 -0
- package/src/stack-tunnel.mjs +4 -2
- package/src/stack.mjs +327 -23
- package/src/vendor/smol-toml/LICENSE +24 -0
- package/src/vendor/smol-toml/date.js +156 -0
- package/src/vendor/smol-toml/error.js +67 -0
- package/src/vendor/smol-toml/extract.js +293 -0
- package/src/vendor/smol-toml/index.js +34 -0
- package/src/vendor/smol-toml/parse.js +164 -0
- package/src/vendor/smol-toml/primitive.js +151 -0
- package/src/vendor/smol-toml/stringify.js +219 -0
- package/src/vendor/smol-toml/struct.js +156 -0
- package/src/vendor/smol-toml/util.js +57 -0
- package/src/vendor/smol-toml/vendored.json +20 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// BOT-2780: one requester listener, reserved before a service generates URLs.
|
|
2
|
+
// The CLI later binds it to exactly one API port on its owned SSH tunnel. This
|
|
3
|
+
// listener is not itself an SSH forward and never supplies forward attestation.
|
|
4
|
+
import {createServer,connect} from 'node:net';
|
|
5
|
+
|
|
6
|
+
const fail=(code,message)=>{const error=Error(message);error.code=code;throw error};
|
|
7
|
+
const validPort=value=>Number.isInteger(value)&&value>=1024&&value<=65535;
|
|
8
|
+
const identity=value=>typeof value==='string'&&value.length>0&&value.length<=4096&&!/[\x00-\x1f\x7f]/.test(value);
|
|
9
|
+
function contextSnapshot(value){
|
|
10
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||!Object.isFrozen(value))fail('REQUESTER_ORIGIN_CONTEXT_REFUSED','immutable requester context required');
|
|
11
|
+
const keys=Object.keys(value).sort();if(!keys.length||keys.length>16||keys.some(key=>!identity(key)||!identity(value[key])))fail('REQUESTER_ORIGIN_CONTEXT_REFUSED','requester context refused');
|
|
12
|
+
return JSON.stringify(keys.map(key=>[key,value[key]]));
|
|
13
|
+
}
|
|
14
|
+
function apiPort(value){
|
|
15
|
+
let url;try{url=new URL(value)}catch{fail('REQUESTER_ORIGIN_FORWARD_REFUSED','owned API URL unavailable')}
|
|
16
|
+
if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.username||url.password||url.pathname!=='/'||url.search||url.hash||!validPort(Number(url.port)))fail('REQUESTER_ORIGIN_FORWARD_REFUSED','API target must be the owned loopback HTTP endpoint');
|
|
17
|
+
return Number(url.port);
|
|
18
|
+
}
|
|
19
|
+
export async function reserveRequesterOrigin({context,port=0}={}){
|
|
20
|
+
const snapshot=contextSnapshot(context);if(port!==0&&!validPort(port))fail('REQUESTER_ORIGIN_PORT_REFUSED','requester origin port refused');
|
|
21
|
+
const sockets=new Set(),lossListeners=new Set();let route,closed=false,lostCode,closePromise;
|
|
22
|
+
const live=()=>{try{return !closed&&route?.isLive()===true}catch{return false}};
|
|
23
|
+
const close=code=>{
|
|
24
|
+
if(closePromise)return closePromise;closed=true;lostCode=code??lostCode;
|
|
25
|
+
for(const socket of sockets)socket.destroy();sockets.clear();
|
|
26
|
+
closePromise=new Promise(resolve=>server.close(()=>resolve()));
|
|
27
|
+
if(code)for(const listener of lossListeners){try{listener({code})}catch{}}
|
|
28
|
+
return closePromise;
|
|
29
|
+
};
|
|
30
|
+
const server=createServer({pauseOnConnect:true},socket=>{
|
|
31
|
+
if(!route){socket.destroy();return}
|
|
32
|
+
if(!live()){socket.destroy();void close('REQUESTER_ORIGIN_TUNNEL_LOST');return}
|
|
33
|
+
sockets.add(socket);const upstream=connect({host:'127.0.0.1',port:route.targetPort});sockets.add(upstream);
|
|
34
|
+
socket.once('error',()=>upstream.destroy());socket.once('close',()=>{sockets.delete(socket);upstream.destroy()});upstream.once('close',()=>sockets.delete(upstream));
|
|
35
|
+
upstream.once('error',()=>{socket.destroy();void close('REQUESTER_ORIGIN_FORWARD_LOST')});
|
|
36
|
+
upstream.once('connect',()=>{
|
|
37
|
+
if(!live()){socket.destroy();upstream.destroy();void close('REQUESTER_ORIGIN_TUNNEL_LOST');return}
|
|
38
|
+
// Pipe only after rechecking custody at connect time. Request paths, auth,
|
|
39
|
+
// bodies and WebSocket upgrade bytes are transparent; there is no URL target.
|
|
40
|
+
socket.pipe(upstream);upstream.pipe(socket);socket.resume();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
server.maxConnections=64;
|
|
44
|
+
try{await new Promise((resolve,reject)=>{server.once('error',reject);server.listen({host:'127.0.0.1',port,exclusive:true},resolve)})}catch(error){await close();fail(error.code==='EADDRINUSE'?'REQUESTER_ORIGIN_PORT_BUSY':'REQUESTER_ORIGIN_BIND_FAILED','requester origin could not be reserved')}
|
|
45
|
+
const boundPort=server.address().port;server.on('error',()=>{void close('REQUESTER_ORIGIN_BIND_FAILED')});
|
|
46
|
+
return{
|
|
47
|
+
origin:'http://127.0.0.1:'+boundPort,
|
|
48
|
+
activate({context:current,leaseId,projectId,sshTarget,remoteConnection,forwardedConnection,forwards,isLive,watchLoss}={}){
|
|
49
|
+
if(closed||route)fail('REQUESTER_ORIGIN_STATE_REFUSED','requester origin cannot be rebound in place');
|
|
50
|
+
if(contextSnapshot(current)!==snapshot||![leaseId,projectId,sshTarget].every(identity)||typeof isLive!=='function'||typeof watchLoss!=='function')fail('REQUESTER_ORIGIN_CONTEXT_REFUSED','owned current lease/forward context required');
|
|
51
|
+
const remote=apiPort(remoteConnection?.api_url),local=apiPort(forwardedConnection?.api_url);
|
|
52
|
+
if(!Array.isArray(forwards)||forwards.length<1||forwards.length>32||forwards.some(row=>!identity(row?.name)||!validPort(row.remote)||!validPort(row.local))||['name','remote','local'].some(key=>new Set(forwards.map(row=>row[key])).size!==forwards.length))fail('REQUESTER_ORIGIN_FORWARD_REFUSED','SSH forward table refused');
|
|
53
|
+
const api=forwards.find(row=>row.remote===remote);if(!api||api.local!==local||local===boundPort)fail('REQUESTER_ORIGIN_FORWARD_REFUSED','API URL differs from actual SSH forward');
|
|
54
|
+
if(isLive()!==true)fail('REQUESTER_ORIGIN_TUNNEL_LOST','owned SSH tunnel is not live');
|
|
55
|
+
route=Object.freeze({leaseId,projectId,sshTarget,targetPort:local,isLive});
|
|
56
|
+
try{watchLoss(()=>{void close('REQUESTER_ORIGIN_TUNNEL_LOST')})}catch{void close('REQUESTER_ORIGIN_TUNNEL_LOST');fail('REQUESTER_ORIGIN_TUNNEL_LOST','owned SSH loss subscription unavailable')}
|
|
57
|
+
if(!live()){void close('REQUESTER_ORIGIN_TUNNEL_LOST');fail('REQUESTER_ORIGIN_TUNNEL_LOST','owned SSH tunnel ended during activation')}
|
|
58
|
+
},
|
|
59
|
+
close:()=>close(),isLive:live,
|
|
60
|
+
onLost(listener){lossListeners.add(listener);if(lostCode)listener({code:lostCode})},
|
|
61
|
+
};
|
|
62
|
+
}
|
package/src/stack-tunnel.mjs
CHANGED
|
@@ -469,7 +469,7 @@ export async function probeGatewayPorts({
|
|
|
469
469
|
* @returns {{ forwards, connection, argv, attempts, onExit, close, pid }}
|
|
470
470
|
*/
|
|
471
471
|
export async function establishSshTunnel({
|
|
472
|
-
connection, sshTarget, hostKey = null, callbackPort = null, sshBin = null, env = process.env,
|
|
472
|
+
connection, sshTarget, hostKey = null, callbackPort = null, reserveLocalPorts = [], sshBin = null, env = process.env,
|
|
473
473
|
attempts = 3, readyTimeoutMs = DEFAULT_TUNNEL_READY_MS, isFree = isLocalPortFree, pickFree = pickFreeLocalPort, spawnImpl = spawn,
|
|
474
474
|
runRemote = spawnSync, probe = probeGatewayPorts, probeReadyTimeoutMs = DEFAULT_PROBE_READY_MS,
|
|
475
475
|
}) {
|
|
@@ -500,7 +500,9 @@ export async function establishSshTunnel({
|
|
|
500
500
|
}
|
|
501
501
|
// Codex #1134 P2: a remapped stack forward must never take the callback port locally; ssh
|
|
502
502
|
// would bind it before the caller's callback service and the -R traffic would loop into it.
|
|
503
|
-
|
|
503
|
+
// BOT-2775 (Codex #1402 P2): a native-callback run opens no -R, but its child still serves the
|
|
504
|
+
// callback on that requester port, so the planner keeps it reserved via reserveLocalPorts.
|
|
505
|
+
let avoid = new Set([...(callbackPort != null ? [Number(callbackPort)] : []), ...reserveLocalPorts.map(Number)]);
|
|
504
506
|
let lastError = null;
|
|
505
507
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
506
508
|
const forwards = await planForwards(remotePorts, { isFree, pickFree, avoid });
|