@runuai/host 0.9.156 → 0.9.158
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/lib/compose-network-recovery.ts +29 -10
- package/lib/credentials-activation-authority.ts +25 -10
- package/lib/orchestrator.ts +35 -0
- package/lib/task-environment/self.ts +19 -5
- package/lib/task-files-machine-retirement-outbox.ts +521 -0
- package/lib/task-files-machine-retirement.ts +33 -5
- package/lib/task-machine-credentials-activation.ts +93 -0
- package/package.json +1 -1
- package/src/cloud-credentials.ts +9 -0
- package/src/index.ts +19 -3
- package/src/main.ts +155 -14
- package/src/protocol.ts +106 -0
package/src/index.ts
CHANGED
|
@@ -346,7 +346,7 @@ function untilLeavingLegacy<T>(legacy: LegacyStartPermit | null, wait: Promise<T
|
|
|
346
346
|
|
|
347
347
|
/** A start the runner does not take while it switches its tasks to cloud
|
|
348
348
|
* credentials: the cloud starts it again shortly. */
|
|
349
|
-
function switchingToCloudCredentials():
|
|
349
|
+
function switchingToCloudCredentials(): HostCommandResult<never> {
|
|
350
350
|
return {
|
|
351
351
|
ok: false,
|
|
352
352
|
code: HostErrorCode.HostUnavailable,
|
|
@@ -621,8 +621,9 @@ async function taskUpOn(
|
|
|
621
621
|
// that failed after Compose created its network can retain an empty subnet
|
|
622
622
|
// while it waits for Resume. Reclaim only networks whose trusted host row,
|
|
623
623
|
// exact Docker identity and two empty-container inventories all agree. The
|
|
624
|
-
// sweep runs before this task's lifecycle slot
|
|
625
|
-
//
|
|
624
|
+
// sweep runs before this task's lifecycle slot and never waits on another
|
|
625
|
+
// task's: a candidate whose slot is busy is skipped, so concurrent starts
|
|
626
|
+
// neither deadlock nor queue behind one another's provisioning.
|
|
626
627
|
if (
|
|
627
628
|
!sidecarMode() &&
|
|
628
629
|
!machineTasksEnabled() &&
|
|
@@ -642,6 +643,12 @@ async function taskUpOn(
|
|
|
642
643
|
"[host-agent] skipped task-network recovery because Docker returned an invalid network inventory",
|
|
643
644
|
);
|
|
644
645
|
}
|
|
646
|
+
if (networkRecovery.busy > 0) {
|
|
647
|
+
console.log(
|
|
648
|
+
`[host-agent] task-network recovery left ${String(networkRecovery.busy)} ` +
|
|
649
|
+
`network(s) alone while their task was busy`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
645
652
|
} catch (error) {
|
|
646
653
|
// Best-effort capacity recovery must not block a task when Docker still
|
|
647
654
|
// has room. `docker compose up` remains the authoritative allocation.
|
|
@@ -1976,11 +1983,17 @@ export const hostCommands: HostCommands = {
|
|
|
1976
1983
|
|
|
1977
1984
|
async channelEnsure(ctx, input) {
|
|
1978
1985
|
logCommand(ctx, "channelEnsure", input.taskId);
|
|
1986
|
+
if (credentialsMode() === "pending") {
|
|
1987
|
+
return switchingToCloudCredentials();
|
|
1988
|
+
}
|
|
1979
1989
|
const admissionFailure = managedMaintenanceUnavailable();
|
|
1980
1990
|
if (admissionFailure) return admissionFailure;
|
|
1981
1991
|
const runtimeFailure = runtimeUnavailable();
|
|
1982
1992
|
if (runtimeFailure) return runtimeFailure;
|
|
1983
1993
|
await agentClisReady;
|
|
1994
|
+
if (credentialsMode() === "pending") {
|
|
1995
|
+
return switchingToCloudCredentials();
|
|
1996
|
+
}
|
|
1984
1997
|
const readyAdmissionFailure = managedMaintenanceUnavailable();
|
|
1985
1998
|
if (readyAdmissionFailure) return readyAdmissionFailure;
|
|
1986
1999
|
const readyRuntimeFailure = runtimeUnavailable();
|
|
@@ -2025,6 +2038,9 @@ export const hostCommands: HostCommands = {
|
|
|
2025
2038
|
|
|
2026
2039
|
async channelDeliver(ctx, taskId, agentId, text, origin) {
|
|
2027
2040
|
logCommand(ctx, "channelDeliver", taskId, agentId);
|
|
2041
|
+
if (credentialsMode() === "pending") {
|
|
2042
|
+
return switchingToCloudCredentials();
|
|
2043
|
+
}
|
|
2028
2044
|
const runtimeFailure = runtimeUnavailable();
|
|
2029
2045
|
if (runtimeFailure) return runtimeFailure;
|
|
2030
2046
|
try {
|
package/src/main.ts
CHANGED
|
@@ -302,6 +302,7 @@ import {
|
|
|
302
302
|
LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
|
|
303
303
|
AGENT_TOKEN_GENERATION_PROTOCOL_FEATURE,
|
|
304
304
|
isEngineAccountScope,
|
|
305
|
+
isTaskMachineRetireAckFrame,
|
|
305
306
|
HOST_LOGS_PROTOCOL_FEATURE,
|
|
306
307
|
HOST_TASK_INVENTORY_PROTOCOL_FEATURE,
|
|
307
308
|
MAX_ENGINE_ACCOUNT_LABEL_CHARS,
|
|
@@ -413,11 +414,29 @@ import {
|
|
|
413
414
|
import { taskContainerBackend, taskContainerCli } from "../lib/task-container-cli";
|
|
414
415
|
import { CloudCredentialRuntime } from "../lib/cloud-credential-runtime";
|
|
415
416
|
import { createTaskFilesV1Consumer } from "../lib/credential-consumers/files";
|
|
416
|
-
import { taskFilesV1Readiness } from "../lib/task-files-readiness";
|
|
417
417
|
import {
|
|
418
|
+
TASK_FILES_SESSION_ROOT,
|
|
419
|
+
taskFilesV1Readiness,
|
|
420
|
+
} from "../lib/task-files-readiness";
|
|
421
|
+
import {
|
|
422
|
+
latchDamagedTaskFilesMachineRetirement,
|
|
418
423
|
requestTaskFilesMachineRetirement,
|
|
424
|
+
setTaskFilesMachineRetirementHandler,
|
|
419
425
|
taskFilesMachineRetirementReady,
|
|
420
426
|
} from "../lib/task-files-machine-retirement";
|
|
427
|
+
import {
|
|
428
|
+
kernelBootId,
|
|
429
|
+
TaskFilesMachineRetirementOutbox,
|
|
430
|
+
TASK_FILES_MACHINE_RETIREMENT_RECORD,
|
|
431
|
+
taskMachinePeerProcessIds,
|
|
432
|
+
} from "../lib/task-files-machine-retirement-outbox";
|
|
433
|
+
import {
|
|
434
|
+
planFreshTaskMachineCredentialsActivation,
|
|
435
|
+
taskMachineCredentialsV1Required,
|
|
436
|
+
verifyFreshTaskMachineCredentialsActivation,
|
|
437
|
+
type TaskMachineCredentialsActivationDeps,
|
|
438
|
+
type TaskMachineCredentialsActivationPlan,
|
|
439
|
+
} from "../lib/task-machine-credentials-activation";
|
|
421
440
|
import { ProjectEnvTaskReplicaManager } from "../lib/project-env-task-replica";
|
|
422
441
|
import {
|
|
423
442
|
purgeDefaultEngineSlots,
|
|
@@ -669,27 +688,82 @@ try {
|
|
|
669
688
|
);
|
|
670
689
|
}
|
|
671
690
|
migrateHostDb();
|
|
691
|
+
const taskMachineCredentialsRequired =
|
|
692
|
+
sidecarMode() && taskMachineCredentialsV1Required();
|
|
693
|
+
const taskFilesReadiness = taskFilesV1Readiness(sidecarMode());
|
|
694
|
+
const taskFilesMachineRetirementOutbox = sidecarMode()
|
|
695
|
+
? new TaskFilesMachineRetirementOutbox({
|
|
696
|
+
// Root-only tmpfs survives runner restarts but not a machine reboot,
|
|
697
|
+
// exactly matching the retirement proof boundary. It also keeps data
|
|
698
|
+
// disk exhaustion from blocking the first durable handoff.
|
|
699
|
+
recordPath: join(
|
|
700
|
+
TASK_FILES_SESSION_ROOT,
|
|
701
|
+
TASK_FILES_MACHINE_RETIREMENT_RECORD,
|
|
702
|
+
),
|
|
703
|
+
send: sendCredentialFrame,
|
|
704
|
+
log: (line) => console.warn(line),
|
|
705
|
+
})
|
|
706
|
+
: null;
|
|
707
|
+
if (taskFilesMachineRetirementOutbox !== null) {
|
|
708
|
+
setTaskFilesMachineRetirementHandler((request) => {
|
|
709
|
+
try {
|
|
710
|
+
return taskFilesMachineRetirementOutbox.handler(request);
|
|
711
|
+
} finally {
|
|
712
|
+
// The terminal latch closes before the handler runs. Withdraw Files-v1
|
|
713
|
+
// even if persisting the outbox record fails and the same exact handoff
|
|
714
|
+
// must be retried.
|
|
715
|
+
sendCapabilities();
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
if (taskFilesMachineRetirementOutbox.startup.kind === "pending") {
|
|
719
|
+
requestTaskFilesMachineRetirement(
|
|
720
|
+
taskFilesMachineRetirementOutbox.startup.request,
|
|
721
|
+
);
|
|
722
|
+
} else if (taskFilesMachineRetirementOutbox.startup.kind === "damaged") {
|
|
723
|
+
latchDamagedTaskFilesMachineRetirement(
|
|
724
|
+
taskFilesMachineRetirementOutbox.startup.error,
|
|
725
|
+
);
|
|
726
|
+
taskFilesMachineRetirementOutbox.quarantineDamagedStartup();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
672
729
|
// ADR-154 §13: the activation authority, before anything reads the mode
|
|
673
730
|
// (the refreshers, the v0 lease, the runtime's recovery, the first
|
|
674
|
-
// capability frame).
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
|
|
731
|
+
// capability frame). A cloud-marked task machine starts pending here—before
|
|
732
|
+
// a v0 lease can be requested—and activates only after its local Files and
|
|
733
|
+
// exact-machine retirement boundaries are installed.
|
|
734
|
+
type CredentialsRetirementPlan =
|
|
735
|
+
| { kind: "owned-runner"; plan: RetirementPlan }
|
|
736
|
+
| TaskMachineCredentialsActivationPlan;
|
|
737
|
+
const credentialsActivation = new CredentialsActivationAuthority<CredentialsRetirementPlan>({
|
|
678
738
|
recordPath: join(env.dataDir, CREDENTIALS_ACTIVATION_RECORD),
|
|
679
739
|
floorClaimed: () => credentialsCapabilityFloorExists(),
|
|
680
|
-
eligible: () =>
|
|
740
|
+
eligible: () =>
|
|
741
|
+
taskMachineCredentialsRequired
|
|
742
|
+
? sidecarTaskId() !== null
|
|
743
|
+
: !sidecarMode() && !signInBoxMode(),
|
|
744
|
+
initiallyPending: () => taskMachineCredentialsRequired,
|
|
681
745
|
ask: askCredentialsActivation,
|
|
682
746
|
pauseLegacyStarts,
|
|
683
|
-
planRetirement:
|
|
747
|
+
planRetirement: planCredentialsRetirement,
|
|
684
748
|
commitLocally: (record) => commitCredentialsSwitchLocally(record),
|
|
685
|
-
retire:
|
|
749
|
+
retire: retireCredentialsPlan,
|
|
686
750
|
reportStopped: reportRetiredTasksStopped,
|
|
687
751
|
claimFloor: claimCredentialsCapabilityFloor,
|
|
688
752
|
leaveLegacy,
|
|
689
753
|
purge: purgeCredentialStores,
|
|
690
754
|
swept: cloudCredentialsSwept,
|
|
691
755
|
sweep: recoverCloudCredentials,
|
|
692
|
-
onActive:
|
|
756
|
+
onActive: (how) => {
|
|
757
|
+
if (
|
|
758
|
+
sidecarMode() &&
|
|
759
|
+
(!taskFilesReadiness.ready || !taskFilesMachineRetirementReady())
|
|
760
|
+
) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
"the task machine cannot activate cloud credentials without its Files and retirement boundaries",
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
cloudCredentialsActive(how);
|
|
766
|
+
},
|
|
693
767
|
// Pending again: the next capability frame advertises no v1.
|
|
694
768
|
onActiveWithdrawn: () => sendCapabilities(),
|
|
695
769
|
restart: (why) => {
|
|
@@ -699,9 +773,13 @@ const credentialsActivation = new CredentialsActivationAuthority({
|
|
|
699
773
|
log: (line) => console.warn(line),
|
|
700
774
|
});
|
|
701
775
|
setCredentialsModeSource(() => credentialsActivation.mode());
|
|
702
|
-
/** ADR-154 §13:
|
|
703
|
-
*
|
|
776
|
+
/** ADR-154 §13: the steady/operator cadence. Active runners report their
|
|
777
|
+
* purge again here too, so keep it deliberately quiet. */
|
|
704
778
|
const CREDENTIALS_ACTIVATION_RETRY_MS = 5 * 60_000;
|
|
779
|
+
/** A newly assigned marked machine is already behind its floor. Retry a
|
|
780
|
+
* transient freshness/Files proof inside Cloud's boot budget, without making
|
|
781
|
+
* its eventual active purge report a five-second heartbeat. */
|
|
782
|
+
const TASK_MACHINE_CREDENTIALS_ACTIVATION_RETRY_MS = 5_000;
|
|
705
783
|
const CREDENTIALS_ACTIVATION_ANSWER_MS = 60_000;
|
|
706
784
|
// Started here, first fired long after the bridge's state exists.
|
|
707
785
|
const credentialsActivationTimer = setInterval(() => {
|
|
@@ -714,6 +792,18 @@ const credentialsActivationTimer = setInterval(() => {
|
|
|
714
792
|
void credentialsActivation.advance();
|
|
715
793
|
}, CREDENTIALS_ACTIVATION_RETRY_MS);
|
|
716
794
|
credentialsActivationTimer.unref?.();
|
|
795
|
+
const taskMachineCredentialsActivationTimer =
|
|
796
|
+
taskMachineCredentialsRequired && sidecarTaskId() !== null
|
|
797
|
+
? setInterval(() => {
|
|
798
|
+
if (
|
|
799
|
+
stopping ||
|
|
800
|
+
!credentialFramesReady ||
|
|
801
|
+
credentialsMode() !== "pending"
|
|
802
|
+
) return;
|
|
803
|
+
void credentialsActivation.advance();
|
|
804
|
+
}, TASK_MACHINE_CREDENTIALS_ACTIVATION_RETRY_MS)
|
|
805
|
+
: null;
|
|
806
|
+
taskMachineCredentialsActivationTimer?.unref?.();
|
|
717
807
|
|
|
718
808
|
const credentialsActivationAsks = new Map<
|
|
719
809
|
string,
|
|
@@ -914,7 +1004,6 @@ if (signInBoxMode()) {
|
|
|
914
1004
|
// ADR-154: the composer for tasks on cloud credentials. Nothing runs on it
|
|
915
1005
|
// until the runner's activation authority makes cloud credentials its
|
|
916
1006
|
// tasks' only ones (lib/cloud-credential-host.ts).
|
|
917
|
-
const taskFilesReadiness = taskFilesV1Readiness(sidecarMode());
|
|
918
1007
|
const taskFiles = sidecarMode()
|
|
919
1008
|
? createTaskFilesV1Consumer({
|
|
920
1009
|
recoveredBinding: (assignment) => {
|
|
@@ -979,8 +1068,12 @@ if (sidecarMode()) {
|
|
|
979
1068
|
}
|
|
980
1069
|
setCloudCredentialRuntime(cloudCredentials);
|
|
981
1070
|
// A runner that was already on cloud credentials follows them from here:
|
|
982
|
-
// once its boot recovery sweep
|
|
983
|
-
|
|
1071
|
+
// once its boot recovery sweep is done. Owned runners get that sweep from
|
|
1072
|
+
// container-runtime activation; a task machine has no container runtime, so
|
|
1073
|
+
// its authority must run the composer sweep itself before it can reattach a
|
|
1074
|
+
// surviving agent or ask for a new grant.
|
|
1075
|
+
if (sidecarMode()) credentialsActivation.sweepItself();
|
|
1076
|
+
else credentialsActivation.booted();
|
|
984
1077
|
void cloudCredentialsRecovered().then(() => {
|
|
985
1078
|
if (!stopping) void credentialsActivation.advance();
|
|
986
1079
|
});
|
|
@@ -1641,6 +1734,7 @@ function sendCredentialFrame(frame: Parameters<typeof send>[1]): boolean {
|
|
|
1641
1734
|
*/
|
|
1642
1735
|
function bridgeResumed(): void {
|
|
1643
1736
|
credentialFramesReady = true;
|
|
1737
|
+
taskFilesMachineRetirementOutbox?.bridgeAuthenticated();
|
|
1644
1738
|
const mode = credentialsMode();
|
|
1645
1739
|
if (mode === "legacy") leaseManager?.reconnected();
|
|
1646
1740
|
else if (mode === "active") {
|
|
@@ -1688,6 +1782,47 @@ function reportCredentialsPurged(): void {
|
|
|
1688
1782
|
/** ADR-154 §13: what the retirement reads (lib/legacy-runtime-retirement.ts):
|
|
1689
1783
|
* the task rows once, the identity fence over those same rows, the pinned
|
|
1690
1784
|
* engine. */
|
|
1785
|
+
async function planCredentialsRetirement(): Promise<CredentialsRetirementPlan> {
|
|
1786
|
+
if (sidecarMode()) {
|
|
1787
|
+
return planFreshTaskMachineCredentialsActivation(
|
|
1788
|
+
taskMachineCredentialsActivationDeps(),
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
return {
|
|
1792
|
+
kind: "owned-runner",
|
|
1793
|
+
plan: await planLegacyTaskRuntimeRetirement(retirementPlanDeps()),
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
async function retireCredentialsPlan(
|
|
1798
|
+
plan: CredentialsRetirementPlan,
|
|
1799
|
+
): Promise<{ stopped: StoppedReport[]; problem: string | null }> {
|
|
1800
|
+
if (plan.kind === "owned-runner") {
|
|
1801
|
+
return retireTaskRuntimesForCloudCredentials(plan.plan);
|
|
1802
|
+
}
|
|
1803
|
+
verifyFreshTaskMachineCredentialsActivation(
|
|
1804
|
+
plan,
|
|
1805
|
+
taskMachineCredentialsActivationDeps(),
|
|
1806
|
+
);
|
|
1807
|
+
return { stopped: [], problem: null };
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function taskMachineCredentialsActivationDeps(): TaskMachineCredentialsActivationDeps {
|
|
1811
|
+
return {
|
|
1812
|
+
required: () => taskMachineCredentialsRequired,
|
|
1813
|
+
taskId: sidecarTaskId,
|
|
1814
|
+
machineBootId: kernelBootId,
|
|
1815
|
+
filesReady: () => taskFilesReadiness.ready,
|
|
1816
|
+
retirementReady: taskFilesMachineRetirementReady,
|
|
1817
|
+
taskRows: () =>
|
|
1818
|
+
getDb()
|
|
1819
|
+
.select({ taskId: schema.hostTasks.taskId })
|
|
1820
|
+
.from(schema.hostTasks)
|
|
1821
|
+
.all(),
|
|
1822
|
+
peerProcessIds: taskMachinePeerProcessIds,
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1691
1826
|
function retirementPlanDeps(): RetirementPlanDeps {
|
|
1692
1827
|
return {
|
|
1693
1828
|
snapshot: () => {
|
|
@@ -2890,6 +3025,9 @@ async function connect(): Promise<void> {
|
|
|
2890
3025
|
eventOutbox.ack(frame.seq);
|
|
2891
3026
|
flushEvents();
|
|
2892
3027
|
break;
|
|
3028
|
+
case "task.machine.retire.ack":
|
|
3029
|
+
taskFilesMachineRetirementOutbox?.acknowledge(frame);
|
|
3030
|
+
break;
|
|
2893
3031
|
case "command":
|
|
2894
3032
|
void handleCommand(socket, frame);
|
|
2895
3033
|
break;
|
|
@@ -4632,6 +4770,7 @@ function parseCloudFrame(data: RawData): CloudToHost | null {
|
|
|
4632
4770
|
if (frame.kind === "event.ack" && typeof frame.seq === "number") {
|
|
4633
4771
|
return { kind: "event.ack", seq: frame.seq };
|
|
4634
4772
|
}
|
|
4773
|
+
if (isTaskMachineRetireAckFrame(frame)) return frame;
|
|
4635
4774
|
if (
|
|
4636
4775
|
frame.kind === "host.inventory.request" &&
|
|
4637
4776
|
isSafeInventoryScanId(frame.scanId) &&
|
|
@@ -5203,6 +5342,7 @@ function rejectFrameDuringShutdown(
|
|
|
5203
5342
|
case "credential.changed":
|
|
5204
5343
|
case "credential.refresh.result":
|
|
5205
5344
|
case "credentials.activate.result":
|
|
5345
|
+
case "task.machine.retire.ack":
|
|
5206
5346
|
return false;
|
|
5207
5347
|
}
|
|
5208
5348
|
}
|
|
@@ -5802,6 +5942,7 @@ function requestShutdown(signal: NodeJS.Signals): void {
|
|
|
5802
5942
|
void stopRemoteOperationManagers();
|
|
5803
5943
|
stopMcpGatewaySubscription();
|
|
5804
5944
|
stopAgentClisReadySubscription();
|
|
5945
|
+
taskFilesMachineRetirementOutbox?.stop();
|
|
5805
5946
|
// ADR-154: no credential is served or taken after this; what was accepted
|
|
5806
5947
|
// drains, bounded, and no cleanup starts (the next process's recovery runs
|
|
5807
5948
|
// it). Exit waits for it.
|
package/src/protocol.ts
CHANGED
|
@@ -2961,6 +2961,26 @@ export const MAX_REMOTE_HOST_MESSAGE_BYTES = 512;
|
|
|
2961
2961
|
export const MAX_HOST_TASK_INVENTORY_PAGE_SIZE = 256;
|
|
2962
2962
|
export const MAX_HOST_TASK_INVENTORY_SCAN_ID_CHARS = 128;
|
|
2963
2963
|
export const MAX_HOST_TASK_INVENTORY_TASK_ID_CHARS = 128;
|
|
2964
|
+
/** A refusal/staleness explanation is diagnostic only and must stay bounded
|
|
2965
|
+
* on the authenticated control socket. */
|
|
2966
|
+
export const MAX_TASK_MACHINE_RETIRE_ERROR_BYTES = 512;
|
|
2967
|
+
/** Files-v1 terminal failures are the only reasons a task sidecar may ask the
|
|
2968
|
+
* cloud to cold-stop its exact machine incarnation. Keep this vocabulary
|
|
2969
|
+
* closed: the cloud persists it as audit provenance. */
|
|
2970
|
+
export const TASK_MACHINE_RETIRE_REASONS = [
|
|
2971
|
+
"setup-unproven",
|
|
2972
|
+
"visibility-unproven",
|
|
2973
|
+
"mount-retirement-unproven",
|
|
2974
|
+
] as const;
|
|
2975
|
+
export type TaskMachineRetireReason =
|
|
2976
|
+
(typeof TASK_MACHINE_RETIRE_REASONS)[number];
|
|
2977
|
+
export const TASK_MACHINE_RETIRE_OUTCOMES = [
|
|
2978
|
+
"accepted",
|
|
2979
|
+
"stale",
|
|
2980
|
+
"refused",
|
|
2981
|
+
] as const;
|
|
2982
|
+
export type TaskMachineRetireOutcome =
|
|
2983
|
+
(typeof TASK_MACHINE_RETIRE_OUTCOMES)[number];
|
|
2964
2984
|
/**
|
|
2965
2985
|
* A redeem response carrying this exact header proves the cloud checks a spent
|
|
2966
2986
|
* token before expiry. That lets a new CLI distinguish a definitive 404/410
|
|
@@ -3561,6 +3581,65 @@ export function isHostOpId(value: unknown): value is HostOpId {
|
|
|
3561
3581
|
);
|
|
3562
3582
|
}
|
|
3563
3583
|
|
|
3584
|
+
function isTaskMachineRetireTaskId(value: unknown): value is string {
|
|
3585
|
+
return (
|
|
3586
|
+
typeof value === "string" &&
|
|
3587
|
+
value.length > 0 &&
|
|
3588
|
+
value.length <= MAX_HOST_TASK_INVENTORY_TASK_ID_CHARS &&
|
|
3589
|
+
/^[a-z0-9][a-z0-9_-]*$/.test(value)
|
|
3590
|
+
);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
function isTaskMachineRetireRunEpoch(value: unknown): value is number {
|
|
3594
|
+
return Number.isSafeInteger(value) && (value as number) > 0;
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
export function isTaskMachineRetireFrame(
|
|
3598
|
+
value: unknown,
|
|
3599
|
+
): value is TaskMachineRetireFrame {
|
|
3600
|
+
const frame = exactWireObject(value, [
|
|
3601
|
+
"kind",
|
|
3602
|
+
"opId",
|
|
3603
|
+
"taskId",
|
|
3604
|
+
"runEpoch",
|
|
3605
|
+
"reason",
|
|
3606
|
+
]);
|
|
3607
|
+
return Boolean(
|
|
3608
|
+
frame &&
|
|
3609
|
+
frame.kind === "task.machine.retire" &&
|
|
3610
|
+
isHostOpId(frame.opId) &&
|
|
3611
|
+
isTaskMachineRetireTaskId(frame.taskId) &&
|
|
3612
|
+
isTaskMachineRetireRunEpoch(frame.runEpoch) &&
|
|
3613
|
+
(TASK_MACHINE_RETIRE_REASONS as readonly unknown[]).includes(
|
|
3614
|
+
frame.reason,
|
|
3615
|
+
),
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
export function isTaskMachineRetireAckFrame(
|
|
3620
|
+
value: unknown,
|
|
3621
|
+
): value is TaskMachineRetireAckFrame {
|
|
3622
|
+
const frame = exactWireObject(
|
|
3623
|
+
value,
|
|
3624
|
+
["kind", "opId", "taskId", "runEpoch", "outcome"],
|
|
3625
|
+
["error"],
|
|
3626
|
+
);
|
|
3627
|
+
return Boolean(
|
|
3628
|
+
frame &&
|
|
3629
|
+
frame.kind === "task.machine.retire.ack" &&
|
|
3630
|
+
isHostOpId(frame.opId) &&
|
|
3631
|
+
isTaskMachineRetireTaskId(frame.taskId) &&
|
|
3632
|
+
isTaskMachineRetireRunEpoch(frame.runEpoch) &&
|
|
3633
|
+
(TASK_MACHINE_RETIRE_OUTCOMES as readonly unknown[]).includes(
|
|
3634
|
+
frame.outcome,
|
|
3635
|
+
) &&
|
|
3636
|
+
(frame.error === undefined ||
|
|
3637
|
+
(typeof frame.error === "string" &&
|
|
3638
|
+
utf8ByteLength(frame.error) <=
|
|
3639
|
+
MAX_TASK_MACHINE_RETIRE_ERROR_BYTES)),
|
|
3640
|
+
);
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3564
3643
|
export function isHostLogsStartFrame(
|
|
3565
3644
|
value: unknown,
|
|
3566
3645
|
): value is HostLogsStartFrame {
|
|
@@ -4470,6 +4549,31 @@ export type GitHubRepositoryAccessErrorCode =
|
|
|
4470
4549
|
| "github_unavailable"
|
|
4471
4550
|
| "invalid_response";
|
|
4472
4551
|
|
|
4552
|
+
/**
|
|
4553
|
+
* A one-task sidecar cannot prove a Files-v1 boundary closed in this boot, so
|
|
4554
|
+
* it asks the cloud to stop the exact EC2 incarnation. `runEpoch` fences a
|
|
4555
|
+
* delayed request from a successor assignment; the authenticated connection
|
|
4556
|
+
* supplies the host identity and is deliberately not sender-controlled here.
|
|
4557
|
+
*/
|
|
4558
|
+
export interface TaskMachineRetireFrame {
|
|
4559
|
+
kind: "task.machine.retire";
|
|
4560
|
+
opId: string;
|
|
4561
|
+
taskId: string;
|
|
4562
|
+
runEpoch: number;
|
|
4563
|
+
reason: TaskMachineRetireReason;
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
/** Durable acceptance, not completion: after `accepted` the cloud owns
|
|
4567
|
+
* retrying the persisted exact-instance stop even if this socket disappears. */
|
|
4568
|
+
export interface TaskMachineRetireAckFrame {
|
|
4569
|
+
kind: "task.machine.retire.ack";
|
|
4570
|
+
opId: string;
|
|
4571
|
+
taskId: string;
|
|
4572
|
+
runEpoch: number;
|
|
4573
|
+
outcome: TaskMachineRetireOutcome;
|
|
4574
|
+
error?: string;
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4473
4577
|
export type CloudToHost =
|
|
4474
4578
|
| {
|
|
4475
4579
|
kind: "command";
|
|
@@ -4633,6 +4737,7 @@ export type CloudToHost =
|
|
|
4633
4737
|
| CredentialChangedFrame
|
|
4634
4738
|
| CredentialRefreshResultFrame
|
|
4635
4739
|
| CredentialsActivateResultFrame
|
|
4740
|
+
| TaskMachineRetireAckFrame
|
|
4636
4741
|
| McpCaptureProbeFrame
|
|
4637
4742
|
| McpCaptureCompleteFrame
|
|
4638
4743
|
| McpCaptureCancelFrame
|
|
@@ -4751,6 +4856,7 @@ export type HostToCloud =
|
|
|
4751
4856
|
| CredentialRefreshFrame
|
|
4752
4857
|
| CredentialsActivateFrame
|
|
4753
4858
|
| CredentialsActivatedFrame
|
|
4859
|
+
| TaskMachineRetireFrame
|
|
4754
4860
|
| McpCaptureAckFrame
|
|
4755
4861
|
| PersonCaptureAckFrame
|
|
4756
4862
|
| GithubRevokeAckFrame
|