@botbuddy/cli 1.63.26 → 1.63.28
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 +18 -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.28",
|
|
3
|
+
"source_commit": "60e4aacb7efe75b27dd73e2481352be8f5a4e478",
|
|
4
|
+
"source_sha256": "d979ad49e64dd5af2d8ba6a993682466747eee6b9502eb647d3814ef860ce7c1",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "1c9c4958b9c65847cdb856728c9f6f644e10094029ddd767ecbc8fae5194ed20"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.28",
|
|
3
|
+
"source_commit": "60e4aacb7efe75b27dd73e2481352be8f5a4e478",
|
|
4
|
+
"source_sha256": "d979ad49e64dd5af2d8ba6a993682466747eee6b9502eb647d3814ef860ce7c1",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "c6dccb7605296c3c7b12ec9598397a7ede559a0640a1a969a5da2c4149860e42"
|
|
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.28",
|
|
4
|
+
"source_commit": "60e4aacb7efe75b27dd73e2481352be8f5a4e478",
|
|
5
|
+
"source_sha256": "d979ad49e64dd5af2d8ba6a993682466747eee6b9502eb647d3814ef860ce7c1",
|
|
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": "1c9c4958b9c65847cdb856728c9f6f644e10094029ddd767ecbc8fae5194ed20",
|
|
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": "c6dccb7605296c3c7b12ec9598397a7ede559a0640a1a969a5da2c4149860e42",
|
|
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
|
@@ -362,9 +362,16 @@ ${bold("REMOTE LEASES")} (BOT-2142, stack run)
|
|
|
362
362
|
4 REQUESTER_ORIGIN_EXPLICIT_CONFLICT an unpinned stable --callback-port run in a repo whose
|
|
363
363
|
supabase/config.toml sets its own [auth] external_url (no lease requested)
|
|
364
364
|
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)
|
|
365
|
+
answer from the stack's own network before the child (lease reaped);
|
|
366
|
+
a typed relay refusal keeps its own code (BOT-2790): CALLBACK_ALIAS_DRIFT,
|
|
367
|
+
CALLBACK_PRODUCER_MISMATCH, CALLBACK_BIND_REFUSED, CALLBACK_ENDPOINT_REFUSED, …
|
|
366
368
|
6 CALLBACK_TRANSPORT_LOST / CALLBACK_AUTHORIZATION_LOST the native route or its current
|
|
367
|
-
authorization closed; the child is stopped and the lease reaped
|
|
369
|
+
authorization closed; the child is stopped and the lease reaped.
|
|
370
|
+
callback_cause names why (BOT-2828): relay CALLBACK_RUNTIME_DRIFT,
|
|
371
|
+
CALLBACK_VALIDATION_UNAVAILABLE (Docker unanswered 20 s), CALLBACK_PRODUCER_ENDED,
|
|
372
|
+
CALLBACK_GENERATION_EXPIRED, CALLBACK_RENEWAL_REFUSED; requester CALLBACK_SSH_LOST,
|
|
373
|
+
CALLBACK_RELAY_EXITED, CALLBACK_GATEWAY_EXPIRED, CALLBACK_RENEWAL_ACK_TIMEOUT,
|
|
374
|
+
CALLBACK_STATUS_UNREADABLE, CALLBACK_PROTOCOL_ERROR, CALLBACK_AUTHORIZATION_LOST
|
|
368
375
|
5 CALLBACK_PORT_BUSY --callback-port is already listening on the serving host (another run or
|
|
369
376
|
service), the host refused this run's -R because another run took the
|
|
370
377
|
port first, or another listener shares the port; this run holds no -R —
|
|
@@ -4826,6 +4833,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4826
4833
|
const transient = !observed?.auth && (observed?.status == null || observed.status >= 500 || observed.status === 408 || observed.status === 429 || observed.error === "invalid_json");
|
|
4827
4834
|
throw Object.assign(new Error(transient ? "current callback status unreadable" : "current callback authorization refused"), transient ? { transient: true } : { code: "CALLBACK_AUTHORIZATION_LOST" });
|
|
4828
4835
|
}
|
|
4836
|
+
// BOT-2790 AC-6 (BOT-2828): a backend RPC_ERROR (e.g. a statement timeout) is an unanswered read.
|
|
4837
|
+
if (observed.data?.success !== true && observed.data?.code === "RPC_ERROR") throw Object.assign(new Error("current callback status unreadable (RPC_ERROR)"), { transient: true });
|
|
4829
4838
|
if (observed.data?.success !== true) throw Object.assign(new Error("current callback authorization unavailable"), { code: "CALLBACK_AUTHORIZATION_LOST" });
|
|
4830
4839
|
return observed.data; },
|
|
4831
4840
|
});
|
|
@@ -4917,7 +4926,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4917
4926
|
}
|
|
4918
4927
|
if (callbackLost || nativeCallback && !nativeCallback.isLive()) {
|
|
4919
4928
|
await closeTunnel(); const cleanup = await requestCleanup();
|
|
4920
|
-
|
|
4929
|
+
const callbackCause = callbackLost?.code ?? "CALLBACK_RELAY_EXITED";
|
|
4930
|
+
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
4931
|
}
|
|
4922
4932
|
if (tunnelLost) {
|
|
4923
4933
|
// The forwards died between being verified and the child starting.
|
|
@@ -4984,7 +4994,9 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4984
4994
|
error: "the requester origin closed while the child was running, so the child was stopped and the lease released" };
|
|
4985
4995
|
}
|
|
4986
4996
|
if (callbackLost && !receivedSignal) {
|
|
4987
|
-
|
|
4997
|
+
// BOT-2828: the receipt names the hop and cause that closed the route, not only the class.
|
|
4998
|
+
const callbackCause = callbackLost.code ?? (callbackLost.reason === "callback_authorization_lost" ? "CALLBACK_AUTHORIZATION_LOST" : "CALLBACK_RELAY_EXITED");
|
|
4999
|
+
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
5000
|
}
|
|
4989
5001
|
if (tunnelLost && !receivedSignal) {
|
|
4990
5002
|
return tunnelLostResult(tunnelLost, { leaseId, cleanup, childExitCode: childResult.code, fenced: fencing, ...(parkIncident ? { incident: parkIncident } : {}) });
|
|
@@ -5092,6 +5104,8 @@ export function runReceiptFields(result) {
|
|
|
5092
5104
|
transport: result.execution?.transport ?? "none",
|
|
5093
5105
|
// BOT-2775: present only when the callback was served over the native route.
|
|
5094
5106
|
...(result.execution?.callbackTransport ? { callback_transport: result.execution.callbackTransport } : {}),
|
|
5107
|
+
// BOT-2828: the typed cause of a native callback route close (e.g. CALLBACK_RUNTIME_DRIFT).
|
|
5108
|
+
...(result.callbackCause ? { callback_cause: result.callbackCause } : {}),
|
|
5095
5109
|
// BOT-2780: the reserved requester origin generated auth links name (never a credential).
|
|
5096
5110
|
...(result.execution?.requesterOrigin ? { requester_origin: result.execution.requesterOrigin } : {}),
|
|
5097
5111
|
// BOT-2788: "previous" (an idle stack's origin re-reserved), "previous_port_busy" or "new".
|