@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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.13",
3
- "source_commit": "36f37695e469a42ec5b85ae0d3db8fefa4c4d5bc",
4
- "source_sha256": "ed337e0fa12ed74a93c4eeef069f30a6733a48cd6b3e2de85dc6e1e3f6b1c2c6",
2
+ "release_version": "1.63.15",
3
+ "source_commit": "420d8fb935d96c0a7afbd66e5f63fc75b0e05613",
4
+ "source_sha256": "73dc245585a15aabe21ee1eafb46a034045292d2ce0de76f26b20364b2c05f77",
5
5
  "target": "darwin-arm64",
6
- "sha256": "bd4b5271deee0be522d4d4ff112b18a48999d7a7fbeccd1e59c001b3e54ab3cb"
6
+ "sha256": "698836042536d3d1f2ab96c42b7828e7ffcf17c8af01373a68f3282d631e91b2"
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
- "release_version": "1.63.13",
3
- "source_commit": "36f37695e469a42ec5b85ae0d3db8fefa4c4d5bc",
4
- "source_sha256": "ed337e0fa12ed74a93c4eeef069f30a6733a48cd6b3e2de85dc6e1e3f6b1c2c6",
2
+ "release_version": "1.63.15",
3
+ "source_commit": "420d8fb935d96c0a7afbd66e5f63fc75b0e05613",
4
+ "source_sha256": "73dc245585a15aabe21ee1eafb46a034045292d2ce0de76f26b20364b2c05f77",
5
5
  "target": "linux-x64",
6
- "sha256": "83d29dc25d53a99c882f1df99f6738d175beed460b12f01fef8a8d9dca1185b2"
6
+ "sha256": "806fff5fe2c473729abc66b3f7281687d99161c9f2aec43d5c8b258707a40989"
7
7
  }
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "schema_version": 1,
3
- "release_version": "1.63.13",
4
- "source_commit": "36f37695e469a42ec5b85ae0d3db8fefa4c4d5bc",
5
- "source_sha256": "ed337e0fa12ed74a93c4eeef069f30a6733a48cd6b3e2de85dc6e1e3f6b1c2c6",
3
+ "release_version": "1.63.15",
4
+ "source_commit": "420d8fb935d96c0a7afbd66e5f63fc75b0e05613",
5
+ "source_sha256": "73dc245585a15aabe21ee1eafb46a034045292d2ce0de76f26b20364b2c05f77",
6
6
  "targets": {
7
7
  "darwin-arm64": {
8
8
  "goos": "darwin",
9
9
  "goarch": "arm64",
10
10
  "binary": "daemon/bin/darwin-arm64/botbuddy-daemon",
11
- "sha256": "bd4b5271deee0be522d4d4ff112b18a48999d7a7fbeccd1e59c001b3e54ab3cb",
11
+ "sha256": "698836042536d3d1f2ab96c42b7828e7ffcf17c8af01373a68f3282d631e91b2",
12
12
  "mode": "0755"
13
13
  },
14
14
  "linux-x64": {
15
15
  "goos": "linux",
16
16
  "goarch": "amd64",
17
17
  "binary": "daemon/bin/linux-x64/botbuddy-daemon",
18
- "sha256": "83d29dc25d53a99c882f1df99f6738d175beed460b12f01fef8a8d9dca1185b2",
18
+ "sha256": "806fff5fe2c473729abc66b3f7281687d99161c9f2aec43d5c8b258707a40989",
19
19
  "mode": "0755"
20
20
  }
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.63.13",
3
+ "version": "1.63.15",
4
4
  "description": "BotBuddy \u2014 Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,50 @@
1
+ // BOT-2775: authenticate an older installed runtime against its exact published
2
+ // npm package. Only archive data are read; no downloaded code is executed.
3
+ import {createHash} from 'node:crypto';
4
+ import {execFile as execFileCallback} from 'node:child_process';
5
+ import {promisify} from 'node:util';
6
+ import {mkdtemp,lstat,readFile,mkdir,writeFile,rm} from 'node:fs/promises';
7
+ import {join} from 'node:path';
8
+ import {tmpdir} from 'node:os';
9
+ import {validateDaemonRelease} from './release.mjs';
10
+ const execFile=promisify(execFileCallback),MAX_ARCHIVE=128*1024*1024,MAX_BINARY=64*1024*1024;
11
+ const fail=reason=>{throw Error('callback_release_'+reason)};
12
+ const matches=(r,v,c,s)=>r.releaseVersion===v&&r.sourceCommit===c&&r.sha256===s;
13
+ async function metadataJSON(response){
14
+ if(!response.ok||!response.body)fail('metadata');let size=0;const chunks=[];
15
+ for await(const chunk of response.body){size+=chunk.length;if(size>65536)fail('metadata_size');chunks.push(chunk)}
16
+ try{return JSON.parse(Buffer.concat(chunks).toString('utf8'))}catch{fail('metadata')}
17
+ }
18
+ export async function verifiedCallbackRelease({version,sourceCommit,sha256,releaseRoot,run=execFile,fetchImpl=fetch,validateRelease=validateDaemonRelease}={}){
19
+ if(!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)||!/^[0-9a-f]{40}$/.test(sourceCommit)||!/^[0-9a-f]{64}$/.test(sha256))fail('identity');
20
+ try{const current=await validateRelease({releaseRoot},{platform:'linux',arch:'x64'});if(matches(current,version,sourceCommit,sha256))return current}catch{}
21
+ const controller=new AbortController(),deadline=setTimeout(()=>controller.abort(),60_000);deadline.unref();
22
+ let root;
23
+ try{
24
+ const metadata=await metadataJSON(await fetchImpl('https://registry.npmjs.org/@botbuddy%2fcli/'+encodeURIComponent(version),{signal:controller.signal,redirect:'error'}));
25
+ const integrity=metadata?.dist?.integrity;
26
+ if(metadata?.name!=='@botbuddy/cli'||metadata.version!==version||metadata.dist?.tarball!==`https://registry.npmjs.org/@botbuddy/cli/-/cli-${version}.tgz`||typeof integrity!=='string'||!/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity))fail('metadata');
27
+ root=await mkdtemp(join(tmpdir(),'botbuddy-callback-release-'));
28
+ const packed=await run('npm',['pack','@botbuddy/cli@'+version,'--ignore-scripts','--json','--pack-destination',root,'--registry=https://registry.npmjs.org','--fetch-retries=0','--fetch-timeout=30000'],{signal:controller.signal,timeout:45_000,maxBuffer:65536,encoding:'utf8'});
29
+ let result;try{result=JSON.parse(packed.stdout)}catch{fail('pack')}
30
+ if(!Array.isArray(result)||result.length!==1||result[0].name!=='@botbuddy/cli'||result[0].version!==version||result[0].integrity!==integrity||!/^[-A-Za-z0-9.]+\.tgz$/.test(result[0].filename))fail('pack');
31
+ const archive=join(root,result[0].filename),stat=await lstat(archive);
32
+ if(!stat.isFile()||stat.isSymbolicLink()||stat.size>MAX_ARCHIVE)fail('archive');
33
+ if('sha512-'+createHash('sha512').update(await readFile(archive)).digest('base64')!==integrity)fail('integrity');
34
+ const tar=async(args,maxBuffer)=>run('tar',args,{signal:controller.signal,timeout:10_000,maxBuffer,encoding:'buffer'});
35
+ const names=(await tar(['-tzf',archive],4*1024*1024)).stdout.toString('utf8').trimEnd().split('\n');
36
+ const details=(await tar(['-tvzf',archive],8*1024*1024)).stdout.toString('utf8').trimEnd().split('\n');
37
+ if(names.length!==details.length||new Set(names).size!==names.length||names.some((name,i)=>!/^package\//.test(name)||/[\\\x00-\x20\x7f]/.test(name)||name.split('/').some(part=>part==='..'||part==='.')||!['-','d'].includes(details[i][0])))fail('archive_entries');
38
+ const entries=['package/package.json','package/daemon-release/manifest.json','package/daemon/bin/linux-x64/botbuddy-daemon'];
39
+ if(entries.some(name=>!names.includes(name)||details[names.indexOf(name)][0]!=='-'))fail('archive_entries');
40
+ const buffers=[];for(const name of entries)buffers.push((await tar(['-xOzf',archive,'--',name],name.endsWith('botbuddy-daemon')?MAX_BINARY:65536)).stdout);
41
+ let pkg;try{pkg=JSON.parse(buffers[0])}catch{fail('package')}
42
+ if(pkg.name!=='@botbuddy/cli'||pkg.version!==version)fail('package');
43
+ const verifiedRoot=join(root,'verified');await mkdir(join(verifiedRoot,'daemon-release'),{recursive:true,mode:0o700});await mkdir(join(verifiedRoot,'daemon/bin/linux-x64'),{recursive:true,mode:0o700});
44
+ await writeFile(join(verifiedRoot,'daemon-release/manifest.json'),buffers[1],{mode:0o600});await writeFile(join(verifiedRoot,'daemon/bin/linux-x64/botbuddy-daemon'),buffers[2],{mode:0o755});
45
+ const release=await validateDaemonRelease({releaseRoot:verifiedRoot},{platform:'linux',arch:'x64'});
46
+ if(!matches(release,version,sourceCommit,sha256))fail('identity');
47
+ // No path or payload escapes the temporary verification scope.
48
+ return Object.freeze({releaseVersion:release.releaseVersion,sourceCommit:release.sourceCommit,sha256:release.sha256,target:release.target});
49
+ }finally{clearTimeout(deadline);controller.abort();if(root)await rm(root,{recursive:true,force:true})}
50
+ }
@@ -436,3 +436,33 @@ export async function serviceStatus({ platform, host, exec = execFile } = {}) {
436
436
  throw error;
437
437
  }
438
438
  }
