@runuai/host 0.9.147 → 0.9.149
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/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/meta/_journal.json +21 -0
- package/db/schema.ts +312 -2
- package/images/standard/Dockerfile +44 -6
- package/images/standard/container/uai-init +85 -2
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/lib/agent-cli.ts +18 -1
- package/lib/agent.ts +99 -34
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/codex.ts +137 -13
- package/lib/agents/cursor.ts +15 -1
- package/lib/agents/durable-proc.ts +2 -0
- package/lib/agents/grok.ts +8 -0
- package/lib/agents/kimi.ts +1 -0
- package/lib/agents/machine-durable.ts +17 -0
- package/lib/agents/opencode.ts +1 -0
- package/lib/agents/proc.ts +6 -1
- package/lib/agents/session-state.ts +107 -0
- package/lib/agents/transport.ts +125 -58
- package/lib/agents/types.ts +3 -3
- package/lib/browser-testing.ts +6 -0
- package/lib/codex-auth.ts +2 -0
- package/lib/credential-store.ts +141 -6
- package/lib/engine-accounts.ts +1 -0
- package/lib/env-file.ts +7 -0
- package/lib/env-schema.ts +841 -51
- package/lib/git-diff.ts +1 -0
- package/lib/git-identity.ts +2 -0
- package/lib/github-tokens.ts +1 -0
- package/lib/host-env.ts +98 -18
- package/lib/lease.ts +11 -3
- package/lib/machine-gateway-tunnel.ts +195 -17
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/managed-runtime.ts +270 -51
- package/lib/mcp-gateway.ts +777 -26
- package/lib/orchestrator.ts +1681 -138
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-loader.ts +237 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/runtime-archive.ts +79 -16
- package/lib/runtime-authority.ts +44 -0
- package/lib/runtime-state.ts +175 -10
- package/lib/secrets-proxy/address.ts +72 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +60 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +141 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +574 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/shared-files-s3.ts +3 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +51 -3
- package/lib/standard-image.ts +50 -12
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +193 -0
- package/lib/task-environment/apple-container.ts +45 -11
- package/lib/task-environment/docker.ts +51 -12
- package/lib/task-environment/index.ts +6 -2
- package/lib/task-environment/machine-task-up.ts +258 -25
- package/lib/task-environment/machine.ts +80 -22
- package/lib/task-environment/registry.ts +3 -2
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +92 -16
- package/lib/task-environment/types.ts +63 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +44 -0
- package/lib/task-gateway-state.ts +497 -0
- package/lib/transcript.ts +1 -0
- package/lib/ulid.ts +10 -1
- package/package.json +13 -1
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/task-up.sh +476 -27
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/src/cli.ts +26 -3
- package/src/index.ts +273 -19
- package/src/main.ts +941 -48
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +281 -0
- package/src/project-env-wording.ts +47 -0
- package/src/protocol.ts +2239 -15
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import type { TaskEnvironmentHandle } from "./task-environment/types";
|
|
6
|
+
|
|
7
|
+
const STEP_TIMEOUT_MS = 60_000;
|
|
8
|
+
const STEP_OUTPUT_BYTES = 256 * 1024;
|
|
9
|
+
|
|
10
|
+
const INSTALL_ROOT_ASSET_SCRIPT = String.raw`set -eu
|
|
11
|
+
destination=$1
|
|
12
|
+
mode=$2
|
|
13
|
+
case "$destination:$mode" in
|
|
14
|
+
/usr/local/share/uai/env-hook.sh:0555|/usr/local/bin/uai-env-exec:0555|/usr/local/share/uai/uai-task-forwarder.mjs:0444) ;;
|
|
15
|
+
*) exit 1 ;;
|
|
16
|
+
esac
|
|
17
|
+
directory=${"$"}{destination%/*}
|
|
18
|
+
[ ! -L "$directory" ] || exit 1
|
|
19
|
+
[ ! -e "$directory" ] || [ -d "$directory" ] || exit 1
|
|
20
|
+
/usr/bin/install -d -o root -g root -m 0755 "$directory"
|
|
21
|
+
[ ! -L "$destination" ] || exit 1
|
|
22
|
+
[ ! -e "$destination" ] || [ -f "$destination" ] || exit 1
|
|
23
|
+
temporary=$(/usr/bin/mktemp "$directory/.uai-loader.XXXXXX")
|
|
24
|
+
trap '/bin/rm -f -- "$temporary"' EXIT
|
|
25
|
+
/bin/cat > "$temporary"
|
|
26
|
+
/bin/chown root:root "$temporary"
|
|
27
|
+
/bin/chmod "$mode" "$temporary"
|
|
28
|
+
/bin/mv -f -- "$temporary" "$destination"
|
|
29
|
+
trap - EXIT`;
|
|
30
|
+
|
|
31
|
+
const INSTALL_MACHINE_ENV_STUBS_SCRIPT = String.raw`set -eu
|
|
32
|
+
ensure_block() {
|
|
33
|
+
file=$1
|
|
34
|
+
line=$2
|
|
35
|
+
case "$file" in
|
|
36
|
+
/etc/bash.bashrc | /etc/profile.d/uai-env.sh) ;;
|
|
37
|
+
*) exit 1 ;;
|
|
38
|
+
esac
|
|
39
|
+
[ ! -L "$file" ] || exit 1
|
|
40
|
+
[ ! -e "$file" ] || [ -f "$file" ] || exit 1
|
|
41
|
+
directory=${"$"}{file%/*}
|
|
42
|
+
[ -d "$directory" ] && [ ! -L "$directory" ] || exit 1
|
|
43
|
+
temporary=$(/usr/bin/mktemp "$directory/.uai-env-stub.XXXXXX")
|
|
44
|
+
trap '/bin/rm -f -- "$temporary"' EXIT
|
|
45
|
+
if [ -f "$file" ]; then
|
|
46
|
+
/usr/bin/awk '
|
|
47
|
+
$0 == "# >>> uai env loader >>>" { skipping = 1; next }
|
|
48
|
+
$0 == "# <<< uai env loader <<<" { skipping = 0; next }
|
|
49
|
+
!skipping { print }
|
|
50
|
+
' "$file" > "$temporary"
|
|
51
|
+
fi
|
|
52
|
+
{
|
|
53
|
+
printf '%s\n' '# >>> uai env loader >>>'
|
|
54
|
+
printf '%s\n' "$line"
|
|
55
|
+
printf '%s\n' '# <<< uai env loader <<<'
|
|
56
|
+
} >> "$temporary"
|
|
57
|
+
/bin/chown root:root "$temporary"
|
|
58
|
+
/bin/chmod 0644 "$temporary"
|
|
59
|
+
/bin/mv -f -- "$temporary" "$file"
|
|
60
|
+
trap - EXIT
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ensure_block /etc/bash.bashrc '[ "${"$"}{UAI_ENV_HOOK-0}" != 1 ] || { . /usr/local/share/uai/env-hook.sh; __uai_env_sync; __uai_env_install_bash_preexec; }'
|
|
64
|
+
ensure_block /etc/profile.d/uai-env.sh '[ "${"$"}{UAI_ENV_HOOK-0}" != 1 ] || { . /usr/local/share/uai/env-hook.sh; if [ -z "${"$"}{BASH_EXECUTION_STRING-}" ] || [ -z "${"$"}{BASH_ENV-}" ]; then __uai_env_sync; fi; }'`;
|
|
65
|
+
|
|
66
|
+
function packagedLoaderAsset(relativePath: readonly string[]): Buffer {
|
|
67
|
+
const path = resolve(
|
|
68
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
69
|
+
"..",
|
|
70
|
+
...relativePath,
|
|
71
|
+
);
|
|
72
|
+
const metadata = lstatSync(path);
|
|
73
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
74
|
+
throw new Error(`packaged task env loader asset is unsafe: ${path}`);
|
|
75
|
+
}
|
|
76
|
+
return readFileSync(path);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function installRootLoaderAsset(
|
|
80
|
+
handle: Pick<TaskEnvironmentHandle<unknown>, "exec">,
|
|
81
|
+
destination: string,
|
|
82
|
+
mode: "0444" | "0555",
|
|
83
|
+
bytes: Uint8Array,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const result = await handle.exec({
|
|
86
|
+
argv: [
|
|
87
|
+
"/bin/sh",
|
|
88
|
+
"-c",
|
|
89
|
+
INSTALL_ROOT_ASSET_SCRIPT,
|
|
90
|
+
"uai-loader-install",
|
|
91
|
+
destination,
|
|
92
|
+
mode,
|
|
93
|
+
],
|
|
94
|
+
user: "root",
|
|
95
|
+
projectEnvProfile: "maintenance",
|
|
96
|
+
stdin: bytes,
|
|
97
|
+
timeoutMs: STEP_TIMEOUT_MS,
|
|
98
|
+
maxOutputBytes: STEP_OUTPUT_BYTES,
|
|
99
|
+
});
|
|
100
|
+
if (result.exitCode !== 0) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`task env loader asset install failed for ${destination}: ${Buffer.from(result.stderr).toString("utf8").trim()}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Root-owned, idempotent loader bootstrap for machine and fixed-sidecar tasks. */
|
|
108
|
+
export async function installMachineProjectEnvLoader(
|
|
109
|
+
handle: Pick<TaskEnvironmentHandle<unknown>, "exec">,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
await installRootLoaderAsset(
|
|
112
|
+
handle,
|
|
113
|
+
"/usr/local/share/uai/env-hook.sh",
|
|
114
|
+
"0555",
|
|
115
|
+
packagedLoaderAsset(["scripts", "agent", "env-hook.sh"]),
|
|
116
|
+
);
|
|
117
|
+
await installRootLoaderAsset(
|
|
118
|
+
handle,
|
|
119
|
+
"/usr/local/bin/uai-env-exec",
|
|
120
|
+
"0555",
|
|
121
|
+
packagedLoaderAsset(["scripts", "agent", "uai-env-exec.sh"]),
|
|
122
|
+
);
|
|
123
|
+
const stubs = await handle.exec({
|
|
124
|
+
argv: ["/bin/sh", "-c", INSTALL_MACHINE_ENV_STUBS_SCRIPT],
|
|
125
|
+
user: "root",
|
|
126
|
+
projectEnvProfile: "maintenance",
|
|
127
|
+
timeoutMs: STEP_TIMEOUT_MS,
|
|
128
|
+
maxOutputBytes: STEP_OUTPUT_BYTES,
|
|
129
|
+
});
|
|
130
|
+
if (stubs.exitCode !== 0) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`task env shell hook install failed: ${Buffer.from(stubs.stderr).toString("utf8").trim()}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Refresh the process asset before a direct-machine launch or recovery. */
|
|
138
|
+
export function installMachineTaskForwarderAsset(
|
|
139
|
+
handle: Pick<TaskEnvironmentHandle<unknown>, "exec">,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
return installRootLoaderAsset(
|
|
142
|
+
handle,
|
|
143
|
+
"/usr/local/share/uai/uai-task-forwarder.mjs",
|
|
144
|
+
"0444",
|
|
145
|
+
packagedLoaderAsset([
|
|
146
|
+
"images",
|
|
147
|
+
"standard",
|
|
148
|
+
"container",
|
|
149
|
+
"uai-task-forwarder.mjs",
|
|
150
|
+
]),
|
|
151
|
+
);
|
|
152
|
+
}
|
package/lib/managed-runtime.ts
CHANGED
|
@@ -35,11 +35,14 @@ import {
|
|
|
35
35
|
import {
|
|
36
36
|
BUNDLED_RUNTIME_CAPABILITY,
|
|
37
37
|
BUNDLED_RUNTIME_ROOT,
|
|
38
|
+
MANAGED_RUNTIME_CAPABILITY,
|
|
38
39
|
extractVerifiedRuntimeArchive,
|
|
39
40
|
readVerifiedArtifactFile,
|
|
40
41
|
type ResolvedRuntimeDescriptor,
|
|
42
|
+
type ResolvedRuntimeCapabilities,
|
|
41
43
|
type RuntimeCapabilityDescriptorV3,
|
|
42
44
|
type RuntimeAssetManifest,
|
|
45
|
+
validateManagedRuntimeCapabilityDescriptor,
|
|
43
46
|
validateRuntimeAssetManifest,
|
|
44
47
|
validateRuntimeCapabilityDescriptor,
|
|
45
48
|
validateRuntimeDescriptor,
|
|
@@ -57,6 +60,7 @@ const OPERATION_LOCK_POLL_MS = 100;
|
|
|
57
60
|
export const MANAGED_MANIFEST_TIMEOUT_MS = 10_000;
|
|
58
61
|
export const MANAGED_ARTIFACT_TIMEOUT_MS = 10 * 60_000;
|
|
59
62
|
const MAX_OPERATION_LOCK_BYTES = 4 * 1024;
|
|
63
|
+
const MAX_RECOVERY_ENTRYPOINT_BYTES = 16 * 1024;
|
|
60
64
|
const MAX_PACKAGE_JSON_BYTES = 64 * 1024;
|
|
61
65
|
const INCOMPLETE_LOCK_STALE_MS = 5 * 60_000;
|
|
62
66
|
export const MANAGED_UPDATE_RESTART_EXIT_CODE = 75;
|
|
@@ -760,9 +764,7 @@ export async function claimManagedRuntimeCapabilityFloor(
|
|
|
760
764
|
minimumCapabilityVersion = 2,
|
|
761
765
|
home?: string,
|
|
762
766
|
): Promise<void> {
|
|
763
|
-
|
|
764
|
-
throw new Error("managed runtime capability floor is invalid");
|
|
765
|
-
}
|
|
767
|
+
assertManagedRuntimeCapabilityFloor(minimumCapabilityVersion);
|
|
766
768
|
const paths = managedRuntimePaths(home);
|
|
767
769
|
// Waiting acquisition: this runs during runtime detection, concurrently
|
|
768
770
|
// with the startup settlement's own locked read. First-collision-dies took
|
|
@@ -779,16 +781,47 @@ export async function claimManagedRuntimeCapabilityFloor(
|
|
|
779
781
|
) {
|
|
780
782
|
throw new Error("managed runtime activation is already in progress");
|
|
781
783
|
}
|
|
782
|
-
await
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
784
|
+
await persistManagedRuntimeCapabilityFloor(
|
|
785
|
+
paths,
|
|
786
|
+
minimumCapabilityVersion,
|
|
787
|
+
);
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function assertManagedRuntimeCapabilityFloor(
|
|
792
|
+
minimumCapabilityVersion: number,
|
|
793
|
+
): void {
|
|
794
|
+
if (
|
|
795
|
+
!Number.isSafeInteger(minimumCapabilityVersion) ||
|
|
796
|
+
minimumCapabilityVersion < 2
|
|
797
|
+
) {
|
|
798
|
+
throw new Error("managed runtime capability floor is invalid");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** Persist a floor while the caller holds the managed-operation lock. */
|
|
803
|
+
async function persistManagedRuntimeCapabilityFloor(
|
|
804
|
+
paths: ManagedRuntimePaths,
|
|
805
|
+
minimumCapabilityVersion: number,
|
|
806
|
+
): Promise<void> {
|
|
807
|
+
// The recovery entrypoint is a second rollback path, independent of this
|
|
808
|
+
// TypeScript process. A 0.9.148 updater leaves its exact v1 entrypoint in
|
|
809
|
+
// place when it boots the first capability-4 generation. Upgrade that
|
|
810
|
+
// owned file before making the floor durable, so standalone recovery can
|
|
811
|
+
// recognize the new cross-platform capability marker immediately.
|
|
812
|
+
const recoveryEntrypoint = await assertRecoveryEntrypointReplaceable(paths);
|
|
813
|
+
if (recoveryEntrypoint === "legacy-v1") {
|
|
814
|
+
await ensureRecoveryEntrypoint(paths);
|
|
815
|
+
}
|
|
816
|
+
await mkdir(path.dirname(paths.minimumRuntimeCapability), {
|
|
817
|
+
recursive: true,
|
|
818
|
+
mode: 0o700,
|
|
819
|
+
});
|
|
820
|
+
const existing = await readMinimumRuntimeCapability(paths);
|
|
821
|
+
if (existing !== null && existing >= minimumCapabilityVersion) return;
|
|
822
|
+
await writeAtomicJson(paths.minimumRuntimeCapability, {
|
|
823
|
+
schemaVersion: 1,
|
|
824
|
+
minimumRuntimeCapability: minimumCapabilityVersion,
|
|
792
825
|
});
|
|
793
826
|
}
|
|
794
827
|
|
|
@@ -915,6 +948,31 @@ export async function reconcileManagedRuntimeActivation(
|
|
|
915
948
|
return managedRuntimeRestartRequired(runningVersion, home);
|
|
916
949
|
}
|
|
917
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Provisional installs settle after their install intent disappears, through
|
|
953
|
+
* the activation watcher rather than cmdRun's immediate acknowledgement.
|
|
954
|
+
* Preserve the same floor-before-unfence transaction on that path.
|
|
955
|
+
*/
|
|
956
|
+
export async function reconcileManagedRuntimeActivationWithCapabilityFloor(
|
|
957
|
+
runningVersion: string,
|
|
958
|
+
minimumCapabilityVersion: number,
|
|
959
|
+
onCapabilityFloorPersisted: () => void,
|
|
960
|
+
home?: string,
|
|
961
|
+
): Promise<boolean> {
|
|
962
|
+
if (pathExistsSync(managedRuntimePaths(home).installIntent)) return false;
|
|
963
|
+
if (managedRuntimeActivationPending(home)) {
|
|
964
|
+
return !(
|
|
965
|
+
await acknowledgeManagedRuntimeActivationWithCapabilityFloor(
|
|
966
|
+
runningVersion,
|
|
967
|
+
minimumCapabilityVersion,
|
|
968
|
+
onCapabilityFloorPersisted,
|
|
969
|
+
home,
|
|
970
|
+
)
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
return managedRuntimeRestartRequired(runningVersion, home);
|
|
974
|
+
}
|
|
975
|
+
|
|
918
976
|
/**
|
|
919
977
|
* Prove that this process is the runtime selected by `current` without opening
|
|
920
978
|
* task admission. `cmdRun` performs this before importing the service module;
|
|
@@ -924,7 +982,7 @@ export async function validateManagedRuntimeSelection(
|
|
|
924
982
|
runningVersion: string,
|
|
925
983
|
home?: string,
|
|
926
984
|
): Promise<boolean> {
|
|
927
|
-
return reconcileManagedRuntimeSelection(runningVersion, home, false);
|
|
985
|
+
return reconcileManagedRuntimeSelection(runningVersion, home, false, null);
|
|
928
986
|
}
|
|
929
987
|
|
|
930
988
|
/**
|
|
@@ -932,7 +990,7 @@ export async function validateManagedRuntimeSelection(
|
|
|
932
990
|
* operation lock held by its own service-definition installer. This never
|
|
933
991
|
* acknowledges activation: the live install intent remains the task-admission
|
|
934
992
|
* fence until shell attestation commits, after which the activation watcher
|
|
935
|
-
* performs the
|
|
993
|
+
* performs the capability-aware locked acknowledgement.
|
|
936
994
|
*/
|
|
937
995
|
export async function validateProvisionalManagedRuntimeSelection(
|
|
938
996
|
runningVersion: string,
|
|
@@ -973,17 +1031,50 @@ export async function acknowledgeManagedRuntimeActivation(
|
|
|
973
1031
|
runningVersion: string,
|
|
974
1032
|
home?: string,
|
|
975
1033
|
): Promise<boolean> {
|
|
976
|
-
return reconcileManagedRuntimeSelection(runningVersion, home, true);
|
|
1034
|
+
return reconcileManagedRuntimeSelection(runningVersion, home, true, null);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Acknowledge a healthy first boot without opening task admission between the
|
|
1039
|
+
* irreversible floor and its in-process capability state. The callback must
|
|
1040
|
+
* synchronously publish only local state: it runs after the floor is durable
|
|
1041
|
+
* and while activationPending still fences every TaskUp.
|
|
1042
|
+
*/
|
|
1043
|
+
export async function acknowledgeManagedRuntimeActivationWithCapabilityFloor(
|
|
1044
|
+
runningVersion: string,
|
|
1045
|
+
minimumCapabilityVersion: number,
|
|
1046
|
+
onCapabilityFloorPersisted: () => void,
|
|
1047
|
+
home?: string,
|
|
1048
|
+
): Promise<boolean> {
|
|
1049
|
+
assertManagedRuntimeCapabilityFloor(minimumCapabilityVersion);
|
|
1050
|
+
if (typeof onCapabilityFloorPersisted !== "function") {
|
|
1051
|
+
throw new Error("managed runtime capability callback is invalid");
|
|
1052
|
+
}
|
|
1053
|
+
return reconcileManagedRuntimeSelection(runningVersion, home, true, {
|
|
1054
|
+
minimumCapabilityVersion,
|
|
1055
|
+
onCapabilityFloorPersisted,
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
interface ManagedRuntimeCapabilityAcknowledgement {
|
|
1060
|
+
readonly minimumCapabilityVersion: number;
|
|
1061
|
+
readonly onCapabilityFloorPersisted: () => void;
|
|
977
1062
|
}
|
|
978
1063
|
|
|
979
1064
|
async function reconcileManagedRuntimeSelection(
|
|
980
1065
|
runningVersion: string,
|
|
981
1066
|
home: string | undefined,
|
|
982
1067
|
acknowledgeHealthyImport: boolean,
|
|
1068
|
+
capabilityAcknowledgement: ManagedRuntimeCapabilityAcknowledgement | null,
|
|
983
1069
|
): Promise<boolean> {
|
|
984
1070
|
assertStableVersion(runningVersion);
|
|
985
1071
|
const paths = managedRuntimePaths(home);
|
|
986
|
-
if (!(await pathExists(paths.root)))
|
|
1072
|
+
if (!(await pathExists(paths.root))) {
|
|
1073
|
+
if (capabilityAcknowledgement !== null) {
|
|
1074
|
+
throw new Error("managed runtime capability acknowledgement has no runtime");
|
|
1075
|
+
}
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
987
1078
|
// Busy handling is holder-dependent. An INSTALLER-owned lock (install
|
|
988
1079
|
// intent on disk) must fail fast: a provisional boot defers to the
|
|
989
1080
|
// installer, and the intent-hold test pins that contract. A lock held by a
|
|
@@ -997,6 +1088,7 @@ async function reconcileManagedRuntimeSelection(
|
|
|
997
1088
|
runningVersion,
|
|
998
1089
|
paths,
|
|
999
1090
|
acknowledgeHealthyImport,
|
|
1091
|
+
capabilityAcknowledgement,
|
|
1000
1092
|
);
|
|
1001
1093
|
} catch (error) {
|
|
1002
1094
|
if (
|
|
@@ -1017,18 +1109,54 @@ async function reconcileManagedRuntimeSelectionLocked(
|
|
|
1017
1109
|
runningVersion: string,
|
|
1018
1110
|
paths: ManagedRuntimePaths,
|
|
1019
1111
|
acknowledgeHealthyImport: boolean,
|
|
1112
|
+
capabilityAcknowledgement: ManagedRuntimeCapabilityAcknowledgement | null,
|
|
1020
1113
|
): Promise<boolean> {
|
|
1021
1114
|
return withManagedOperationLock(paths, async () => {
|
|
1022
1115
|
await ensureManagedDirectories(paths);
|
|
1023
1116
|
await recoverRollback(paths);
|
|
1024
1117
|
const selectedVersion = await readVersionLink(paths.current, paths.runtime);
|
|
1118
|
+
let selectedDescriptor: ResolvedRuntimeDescriptor | null = null;
|
|
1025
1119
|
if (selectedVersion !== null) {
|
|
1026
|
-
|
|
1120
|
+
selectedDescriptor = await validateInstalledRuntime(
|
|
1027
1121
|
path.join(paths.runtime, selectedVersion),
|
|
1028
1122
|
selectedVersion,
|
|
1029
1123
|
);
|
|
1030
1124
|
await assertRuntimeMeetsDurableCapabilityFloor(paths, selectedDescriptor);
|
|
1031
1125
|
}
|
|
1126
|
+
const persistAcknowledgedCapabilityFloor = async (): Promise<void> => {
|
|
1127
|
+
if (capabilityAcknowledgement === null) return;
|
|
1128
|
+
if (
|
|
1129
|
+
selectedVersion !== runningVersion ||
|
|
1130
|
+
selectedDescriptor === null
|
|
1131
|
+
) {
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
"managed runtime capability acknowledgement has no selected runtime",
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
if (
|
|
1137
|
+
selectedDescriptor.runtimeCapabilityVersion <
|
|
1138
|
+
capabilityAcknowledgement.minimumCapabilityVersion
|
|
1139
|
+
) {
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
`refusing runtime capability ${selectedDescriptor.runtimeCapabilityVersion}; activation requires capability ${capabilityAcknowledgement.minimumCapabilityVersion}`,
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
await assertNoManagedInstallIntent(
|
|
1145
|
+
paths,
|
|
1146
|
+
"runtime capability acknowledgement",
|
|
1147
|
+
);
|
|
1148
|
+
if (pathExistsSync(paths.maintenancePending)) {
|
|
1149
|
+
throw new Error("managed runtime maintenance is already in progress");
|
|
1150
|
+
}
|
|
1151
|
+
await persistManagedRuntimeCapabilityFloor(
|
|
1152
|
+
paths,
|
|
1153
|
+
capabilityAcknowledgement.minimumCapabilityVersion,
|
|
1154
|
+
);
|
|
1155
|
+
const callbackResult = capabilityAcknowledgement.onCapabilityFloorPersisted();
|
|
1156
|
+
if (callbackResult !== undefined) {
|
|
1157
|
+
throw new Error("managed runtime capability callback must be synchronous");
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1032
1160
|
let marker: ActivationMarker;
|
|
1033
1161
|
try {
|
|
1034
1162
|
marker = parseActivationMarker(
|
|
@@ -1041,7 +1169,11 @@ async function reconcileManagedRuntimeSelectionLocked(
|
|
|
1041
1169
|
// before allowing this process to continue.
|
|
1042
1170
|
if (isMissing(error)) {
|
|
1043
1171
|
const current = await readVersionLink(paths.current, paths.runtime);
|
|
1044
|
-
|
|
1172
|
+
const selected = current === null || current === runningVersion;
|
|
1173
|
+
if (selected && acknowledgeHealthyImport) {
|
|
1174
|
+
await persistAcknowledgedCapabilityFloor();
|
|
1175
|
+
}
|
|
1176
|
+
return selected;
|
|
1045
1177
|
}
|
|
1046
1178
|
throw error;
|
|
1047
1179
|
}
|
|
@@ -1055,6 +1187,7 @@ async function reconcileManagedRuntimeSelectionLocked(
|
|
|
1055
1187
|
}
|
|
1056
1188
|
if (marker.version !== runningVersion) return false;
|
|
1057
1189
|
if (acknowledgeHealthyImport) {
|
|
1190
|
+
await persistAcknowledgedCapabilityFloor();
|
|
1058
1191
|
await removeDurably(paths.activationPending);
|
|
1059
1192
|
}
|
|
1060
1193
|
return true;
|
|
@@ -1066,6 +1199,7 @@ async function reconcileManagedRuntimeSelectionLocked(
|
|
|
1066
1199
|
// runtime.
|
|
1067
1200
|
const selected = current === null || current === runningVersion;
|
|
1068
1201
|
if (selected && acknowledgeHealthyImport) {
|
|
1202
|
+
await persistAcknowledgedCapabilityFloor();
|
|
1069
1203
|
await removeDurably(paths.activationPending);
|
|
1070
1204
|
}
|
|
1071
1205
|
return selected;
|
|
@@ -1733,8 +1867,8 @@ async function restorePreInstallRuntimeSelection(
|
|
|
1733
1867
|
await removeOwnedSymlink(paths.recoveryShim, paths.recoveryEntrypoint);
|
|
1734
1868
|
}
|
|
1735
1869
|
if (!intent.recoveryEntrypointExisted) {
|
|
1736
|
-
const
|
|
1737
|
-
if (
|
|
1870
|
+
const state = await assertRecoveryEntrypointReplaceable(paths);
|
|
1871
|
+
if (state !== "missing") await removeDurably(paths.recoveryEntrypoint);
|
|
1738
1872
|
}
|
|
1739
1873
|
}
|
|
1740
1874
|
|
|
@@ -1834,6 +1968,22 @@ export async function validateInstalledRuntime(
|
|
|
1834
1968
|
async function resolveInstalledRuntimeCapabilities(
|
|
1835
1969
|
root: string,
|
|
1836
1970
|
platform: string,
|
|
1971
|
+
): Promise<ResolvedRuntimeCapabilities> {
|
|
1972
|
+
const platformCapabilities = await resolveInstalledPlatformCapabilities(
|
|
1973
|
+
root,
|
|
1974
|
+
platform,
|
|
1975
|
+
);
|
|
1976
|
+
const runtimeCapabilityVersion =
|
|
1977
|
+
await resolveInstalledManagedRuntimeCapability(
|
|
1978
|
+
root,
|
|
1979
|
+
platformCapabilities.capabilitySchemaVersion,
|
|
1980
|
+
);
|
|
1981
|
+
return { ...platformCapabilities, runtimeCapabilityVersion };
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
async function resolveInstalledPlatformCapabilities(
|
|
1985
|
+
root: string,
|
|
1986
|
+
platform: string,
|
|
1837
1987
|
): Promise<{ readonly capabilitySchemaVersion: 1 } | RuntimeCapabilityDescriptorV3> {
|
|
1838
1988
|
const bundledRoot = path.join(root, ...BUNDLED_RUNTIME_ROOT.split("/"));
|
|
1839
1989
|
let bundledMetadata;
|
|
@@ -1868,6 +2018,39 @@ async function resolveInstalledRuntimeCapabilities(
|
|
|
1868
2018
|
return validateRuntimeCapabilityDescriptor(raw, platform);
|
|
1869
2019
|
}
|
|
1870
2020
|
|
|
2021
|
+
async function resolveInstalledManagedRuntimeCapability(
|
|
2022
|
+
root: string,
|
|
2023
|
+
legacyCapabilityVersion: 1 | 3,
|
|
2024
|
+
): Promise<1 | 3 | 4> {
|
|
2025
|
+
const capabilityPath = path.join(
|
|
2026
|
+
root,
|
|
2027
|
+
...MANAGED_RUNTIME_CAPABILITY.split("/"),
|
|
2028
|
+
);
|
|
2029
|
+
let metadata;
|
|
2030
|
+
try {
|
|
2031
|
+
metadata = await lstat(capabilityPath);
|
|
2032
|
+
} catch (error) {
|
|
2033
|
+
if (isMissing(error)) return legacyCapabilityVersion;
|
|
2034
|
+
throw error;
|
|
2035
|
+
}
|
|
2036
|
+
if (
|
|
2037
|
+
!metadata.isFile() ||
|
|
2038
|
+
metadata.isSymbolicLink() ||
|
|
2039
|
+
metadata.size > MAX_OPERATION_LOCK_BYTES ||
|
|
2040
|
+
!isInside(root, await realpath(capabilityPath))
|
|
2041
|
+
) {
|
|
2042
|
+
throw new Error("managed runtime capability descriptor is invalid");
|
|
2043
|
+
}
|
|
2044
|
+
let raw: unknown;
|
|
2045
|
+
try {
|
|
2046
|
+
raw = JSON.parse(await readFile(capabilityPath, "utf8"));
|
|
2047
|
+
} catch {
|
|
2048
|
+
throw new Error("managed runtime capability descriptor is invalid JSON");
|
|
2049
|
+
}
|
|
2050
|
+
return validateManagedRuntimeCapabilityDescriptor(raw)
|
|
2051
|
+
.capabilitySchemaVersion;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
1871
2054
|
async function validateInstalledRuntimeAssets(
|
|
1872
2055
|
root: string,
|
|
1873
2056
|
capabilities: RuntimeCapabilityDescriptorV3,
|
|
@@ -2106,9 +2289,8 @@ async function capturePreInstallState(paths: ManagedRuntimePaths): Promise<
|
|
|
2106
2289
|
`refusing to replace unrelated host recovery command at ${paths.recoveryShim}`,
|
|
2107
2290
|
);
|
|
2108
2291
|
}
|
|
2109
|
-
const recoveryEntrypointExisted =
|
|
2110
|
-
paths
|
|
2111
|
-
);
|
|
2292
|
+
const recoveryEntrypointExisted =
|
|
2293
|
+
(await assertRecoveryEntrypointReplaceable(paths)) !== "missing";
|
|
2112
2294
|
return {
|
|
2113
2295
|
rootExisted,
|
|
2114
2296
|
currentVersion,
|
|
@@ -2456,18 +2638,20 @@ async function ensureRecoveryEntrypoint(paths: ManagedRuntimePaths): Promise<voi
|
|
|
2456
2638
|
}
|
|
2457
2639
|
await chmod(directory, 0o700);
|
|
2458
2640
|
const expected = recoveryEntrypointContents(paths);
|
|
2459
|
-
const
|
|
2460
|
-
if (
|
|
2641
|
+
const state = await assertRecoveryEntrypointReplaceable(paths);
|
|
2642
|
+
if (state !== "current") {
|
|
2461
2643
|
await writeAtomicExecutable(paths.recoveryEntrypoint, expected);
|
|
2462
2644
|
} else {
|
|
2463
2645
|
await chmod(paths.recoveryEntrypoint, 0o700);
|
|
2464
2646
|
}
|
|
2465
2647
|
}
|
|
2466
2648
|
|
|
2467
|
-
|
|
2649
|
+
type RecoveryEntrypointState = "missing" | "legacy-v1" | "current";
|
|
2650
|
+
|
|
2651
|
+
/** Classify only entrypoints this release owns exactly. */
|
|
2468
2652
|
async function assertRecoveryEntrypointReplaceable(
|
|
2469
2653
|
paths: ManagedRuntimePaths,
|
|
2470
|
-
): Promise<
|
|
2654
|
+
): Promise<RecoveryEntrypointState> {
|
|
2471
2655
|
try {
|
|
2472
2656
|
const stat = await lstat(paths.recoveryEntrypoint);
|
|
2473
2657
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
@@ -2475,25 +2659,71 @@ async function assertRecoveryEntrypointReplaceable(
|
|
|
2475
2659
|
`refusing to replace non-regular host recovery entrypoint at ${paths.recoveryEntrypoint}`,
|
|
2476
2660
|
);
|
|
2477
2661
|
}
|
|
2478
|
-
const existing = await readBoundedRegularFile(
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2662
|
+
const existing = await readBoundedRegularFile(
|
|
2663
|
+
paths.recoveryEntrypoint,
|
|
2664
|
+
MAX_RECOVERY_ENTRYPOINT_BYTES,
|
|
2665
|
+
);
|
|
2666
|
+
if (existing.equals(Buffer.from(recoveryEntrypointContents(paths)))) {
|
|
2667
|
+
return "current";
|
|
2483
2668
|
}
|
|
2484
|
-
|
|
2669
|
+
if (existing.equals(Buffer.from(recoveryEntrypointContents(paths, 1)))) {
|
|
2670
|
+
return "legacy-v1";
|
|
2671
|
+
}
|
|
2672
|
+
throw new Error(
|
|
2673
|
+
`refusing to replace modified host recovery entrypoint at ${paths.recoveryEntrypoint}`,
|
|
2674
|
+
);
|
|
2485
2675
|
} catch (error) {
|
|
2486
|
-
if (isMissing(error)) return
|
|
2676
|
+
if (isMissing(error)) return "missing";
|
|
2487
2677
|
throw error;
|
|
2488
2678
|
}
|
|
2489
2679
|
}
|
|
2490
2680
|
|
|
2491
|
-
function recoveryEntrypointContents(
|
|
2681
|
+
function recoveryEntrypointContents(
|
|
2682
|
+
paths: ManagedRuntimePaths,
|
|
2683
|
+
revision: 1 | 2 = 2,
|
|
2684
|
+
): string {
|
|
2492
2685
|
const runtime = shellSingleQuote(paths.runtime);
|
|
2493
2686
|
const capabilityFloor = shellSingleQuote(paths.minimumRuntimeCapability);
|
|
2494
2687
|
const providerState = shellSingleQuote(paths.runtimeProviderState);
|
|
2688
|
+
const capabilityProbe =
|
|
2689
|
+
revision === 1
|
|
2690
|
+
? ` capability=$generation/agent/bundled-runtime/capability.json
|
|
2691
|
+
# The TypeScript rollback/install path validates the exact JSON descriptor
|
|
2692
|
+
# and closed asset tree. This stable recovery guard runs only after those
|
|
2693
|
+
# authenticated generation checks and compares the generation's numeric
|
|
2694
|
+
# capability schema against the preserved floor, so a lower-capability
|
|
2695
|
+
# generation can never be launched over provider state that outranks it.
|
|
2696
|
+
if [ ! -f "$capability" ] || [ -L "$capability" ]; then
|
|
2697
|
+
fail "the previous runtime predates this host's provider state"
|
|
2698
|
+
fi
|
|
2699
|
+
generation_capability=$(LC_ALL=C tr -d '[:space:]' < "$capability" |
|
|
2700
|
+
sed -n 's/.*"capabilitySchemaVersion":\\([0-9][0-9]*\\).*/\\1/p')
|
|
2701
|
+
`
|
|
2702
|
+
: ` managed_capability=$generation/agent/managed-runtime-capability.json
|
|
2703
|
+
capability=$generation/agent/bundled-runtime/capability.json
|
|
2704
|
+
# The TypeScript rollback/install path validates the exact JSON descriptor
|
|
2705
|
+
# and (on Darwin) the closed asset tree. The cross-platform managed marker
|
|
2706
|
+
# was added compatibly at capability 4; older generations derive their
|
|
2707
|
+
# effective capability from the Darwin-only bundled descriptor.
|
|
2708
|
+
if [ -e "$managed_capability" ] || [ -L "$managed_capability" ]; then
|
|
2709
|
+
if [ ! -f "$managed_capability" ] || [ -L "$managed_capability" ]; then
|
|
2710
|
+
fail "the previous runtime capability descriptor is invalid"
|
|
2711
|
+
fi
|
|
2712
|
+
if ! LC_ALL=C tr -d '[:space:]' < "$managed_capability" |
|
|
2713
|
+
grep -Eq '^\\{"capabilitySchemaVersion":4\\}$'; then
|
|
2714
|
+
fail "the previous runtime capability descriptor is invalid"
|
|
2715
|
+
fi
|
|
2716
|
+
generation_capability=4
|
|
2717
|
+
else
|
|
2718
|
+
if [ ! -f "$capability" ] || [ -L "$capability" ]; then
|
|
2719
|
+
fail "the previous runtime predates this host's provider state"
|
|
2720
|
+
fi
|
|
2721
|
+
generation_capability=$(LC_ALL=C tr -d '[:space:]' < "$capability" |
|
|
2722
|
+
sed -n 's/.*"capabilitySchemaVersion":\\([0-9][0-9]*\\).*/\\1/p')
|
|
2723
|
+
fi
|
|
2724
|
+
`;
|
|
2495
2725
|
return `#!/bin/sh
|
|
2496
|
-
# uai-managed-host-recovery-
|
|
2726
|
+
# uai-managed-host-recovery-v${revision}
|
|
2497
2727
|
set -eu
|
|
2498
2728
|
|
|
2499
2729
|
runtime=${runtime}
|
|
@@ -2563,18 +2793,7 @@ if [ "$floor_required" = true ]; then
|
|
|
2563
2793
|
case $required_floor in
|
|
2564
2794
|
'' | *[!0-9]*) fail "the managed runtime capability floor is invalid" ;;
|
|
2565
2795
|
esac
|
|
2566
|
-
|
|
2567
|
-
# The TypeScript rollback/install path validates the exact JSON descriptor
|
|
2568
|
-
# and closed asset tree. This stable recovery guard runs only after those
|
|
2569
|
-
# authenticated generation checks and compares the generation's numeric
|
|
2570
|
-
# capability schema against the preserved floor, so a lower-capability
|
|
2571
|
-
# generation can never be launched over provider state that outranks it.
|
|
2572
|
-
if [ ! -f "$capability" ] || [ -L "$capability" ]; then
|
|
2573
|
-
fail "the previous runtime predates this host's provider state"
|
|
2574
|
-
fi
|
|
2575
|
-
generation_capability=$(LC_ALL=C tr -d '[:space:]' < "$capability" |
|
|
2576
|
-
sed -n 's/.*"capabilitySchemaVersion":\\([0-9][0-9]*\\).*/\\1/p')
|
|
2577
|
-
case $generation_capability in
|
|
2796
|
+
${capabilityProbe} case $generation_capability in
|
|
2578
2797
|
'' | *[!0-9]*) fail "the previous runtime capability descriptor is invalid" ;;
|
|
2579
2798
|
esac
|
|
2580
2799
|
if [ "$generation_capability" -lt "$required_floor" ]; then
|
|
@@ -2729,9 +2948,9 @@ async function assertRuntimeMeetsDurableCapabilityFloor(
|
|
|
2729
2948
|
descriptor: ResolvedRuntimeDescriptor,
|
|
2730
2949
|
): Promise<void> {
|
|
2731
2950
|
const minimum = await readMinimumRuntimeCapability(paths);
|
|
2732
|
-
if (minimum !== null && descriptor.
|
|
2951
|
+
if (minimum !== null && descriptor.runtimeCapabilityVersion < minimum) {
|
|
2733
2952
|
throw new Error(
|
|
2734
|
-
`refusing runtime capability ${descriptor.
|
|
2953
|
+
`refusing runtime capability ${descriptor.runtimeCapabilityVersion}; this host requires capability ${minimum} after the bundled runtime owned task state`,
|
|
2735
2954
|
);
|
|
2736
2955
|
}
|
|
2737
2956
|
}
|