@botbuddy/cli 1.63.27 → 1.63.29
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/stack-native-callback.mjs +16 -6
- package/src/stack.mjs +27 -4
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.29",
|
|
3
|
+
"source_commit": "732c555dd44141d581f96959ed590007b3ec09de",
|
|
4
|
+
"source_sha256": "c2f0fc90169b4580f2d8918fb1100b853e0663a65fe470dc399c2a33787d7639",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "05fd954131e610edd04ff1ee4ee4a0e37047f3ab56bbb0d4fd5fc3cee82bcd86"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.29",
|
|
3
|
+
"source_commit": "732c555dd44141d581f96959ed590007b3ec09de",
|
|
4
|
+
"source_sha256": "c2f0fc90169b4580f2d8918fb1100b853e0663a65fe470dc399c2a33787d7639",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "1b3d42884f57dd70d3946137b704009d42ee027e2ca060b2e4df70325c52c5b7"
|
|
7
7
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"release_version": "1.63.
|
|
4
|
-
"source_commit": "
|
|
5
|
-
"source_sha256": "
|
|
3
|
+
"release_version": "1.63.29",
|
|
4
|
+
"source_commit": "732c555dd44141d581f96959ed590007b3ec09de",
|
|
5
|
+
"source_sha256": "c2f0fc90169b4580f2d8918fb1100b853e0663a65fe470dc399c2a33787d7639",
|
|
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": "
|
|
11
|
+
"sha256": "05fd954131e610edd04ff1ee4ee4a0e37047f3ab56bbb0d4fd5fc3cee82bcd86",
|
|
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": "
|
|
18
|
+
"sha256": "1b3d42884f57dd70d3946137b704009d42ee027e2ca060b2e4df70325c52c5b7",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
|
@@ -19,6 +19,8 @@ const version=/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
|
19
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
20
|
const fail=(code,message)=>{throw Object.assign(Error(message),{code});};
|
|
21
21
|
const quote=value=>"'"+String(value).replaceAll("'","'\\''")+"'";
|
|
22
|
+
// BOT-2828: a relay-reported cause is one fixed upper-case code, never free text.
|
|
23
|
+
const typedCause=value=>typeof value==='string'&&/^CALLBACK_[A-Z_]{1,64}$/.test(value)?value:null;
|
|
22
24
|
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
25
|
|
|
24
26
|
export function validatedCallbackDescriptor(value){
|
|
@@ -86,10 +88,17 @@ export async function openNativeCallbackTransport({current,leaseId,hostKey,callb
|
|
|
86
88
|
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
89
|
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
90
|
child.stdin.on('error',()=>{});child.stderr.on('data',()=>{});
|
|
89
|
-
let spawnError=false;child.once('error',()=>{spawnError=true})
|
|
90
|
-
|
|
91
|
+
let spawnError=false;child.once('error',()=>{spawnError=true});// BOT-2828: a relay that ends without a typed close frame is classified by its exit: ssh's own
|
|
92
|
+
// failure (255) or a signal is the SSH hop, any other exit is the relay process itself.
|
|
93
|
+
terminal=new Promise(resolve=>child.once('close',(exitCode,signal)=>resolve({reason:spawnError?'callback_process_error':'callback_process_exit',code:spawnError||exitCode===255||signal?'CALLBACK_SSH_LOST':'CALLBACK_RELAY_EXITED',exit_code:exitCode,signal})));
|
|
94
|
+
terminal.then(exit=>{if(!closed){reportExit(exit);void close().catch(()=>reportExit({reason:'callback_cleanup_uncertain',code:'CALLBACK_CLEANUP_UNCERTAIN'}))}});
|
|
91
95
|
const lines=createInterface({input:child.stdout});
|
|
92
|
-
lines.on('line',line=>{try{const event=JSON.parse(line);
|
|
96
|
+
lines.on('line',line=>{try{const event=JSON.parse(line);
|
|
97
|
+
// BOT-2828: the relay names why it closed a live route ({closed:true,code}).
|
|
98
|
+
if(event?.closed===true&&typedCause(event.code)&&!pendingReady){if(!closed){reportExit({reason:'callback_relay_closed',code:event.code});void close().catch(()=>{})}return;}
|
|
99
|
+
// BOT-2790 AC-5: a typed readiness (or first-renewal) refusal keeps its exact code.
|
|
100
|
+
if(event?.refused===true&&(pendingReady||pendingAck)){const waiter=pendingReady??pendingAck;pendingReady=pendingAck=undefined;const refusal=Error('native callback relay refused readiness');refusal.code=typedCause(event.code)??'CALLBACK_NOT_READY';waiter.reject(refusal);return;}
|
|
101
|
+
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{if(!closed){reportExit({reason:'callback_protocol_error',code:'CALLBACK_PROTOCOL_ERROR'});void close().catch(()=>{})}}});
|
|
93
102
|
terminal.then(()=>{lines.close();pendingReady?.reject(Error('callback process ended'));pendingAck?.reject(Error('callback process ended'));});
|
|
94
103
|
const ready=await new Promise((resolve,reject)=>{
|
|
95
104
|
const timeout=setTimeout(()=>reject(Object.assign(Error('callback readiness deadline'),{code:'CALLBACK_NOT_READY'})),20_000);timeout.unref();
|
|
@@ -97,7 +106,7 @@ export async function openNativeCallbackTransport({current,leaseId,hostKey,callb
|
|
|
97
106
|
child.stdin.write(JSON.stringify({binding,capability,socket_path:socket,readiness_nonce:readinessNonce})+'\n');
|
|
98
107
|
});
|
|
99
108
|
const callbackUrl=callbackReadiness(ready,callbackPort);
|
|
100
|
-
gateway.onClose(()=>{if(!closed){reportExit({
|
|
109
|
+
gateway.onClose(()=>{if(!closed){reportExit({reason:'callback_gateway_closed',code:'CALLBACK_GATEWAY_EXPIRED'});void close().catch(()=>{})}});
|
|
101
110
|
let renewedAt=null;
|
|
102
111
|
const refresh=async(first=false)=>{
|
|
103
112
|
if(closed||refreshing)return;refreshing=true;
|
|
@@ -108,15 +117,16 @@ export async function openNativeCallbackTransport({current,leaseId,hostKey,callb
|
|
|
108
117
|
// A failed status READ is not an authorization answer: keep the live generation (relay and
|
|
109
118
|
// gateway still expire it at 30 s) and retry on the next tick while inside the grace window.
|
|
110
119
|
if(!first&&error?.transient===true&&renewedAt!==null&&performance.now()-renewedAt<transientGraceMs){const retry=setTimeout(()=>{void refresh()},transientRetryMs);retry.unref();return;}
|
|
120
|
+
if(error?.transient===true&&!error.code&&!first)error.code='CALLBACK_STATUS_UNREADABLE';
|
|
111
121
|
throw error;
|
|
112
122
|
}
|
|
113
123
|
if(closed)return;const renewed=currentCallbackBinding(status,options);if(JSON.stringify(renewed)!==snapshot)fail('CALLBACK_AUTHORIZATION_LOST','callback context changed');
|
|
114
124
|
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');});
|
|
125
|
+
await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(Object.assign(Error('callback renewal acknowledgement deadline'),first?{}:{code:'CALLBACK_RENEWAL_ACK_TIMEOUT',transport:true})),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
126
|
if(closed)return;renewedAt=sentAt;gateway.refresh(Object.freeze({snapshot}),30_000-(performance.now()-sentAt));
|
|
117
127
|
}
|
|
118
128
|
// The first renewal carries the consuming-namespace probe: its uncoded failure is readiness.
|
|
119
|
-
catch(error){if(!closed){reportExit({
|
|
129
|
+
catch(error){if(!closed){reportExit({reason:error?.transport===true?'callback_renewal_unacknowledged':'callback_authorization_lost',code:error?.code??(first?'CALLBACK_NOT_READY':'CALLBACK_AUTHORIZATION_LOST')});void close().catch(()=>{});}}
|
|
120
130
|
finally{refreshing=false}
|
|
121
131
|
};
|
|
122
132
|
await refresh(true);if(closed||!gateway.isLive())fail(exitResult?.code??'CALLBACK_NOT_READY','callback route not ready after readiness');
|
package/src/stack.mjs
CHANGED
|
@@ -351,6 +351,8 @@ ${bold("REMOTE LEASES")} (BOT-2142, stack run)
|
|
|
351
351
|
5 HOST_NO_SSH_TARGET the (pinned) remote host has no ssh_target — operator: set container_hosts.ssh_target
|
|
352
352
|
5 HOST_HELPER_OUTDATED the (pinned) remote host's Helper predates protocol 4 — upgrade that Helper
|
|
353
353
|
5 NO_HELPER_HOST no co-located host admits and no owned host qualifies as remote
|
|
354
|
+
5 NO_COLOCATED_HELPER_HOST --local-only (or stack up / --local-exec): this machine has no active Docker
|
|
355
|
+
host (none enrolled, or draining/maintenance/retired) — run without --local-only
|
|
354
356
|
4 STACK_EXECUTION_MODE_MISMATCH the slot already holds a lease in another mode/host — stack done it first
|
|
355
357
|
(the four codes above create NO lease)
|
|
356
358
|
5 BUNDLE_UPLOAD_FAILED the signed bundle upload failed (the lease is released first)
|
|
@@ -362,9 +364,16 @@ ${bold("REMOTE LEASES")} (BOT-2142, stack run)
|
|
|
362
364
|
4 REQUESTER_ORIGIN_EXPLICIT_CONFLICT an unpinned stable --callback-port run in a repo whose
|
|
363
365
|
supabase/config.toml sets its own [auth] external_url (no lease requested)
|
|
364
366
|
5 CALLBACK_NOT_READY / CALLBACK_CONSUMING_NAMESPACE_UNREACHABLE the native route did not
|
|
365
|
-
answer from the stack's own network before the child (lease reaped)
|
|
367
|
+
answer from the stack's own network before the child (lease reaped);
|
|
368
|
+
a typed relay refusal keeps its own code (BOT-2790): CALLBACK_ALIAS_DRIFT,
|
|
369
|
+
CALLBACK_PRODUCER_MISMATCH, CALLBACK_BIND_REFUSED, CALLBACK_ENDPOINT_REFUSED, …
|
|
366
370
|
6 CALLBACK_TRANSPORT_LOST / CALLBACK_AUTHORIZATION_LOST the native route or its current
|
|
367
|
-
authorization closed; the child is stopped and the lease reaped
|
|
371
|
+
authorization closed; the child is stopped and the lease reaped.
|
|
372
|
+
callback_cause names why (BOT-2828): relay CALLBACK_RUNTIME_DRIFT,
|
|
373
|
+
CALLBACK_VALIDATION_UNAVAILABLE (Docker unanswered 20 s), CALLBACK_PRODUCER_ENDED,
|
|
374
|
+
CALLBACK_GENERATION_EXPIRED, CALLBACK_RENEWAL_REFUSED; requester CALLBACK_SSH_LOST,
|
|
375
|
+
CALLBACK_RELAY_EXITED, CALLBACK_GATEWAY_EXPIRED, CALLBACK_RENEWAL_ACK_TIMEOUT,
|
|
376
|
+
CALLBACK_STATUS_UNREADABLE, CALLBACK_PROTOCOL_ERROR, CALLBACK_AUTHORIZATION_LOST
|
|
368
377
|
5 CALLBACK_PORT_BUSY --callback-port is already listening on the serving host (another run or
|
|
369
378
|
service), the host refused this run's -R because another run took the
|
|
370
379
|
port first, or another listener shares the port; this run holds no -R —
|
|
@@ -1363,6 +1372,13 @@ export const REMOTE_REFUSAL_GUIDANCE = Object.freeze({
|
|
|
1363
1372
|
NO_HELPER_HOST: "no host can serve this lease: no co-located Helper admits it and no other host you own qualifies as remote " +
|
|
1364
1373
|
"(an owned, device-consented, beacon-fresh Helper on protocol 4 with an operator-set ssh_target). No lease was created.",
|
|
1365
1374
|
HOST_NOT_OWNED: "the pinned host is not a device-consented Helper host your owner owns. No lease was created.",
|
|
1375
|
+
// BOT-2845: a co-located request (`stack up`, `--local-exec`, `stack run --local-only`) from a
|
|
1376
|
+
// machine that no longer hosts Docker. The server considers only ACTIVE, device-consented,
|
|
1377
|
+
// owner-bound hosts for this hardware id and sends no message with this code.
|
|
1378
|
+
NO_COLOCATED_HELPER_HOST: "this machine has no active, device-consented Docker host your owner owns (none is enrolled for " +
|
|
1379
|
+
"this hardware id, or its host is draining, in maintenance or retired), so a co-located lease (`stack up`, `--local-exec`, " +
|
|
1380
|
+
"`stack run --local-only`) cannot be placed here. No lease was created. Run `bb stack run` without --local-only " +
|
|
1381
|
+
"(execution auto) or with --remote, so another host you own serves the stack.",
|
|
1366
1382
|
HOST_STALE: "the pinned remote host's Helper beacon is stale. No lease was created; retry once it beacons, or drop --host.",
|
|
1367
1383
|
HOST_NOT_REMOTE: "--remote pinned this machine's own host; drop --host or --remote. No lease was created.",
|
|
1368
1384
|
BUNDLE_REQUIRED: "the server picked a remote host but this request carried no bundle digest. No lease was created; " +
|
|
@@ -4826,6 +4842,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4826
4842
|
const transient = !observed?.auth && (observed?.status == null || observed.status >= 500 || observed.status === 408 || observed.status === 429 || observed.error === "invalid_json");
|
|
4827
4843
|
throw Object.assign(new Error(transient ? "current callback status unreadable" : "current callback authorization refused"), transient ? { transient: true } : { code: "CALLBACK_AUTHORIZATION_LOST" });
|
|
4828
4844
|
}
|
|
4845
|
+
// BOT-2790 AC-6 (BOT-2828): a backend RPC_ERROR (e.g. a statement timeout) is an unanswered read.
|
|
4846
|
+
if (observed.data?.success !== true && observed.data?.code === "RPC_ERROR") throw Object.assign(new Error("current callback status unreadable (RPC_ERROR)"), { transient: true });
|
|
4829
4847
|
if (observed.data?.success !== true) throw Object.assign(new Error("current callback authorization unavailable"), { code: "CALLBACK_AUTHORIZATION_LOST" });
|
|
4830
4848
|
return observed.data; },
|
|
4831
4849
|
});
|
|
@@ -4917,7 +4935,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4917
4935
|
}
|
|
4918
4936
|
if (callbackLost || nativeCallback && !nativeCallback.isLive()) {
|
|
4919
4937
|
await closeTunnel(); const cleanup = await requestCleanup();
|
|
4920
|
-
|
|
4938
|
+
const callbackCause = callbackLost?.code ?? "CALLBACK_RELAY_EXITED";
|
|
4939
|
+
return { exitCode: cleanup.ok && !callbackCleanupError ? EXIT.LEASE_FAILED : EXIT.CLEANUP_FAILED, outcome: "error", code: "CALLBACK_TRANSPORT_LOST", callbackCause, leaseId, error: `callback transport closed before child start (${callbackCause})`, cleanup };
|
|
4921
4940
|
}
|
|
4922
4941
|
if (tunnelLost) {
|
|
4923
4942
|
// The forwards died between being verified and the child starting.
|
|
@@ -4984,7 +5003,9 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4984
5003
|
error: "the requester origin closed while the child was running, so the child was stopped and the lease released" };
|
|
4985
5004
|
}
|
|
4986
5005
|
if (callbackLost && !receivedSignal) {
|
|
4987
|
-
|
|
5006
|
+
// BOT-2828: the receipt names the hop and cause that closed the route, not only the class.
|
|
5007
|
+
const callbackCause = callbackLost.code ?? (callbackLost.reason === "callback_authorization_lost" ? "CALLBACK_AUTHORIZATION_LOST" : "CALLBACK_RELAY_EXITED");
|
|
5008
|
+
return { exitCode: cleanup.ok && !callbackCleanupError ? EXIT.LEASE_FAILED : EXIT.CLEANUP_FAILED, outcome: "failed", code: callbackLost.reason === "callback_authorization_lost" ? "CALLBACK_AUTHORIZATION_LOST" : "CALLBACK_TRANSPORT_LOST", callbackCause, leaseId, childExitCode: childResult.code, cleanup, fenced: true, error: `callback transport authorization or runtime context was lost (${callbackCause}); child stopped` };
|
|
4988
5009
|
}
|
|
4989
5010
|
if (tunnelLost && !receivedSignal) {
|
|
4990
5011
|
return tunnelLostResult(tunnelLost, { leaseId, cleanup, childExitCode: childResult.code, fenced: fencing, ...(parkIncident ? { incident: parkIncident } : {}) });
|
|
@@ -5092,6 +5113,8 @@ export function runReceiptFields(result) {
|
|
|
5092
5113
|
transport: result.execution?.transport ?? "none",
|
|
5093
5114
|
// BOT-2775: present only when the callback was served over the native route.
|
|
5094
5115
|
...(result.execution?.callbackTransport ? { callback_transport: result.execution.callbackTransport } : {}),
|
|
5116
|
+
// BOT-2828: the typed cause of a native callback route close (e.g. CALLBACK_RUNTIME_DRIFT).
|
|
5117
|
+
...(result.callbackCause ? { callback_cause: result.callbackCause } : {}),
|
|
5095
5118
|
// BOT-2780: the reserved requester origin generated auth links name (never a credential).
|
|
5096
5119
|
...(result.execution?.requesterOrigin ? { requester_origin: result.execution.requesterOrigin } : {}),
|
|
5097
5120
|
// BOT-2788: "previous" (an idle stack's origin re-reserved), "previous_port_busy" or "new".
|