439
+
440
+ // BOT-2775 uses the same immutable release layout, root ancestry and external
441
+ // checksum checks as installation before invoking a producer-published mode.
442
+ // This read-only check runs as the already-authorized SSH/service user, no sudo.
443
+ export const remoteVerifyInstalledCallbackScript = String.raw`set -eu
444
+ binary=$1
445
+ digest=$2
446
+ service_uid=$3
447
+ test "$(uname -s)" = Linux
448
+ test "$(id -u)" = "$service_uid"
449
+ parent=$(dirname "$binary")
450
+ while :; do
451
+ test ! -L "$parent"
452
+ test -d "$parent"
453
+ test "$(stat -c '%u' "$parent")" = 0
454
+ mode=$(stat -c '%a' "$parent")
455
+ case "$mode" in *[2367][0-7]|*[0-7][2367]) exit 82 ;; esac
456
+ test "$parent" != / || break
457
+ parent=$(dirname "$parent")
458
+ done
459
+ test ! -L "$binary"
460
+ test -f "$binary"
461
+ test -x "$binary"
462
+ test "$(stat -c '%u' "$binary")" = 0
463
+ mode=$(stat -c '%a' "$binary")
464
+ case "$mode" in *[2367][0-7]|*[0-7][2367]) exit 82 ;; esac
465
+ actual=$(sha256sum "$binary" | awk '{print $1}')
466
+ test "$actual" = "$digest"
467
+ printf '%s\n' '{"verified":true}'
468
+ `;
@@ -0,0 +1,32 @@
1
+ {
2
+ "fixture_scope": "Synthetic contract fixture only; not an actual installed producer receipt.",
3
+ "callback_transport": "native_origin_v1",
4
+ "callback_current_authorization": true,
5
+ "callback_context": {
6
+ "owner": "operator@example.test",
7
+ "tenant_id": "botbuddy",
8
+ "requester_machine_id": "30000000-0000-4000-8000-000000000001",
9
+ "descriptor": {
10
+ "protocol": 1,
11
+ "binary_path": "/opt/botbuddy/daemon/releases/1.99.0/linux-x64/botbuddy-daemon",
12
+ "artifact_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
13
+ "config_path": "/home/botbuddy/.botbuddy-daemon/runtime.json",
14
+ "service_uid": 1001,
15
+ "runtime_version": "1.99.0",
16
+ "runtime_git_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
17
+ "runtime_target": "linux-amd64",
18
+ "host_key": "bbh-cccccccccccccccccccccccc",
19
+ "machine_id": "exact-linux-machine-id-spelling",
20
+ "engine_id": "engine-fixture",
21
+ "lease_id": "10000000-0000-4000-8000-000000000001",
22
+ "job_id": "20000000-0000-4000-8000-000000000001",
23
+ "attempt": 1,
24
+ "tenant_id": "botbuddy",
25
+ "project_id": "bbbot2775fixture",
26
+ "callback_port": 4600,
27
+ "producer_pid": 4242,
28
+ "producer_start_ticks": "100000",
29
+ "producer_boot_id": "70000000-0000-4000-8000-000000000001"
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "provenance": {
3
+ "captured_at": "2026-10-27",
4
+ "source": "BOT-2779 actual isolated get_stack_lease handler/SQL fixture run 1afcb247",
5
+ "producer_descriptor": "synthetic, not a native producer receipt",
6
+ "projection": "sanitized by backend test owner; only context/project metadata"
7
+ },
8
+ "response": {
9
+ "lease_id": "74704da2-d98a-45c2-9dcc-f3d69e901d24",
10
+ "state": "active",
11
+ "host_key": "bbh-09093a8a9efa4f45691a61e7",
12
+ "execution_mode": "remote",
13
+ "callback_port": 4600,
14
+ "callback_transport": "native_origin_v1",
15
+ "callback_current_authorization": true,
16
+ "callback_context": {
17
+ "owner": "jono@bot-buddy.ai",
18
+ "tenant_id": "botbuddy",
19
+ "requester_machine_id": "e0c36c64-5055-4456-9df8-5c1d1b60fa43",
20
+ "descriptor": {
21
+ "protocol": 1,
22
+ "binary_path": "/opt/botbuddy/daemon/releases/1.99.0/linux-x64/botbuddy-daemon",
23
+ "artifact_sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
24
+ "config_path": "/home/botbuddy/.botbuddy-daemon/runtime.json",
25
+ "service_uid": 1001,
26
+ "runtime_version": "1.99.0",
27
+ "runtime_git_sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
28
+ "runtime_target": "linux-amd64",
29
+ "host_key": "bbh-09093a8a9efa4f45691a61e7",
30
+ "machine_id": "57d36d8c-a574-4971-98d1-3bea0def5a6f",
31
+ "engine_id": "engine-fixture",
32
+ "lease_id": "74704da2-d98a-45c2-9dcc-f3d69e901d24",
33
+ "job_id": "098b322e-d162-49d9-8f5a-44d31441d88f",
34
+ "attempt": 1,
35
+ "tenant_id": "botbuddy",
36
+ "project_id": "bb214530925b62",
37
+ "callback_port": 4600,
38
+ "producer_pid": 4242,
39
+ "producer_start_ticks": "100000",
40
+ "producer_boot_id": "70000000-0000-4000-8000-000000000001"
41
+ }
42
+ },
43
+ "connection": {
44
+ "project_id": "bb214530925b62"
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,44 @@
1
+ // BOT-2775: private requester gateway for one same-owner SSH callback generation.
2
+ // The caller supplies only a binding from the authenticated CURRENT lease read.
3
+ import {createServer, request} from 'node:http';
4
+ import {createHash,createHmac, timingSafeEqual} from 'node:crypto';
5
+ import {Transform} from 'node:stream';
6
+ export const CALLBACK_CAP_HEADER='x-botbuddy-callback-capability';
7
+ const digest=value=>createHash('sha256').update(value).digest();
8
+ export const CALLBACK_MAX_STREAM_BYTES=32*1024*1024;
9
+ const sameBinding=(a,b)=>b&&typeof b==='object'&&Object.keys(a).length===Object.keys(b).length&&Object.keys(a).every(key=>a[key]===b[key]);
10
+ export async function openCallbackGateway({callbackPort,binding,capability,lifetimeMs=30_000,now=Date.now,activate=true,readinessNonce=null}){
11
+ if(!Number.isInteger(callbackPort)||callbackPort<1024||callbackPort>65535||!binding||typeof binding!=='object'||!Object.isFrozen(binding)||typeof capability!=='string'||!/^[0-9a-f]{64}$/.test(capability)||lifetimeMs<1||lifetimeMs>30_000)throw Error('callback gateway admission refused');
12
+ if(readinessNonce!==null&&!/^[0-9a-f]{32}$/.test(readinessNonce))throw Error('callback readiness identity refused');
13
+ const bootstrapDeadline=now()+30_000,readinessPath=readinessNonce?'/.botbuddy/callback-ready/'+readinessNonce:null;
14
+ const readinessProof=readinessNonce?createHmac('sha256',Buffer.from(capability,'hex')).update('native_origin_v1/readiness/'+readinessNonce).digest('hex'):null;
15
+ const expected=digest(capability),active=new Set(),closeListeners=new Set();let deadline=0,timer,closed=false;
16
+ const server=createServer((incoming,outgoing)=>{
17
+ const presented=incoming.headers[CALLBACK_CAP_HEADER];
18
+ const readiness=readinessPath&&incoming.method==='HEAD'&&incoming.url===readinessPath;
19
+ if(closed||(now()>=deadline&&!(readiness&&deadline===0&&now()<bootstrapDeadline))||typeof presented!=='string'||!/^[0-9a-f]{64}$/.test(presented)||!timingSafeEqual(digest(presented),expected)||!incoming.url?.startsWith('/')||incoming.url.startsWith('//')||incoming.method==='TRACE'||incoming.headers.upgrade){outgoing.writeHead(403);outgoing.end('CALLBACK_REFUSED');return;}
20
+ if(readiness){outgoing.writeHead(204,{'x-botbuddy-callback-ready':readinessNonce,'x-botbuddy-callback-proof':readinessProof});outgoing.end();return;}
21
+ if(Number(incoming.headers['content-length'])>CALLBACK_MAX_STREAM_BYTES){outgoing.writeHead(413,{connection:'close'});outgoing.end('CALLBACK_STREAM_LIMIT');return;}
22
+ const headers={...incoming.headers,host:'127.0.0.1:'+callbackPort};delete headers[CALLBACK_CAP_HEADER];delete headers['x-botbuddy-callback-ready'];delete headers['x-botbuddy-callback-proof'];delete headers['proxy-authorization'];delete headers['proxy-connection'];
23
+ const abort=new AbortController();active.add(abort);let limited=false;
24
+ const finish=()=>{active.delete(abort);abort.abort();};outgoing.once('close',finish);incoming.once('aborted',finish);
25
+ const limitStream=()=>{let bytes=0;return new Transform({transform(chunk,_encoding,done){bytes+=chunk.length;if(bytes>CALLBACK_MAX_STREAM_BYTES)done(Error('CALLBACK_STREAM_LIMIT'));else done(null,chunk);}});};
26
+ const upstream=request({host:'127.0.0.1',port:callbackPort,path:incoming.url,method:incoming.method,headers,signal:abort.signal},response=>{
27
+ const responseHeaders={...response.headers};delete responseHeaders[CALLBACK_CAP_HEADER];delete responseHeaders['x-botbuddy-callback-ready'];delete responseHeaders['x-botbuddy-callback-proof'];
28
+ if(incoming.method!=='HEAD'&&Number(response.headers['content-length'])>CALLBACK_MAX_STREAM_BYTES){limited=true;response.destroy();outgoing.writeHead(502,{connection:'close'});outgoing.end('CALLBACK_STREAM_LIMIT');return;}
29
+ const counter=limitStream();counter.on('error',()=>{limited=true;response.destroy();outgoing.destroy();finish();});
30
+ response.on('error',()=>outgoing.destroy());outgoing.writeHead(response.statusCode,responseHeaders);response.pipe(counter).pipe(outgoing);
31
+ });
32
+ upstream.on('error',()=>{if(limited)return;if(!outgoing.headersSent){outgoing.writeHead(503);outgoing.end('CALLBACK_UPSTREAM_UNAVAILABLE');}else outgoing.destroy();});
33
+ // Count both streaming directions without buffering. Application auth and
34
+ // HMAC/body bytes are preserved; a stream over32MiB is aborted.
35
+ const counter=limitStream();counter.on('error',()=>{limited=true;upstream.destroy();incoming.unpipe(counter);if(!outgoing.headersSent){outgoing.writeHead(413,{connection:'close'});outgoing.end('CALLBACK_STREAM_LIMIT',()=>incoming.destroy());}else outgoing.destroy();finish();});incoming.pipe(counter).pipe(upstream);
36
+ });
37
+ server.maxConnections=64;server.maxHeadersCount=100;server.headersTimeout=10_000;server.requestTimeout=0;
38
+ const denySocket=(_req,socket)=>socket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');server.on('connect',denySocket);server.on('upgrade',denySocket);
39
+ let closePromise;const close=()=>{if(closePromise)return closePromise;closed=true;clearTimeout(timer);for(const controller of active)controller.abort();active.clear();server.closeAllConnections();closePromise=new Promise(resolve=>server.close(resolve));for(const listener of closeListeners)listener();return closePromise;};
40
+ const refresh=(current,validForMs=lifetimeMs)=>{let matches=false;try{matches=sameBinding(binding,current)}catch{}if(closed||!matches||!Number.isFinite(validForMs)||validForMs<=0||validForMs>lifetimeMs){close();throw Error('callback binding changed');}deadline=now()+validForMs;clearTimeout(timer);timer=setTimeout(close,validForMs);timer.unref();};
41
+ await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
42
+ if(activate)refresh(binding);else{timer=setTimeout(close,30_000);timer.unref();}
43
+ return {port:server.address().port,refresh,close,onClose(listener){closeListeners.add(listener);if(closed)listener()},isLive:()=>!closed&&now()<deadline};
44
+ }
@@ -0,0 +1,17 @@
1
+ // BOT-2780 consumer of BOT-2499's identified admission ACK (not a second allocator).
2
+ // A durable ACK is recovery identity; pending delivery is never dispatch authority.
3
+ const uuid = value => typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
4
+ export function identifiedAdmissionAck(value, requestId) {
5
+ if(value?.success!==true||!['ADMITTED','EXACT_REPLAY'].includes(value.code)||!uuid(requestId)||!uuid(value.request_id)||value.request_id.toLowerCase()!==requestId.toLowerCase()||!uuid(value.lease_id)||!['created','readopted'].includes(value.admission_kind)||!['queued','provisioning','active'].includes(value.state)||!['pending','bound','none'].includes(value.job_binding)||!(value.provision_job_id===null||uuid(value.provision_job_id))||((value.job_binding==='bound')!==uuid(value.provision_job_id))||(value.admission_kind==='created'&&value.job_binding==='none')||(value.admission_kind==='readopted'&&value.job_binding!=='none')||!['ready','pending'].includes(value.delivery_status))return null;
6
+ return Object.freeze(Object.fromEntries(['code','request_id','lease_id','admission_kind','state','job_binding','provision_job_id','delivery_status'].map(key=>[key,value[key]])));
7
+ }
8
+ export function identifiedAdmissionDelivery(value, ack) {
9
+ const delivery=value?.delivery;
10
+ if(!ack||ack.delivery_status!=='ready'||!delivery||typeof delivery!=='object'||Array.isArray(delivery)||delivery.success===false||delivery.lease_id!=null&&delivery.lease_id!==ack.lease_id||delivery.state!=null&&delivery.state!==ack.state)return null;
11
+ return {...delivery,success:true,lease_id:ack.lease_id,state:ack.state};
12
+ }
13
+ export function identifiedAdmissionRecovery(request, contextFile, ack=null) {
14
+ return {request_id:request.request_id,context_file:contextFile,
15
+ read_tool:'get_stack_lease_admission',read_args:{request_id:request.request_id},
16
+ ...(ack?{committed_ack:ack}:{})};
17
+ }
@@ -786,6 +786,39 @@ export function stableRunStackReusable(worktreeRoot, dirName, { read = readFileS
786
786
  return marker.idle_retained === true || marker.released === true;
787
787
  }
788
788
 
789
+ /**
790
+ * BOT-2780: a beta requester-origin run never resumes a retained slot context. Its
791
+ * generated links name the listener of the invocation that started that stack, so
792
+ * refuse any unreleased marker for this slot before deriving a new bundle or ports.
793
+ */
794
+ export function assertFreshRunStackAvailable(worktreeRoot, slot, { read = readFileSync, readdir = readdirSync, lstat = lstatSync } = {}) {
795
+ const refuse = () => {
796
+ const error = new Error("requester origin cannot resume a retained slot context; inspect its owning lease with `bb stack status <lease-id>` and use `bb stack done <lease-id> --reap` only after that owner finishes, then confirm reaped before a fresh run");
797
+ error.code = "REQUESTER_ORIGIN_RETAINED_CONTEXT";
798
+ throw error;
799
+ };
800
+ const stacks = join(worktreeRoot, LEASED_STACKS_DIR);
801
+ let entries;
802
+ try { entries = readdir(stacks); } catch (error) { if (error.code === "ENOENT") return; refuse(); }
803
+ if (entries.length > 4096) refuse();
804
+ for (const entry of entries) {
805
+ const path = join(stacks, entry, LEASED_STACK_MARKER);
806
+ let info;
807
+ try { info = lstat(path); } catch (error) { if (error.code === "ENOENT" || error.code === "ENOTDIR") continue; refuse(); }
808
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 65536) refuse();
809
+ let marker;
810
+ try { marker = JSON.parse(read(path, "utf8")); } catch { refuse(); }
811
+ if (!marker || typeof marker.slot !== "string") refuse();
812
+ if (marker.slot !== slot || marker.released === true) continue;
813
+ // An expired context whose owner is gone is what materialization GC collects anyway; the
814
+ // control plane still refuses a new remote lease while any lease on the slot is unreaped.
815
+ const ownerAlive = Number.isInteger(marker.owner_pid) && marker.owner_pid > 0 &&
816
+ (!marker.host || marker.host === hostname()) && pidIsAlive(marker.owner_pid);
817
+ const expired = marker.idle_retained !== true && Number.isFinite(marker.retain_until) && Date.now() >= marker.retain_until;
818
+ if (!expired || ownerAlive) refuse();
819
+ }
820
+ }
821
+
789
822
  /** BOT-1803: record a PROVEN reap on the stack's marker, so the next run may reuse the directory. */
790
823
  export function markLeasedStackReleased(stackDir, { read = readFileSync, write = writeFileSync, rename = renameSync } = {}) {
791
824
  const marker = readStackMarker(stackDir, read);
@@ -1246,6 +1279,22 @@ export function bindLeasedStackClaim(stackDir, leaseId, { registryDir = defaultP
1246
1279
  });
1247
1280
  }
1248
1281
 
1282
+ /**
1283
+ * BOT-2780: associate this invocation's claims and marker with its identified admission
1284
+ * request BEFORE the request leaves, so a possibly committed but never acknowledged lease
1285
+ * can later release them through matching accepted cleanup (recoverLeasedStackClaim).
1286
+ */
1287
+ export function recordAdmissionRequest(stackDir, requestId, { registryDir = defaultPortRegistryDir() } = {}) {
1288
+ const marker = readStackMarker(stackDir, readFileSync);
1289
+ const token = marker?.invocation_token;
1290
+ if (!token || typeof requestId !== "string" || !requestId) throw new Error("local port claim has no invocation identity");
1291
+ return mutateBoundClaims(stackDir, registryDir, (claims) => {
1292
+ if (claims.some(({ claim }) => claim.invocation_token !== token || claim.lease_id != null)) throw new Error("local port claim was replaced or already bound");
1293
+ for (const { path, claim } of claims) atomicWrite(path, JSON.stringify({ ...claim, admission_request_id: requestId }), { write: writeFileSync, rename: renameSync });
1294
+ atomicWrite(join(stackDir, LEASED_STACK_MARKER), JSON.stringify({ ...marker, admission_request_id: requestId }), { write: writeFileSync, rename: renameSync });
1295
+ });
1296
+ }
1297
+
1249
1298
  /** Allocation and recovery serialize the inspect/delete against replacement claims. */
1250
1299
  function mutateBoundClaims(stackDir, registryDir, mutate) {
1251
1300
  const config = readFileSync(join(stackDir, "supabase/config.toml"), "utf8");
@@ -1286,8 +1335,12 @@ export function recoverLeasedStackClaim(stackDir, leaseId, proof, { registryDir
1286
1335
  if (!claims.length) return { released: true, reason: "no local claim remains for this stack" };
1287
1336
  const token = claims[0].claim.invocation_token;
1288
1337
  const project = claims[0].claim.project_id;
1338
+ // BOT-2780: an unbound claim recorded for an identified admission request is this lease's
1339
+ // only when the caller proved (owner-scoped admission read) that request named the lease.
1340
+ const requestBound = (entry) => entry?.lease_id == null && typeof proof.admission_request_id === "string" &&
1341
+ proof.admission_request_id.length > 0 && entry?.admission_request_id === proof.admission_request_id;
1289
1342
  if (typeof token !== "string" || !token || typeof project !== "string" || !/^[A-Za-z0-9_-]+$/.test(project) || claims.some(({ claim }) =>
1290
- claim.lease_id !== leaseId || claim.invocation_token !== token || claim.project_id !== project || claim.host !== hostname() ||
1343
+ (claim.lease_id !== leaseId && !requestBound(claim)) || claim.invocation_token !== token || claim.project_id !== project || claim.host !== hostname() ||
1291
1344
  !Number.isInteger(claim.owner_pid) || claim.owner_pid <= 0 || pidIsAlive(claim.owner_pid))) {
1292
1345
  return { released: false, reason: "local claim is live, replaced, or lacks this lease binding" };
1293
1346
  }
@@ -1302,7 +1355,7 @@ export function recoverLeasedStackClaim(stackDir, leaseId, proof, { registryDir
1302
1355
  if (error?.code !== "ENOENT") return { released: false, reason: "stack marker is unreadable or corrupt; local claim retained" };
1303
1356
  marker = null;
1304
1357
  }
1305
- if (marker && (marker.invocation_token !== token || marker.lease_id !== leaseId || marker.project_id !== project)) {
1358
+ if (marker && (marker.invocation_token !== token || (marker.lease_id !== leaseId && !requestBound(marker)) || marker.project_id !== project)) {
1306
1359
  return { released: false, reason: "stack marker was replaced" };
1307
1360
  }
1308
1361
  for (const { path } of claims) rmSync(path);
@@ -1687,7 +1740,8 @@ export function materializeRunStackDir(cwd, { slot, ticket, nonce, idleTtlSecs }
1687
1740
  const configPath = join(sourceDir, "config.toml");
1688
1741
  let rootConfig;
1689
1742
  try {
1690
- rootConfig = read(configPath, "utf8");
1743
+ // BOT-2780: a beta requester-origin run supplies the already patched private copy.
1744
+ rootConfig = io.configText ?? read(configPath, "utf8");
1691
1745
  } catch (error) {
1692
1746
  throw new Error(
1693
1747
  `cannot materialize an isolated leased stack: ${configPath} is missing or unreadable (${error?.code ?? error?.message ?? error}). ` +
@@ -0,0 +1,126 @@
1
+ // BOT-2775 same-owner, required native HTTP callback transport. No child capability.
2
+ import {execFile as execFileCallback,spawn} from 'node:child_process';
3
+ import {promisify} from 'node:util';
4
+ import {randomBytes,randomUUID} from 'node:crypto';
5
+ import {createInterface} from 'node:readline';
6
+ import {posix} from 'node:path';
7
+ import {isIP as netIsIP} from 'node:net';
8
+ import {fileURLToPath} from 'node:url';
9
+ import {validateDaemonRelease} from './daemon/release.mjs';
10
+ import {verifiedCallbackRelease} from './daemon/callback-release.mjs';
11
+ import {assertSshTarget,SSH_FORWARD_OPTIONS} from './stack-tunnel.mjs';
12
+ import {installedBinaryPath,remoteVerifyInstalledCallbackScript} from './daemon/installer.mjs';
13
+ import {openCallbackGateway} from './stack-callback-gateway.mjs';
14
+ export const NATIVE_CALLBACK_TRANSPORT='native_origin_v1';
15
+ const execFile=promisify(execFileCallback),uuid=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
16
+ const safePath=value=>typeof value==='string'&&value.startsWith('/')&&!/[\x00-\x1f\x7f]/.test(value)&&posix.normalize(value)===value;
17
+ const text=value=>typeof value==='string'&&value.length>0&&value.length<=512&&!/[\x00-\x20\x7f]/.test(value);
18
+ const version=/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
19
+ const fields=['protocol','binary_path','artifact_sha256','config_path','service_uid','runtime_version','runtime_git_sha','runtime_target','host_key','machine_id','engine_id','lease_id','job_id','attempt','tenant_id','project_id','callback_port','producer_pid','producer_start_ticks','producer_boot_id'];
20
+ const fail=(code,message)=>{throw Object.assign(Error(message),{code});};
21
+ const quote=value=>"'"+String(value).replaceAll("'","'\\''")+"'";
22
+ const canonicalMachine=value=>typeof value==='string'&&/^(?:[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i.test(value)?value.replaceAll('-','').toLowerCase():null;
23
+
24
+ export function validatedCallbackDescriptor(value){
25
+ if(!value||typeof value!=='object'||Array.isArray(value)||Object.keys(value).length!==fields.length||fields.some(field=>!(field in value)))fail('HOST_CALLBACK_TRANSPORT_UNSUPPORTED','native callback descriptor missing or unknown');
26
+ const d=Object.fromEntries(fields.map(field=>[field,value[field]]));
27
+ if(d.protocol!==1||typeof d.runtime_version!=='string'||!version.test(d.runtime_version)||d.runtime_target!=='linux-amd64'||d.binary_path!==installedBinaryPath({releaseVersion:d.runtime_version,target:'linux-x64'})||!safePath(d.config_path)||!/^sha256:[0-9a-f]{64}$/.test(d.artifact_sha256)||!Number.isSafeInteger(d.service_uid)||d.service_uid<1||!Number.isSafeInteger(d.producer_pid)||d.producer_pid<=1||typeof d.producer_start_ticks!=='string'||!/^[1-9][0-9]*$/.test(d.producer_start_ticks)||!uuid.test(d.producer_boot_id)||!text(d.host_key)||!text(d.machine_id)||!text(d.engine_id)||!uuid.test(d.lease_id)||!uuid.test(d.job_id)||!Number.isSafeInteger(d.attempt)||d.attempt<1||!text(d.tenant_id)||typeof d.project_id!=='string'||!/^[a-z0-9][a-z0-9_-]{1,62}$/.test(d.project_id)||!Number.isInteger(d.callback_port)||d.callback_port<1024||d.callback_port>65535||!/^[0-9a-f]{40}$/.test(d.runtime_git_sha))fail('HOST_CALLBACK_TRANSPORT_UNSUPPORTED','native callback runtime identity refused');
28
+ return Object.freeze(d);
29
+ }
30
+
31
+ export function currentCallbackBinding(current,{leaseId,hostKey,callbackPort,requesterMachineId,routeGeneration}){
32
+ if(current?.callback_transport!==NATIVE_CALLBACK_TRANSPORT)fail('HOST_CALLBACK_TRANSPORT_UNSUPPORTED','backend did not acknowledge required callback transport');
33
+ if(current.callback_current_authorization!==true||current.state!=='active'||current.execution_mode!=='remote')fail('CALLBACK_AUTHORIZATION_LOST','current callback authorization unavailable');
34
+ const context=current.callback_context,d=validatedCallbackDescriptor(context?.descriptor);
35
+ if(current.lease_id!==leaseId||current.host_key!==hostKey||d.lease_id!==leaseId||d.host_key!==hostKey||d.callback_port!==callbackPort||current.callback_port!==callbackPort||d.project_id!==current.connection?.project_id||!context||context.tenant_id!==d.tenant_id||!text(context.owner)||!uuid.test(context.requester_machine_id)||canonicalMachine(requesterMachineId)!==canonicalMachine(context.requester_machine_id)||!uuid.test(routeGeneration))fail('CALLBACK_AUTHORIZATION_LOST','current callback context changed');
36
+ return Object.freeze({owner:context.owner,tenant_id:context.tenant_id,requester_machine_id:context.requester_machine_id,route_generation:routeGeneration,origin:'requester_http',descriptor:d});
37
+ }
38
+
39
+ export async function verifyInstalledCallbackRuntime({descriptor,sshTarget,sshBin='ssh',run=execFile,env=process.env,releaseRoot=fileURLToPath(new URL('../',import.meta.url)),validateRelease=validateDaemonRelease,resolveRelease=verifiedCallbackRelease}){
40
+ const d=validatedCallbackDescriptor(descriptor),target=assertSshTarget(sshTarget);
41
+ let release;try{release=await resolveRelease({version:d.runtime_version,sourceCommit:d.runtime_git_sha,sha256:d.artifact_sha256.slice(7),releaseRoot,validateRelease});}catch{fail('HOST_CALLBACK_TRANSPORT_UNSUPPORTED','verified callback release unavailable');}
42
+ if(release.releaseVersion!==d.runtime_version||release.sourceCommit!==d.runtime_git_sha||'sha256:'+release.sha256!==d.artifact_sha256)fail('HOST_CALLBACK_TRANSPORT_UNSUPPORTED','callback runtime differs from verified release');
43
+ const command='sh -c '+quote(remoteVerifyInstalledCallbackScript)+' sh '+[d.binary_path,d.artifact_sha256.slice(7),d.service_uid].map(quote).join(' ');
44
+ let result;try{result=await run(sshBin,['-T','-o','BatchMode=yes','-o','ControlMaster=no','-o','ControlPath=none','--',target,command],{env,timeout:15_000,maxBuffer:4096});}catch{fail('HOST_CALLBACK_TRANSPORT_UNSUPPORTED','installed callback executable verification failed');}
45
+ if(result.stdout.trim()!=='{"verified":true}')fail('HOST_CALLBACK_TRANSPORT_UNSUPPORTED','installed callback executable was not verified');
46
+ return d;
47
+ }
48
+
49
+ const privateIPv4=value=>netIsIP(value)===4&&(value.startsWith('10.')||value.startsWith('192.168.')||/^172\.(?:1[6-9]|2[0-9]|3[01])\./.test(value));
50
+ function callbackReadiness(value,callbackPort){
51
+ let url;try{url=new URL(value.callback_url)}catch{fail('CALLBACK_NOT_READY','native callback readiness refused');}
52
+ if(value.ready!==true||url.protocol!=='http:'||!privateIPv4(url.hostname)||Number(url.port)!==callbackPort||url.pathname!=='/'||url.search||url.hash||url.username||url.password||!privateIPv4(value.peer_ip)||!['container_id','network_id','endpoint_id'].every(key=>/^[0-9a-f]{64}$/.test(value[key]??'')))fail('CALLBACK_NOT_READY','native callback endpoint refused');
53
+ return url.origin;
54
+ }
55
+
56
+ // Renewal cadence and acknowledgement budgets. The first renewal also runs the relay's
57
+ // consuming-namespace probe (validation + docker exec), so it gets the larger budget.
58
+ const GENERATION_MS=30_000,RENEW_INTERVAL_MS=10_000,FIRST_ACK_MS=15_000,RENEW_ACK_MS=6_000,STATUS_DEADLINE_MS=5_000,TRANSIENT_RETRY_MS=1_000;
59
+ // A transient status read is retried after TRANSIENT_RETRY_MS only while that retry (read +
60
+ // acknowledgement) can still land inside the current 30 s generation; otherwise the route closes.
61
+ const TRANSIENT_GRACE_MS=GENERATION_MS-TRANSIENT_RETRY_MS-STATUS_DEADLINE_MS-RENEW_ACK_MS;
62
+ export async function openNativeCallbackTransport({current,leaseId,hostKey,callbackPort,requesterMachineId,sshTarget,readCurrent,sshBin='ssh',env=process.env,run=execFile,spawnImpl=spawn,verifyRuntime=verifyInstalledCallbackRuntime,openGateway=openCallbackGateway,renewIntervalMs=RENEW_INTERVAL_MS,firstAckMs=FIRST_ACK_MS,renewAckMs=RENEW_ACK_MS,statusDeadlineMs=STATUS_DEADLINE_MS,transientRetryMs=TRANSIENT_RETRY_MS,transientGraceMs=TRANSIENT_GRACE_MS}){
63
+ if(typeof readCurrent!=='function')fail('CALLBACK_AUTHORIZATION_LOST','current callback status reader required');
64
+ const routeGeneration=randomUUID(),options={leaseId,hostKey,callbackPort,requesterMachineId,routeGeneration};
65
+ const binding=currentCallbackBinding(current,options),snapshot=JSON.stringify(binding),target=assertSshTarget(sshTarget),d=binding.descriptor;
66
+ await verifyRuntime({descriptor:d,sshTarget:target,sshBin,env,run});
67
+ const capability=randomBytes(32).toString('hex'),readinessNonce=randomBytes(16).toString('hex'),directory='/tmp/bb-callback-'+randomUUID(),socket=directory+'/callback.sock';
68
+ const sshArgs=['-T','-o','BatchMode=yes','-o','ControlMaster=no','-o','ControlPath=none','--',target];
69
+ let gateway,child,terminal,refreshTimer,statusAbort,pendingReady,pendingAck,closed=false,refreshing=false,cleanupPromise,exitResult;const exits=new Set();
70
+ const reportExit=exit=>{exitResult=exit;for(const listener of exits)listener(exit)};
71
+ const remote=async command=>{try{return await run(sshBin,[...sshArgs,command],{env,timeout:15_000,maxBuffer:4096})}catch{fail('CALLBACK_REMOTE_OPERATION_FAILED','owned callback operation failed');}};
72
+ const retire=async()=>{
73
+ // Every check tolerates the relay's own EXIT trap removing the socket/directory concurrently
74
+ // (a SIGKILLed local ssh): absence is success, anything present must be exactly ours.
75
+ const command=`set -eu; directory=${quote(directory)}; socket=${quote(socket)}; gone(){ [ ! -e "$directory" ] && [ ! -L "$directory" ]; }; if gone; then exit 0; fi; test ! -L "$directory"; if ! mode=$(stat -c '%u:%a' "$directory" 2>/dev/null); then gone; exit 0; fi; if [ ! -d "$directory" ]; then gone; exit 0; fi; test "$mode" = ${quote(d.service_uid+':700')}; if [ -e "$socket" ] || [ -L "$socket" ]; then test ! -L "$socket"; if smode=$(stat -c '%u:%a' "$socket" 2>/dev/null); then if [ -e "$socket" ]; then test -S "$socket"; fi; test "$smode" = ${quote(d.service_uid+':600')}; fi; rm -f -- "$socket"; fi; rmdir -- "$directory" 2>/dev/null || gone`;
76
+ await remote(command);
77
+ };
78
+ const close=()=>cleanupPromise??=(async()=>{
79
+ closed=true;clearInterval(refreshTimer);statusAbort?.abort();pendingReady?.reject(Error('callback closed'));pendingAck?.reject(Error('callback closed'));await gateway?.close();
80
+ if(child){child.stdin.end();const term=setTimeout(()=>child.kill('SIGTERM'),3000);const kill=setTimeout(()=>child.kill('SIGKILL'),6000);term.unref();kill.unref();try{await terminal}finally{clearTimeout(term);clearTimeout(kill)}}
81
+ try{await retire()}catch{fail('CALLBACK_CLEANUP_UNCERTAIN','owned callback cleanup could not be verified');}
82
+ })();
83
+ try{
84
+ await remote(`set -eu; test "$(id -u)" = ${quote(d.service_uid)}; umask 077; mkdir -- ${quote(directory)}; chmod 0700 -- ${quote(directory)}`);
85
+ gateway=await openGateway({callbackPort,binding:Object.freeze({snapshot}),capability,activate:false,readinessNonce});
86
+ const command=`set -eu; relayPid=; cleanup(){ trap - EXIT HUP INT TERM; if [ -n "$relayPid" ]; then kill -TERM "$relayPid" 2>/dev/null || :; wait "$relayPid" 2>/dev/null || :; fi; rm -f -- ${quote(socket)}; rmdir -- ${quote(directory)}; }; trap cleanup EXIT HUP INT TERM; ${quote(d.binary_path)} --callback-relay --config ${quote(d.config_path)} <&0 & relayPid=$!; status=0; wait "$relayPid" || status=$?; relayPid=; exit "$status"`;
87
+ child=spawnImpl(sshBin,[...SSH_FORWARD_OPTIONS.filter(arg=>arg!=='-N'),'-o','StreamLocalBindMask=0177','-R',socket+':127.0.0.1:'+gateway.port,'--',target,command],{env,stdio:['pipe','pipe','pipe']});
88
+ child.stdin.on('error',()=>{});child.stderr.on('data',()=>{});
89
+ let spawnError=false;child.once('error',()=>{spawnError=true});terminal=new Promise(resolve=>child.once('close',(code,signal)=>resolve({code,signal,reason:spawnError?'callback_process_error':'callback_process_exit'})));
90
+ terminal.then(exit=>{if(!closed){reportExit(exit);void close().catch(()=>reportExit({code:null,reason:'callback_cleanup_uncertain'}))}});
91
+ const lines=createInterface({input:child.stdout});
92
+ lines.on('line',line=>{try{const event=JSON.parse(line);if(event.refused===true&&event.code==='CALLBACK_CONSUMING_NAMESPACE_UNREACHABLE'&&pendingAck){const waiter=pendingAck;pendingAck=undefined;const refusal=Error('consuming namespace callback probe failed');refusal.code='CALLBACK_CONSUMING_NAMESPACE_UNREACHABLE';waiter.reject(refusal);}else if(pendingReady){const waiter=pendingReady;pendingReady=undefined;waiter.resolve(event);}else if(pendingAck&&event.renewed===true&&event.route_generation===routeGeneration&&Number.isFinite(Date.parse(event.expires_at))){const waiter=pendingAck;pendingAck=undefined;waiter.resolve(event);}else throw Error('unexpected callback frame');}catch{reportExit({code:null,reason:'callback_protocol_error'});void close().catch(()=>{})}});
93
+ terminal.then(()=>{lines.close();pendingReady?.reject(Error('callback process ended'));pendingAck?.reject(Error('callback process ended'));});
94
+ const ready=await new Promise((resolve,reject)=>{
95
+ const timeout=setTimeout(()=>reject(Object.assign(Error('callback readiness deadline'),{code:'CALLBACK_NOT_READY'})),20_000);timeout.unref();
96
+ pendingReady={resolve:value=>{clearTimeout(timeout);resolve(value)},reject:error=>{clearTimeout(timeout);reject(error)}};
97
+ child.stdin.write(JSON.stringify({binding,capability,socket_path:socket,readiness_nonce:readinessNonce})+'\n');
98
+ });
99
+ const callbackUrl=callbackReadiness(ready,callbackPort);
100
+ gateway.onClose(()=>{if(!closed){reportExit({code:null,reason:'callback_gateway_closed'});void close().catch(()=>{})}});
101
+ let renewedAt=null;
102
+ const refresh=async(first=false)=>{
103
+ if(closed||refreshing)return;refreshing=true;
104
+ try{
105
+ statusAbort=new AbortController();let statusTimeout,status;
106
+ try{status=await new Promise((resolve,reject)=>{statusTimeout=setTimeout(()=>{statusAbort.abort();reject(Object.assign(Error('callback status deadline'),{transient:true}));},statusDeadlineMs);statusTimeout.unref();Promise.resolve().then(()=>readCurrent(statusAbort.signal)).then(resolve,reject).finally(()=>clearTimeout(statusTimeout));});}
107
+ catch(error){
108
+ // A failed status READ is not an authorization answer: keep the live generation (relay and
109
+ // gateway still expire it at 30 s) and retry on the next tick while inside the grace window.
110
+ if(!first&&error?.transient===true&&renewedAt!==null&&performance.now()-renewedAt<transientGraceMs){const retry=setTimeout(()=>{void refresh()},transientRetryMs);retry.unref();return;}
111
+ throw error;
112
+ }
113
+ if(closed)return;const renewed=currentCallbackBinding(status,options);if(JSON.stringify(renewed)!==snapshot)fail('CALLBACK_AUTHORIZATION_LOST','callback context changed');
114
+ const sentAt=performance.now();
115
+ await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(Error('callback renewal acknowledgement deadline')),first?firstAckMs:renewAckMs);timeout.unref();pendingAck={resolve:value=>{clearTimeout(timeout);resolve(value)},reject:error=>{clearTimeout(timeout);reject(error)}};child.stdin.write(JSON.stringify({binding:renewed,renew:true})+'\n');});
116
+ if(closed)return;renewedAt=sentAt;gateway.refresh(Object.freeze({snapshot}),30_000-(performance.now()-sentAt));
117
+ }
118
+ // The first renewal carries the consuming-namespace probe: its uncoded failure is readiness.
119
+ catch(error){if(!closed){reportExit({code:null,reason:'callback_authorization_lost',...(error.code?{code:error.code}:first?{code:'CALLBACK_NOT_READY'}:{})});void close().catch(()=>{});}}
120
+ finally{refreshing=false}
121
+ };
122
+ await refresh(true);if(closed||!gateway.isLive())fail(exitResult?.code??'CALLBACK_NOT_READY','callback route not ready after readiness');
123
+ refreshTimer=setInterval(refresh,renewIntervalMs);refreshTimer.unref();
124
+ return{callbackUrl,close,isLive:()=>!closed&&gateway.isLive(),onExit(listener){exits.add(listener);if(exitResult)listener(exitResult)}};
125
+ }catch(error){try{await close()}catch{fail('CALLBACK_CLEANUP_UNCERTAIN','owned callback cleanup could not be verified');}throw error;}
126
+ }
@@ -0,0 +1,21 @@
1
+ // BOT-2780: parse and roundtrip the private derived config before bundling.
2
+ // The vendored, hash-pinned smol-toml 1.9.1 parser (src/vendor/smol-toml) is loaded only
3
+ // for the beta requester-origin feature; help and legacy paths never import it.
4
+ import {isDeepStrictEqual} from 'node:util';
5
+ const fail=(code,message)=>{const error=Error(message);error.code=code;throw error};
6
+ let parser;
7
+ export async function patchRequesterAuthOrigin(configText,origin){
8
+ let url;try{url=new URL(origin)}catch{fail('REQUESTER_ORIGIN_URL_REFUSED','requester origin refused')}
9
+ if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.username||url.password||url.pathname!=='/'||url.search||url.hash||origin!==url.origin||!Number.isInteger(Number(url.port))||Number(url.port)<1024||Number(url.port)>65535)fail('REQUESTER_ORIGIN_URL_REFUSED','requester origin must be its reserved loopback HTTP listener');
10
+ if(typeof configText!=='string'||Buffer.byteLength(configText)>1024*1024)fail('REQUESTER_ORIGIN_TOML_REFUSED','private stack config is unavailable or exceeds its bound');
11
+ try{parser??=import('./vendor/smol-toml/index.js');const {parse,stringify}=await parser;let document;try{document=parse(configText,{integersAsBigInt:true,unsafeKeyBehaviour:'throw'})}catch{fail('REQUESTER_ORIGIN_TOML_REFUSED','private stack config is not supported valid TOML')}
12
+ if(!Object.hasOwn(document,'auth'))Object.defineProperty(document,'auth',{value:{},writable:true,enumerable:true,configurable:true});
13
+ const auth=document.auth;if(!auth||typeof auth!=='object'||Array.isArray(auth))fail('REQUESTER_ORIGIN_AUTH_REFUSED','private auth configuration is ambiguous');
14
+ const target=origin+'/auth/v1';
15
+ if(Object.hasOwn(auth,'external_url')&&(typeof auth.external_url!=='string'||auth.external_url!==''&&auth.external_url!==target))fail('REQUESTER_ORIGIN_EXPLICIT_CONFLICT','explicit auth external URL conflicts with the owned requester origin');
16
+ Object.defineProperty(auth,'external_url',{value:target,writable:true,enumerable:true,configurable:true});
17
+ let text,roundtrip;try{text=stringify(document,{numbersAsFloat:true,strictTemporal:true});roundtrip=parse(text,{integersAsBigInt:true,unsafeKeyBehaviour:'throw'})}catch{fail('REQUESTER_ORIGIN_TOML_REFUSED','private stack config could not roundtrip safely')}
18
+ if(!isDeepStrictEqual(roundtrip,document))fail('REQUESTER_ORIGIN_TOML_REFUSED','private stack config changed during TOML roundtrip');
19
+ return text;
20
+ }catch(error){if(error.code?.startsWith('REQUESTER_ORIGIN_'))throw error;fail('REQUESTER_ORIGIN_PARSER_UNAVAILABLE','vendored requester-origin TOML parser unavailable')}
21
+ }