@runuai/host 0.9.150 → 0.9.152
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/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/meta/_journal.json +14 -0
- package/db/schema.ts +188 -0
- package/lib/agent-cli.ts +11 -13
- package/lib/agent.ts +177 -7
- package/lib/agents/durable-proc.ts +56 -44
- package/lib/agents/registry.ts +19 -3
- package/lib/agents/session-state.ts +23 -10
- package/lib/agents/transport.ts +101 -69
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +4 -3
- package/lib/attachments.ts +19 -17
- package/lib/capture-flow.ts +319 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2544 -0
- package/lib/cloud-credential-runtime.ts +1035 -0
- package/lib/codex-auth.ts +21 -1
- package/lib/container-runtime.ts +76 -0
- package/lib/credential-cleanup-inventory.ts +405 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +773 -0
- package/lib/credential-consumers/github.ts +594 -0
- package/lib/credential-consumers/mcp.ts +384 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-store-pickup.ts +15 -2
- package/lib/credential-store.ts +361 -33
- package/lib/credentials-activation-authority.ts +546 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/db.ts +11 -0
- package/lib/docker-exec.ts +3 -1
- package/lib/engine-accounts.ts +42 -32
- package/lib/engine-connect.ts +47 -2
- package/lib/engine-credential-env.ts +50 -0
- package/lib/engine-login.ts +83 -1
- package/lib/env-file.ts +62 -8
- package/lib/env.ts +5 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +219 -177
- package/lib/git-identity.ts +59 -14
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +56 -21
- package/lib/host-env.ts +17 -4
- package/lib/launch-mode.ts +25 -0
- package/lib/legacy-runtime-retirement.ts +388 -0
- package/lib/machine-exec.ts +14 -3
- package/lib/machine-provider-aws.ts +50 -8
- package/lib/machine-provider-local.ts +4 -0
- package/lib/machine-provider.ts +4 -0
- package/lib/managed-operation-drain.ts +8 -0
- package/lib/managed-runtime.ts +371 -7
- package/lib/mcp-capture.ts +860 -0
- package/lib/mcp-connections.ts +8 -1
- package/lib/mcp-gateway.ts +168 -10
- package/lib/obs.ts +2 -6
- package/lib/orchestrator.ts +1837 -224
- package/lib/person-capture.ts +327 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +50 -3
- package/lib/runtime-archive.ts +68 -1
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +429 -13
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/task-trust.ts +34 -3
- package/lib/sidecar-resume.ts +18 -5
- package/lib/skills.ts +47 -8
- package/lib/standard-image.ts +157 -103
- package/lib/stopped-task-status-queue.ts +68 -10
- package/lib/task-credential-cleanup.ts +525 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +66 -163
- package/lib/task-env-source.ts +68 -14
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +69 -28
- package/lib/task-environment/docker.ts +101 -20
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +459 -96
- package/lib/task-environment/machine.ts +101 -11
- package/lib/task-environment/self.ts +108 -50
- package/lib/task-environment/types.ts +89 -5
- package/lib/task-environment/workspace-files.ts +650 -25
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-gateway-recovery.ts +4 -1
- package/lib/task-gateway-state.ts +15 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +56 -2
- package/lib/token-broker-local.ts +28 -4
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +368 -28
- package/lib/token-lanes/openai-oauth.ts +7 -3
- package/lib/token-lanes/xai-oauth.ts +7 -3
- package/lib/transcript.ts +9 -5
- package/package.json +6 -2
- package/scripts/agent/task-up.sh +242 -100
- package/src/cloud-credentials.ts +2399 -0
- package/src/event-outbox.ts +8 -6
- package/src/index.ts +1386 -875
- package/src/main.ts +1061 -139
- package/src/outbound-frame.ts +31 -0
- package/src/project-env-policy.ts +125 -10
- package/src/protocol.ts +699 -23
- package/src/ui/server.ts +48 -10
package/lib/managed-runtime.ts
CHANGED
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
BUNDLED_RUNTIME_CAPABILITY,
|
|
37
37
|
BUNDLED_RUNTIME_ROOT,
|
|
38
38
|
MANAGED_RUNTIME_CAPABILITY,
|
|
39
|
+
MANAGED_RUNTIME_CREDENTIALS,
|
|
40
|
+
MANAGED_RUNTIME_CREDENTIALS_VERSION,
|
|
39
41
|
extractVerifiedRuntimeArchive,
|
|
40
42
|
readVerifiedArtifactFile,
|
|
41
43
|
type ResolvedRuntimeDescriptor,
|
|
@@ -43,6 +45,7 @@ import {
|
|
|
43
45
|
type RuntimeCapabilityDescriptorV3,
|
|
44
46
|
type RuntimeAssetManifest,
|
|
45
47
|
validateManagedRuntimeCapabilityDescriptor,
|
|
48
|
+
validateManagedRuntimeCredentialsDescriptor,
|
|
46
49
|
validateRuntimeAssetManifest,
|
|
47
50
|
validateRuntimeCapabilityDescriptor,
|
|
48
51
|
validateRuntimeDescriptor,
|
|
@@ -77,6 +80,10 @@ const APPLE_CONTAINER_PLUGINS = Object.freeze([
|
|
|
77
80
|
"machine-apiserver",
|
|
78
81
|
]);
|
|
79
82
|
|
|
83
|
+
/** ADR-154 §13: the activation authority's record, in the runner's data
|
|
84
|
+
* directory (lib/credentials-activation-authority.ts). */
|
|
85
|
+
export const CREDENTIALS_ACTIVATION_RECORD = "credentials-activation.json";
|
|
86
|
+
|
|
80
87
|
export interface ManagedRuntimePaths {
|
|
81
88
|
readonly home: string;
|
|
82
89
|
readonly root: string;
|
|
@@ -93,6 +100,17 @@ export interface ManagedRuntimePaths {
|
|
|
93
100
|
* task state. It lives outside version generations so pruning/rollback
|
|
94
101
|
* cannot erase the evidence an older runtime must honor. */
|
|
95
102
|
readonly minimumRuntimeCapability: string;
|
|
103
|
+
/** ADR-154 §13's second, independent floor, claimed when the runner's
|
|
104
|
+
* credentials move to the cloud. The runtime capability floor cannot mark
|
|
105
|
+
* that cutover (see MANAGED_RUNTIME_CREDENTIALS), and like it this lives
|
|
106
|
+
* outside every generation, beside it in the data directory, so it holds on
|
|
107
|
+
* a runner installed through npm too. */
|
|
108
|
+
readonly minimumCredentialsCapability: string;
|
|
109
|
+
/** ADR-154 §13's activation record, beside the runner's database: once it
|
|
110
|
+
* exists the runner has begun its switch to cloud credentials, which no
|
|
111
|
+
* release built before the cutover may undo, so it counts as a credentials
|
|
112
|
+
* floor of 1 even before that floor's own file is written. */
|
|
113
|
+
readonly credentialsActivationRecord: string;
|
|
96
114
|
/** Older Phase-B builds may have claimed provider state before publishing
|
|
97
115
|
* the durable capability marker. This preserved marker implies floor 2. */
|
|
98
116
|
readonly runtimeProviderState: string;
|
|
@@ -295,6 +313,11 @@ export function managedRuntimePaths(home?: string): ManagedRuntimePaths {
|
|
|
295
313
|
configuredUaiHome,
|
|
296
314
|
".minimum-runtime-capability.json",
|
|
297
315
|
),
|
|
316
|
+
minimumCredentialsCapability: path.join(
|
|
317
|
+
configuredUaiHome,
|
|
318
|
+
".minimum-credentials-capability.json",
|
|
319
|
+
),
|
|
320
|
+
credentialsActivationRecord: path.join(configuredDataDir, CREDENTIALS_ACTIVATION_RECORD),
|
|
298
321
|
runtimeProviderState: path.join(configuredDataDir, "container-runtime-provider.json"),
|
|
299
322
|
// The lock must not live inside `root`: uninstall atomically renames that
|
|
300
323
|
// whole tree, and moving the lock would let another process create a new
|
|
@@ -464,6 +487,7 @@ export async function installManagedRuntimeArchive(
|
|
|
464
487
|
nativePlatform,
|
|
465
488
|
);
|
|
466
489
|
await assertRuntimeMeetsDurableCapabilityFloor(paths, currentDescriptor);
|
|
490
|
+
await assertRuntimeMeetsDurableCredentialsFloor(paths, currentDescriptor);
|
|
467
491
|
if (
|
|
468
492
|
installIntent?.serviceOwnership === "legacy" &&
|
|
469
493
|
!(await options.canActivate!({ legacyConversion: true }))
|
|
@@ -501,10 +525,12 @@ export async function installManagedRuntimeArchive(
|
|
|
501
525
|
version: options.version,
|
|
502
526
|
platform: options.platform,
|
|
503
527
|
});
|
|
504
|
-
|
|
528
|
+
const stagedDescriptor: ResolvedRuntimeDescriptor = {
|
|
505
529
|
...inspected.descriptor,
|
|
506
530
|
...inspected.capabilities,
|
|
507
|
-
}
|
|
531
|
+
};
|
|
532
|
+
await assertRuntimeMeetsDurableCapabilityFloor(paths, stagedDescriptor);
|
|
533
|
+
await assertRuntimeMeetsDurableCredentialsFloor(paths, stagedDescriptor);
|
|
508
534
|
if (
|
|
509
535
|
options.canActivate &&
|
|
510
536
|
!(await options.canActivate(activationContext))
|
|
@@ -545,6 +571,7 @@ export async function installManagedRuntimeArchive(
|
|
|
545
571
|
options.platform,
|
|
546
572
|
);
|
|
547
573
|
await assertRuntimeMeetsDurableCapabilityFloor(paths, targetDescriptor);
|
|
574
|
+
await assertRuntimeMeetsDurableCredentialsFloor(paths, targetDescriptor);
|
|
548
575
|
} else {
|
|
549
576
|
await rename(staged, target);
|
|
550
577
|
await syncDirectory(paths.runtime);
|
|
@@ -716,6 +743,7 @@ export async function rollbackManagedRuntime(
|
|
|
716
743
|
platform,
|
|
717
744
|
);
|
|
718
745
|
await assertRuntimeMeetsDurableCapabilityFloor(paths, targetDescriptor);
|
|
746
|
+
await assertRuntimeMeetsDurableCredentialsFloor(paths, targetDescriptor);
|
|
719
747
|
if (canActivate && !(await canActivate())) {
|
|
720
748
|
throw new Error("refusing to roll back while host tasks are active");
|
|
721
749
|
}
|
|
@@ -810,7 +838,7 @@ async function persistManagedRuntimeCapabilityFloor(
|
|
|
810
838
|
// owned file before making the floor durable, so standalone recovery can
|
|
811
839
|
// recognize the new cross-platform capability marker immediately.
|
|
812
840
|
const recoveryEntrypoint = await assertRecoveryEntrypointReplaceable(paths);
|
|
813
|
-
if (recoveryEntrypoint === "legacy-v1") {
|
|
841
|
+
if (recoveryEntrypoint === "legacy-v1" || recoveryEntrypoint === "legacy-v2") {
|
|
814
842
|
await ensureRecoveryEntrypoint(paths);
|
|
815
843
|
}
|
|
816
844
|
await mkdir(path.dirname(paths.minimumRuntimeCapability), {
|
|
@@ -825,6 +853,178 @@ async function persistManagedRuntimeCapabilityFloor(
|
|
|
825
853
|
});
|
|
826
854
|
}
|
|
827
855
|
|
|
856
|
+
/**
|
|
857
|
+
* ADR-154 §13's local commit: claim the credentials floor. From then on this
|
|
858
|
+
* release and every later one refuse to update to, roll back to, or boot a
|
|
859
|
+
* generation built before the cloud-only credentials cutover. It only rises,
|
|
860
|
+
* is written under the managed-operation lock, and is independent of the
|
|
861
|
+
* runtime capability floor, which cannot rise to mark the cutover.
|
|
862
|
+
*
|
|
863
|
+
* On a runner installed through npm there is no managed runtime: the floor is
|
|
864
|
+
* only persisted, nothing reads it yet, and the managed tree is not created.
|
|
865
|
+
*
|
|
866
|
+
* It refuses, and the activation authority retries, while an installation
|
|
867
|
+
* intent, an activation marker, a rollback journal or an uninstall marker is
|
|
868
|
+
* present, and while the selected generation itself predates the floor. Each
|
|
869
|
+
* of those means the generation this runner boots next may not be the one
|
|
870
|
+
* claiming, or is about to be removed, which is the window the runtime
|
|
871
|
+
* capability claim closes for the same reason. Refusing is safe because the
|
|
872
|
+
* local commit precedes the cloud commit: a failed claim leaves the runner on
|
|
873
|
+
* v0 with its stores intact. Standalone recovery (revision 3) enforces it
|
|
874
|
+
* too (see `recoveryEntrypointContents`).
|
|
875
|
+
*/
|
|
876
|
+
export async function claimCredentialsCapabilityFloor(
|
|
877
|
+
minimumCredentialsCapability: number = MANAGED_RUNTIME_CREDENTIALS_VERSION,
|
|
878
|
+
home?: string,
|
|
879
|
+
): Promise<void> {
|
|
880
|
+
assertCredentialsCapabilityFloor(minimumCredentialsCapability);
|
|
881
|
+
const paths = managedRuntimePaths(home);
|
|
882
|
+
await withManagedOperationLockWaiting(paths, async () => {
|
|
883
|
+
await assertNoManagedInstallIntent(paths, "credentials capability claim");
|
|
884
|
+
if (
|
|
885
|
+
pathExistsSync(paths.activationPending) ||
|
|
886
|
+
pathExistsSync(paths.rollbackJournal)
|
|
887
|
+
) {
|
|
888
|
+
throw new Error("managed runtime activation is already in progress");
|
|
889
|
+
}
|
|
890
|
+
if (pathExistsSync(paths.maintenancePending)) {
|
|
891
|
+
throw new Error("managed runtime maintenance is already in progress");
|
|
892
|
+
}
|
|
893
|
+
const install = await managedInstallState(paths);
|
|
894
|
+
if (install.kind === "damaged") throw new Error(install.detail);
|
|
895
|
+
if (install.kind === "selected") {
|
|
896
|
+
const selectedCapability = await resolveInstalledCredentialsCapability(
|
|
897
|
+
await realpath(path.join(paths.runtime, install.version)),
|
|
898
|
+
);
|
|
899
|
+
if (selectedCapability < minimumCredentialsCapability) {
|
|
900
|
+
throw new Error(
|
|
901
|
+
`refusing to claim credentials capability ${minimumCredentialsCapability}; the selected managed runtime ${install.version} carries credentials capability ${selectedCapability}`,
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
// Standalone recovery must refuse what this floor refuses before the
|
|
905
|
+
// floor counts.
|
|
906
|
+
await ensureCurrentRecoveryEntrypoint(paths);
|
|
907
|
+
}
|
|
908
|
+
await mkdir(path.dirname(paths.minimumCredentialsCapability), {
|
|
909
|
+
recursive: true,
|
|
910
|
+
mode: 0o700,
|
|
911
|
+
});
|
|
912
|
+
const existing = await readMinimumCredentialsCapabilityFile(paths);
|
|
913
|
+
if (existing !== null && existing >= minimumCredentialsCapability) return;
|
|
914
|
+
await writeAtomicJson(paths.minimumCredentialsCapability, {
|
|
915
|
+
schemaVersion: 1,
|
|
916
|
+
minimumCredentialsCapability,
|
|
917
|
+
});
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/** Whether the credentials floor's file exists, valid or not: a runner
|
|
922
|
+
* that claimed it has begun its switch, whatever its activation record
|
|
923
|
+
* says (ADR-154 §13). */
|
|
924
|
+
export function credentialsCapabilityFloorExists(home?: string): boolean {
|
|
925
|
+
return pathExistsSync(managedRuntimePaths(home).minimumCredentialsCapability);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* ADR-154 §13, the runner's local commit: from its record on, the switch
|
|
930
|
+
* never goes back, so no release built before the cutover may boot here
|
|
931
|
+
* again. Under the managed-operation lock, which every install, update and
|
|
932
|
+
* rollback holds from its checks to its link commit, so none crosses this:
|
|
933
|
+
* nothing is being installed, activated, rolled back or uninstalled; the
|
|
934
|
+
* selected generation carries the credentials marker, so the next boot is a
|
|
935
|
+
* release that reads the record; standalone recovery reads it too (its
|
|
936
|
+
* entrypoint upgraded); then `record()` writes the record, still under the
|
|
937
|
+
* lock. An update or rollback after it finds the record a credentials floor.
|
|
938
|
+
* A managed tree that is damaged (its current link dangling or missing
|
|
939
|
+
* beside other managed state) fails closed. A runner with no managed
|
|
940
|
+
* install (an npm one) has none of this: `record()` alone.
|
|
941
|
+
*
|
|
942
|
+
* Standalone recovery takes no lock: it reads the record when it starts,
|
|
943
|
+
* then hands the command to the previous generation's own launcher, which
|
|
944
|
+
* does not. The upgrade here fences that launcher's rollback: every managed
|
|
945
|
+
* release before the cutover rolls back under this lock only, and before
|
|
946
|
+
* its journal or any link refuses a recovery entrypoint other than its own
|
|
947
|
+
* revisions'. So such a rollback either took the lock first, and this finds
|
|
948
|
+
* its generation selected, or comes after the upgrade and refuses.
|
|
949
|
+
*/
|
|
950
|
+
export async function commitCredentialsSwitchLocally(record: () => void, home?: string): Promise<void> {
|
|
951
|
+
const paths = managedRuntimePaths(home);
|
|
952
|
+
await withManagedOperationLockWaiting(paths, async () => {
|
|
953
|
+
await assertNoManagedInstallIntent(paths, "the switch to cloud credentials");
|
|
954
|
+
const install = await managedInstallState(paths);
|
|
955
|
+
if (install.kind === "damaged") throw new Error(install.detail);
|
|
956
|
+
if (install.kind === "selected") {
|
|
957
|
+
if (pathExistsSync(paths.activationPending) || pathExistsSync(paths.rollbackJournal)) {
|
|
958
|
+
throw new Error("managed runtime activation is already in progress");
|
|
959
|
+
}
|
|
960
|
+
if (pathExistsSync(paths.maintenancePending)) {
|
|
961
|
+
throw new Error("managed runtime maintenance is already in progress");
|
|
962
|
+
}
|
|
963
|
+
const capability = await resolveInstalledCredentialsCapability(
|
|
964
|
+
await realpath(path.join(paths.runtime, install.version)),
|
|
965
|
+
);
|
|
966
|
+
if (capability < MANAGED_RUNTIME_CREDENTIALS_VERSION) {
|
|
967
|
+
throw new Error(
|
|
968
|
+
`the selected managed runtime ${install.version} was built before the switch to cloud credentials`,
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
await ensureCurrentRecoveryEntrypoint(paths);
|
|
972
|
+
}
|
|
973
|
+
record();
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
type ManagedInstallState =
|
|
978
|
+
| { kind: "absent" }
|
|
979
|
+
| { kind: "selected"; version: string }
|
|
980
|
+
| { kind: "damaged"; detail: string };
|
|
981
|
+
|
|
982
|
+
/** A managed install, if any, and the generation it boots next. A missing or
|
|
983
|
+
* dangling current link is no npm install when other managed state (the
|
|
984
|
+
* runtime directory, a previous link, the recovery entrypoint) is there:
|
|
985
|
+
* that tree is damaged, and nothing may be decided from it. */
|
|
986
|
+
async function managedInstallState(paths: ManagedRuntimePaths): Promise<ManagedInstallState> {
|
|
987
|
+
let currentLink = true;
|
|
988
|
+
try {
|
|
989
|
+
await lstat(paths.current);
|
|
990
|
+
} catch (error) {
|
|
991
|
+
if (!isMissing(error)) throw error;
|
|
992
|
+
currentLink = false;
|
|
993
|
+
}
|
|
994
|
+
if (currentLink) {
|
|
995
|
+
const version = await readVersionLink(paths.current, paths.runtime);
|
|
996
|
+
return version === null
|
|
997
|
+
? { kind: "damaged", detail: "the managed runtime's current link leads to nothing; repair it first" }
|
|
998
|
+
: { kind: "selected", version };
|
|
999
|
+
}
|
|
1000
|
+
for (const [state, what] of [
|
|
1001
|
+
[paths.runtime, "runtime directory"],
|
|
1002
|
+
[paths.previous, "previous runtime link"],
|
|
1003
|
+
[paths.recoveryEntrypoint, "recovery entrypoint"],
|
|
1004
|
+
] as const) {
|
|
1005
|
+
try {
|
|
1006
|
+
await lstat(state);
|
|
1007
|
+
return { kind: "damaged", detail: `the managed runtime has a ${what} but no current runtime; repair it first` };
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
if (!isMissing(error)) throw error;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return { kind: "absent" };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/** The caller holds the operation lock and the runtime is managed. */
|
|
1016
|
+
async function ensureCurrentRecoveryEntrypoint(paths: ManagedRuntimePaths): Promise<void> {
|
|
1017
|
+
if ((await assertRecoveryEntrypointReplaceable(paths)) !== "current") {
|
|
1018
|
+
await ensureRecoveryEntrypoint(paths);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function assertCredentialsCapabilityFloor(minimum: number): void {
|
|
1023
|
+
if (!Number.isSafeInteger(minimum) || minimum < 1) {
|
|
1024
|
+
throw new Error("managed runtime credentials floor is invalid");
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
828
1028
|
/** True while `current` has changed but the supervisor has not exec'd it yet. */
|
|
829
1029
|
export function managedRuntimeActivationPending(home?: string): boolean {
|
|
830
1030
|
return pathExistsSync(managedRuntimePaths(home).activationPending);
|
|
@@ -1122,6 +1322,7 @@ async function reconcileManagedRuntimeSelectionLocked(
|
|
|
1122
1322
|
selectedVersion,
|
|
1123
1323
|
);
|
|
1124
1324
|
await assertRuntimeMeetsDurableCapabilityFloor(paths, selectedDescriptor);
|
|
1325
|
+
await assertRuntimeMeetsDurableCredentialsFloor(paths, selectedDescriptor);
|
|
1125
1326
|
}
|
|
1126
1327
|
const persistAcknowledgedCapabilityFloor = async (): Promise<void> => {
|
|
1127
1328
|
if (capabilityAcknowledgement === null) return;
|
|
@@ -1978,7 +2179,13 @@ async function resolveInstalledRuntimeCapabilities(
|
|
|
1978
2179
|
root,
|
|
1979
2180
|
platformCapabilities.capabilitySchemaVersion,
|
|
1980
2181
|
);
|
|
1981
|
-
|
|
2182
|
+
const credentialsCapabilityVersion =
|
|
2183
|
+
await resolveInstalledCredentialsCapability(root);
|
|
2184
|
+
return {
|
|
2185
|
+
...platformCapabilities,
|
|
2186
|
+
runtimeCapabilityVersion,
|
|
2187
|
+
credentialsCapabilityVersion,
|
|
2188
|
+
};
|
|
1982
2189
|
}
|
|
1983
2190
|
|
|
1984
2191
|
async function resolveInstalledPlatformCapabilities(
|
|
@@ -2051,6 +2258,39 @@ async function resolveInstalledManagedRuntimeCapability(
|
|
|
2051
2258
|
.capabilitySchemaVersion;
|
|
2052
2259
|
}
|
|
2053
2260
|
|
|
2261
|
+
/** `root` must already be the generation's real path. */
|
|
2262
|
+
async function resolveInstalledCredentialsCapability(
|
|
2263
|
+
root: string,
|
|
2264
|
+
): Promise<number> {
|
|
2265
|
+
const descriptorPath = path.join(
|
|
2266
|
+
root,
|
|
2267
|
+
...MANAGED_RUNTIME_CREDENTIALS.split("/"),
|
|
2268
|
+
);
|
|
2269
|
+
let metadata;
|
|
2270
|
+
try {
|
|
2271
|
+
metadata = await lstat(descriptorPath);
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
if (isMissing(error)) return 0;
|
|
2274
|
+
throw error;
|
|
2275
|
+
}
|
|
2276
|
+
if (
|
|
2277
|
+
!metadata.isFile() ||
|
|
2278
|
+
metadata.isSymbolicLink() ||
|
|
2279
|
+
metadata.size > MAX_OPERATION_LOCK_BYTES ||
|
|
2280
|
+
!isInside(root, await realpath(descriptorPath))
|
|
2281
|
+
) {
|
|
2282
|
+
throw new Error("managed runtime credentials descriptor is invalid");
|
|
2283
|
+
}
|
|
2284
|
+
let raw: unknown;
|
|
2285
|
+
try {
|
|
2286
|
+
raw = JSON.parse(await readFile(descriptorPath, "utf8"));
|
|
2287
|
+
} catch {
|
|
2288
|
+
throw new Error("managed runtime credentials descriptor is invalid JSON");
|
|
2289
|
+
}
|
|
2290
|
+
return validateManagedRuntimeCredentialsDescriptor(raw)
|
|
2291
|
+
.credentialsCapabilityVersion;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2054
2294
|
async function validateInstalledRuntimeAssets(
|
|
2055
2295
|
root: string,
|
|
2056
2296
|
capabilities: RuntimeCapabilityDescriptorV3,
|
|
@@ -2646,7 +2886,7 @@ async function ensureRecoveryEntrypoint(paths: ManagedRuntimePaths): Promise<voi
|
|
|
2646
2886
|
}
|
|
2647
2887
|
}
|
|
2648
2888
|
|
|
2649
|
-
type RecoveryEntrypointState = "missing" | "legacy-v1" | "current";
|
|
2889
|
+
type RecoveryEntrypointState = "missing" | "legacy-v1" | "legacy-v2" | "current";
|
|
2650
2890
|
|
|
2651
2891
|
/** Classify only entrypoints this release owns exactly. */
|
|
2652
2892
|
async function assertRecoveryEntrypointReplaceable(
|
|
@@ -2669,6 +2909,9 @@ async function assertRecoveryEntrypointReplaceable(
|
|
|
2669
2909
|
if (existing.equals(Buffer.from(recoveryEntrypointContents(paths, 1)))) {
|
|
2670
2910
|
return "legacy-v1";
|
|
2671
2911
|
}
|
|
2912
|
+
if (existing.equals(Buffer.from(recoveryEntrypointContents(paths, 2)))) {
|
|
2913
|
+
return "legacy-v2";
|
|
2914
|
+
}
|
|
2672
2915
|
throw new Error(
|
|
2673
2916
|
`refusing to replace modified host recovery entrypoint at ${paths.recoveryEntrypoint}`,
|
|
2674
2917
|
);
|
|
@@ -2678,13 +2921,57 @@ async function assertRecoveryEntrypointReplaceable(
|
|
|
2678
2921
|
}
|
|
2679
2922
|
}
|
|
2680
2923
|
|
|
2924
|
+
/**
|
|
2925
|
+
* The standalone recovery shell. Revision 3 enforces ADR-154's credentials
|
|
2926
|
+
* floor too: once the activation record or the credentials floor exists, a
|
|
2927
|
+
* previous generation without the credentials marker (built before the
|
|
2928
|
+
* cutover) is refused, so recovery cannot undo a switch to cloud credentials,
|
|
2929
|
+
* even between the runner's local commit and the cloud's. Any change to these
|
|
2930
|
+
* bytes makes a deployed entrypoint "modified", so each revision is a new one
|
|
2931
|
+
* that this release's floors upgrade the older owned revisions to before they
|
|
2932
|
+
* count (commitCredentialsSwitchLocally, claimCredentialsCapabilityFloor).
|
|
2933
|
+
*/
|
|
2681
2934
|
function recoveryEntrypointContents(
|
|
2682
2935
|
paths: ManagedRuntimePaths,
|
|
2683
|
-
revision: 1 | 2 =
|
|
2936
|
+
revision: 1 | 2 | 3 = 3,
|
|
2684
2937
|
): string {
|
|
2685
2938
|
const runtime = shellSingleQuote(paths.runtime);
|
|
2686
2939
|
const capabilityFloor = shellSingleQuote(paths.minimumRuntimeCapability);
|
|
2687
2940
|
const providerState = shellSingleQuote(paths.runtimeProviderState);
|
|
2941
|
+
const credentialsGuard =
|
|
2942
|
+
revision < 3
|
|
2943
|
+
? ""
|
|
2944
|
+
: `credentials_floor=${shellSingleQuote(paths.minimumCredentialsCapability)}
|
|
2945
|
+
activation_record=${shellSingleQuote(paths.credentialsActivationRecord)}
|
|
2946
|
+
required_credentials=0
|
|
2947
|
+
if [ -e "$credentials_floor" ] || [ -L "$credentials_floor" ]; then
|
|
2948
|
+
if [ ! -f "$credentials_floor" ] || [ -L "$credentials_floor" ]; then
|
|
2949
|
+
fail "the credentials floor is invalid"
|
|
2950
|
+
fi
|
|
2951
|
+
required_credentials=$(LC_ALL=C tr -d '[:space:]' < "$credentials_floor" |
|
|
2952
|
+
sed -n 's/^{"schemaVersion":1,"minimumCredentialsCapability":\\([1-9][0-9]*\\)}$/\\1/p')
|
|
2953
|
+
case $required_credentials in
|
|
2954
|
+
'' | *[!0-9]*) fail "the credentials floor is invalid" ;;
|
|
2955
|
+
esac
|
|
2956
|
+
fi
|
|
2957
|
+
if [ "$required_credentials" -lt 1 ] && { [ -e "$activation_record" ] || [ -L "$activation_record" ]; }; then
|
|
2958
|
+
required_credentials=1
|
|
2959
|
+
fi
|
|
2960
|
+
if [ "$required_credentials" -ge 1 ]; then
|
|
2961
|
+
credentials=$generation/agent/managed-runtime-credentials.json
|
|
2962
|
+
if [ ! -f "$credentials" ] || [ -L "$credentials" ]; then
|
|
2963
|
+
fail "the previous runtime predates this host's switch to cloud credentials"
|
|
2964
|
+
fi
|
|
2965
|
+
generation_credentials=$(LC_ALL=C tr -d '[:space:]' < "$credentials" |
|
|
2966
|
+
sed -n 's/^{"credentialsCapabilityVersion":\\([1-9][0-9]*\\)}$/\\1/p')
|
|
2967
|
+
case $generation_credentials in
|
|
2968
|
+
'' | *[!0-9]*) fail "the previous runtime credentials descriptor is invalid" ;;
|
|
2969
|
+
esac
|
|
2970
|
+
if [ "$generation_credentials" -lt "$required_credentials" ]; then
|
|
2971
|
+
fail "the previous runtime predates this host's switch to cloud credentials"
|
|
2972
|
+
fi
|
|
2973
|
+
fi
|
|
2974
|
+
`;
|
|
2688
2975
|
const capabilityProbe =
|
|
2689
2976
|
revision === 1
|
|
2690
2977
|
? ` capability=$generation/agent/bundled-runtime/capability.json
|
|
@@ -2800,7 +3087,7 @@ ${capabilityProbe} case $generation_capability in
|
|
|
2800
3087
|
fail "the previous runtime capability $generation_capability is below this host's floor $required_floor"
|
|
2801
3088
|
fi
|
|
2802
3089
|
fi
|
|
2803
|
-
launcher_dir=$generation/bin
|
|
3090
|
+
${credentialsGuard}launcher_dir=$generation/bin
|
|
2804
3091
|
if [ ! -d "$launcher_dir" ] || [ -L "$launcher_dir" ]; then
|
|
2805
3092
|
fail "the previous managed runtime launcher directory is invalid"
|
|
2806
3093
|
fi
|
|
@@ -2813,6 +3100,14 @@ exec "$launcher" "$@"
|
|
|
2813
3100
|
`;
|
|
2814
3101
|
}
|
|
2815
3102
|
|
|
3103
|
+
/** Tests only: an exact revision's bytes, as it was or is deployed. */
|
|
3104
|
+
export function recoveryEntrypointContentsForTests(
|
|
3105
|
+
paths: ManagedRuntimePaths,
|
|
3106
|
+
revision: 1 | 2 | 3,
|
|
3107
|
+
): string {
|
|
3108
|
+
return recoveryEntrypointContents(paths, revision);
|
|
3109
|
+
}
|
|
3110
|
+
|
|
2816
3111
|
function shellSingleQuote(value: string): string {
|
|
2817
3112
|
return `'${value.split("'").join(`'"'"'`)}'`;
|
|
2818
3113
|
}
|
|
@@ -2936,6 +3231,7 @@ async function recoverRollback(
|
|
|
2936
3231
|
journal.version,
|
|
2937
3232
|
);
|
|
2938
3233
|
await assertRuntimeMeetsDurableCapabilityFloor(paths, targetDescriptor);
|
|
3234
|
+
await assertRuntimeMeetsDurableCredentialsFloor(paths, targetDescriptor);
|
|
2939
3235
|
await atomicVersionLink(paths.current, journal.version);
|
|
2940
3236
|
await atomicVersionLink(paths.previous, journal.fromVersion);
|
|
2941
3237
|
await writeRollbackUpdateHold(paths, journal.fromVersion, journal.version);
|
|
@@ -2955,6 +3251,74 @@ async function assertRuntimeMeetsDurableCapabilityFloor(
|
|
|
2955
3251
|
}
|
|
2956
3252
|
}
|
|
2957
3253
|
|
|
3254
|
+
/** Before the switch every generation passes, exactly as before the cutover. */
|
|
3255
|
+
async function assertRuntimeMeetsDurableCredentialsFloor(
|
|
3256
|
+
paths: ManagedRuntimePaths,
|
|
3257
|
+
descriptor: ResolvedRuntimeDescriptor,
|
|
3258
|
+
): Promise<void> {
|
|
3259
|
+
const minimum = await requiredCredentialsCapability(paths);
|
|
3260
|
+
if (minimum !== null && descriptor.credentialsCapabilityVersion < minimum) {
|
|
3261
|
+
throw new Error(
|
|
3262
|
+
`refusing runtime ${descriptor.hostVersion} with credentials capability ${descriptor.credentialsCapabilityVersion}; this host requires credentials capability ${minimum} since its credentials moved to the cloud`,
|
|
3263
|
+
);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
/** The credentials floor in force: its file's, and at least 1 once the
|
|
3268
|
+
* activation record exists (the switch has begun). */
|
|
3269
|
+
async function requiredCredentialsCapability(paths: ManagedRuntimePaths): Promise<number | null> {
|
|
3270
|
+
const floor = await readMinimumCredentialsCapabilityFile(paths);
|
|
3271
|
+
let recorded = false;
|
|
3272
|
+
try {
|
|
3273
|
+
await lstat(paths.credentialsActivationRecord);
|
|
3274
|
+
recorded = true;
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
if (!isMissing(error)) throw error;
|
|
3277
|
+
}
|
|
3278
|
+
if (!recorded) return floor;
|
|
3279
|
+
return Math.max(floor ?? 0, 1);
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
async function readMinimumCredentialsCapabilityFile(
|
|
3283
|
+
paths: ManagedRuntimePaths,
|
|
3284
|
+
): Promise<number | null> {
|
|
3285
|
+
let bytes: string;
|
|
3286
|
+
try {
|
|
3287
|
+
const stat = await lstat(paths.minimumCredentialsCapability);
|
|
3288
|
+
if (
|
|
3289
|
+
!stat.isFile() ||
|
|
3290
|
+
stat.isSymbolicLink() ||
|
|
3291
|
+
stat.size > MAX_OPERATION_LOCK_BYTES
|
|
3292
|
+
) {
|
|
3293
|
+
throw new Error("managed runtime credentials floor is invalid");
|
|
3294
|
+
}
|
|
3295
|
+
bytes = await readFile(paths.minimumCredentialsCapability, "utf8");
|
|
3296
|
+
} catch (error) {
|
|
3297
|
+
if (isMissing(error)) return null;
|
|
3298
|
+
throw error;
|
|
3299
|
+
}
|
|
3300
|
+
let raw: unknown;
|
|
3301
|
+
try {
|
|
3302
|
+
raw = JSON.parse(bytes);
|
|
3303
|
+
} catch {
|
|
3304
|
+
throw new Error("managed runtime credentials floor is invalid");
|
|
3305
|
+
}
|
|
3306
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
3307
|
+
throw new Error("managed runtime credentials floor is invalid");
|
|
3308
|
+
}
|
|
3309
|
+
const marker = raw as Record<string, unknown>;
|
|
3310
|
+
if (
|
|
3311
|
+
Object.keys(marker).sort().join("\0") !==
|
|
3312
|
+
["minimumCredentialsCapability", "schemaVersion"].join("\0") ||
|
|
3313
|
+
marker.schemaVersion !== 1 ||
|
|
3314
|
+
!Number.isSafeInteger(marker.minimumCredentialsCapability) ||
|
|
3315
|
+
Number(marker.minimumCredentialsCapability) < 1
|
|
3316
|
+
) {
|
|
3317
|
+
throw new Error("managed runtime credentials floor is invalid");
|
|
3318
|
+
}
|
|
3319
|
+
return Number(marker.minimumCredentialsCapability);
|
|
3320
|
+
}
|
|
3321
|
+
|
|
2958
3322
|
async function readMinimumRuntimeCapability(
|
|
2959
3323
|
paths: ManagedRuntimePaths,
|
|
2960
3324
|
): Promise<number | null> {
|