@botbuddy/cli 1.63.42 → 1.63.43
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 +41 -11
- package/src/stack-run-outcome.mjs +4 -0
- package/src/stack.mjs +26 -7
|
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.43",
|
|
3
|
+
"source_commit": "580bdbe602c9281c639efbd317018a75011ea70c",
|
|
4
|
+
"source_sha256": "d04edc606580d8c6304eb6ed88d1fbb9b5a6c8d9b0aad2fea9f92e1cc1cedbb7",
|
|
5
5
|
"target": "darwin-arm64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "ec91fd06a1a4da1a271c5343619f66f18ea16c2d676247fbd22e5bfaa9a9a976"
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"release_version": "1.63.
|
|
3
|
-
"source_commit": "
|
|
4
|
-
"source_sha256": "
|
|
2
|
+
"release_version": "1.63.43",
|
|
3
|
+
"source_commit": "580bdbe602c9281c639efbd317018a75011ea70c",
|
|
4
|
+
"source_sha256": "d04edc606580d8c6304eb6ed88d1fbb9b5a6c8d9b0aad2fea9f92e1cc1cedbb7",
|
|
5
5
|
"target": "linux-x64",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "8437df29241846a8dddfad4ce6c5a77237c0dc38bf8006512e4c22fe911fb380"
|
|
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.43",
|
|
4
|
+
"source_commit": "580bdbe602c9281c639efbd317018a75011ea70c",
|
|
5
|
+
"source_sha256": "d04edc606580d8c6304eb6ed88d1fbb9b5a6c8d9b0aad2fea9f92e1cc1cedbb7",
|
|
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": "ec91fd06a1a4da1a271c5343619f66f18ea16c2d676247fbd22e5bfaa9a9a976",
|
|
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": "8437df29241846a8dddfad4ce6c5a77237c0dc38bf8006512e4c22fe911fb380",
|
|
19
19
|
"mode": "0755"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
|
@@ -21,6 +21,19 @@ const fail=(code,message)=>{throw Object.assign(Error(message),{code});};
|
|
|
21
21
|
const quote=value=>"'"+String(value).replaceAll("'","'\\''")+"'";
|
|
22
22
|
// BOT-2828: a relay-reported cause is one fixed upper-case code, never free text.
|
|
23
23
|
const typedCause=value=>typeof value==='string'&&/^CALLBACK_[A-Z_]{1,64}$/.test(value)?value:null;
|
|
24
|
+
// BOT-2880: which check refused. A bounded token naming the check or changed field, never its value.
|
|
25
|
+
const AUTHORIZATION_REASON=/^[a-z0-9_.:-]{1,64}$/;
|
|
26
|
+
export const boundedAuthorizationReason=value=>typeof value==='string'&&AUTHORIZATION_REASON.test(value)?value:null;
|
|
27
|
+
const refuse=(reason,message)=>{throw Object.assign(Error(message),{code:'CALLBACK_AUTHORIZATION_LOST',authorizationReason:reason});};
|
|
28
|
+
const tokenReason=(name,value)=>boundedAuthorizationReason(name+':'+value)??name;
|
|
29
|
+
function changedBindingField(previous,next,path=''){
|
|
30
|
+
for(const key of new Set([...Object.keys(previous),...Object.keys(next)])){
|
|
31
|
+
const a=previous[key],b=next[key],field=path+key;
|
|
32
|
+
if(a&&b&&typeof a==='object'&&typeof b==='object'){const nested=changedBindingField(a,b,field+'.');if(nested)return nested;}
|
|
33
|
+
else if(a!==b)return field;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
24
37
|
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;
|
|
25
38
|
|
|
26
39
|
export function validatedCallbackDescriptor(value){
|
|
@@ -31,10 +44,21 @@ export function validatedCallbackDescriptor(value){
|
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
export function currentCallbackBinding(current,{leaseId,hostKey,callbackPort,requesterMachineId,routeGeneration}){
|
|
34
|
-
if(current?.callback_transport!==NATIVE_CALLBACK_TRANSPORT)
|
|
35
|
-
if(current.
|
|
36
|
-
|
|
37
|
-
if(current.
|
|
47
|
+
if(current?.callback_transport!==NATIVE_CALLBACK_TRANSPORT)throw Object.assign(Error('backend did not acknowledge required callback transport'),{code:'HOST_CALLBACK_TRANSPORT_UNSUPPORTED',authorizationReason:'binding_callback_transport'});
|
|
48
|
+
if(current.state!=='active')refuse(tokenReason('lease_state',current.state),'current callback authorization unavailable');
|
|
49
|
+
if(current.execution_mode!=='remote')refuse(tokenReason('lease_execution_mode',current.execution_mode),'current callback authorization unavailable');
|
|
50
|
+
if(current.callback_current_authorization!==true)refuse(boundedAuthorizationReason(current.callback_authorization_reason)??'server_not_authorized','current callback authorization unavailable');
|
|
51
|
+
const context=current.callback_context;let d;
|
|
52
|
+
try{d=validatedCallbackDescriptor(context?.descriptor)}catch(error){throw Object.assign(error,{authorizationReason:'descriptor_refused'});}
|
|
53
|
+
for(const [changed,reason] of [
|
|
54
|
+
[current.lease_id!==leaseId,'binding_lease_id'],[current.host_key!==hostKey,'binding_host_key'],
|
|
55
|
+
[d.lease_id!==leaseId,'binding_descriptor_lease_id'],[d.host_key!==hostKey,'binding_descriptor_host_key'],
|
|
56
|
+
[d.callback_port!==callbackPort,'binding_descriptor_callback_port'],[current.callback_port!==callbackPort,'binding_callback_port'],
|
|
57
|
+
[d.project_id!==current.connection?.project_id,'binding_project_id'],[context.tenant_id!==d.tenant_id,'binding_tenant_id'],
|
|
58
|
+
[!text(context.owner),'binding_owner'],
|
|
59
|
+
[!uuid.test(context.requester_machine_id)||canonicalMachine(requesterMachineId)!==canonicalMachine(context.requester_machine_id),'binding_requester_machine_id'],
|
|
60
|
+
[!uuid.test(routeGeneration),'binding_route_generation'],
|
|
61
|
+
])if(changed)refuse(reason,'current callback context changed');
|
|
38
62
|
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});
|
|
39
63
|
}
|
|
40
64
|
|
|
@@ -107,29 +131,35 @@ export async function openNativeCallbackTransport({current,leaseId,hostKey,callb
|
|
|
107
131
|
});
|
|
108
132
|
const callbackUrl=callbackReadiness(ready,callbackPort);
|
|
109
133
|
gateway.onClose(()=>{if(!closed){reportExit({reason:'callback_gateway_closed',code:'CALLBACK_GATEWAY_EXPIRED'});void close().catch(()=>{})}});
|
|
110
|
-
let renewedAt=null;
|
|
134
|
+
let renewedAt=null,refusalPending=false;
|
|
111
135
|
const refresh=async(first=false)=>{
|
|
112
136
|
if(closed||refreshing)return;refreshing=true;
|
|
113
137
|
try{
|
|
114
|
-
statusAbort=new AbortController();let statusTimeout,
|
|
115
|
-
try{
|
|
116
|
-
|
|
138
|
+
statusAbort=new AbortController();let statusTimeout,renewed;
|
|
139
|
+
try{
|
|
140
|
+
const 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));});
|
|
141
|
+
if(closed)return;renewed=currentCallbackBinding(status,options);
|
|
142
|
+
if(JSON.stringify(renewed)!==snapshot)refuse(tokenReason('binding_changed',changedBindingField(binding,renewed)),'callback context changed');
|
|
143
|
+
refusalPending=false;
|
|
144
|
+
}catch(error){
|
|
117
145
|
// A failed status READ is not an authorization answer: keep the live generation (relay and
|
|
118
146
|
// gateway still expire it at 30 s) and retry on the next tick while inside the grace window.
|
|
119
147
|
if(!first&&error?.transient===true&&renewedAt!==null&&performance.now()-renewedAt<transientGraceMs){const retry=setTimeout(()=>{void refresh()},transientRetryMs);retry.unref();return;}
|
|
120
148
|
if(error?.transient===true&&!error.code&&!first)error.code='CALLBACK_STATUS_UNREADABLE';
|
|
149
|
+
// BOT-2880: a definitive answer stops the child only if one re-read repeats it, while the live
|
|
150
|
+
// generation can still carry that re-read. The opening renewal and an expired grace fail closed.
|
|
151
|
+
if(!first&&error?.transient!==true&&!refusalPending&&renewedAt!==null&&performance.now()-renewedAt<transientGraceMs){refusalPending=true;const retry=setTimeout(()=>{void refresh()},transientRetryMs);retry.unref();return;}
|
|
121
152
|
throw error;
|
|
122
153
|
}
|
|
123
|
-
if(closed)return;const renewed=currentCallbackBinding(status,options);if(JSON.stringify(renewed)!==snapshot)fail('CALLBACK_AUTHORIZATION_LOST','callback context changed');
|
|
124
154
|
const sentAt=performance.now();
|
|
125
155
|
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');});
|
|
126
156
|
if(closed)return;renewedAt=sentAt;gateway.refresh(Object.freeze({snapshot}),30_000-(performance.now()-sentAt));
|
|
127
157
|
}
|
|
128
158
|
// The first renewal carries the consuming-namespace probe: its uncoded failure is readiness.
|
|
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(()=>{});}}
|
|
159
|
+
catch(error){if(!closed){const authorizationReason=boundedAuthorizationReason(error?.authorizationReason);reportExit({reason:error?.transport===true?'callback_renewal_unacknowledged':'callback_authorization_lost',code:error?.code??(first?'CALLBACK_NOT_READY':'CALLBACK_AUTHORIZATION_LOST'),...(authorizationReason?{authorization_reason:authorizationReason}:{})});void close().catch(()=>{});}}
|
|
130
160
|
finally{refreshing=false}
|
|
131
161
|
};
|
|
132
|
-
await refresh(true);if(closed||!gateway.isLive())
|
|
162
|
+
await refresh(true);if(closed||!gateway.isLive())throw Object.assign(Error('callback route not ready after readiness'),{code:exitResult?.code??'CALLBACK_NOT_READY',...(exitResult?.authorization_reason?{authorizationReason:exitResult.authorization_reason}:{})});
|
|
133
163
|
refreshTimer=setInterval(refresh,renewIntervalMs);refreshTimer.unref();
|
|
134
164
|
return{callbackUrl,close,isLive:()=>!closed&&gateway.isLive(),onExit(listener){exits.add(listener);if(exitResult)listener(exitResult)}};
|
|
135
165
|
}catch(error){try{await close()}catch{fail('CALLBACK_CLEANUP_UNCERTAIN','owned callback cleanup could not be verified');}throw error;}
|
|
@@ -31,6 +31,7 @@ const LANE_RE = /^[A-Za-z0-9:_.-]{1,128}$/;
|
|
|
31
31
|
const TICKET_RE = /^[A-Za-z][A-Za-z0-9]*-\d{1,9}$/;
|
|
32
32
|
const REPO_RE = /^[A-Za-z0-9._/-]{1,128}$/;
|
|
33
33
|
const VERSION_RE = /^[0-9A-Za-z.+-]{1,32}$/;
|
|
34
|
+
const AUTHORIZATION_REASON_RE = /^[a-z0-9_.:-]{1,64}$/;
|
|
34
35
|
|
|
35
36
|
// Mirrors stack.mjs EXIT (kept local so this module stays dependency-free for tests).
|
|
36
37
|
const EXIT_TIMEOUT = 2;
|
|
@@ -109,6 +110,8 @@ const exitInt = (value) => (Number.isInteger(value) && value >= 0 && value <= 25
|
|
|
109
110
|
*/
|
|
110
111
|
export function buildStackRunOutcomePayload({ fields = {}, exitCode, runId = randomUUID(), opts = {}, env = process.env, durationMs = null }) {
|
|
111
112
|
const { category, code } = classifyStackRunOutcome(fields, exitCode);
|
|
113
|
+
// BOT-2880: sent only when present, because an older server refuses an unknown key.
|
|
114
|
+
const callbackAuthorizationReason = pick(fields.callback_authorization_reason, AUTHORIZATION_REASON_RE);
|
|
112
115
|
return {
|
|
113
116
|
run_id: runId,
|
|
114
117
|
lease_id: validStackLeaseId(fields.lease_id),
|
|
@@ -126,6 +129,7 @@ export function buildStackRunOutcomePayload({ fields = {}, exitCode, runId = ran
|
|
|
126
129
|
repo: pick(opts.repo, REPO_RE),
|
|
127
130
|
duration_ms: Number.isFinite(durationMs) && durationMs >= 0 ? Math.round(durationMs) : null,
|
|
128
131
|
cli_version: pick(VERSION, VERSION_RE) ?? "unknown",
|
|
132
|
+
...(callbackAuthorizationReason ? { callback_authorization_reason: callbackAuthorizationReason } : {}),
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
135
|
|
package/src/stack.mjs
CHANGED
|
@@ -53,7 +53,7 @@ import { checkReadoptedStackMounts } from "./stack-mount-health.mjs";
|
|
|
53
53
|
import { machineUuid } from "./machine-id.mjs";
|
|
54
54
|
import { DEFAULT_BUNDLE_MAX_BYTES, buildStackBundle, uploadStackBundle, validateBundleIncludePath } from "./stack-bundle.mjs";
|
|
55
55
|
import { assertSshTarget, establishSshTunnel } from "./stack-tunnel.mjs";
|
|
56
|
-
import { NATIVE_CALLBACK_TRANSPORT, openNativeCallbackTransport } from "./stack-native-callback.mjs";
|
|
56
|
+
import { NATIVE_CALLBACK_TRANSPORT, boundedAuthorizationReason, openNativeCallbackTransport } from "./stack-native-callback.mjs";
|
|
57
57
|
import { FORWARD_ATTESTATION_ENV, startForwardAttestation, readForwardAttestation } from "./stack-forward-attestation.mjs";
|
|
58
58
|
import { stackServicesForForwards } from "./stack-service-discovery.mjs";
|
|
59
59
|
import { ENGINE_PUBLISHES_BEYOND_LOOPBACK, engineSocketIsLocal, formatPublishIp, isLoopbackIp, publishIpFix, resolvePublishIp } from "./docker-publish-ip.mjs";
|
|
@@ -374,7 +374,11 @@ ${bold("REMOTE LEASES")} (BOT-2142, stack run)
|
|
|
374
374
|
CALLBACK_VALIDATION_UNAVAILABLE (Docker unanswered 20 s), CALLBACK_PRODUCER_ENDED,
|
|
375
375
|
CALLBACK_GENERATION_EXPIRED, CALLBACK_RENEWAL_REFUSED; requester CALLBACK_SSH_LOST,
|
|
376
376
|
CALLBACK_RELAY_EXITED, CALLBACK_GATEWAY_EXPIRED, CALLBACK_RENEWAL_ACK_TIMEOUT,
|
|
377
|
-
CALLBACK_STATUS_UNREADABLE, CALLBACK_PROTOCOL_ERROR, CALLBACK_AUTHORIZATION_LOST
|
|
377
|
+
CALLBACK_STATUS_UNREADABLE, CALLBACK_PROTOCOL_ERROR, CALLBACK_AUTHORIZATION_LOST.
|
|
378
|
+
A refusal must repeat on one re-read before it stops the child (BOT-2880), and
|
|
379
|
+
callback_authorization_reason names the refused check, e.g. requester_host_disabled
|
|
380
|
+
(the requester machine's own Docker host was set to disabled: use maintenance),
|
|
381
|
+
lease_state:reaping, binding_changed:descriptor.producer_pid
|
|
378
382
|
5 CALLBACK_PORT_BUSY --callback-port is already listening on the serving host (another run or
|
|
379
383
|
service), the host refused this run's -R because another run took the
|
|
380
384
|
port first, or another listener shares the port; this run holds no -R —
|
|
@@ -4903,18 +4907,19 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4903
4907
|
// that survived the auth refresh or a typed JSON-RPC refusal is a definitive answer.
|
|
4904
4908
|
// BOT-2847: the shared classification (stack-release-retry.mjs) the cleanup release retries on.
|
|
4905
4909
|
const transient = observed == null || isTransientStackRpcResult(observed);
|
|
4906
|
-
throw Object.assign(new Error(transient ? "current callback status unreadable" : "current callback authorization refused"), transient ? { transient: true } : { code: "CALLBACK_AUTHORIZATION_LOST" });
|
|
4910
|
+
throw Object.assign(new Error(transient ? "current callback status unreadable" : "current callback authorization refused"), transient ? { transient: true } : { code: "CALLBACK_AUTHORIZATION_LOST", authorizationReason: refusedRpcReason(observed) });
|
|
4907
4911
|
}
|
|
4908
4912
|
// BOT-2790 AC-6 (BOT-2828): a backend RPC_ERROR (e.g. a statement timeout) is an unanswered read.
|
|
4909
4913
|
if (isTransientStackRpcResult(observed)) throw Object.assign(new Error("current callback status unreadable (RPC_ERROR)"), { transient: true });
|
|
4910
|
-
if (observed.data?.success !== true) throw Object.assign(new Error("current callback authorization unavailable"), { code: "CALLBACK_AUTHORIZATION_LOST" });
|
|
4914
|
+
if (observed.data?.success !== true) throw Object.assign(new Error("current callback authorization unavailable"), { code: "CALLBACK_AUTHORIZATION_LOST", authorizationReason: refusedRpcReason(observed) });
|
|
4911
4915
|
return observed.data; },
|
|
4912
4916
|
});
|
|
4913
4917
|
nativeCallback.onExit((exit) => { if (childResult) return; callbackLost = exit; fencing = true; stopChild("SIGTERM", EXIT.LEASE_FAILED); });
|
|
4914
4918
|
placement.callbackTransport = NATIVE_CALLBACK_TRANSPORT;
|
|
4915
4919
|
} catch (error) {
|
|
4916
4920
|
await closeTunnel(); const cleanup = await requestCleanup();
|
|
4917
|
-
|
|
4921
|
+
const openReason = boundedAuthorizationReason(error?.authorizationReason);
|
|
4922
|
+
return { exitCode: cleanup.ok && !callbackCleanupError && error?.code !== "CALLBACK_CLEANUP_UNCERTAIN" ? EXIT.BACKEND : EXIT.CLEANUP_FAILED, outcome: "error", code: error?.code ?? "CALLBACK_NOT_READY", leaseId, error: String(error?.message ?? "callback transport unavailable"), cleanup, ...(openReason ? { callbackAuthorizationReason: openReason } : {}) };
|
|
4918
4923
|
}
|
|
4919
4924
|
}
|
|
4920
4925
|
placement.transport = "ssh-forward";
|
|
@@ -4999,7 +5004,8 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
4999
5004
|
if (callbackLost || nativeCallback && !nativeCallback.isLive()) {
|
|
5000
5005
|
await closeTunnel(); const cleanup = await requestCleanup();
|
|
5001
5006
|
const callbackCause = callbackLost?.code ?? "CALLBACK_RELAY_EXITED";
|
|
5002
|
-
|
|
5007
|
+
const lostReason = boundedAuthorizationReason(callbackLost?.authorization_reason);
|
|
5008
|
+
return { exitCode: cleanup.ok && !callbackCleanupError ? EXIT.LEASE_FAILED : EXIT.CLEANUP_FAILED, outcome: "error", code: "CALLBACK_TRANSPORT_LOST", callbackCause, ...(lostReason ? { callbackAuthorizationReason: lostReason } : {}), leaseId, error: `callback transport closed before child start (${callbackCause})`, cleanup };
|
|
5003
5009
|
}
|
|
5004
5010
|
if (tunnelLost) {
|
|
5005
5011
|
// The forwards died between being verified and the child starting.
|
|
@@ -5065,7 +5071,10 @@ async function runStackLifecycleOnce(opts, childArgv, adapters, incidentBox) {
|
|
|
5065
5071
|
if (callbackLost && !receivedSignal) {
|
|
5066
5072
|
// BOT-2828: the receipt names the hop and cause that closed the route, not only the class.
|
|
5067
5073
|
const callbackCause = callbackLost.code ?? (callbackLost.reason === "callback_authorization_lost" ? "CALLBACK_AUTHORIZATION_LOST" : "CALLBACK_RELAY_EXITED");
|
|
5068
|
-
|
|
5074
|
+
const authorizationLost = callbackLost.reason === "callback_authorization_lost";
|
|
5075
|
+
// BOT-2880: an authorization loss always names the refused check, even as "unclassified".
|
|
5076
|
+
const callbackAuthorizationReason = authorizationLost ? boundedAuthorizationReason(callbackLost.authorization_reason) ?? "unclassified" : null;
|
|
5077
|
+
return { exitCode: cleanup.ok && !callbackCleanupError ? EXIT.LEASE_FAILED : EXIT.CLEANUP_FAILED, outcome: "failed", code: authorizationLost ? "CALLBACK_AUTHORIZATION_LOST" : "CALLBACK_TRANSPORT_LOST", callbackCause, ...(callbackAuthorizationReason ? { callbackAuthorizationReason } : {}), leaseId, childExitCode: childResult.code, cleanup, fenced: true, error: `callback transport authorization or runtime context was lost (${callbackCause}${callbackAuthorizationReason ? `: ${callbackAuthorizationReason}` : ""}); child stopped` };
|
|
5069
5078
|
}
|
|
5070
5079
|
if (tunnelLost && !receivedSignal) {
|
|
5071
5080
|
return tunnelLostResult(tunnelLost, { leaseId, cleanup, childExitCode: childResult.code, fenced: fencing, ...(parkIncident ? { incident: parkIncident } : {}) });
|
|
@@ -5177,6 +5186,8 @@ export function runReceiptFields(result) {
|
|
|
5177
5186
|
...(result.execution?.callbackTransport ? { callback_transport: result.execution.callbackTransport } : {}),
|
|
5178
5187
|
// BOT-2828: the typed cause of a native callback route close (e.g. CALLBACK_RUNTIME_DRIFT).
|
|
5179
5188
|
...(result.callbackCause ? { callback_cause: result.callbackCause } : {}),
|
|
5189
|
+
// BOT-2880: which check refused a CALLBACK_AUTHORIZATION_LOST (a bounded token, never a value).
|
|
5190
|
+
...(result.callbackAuthorizationReason ? { callback_authorization_reason: result.callbackAuthorizationReason } : {}),
|
|
5180
5191
|
// BOT-2780: the reserved requester origin generated auth links name (never a credential).
|
|
5181
5192
|
...(result.execution?.requesterOrigin ? { requester_origin: result.execution.requesterOrigin } : {}),
|
|
5182
5193
|
// BOT-2788: "previous" (an idle stack's origin re-reserved), "previous_port_busy" or "new".
|
|
@@ -5235,6 +5246,14 @@ export async function cmdRun(opts, childArgv, {
|
|
|
5235
5246
|
* receipt deliberately does not carry (which wait a timeout ended and the original
|
|
5236
5247
|
* lease fence). Never emitted.
|
|
5237
5248
|
*/
|
|
5249
|
+
// BOT-2880: a definitive RPC refusal of the callback status read names itself by the backend's
|
|
5250
|
+
// typed code or its HTTP status. The shared bound drops anything that is not a plain token.
|
|
5251
|
+
function refusedRpcReason(observed) {
|
|
5252
|
+
const code = observed?.data?.code ?? observed?.code;
|
|
5253
|
+
const named = typeof code === "string" && code ? code.toLowerCase() : Number.isInteger(observed?.status) ? `http_${observed.status}` : null;
|
|
5254
|
+
return (named && boundedAuthorizationReason(`refused:${named}`)) ?? "refused";
|
|
5255
|
+
}
|
|
5256
|
+
|
|
5238
5257
|
export function stackRunOutcomeFields(result) {
|
|
5239
5258
|
return {
|
|
5240
5259
|
...runReceiptFields(result), lease_fenced: result?.fenced === true,
|