@runuai/host 0.9.148 → 0.9.150

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.
Files changed (93) hide show
  1. package/db/migrations/0024_task_env_mode.sql +9 -0
  2. package/db/migrations/0025_project_env_authority.sql +147 -0
  3. package/db/migrations/0026_project_env_replica.sql +150 -0
  4. package/db/migrations/meta/_journal.json +21 -0
  5. package/db/schema.ts +312 -2
  6. package/images/standard/Dockerfile +44 -6
  7. package/images/standard/container/uai-init +85 -2
  8. package/images/standard/container/uai-task-forwarder.mjs +303 -0
  9. package/lib/agent-cli.ts +18 -1
  10. package/lib/agent.ts +99 -34
  11. package/lib/agents/codex.ts +9 -1
  12. package/lib/agents/cursor.ts +15 -1
  13. package/lib/agents/grok.ts +8 -0
  14. package/lib/agents/kimi.ts +1 -0
  15. package/lib/agents/machine-durable.ts +2 -0
  16. package/lib/agents/opencode.ts +1 -0
  17. package/lib/agents/proc.ts +6 -1
  18. package/lib/agents/transport.ts +9 -5
  19. package/lib/browser-testing.ts +6 -0
  20. package/lib/codex-auth.ts +2 -0
  21. package/lib/credential-store.ts +141 -6
  22. package/lib/engine-accounts.ts +1 -0
  23. package/lib/env-file.ts +7 -0
  24. package/lib/env-schema.ts +841 -51
  25. package/lib/git-diff.ts +1 -0
  26. package/lib/git-identity.ts +2 -0
  27. package/lib/github-tokens.ts +1 -0
  28. package/lib/host-env.ts +98 -18
  29. package/lib/lease.ts +11 -3
  30. package/lib/machine-gateway-tunnel.ts +195 -17
  31. package/lib/machine-project-env-loader.ts +152 -0
  32. package/lib/managed-runtime.ts +270 -51
  33. package/lib/mcp-gateway.ts +777 -26
  34. package/lib/orchestrator.ts +408 -81
  35. package/lib/project-env-authority.ts +1319 -0
  36. package/lib/project-env-feature.ts +74 -0
  37. package/lib/project-env-loader.ts +237 -0
  38. package/lib/project-env-replica.ts +2094 -0
  39. package/lib/project-env-schema-capture.ts +342 -0
  40. package/lib/project-env-staging.ts +148 -0
  41. package/lib/project-env-task-policy.ts +176 -0
  42. package/lib/runtime-archive.ts +79 -16
  43. package/lib/runtime-authority.ts +44 -0
  44. package/lib/runtime-state.ts +175 -10
  45. package/lib/secrets-proxy/address.ts +72 -0
  46. package/lib/secrets-proxy/audit.ts +30 -0
  47. package/lib/secrets-proxy/ca-store.ts +200 -0
  48. package/lib/secrets-proxy/ca.ts +470 -0
  49. package/lib/secrets-proxy/container-steps.ts +138 -0
  50. package/lib/secrets-proxy/der.ts +273 -0
  51. package/lib/secrets-proxy/engine.ts +1633 -0
  52. package/lib/secrets-proxy/host.ts +93 -0
  53. package/lib/secrets-proxy/hostname.ts +60 -0
  54. package/lib/secrets-proxy/placeholders.ts +196 -0
  55. package/lib/secrets-proxy/policy.ts +141 -0
  56. package/lib/secrets-proxy/readiness.ts +148 -0
  57. package/lib/secrets-proxy/runtime.ts +582 -0
  58. package/lib/secrets-proxy/scrub.ts +559 -0
  59. package/lib/secrets-proxy/sidecar-view.ts +139 -0
  60. package/lib/secrets-proxy/task-scripts.ts +262 -0
  61. package/lib/secrets-proxy/task-trust.ts +574 -0
  62. package/lib/secrets-proxy/trust-watch.ts +80 -0
  63. package/lib/shared-files-s3.ts +3 -0
  64. package/lib/sidecar-guard.ts +58 -0
  65. package/lib/sidecar-resume.ts +51 -3
  66. package/lib/standard-image.ts +50 -12
  67. package/lib/task-env-forwarder.ts +228 -0
  68. package/lib/task-env-source.ts +193 -0
  69. package/lib/task-environment/apple-container.ts +45 -11
  70. package/lib/task-environment/docker.ts +51 -12
  71. package/lib/task-environment/index.ts +6 -2
  72. package/lib/task-environment/machine-task-up.ts +258 -25
  73. package/lib/task-environment/machine.ts +80 -22
  74. package/lib/task-environment/registry.ts +3 -2
  75. package/lib/task-environment/self-identity.ts +20 -0
  76. package/lib/task-environment/self.ts +92 -16
  77. package/lib/task-environment/types.ts +63 -0
  78. package/lib/task-gateway-port.ts +22 -0
  79. package/lib/task-gateway-protocol.ts +190 -0
  80. package/lib/task-gateway-recovery.ts +44 -0
  81. package/lib/task-gateway-state.ts +497 -0
  82. package/lib/transcript.ts +1 -0
  83. package/package.json +13 -1
  84. package/scripts/agent/env-hook.sh +626 -0
  85. package/scripts/agent/task-up.sh +476 -27
  86. package/scripts/agent/uai-env-exec.sh +26 -0
  87. package/src/cli.ts +26 -3
  88. package/src/index.ts +273 -19
  89. package/src/main.ts +939 -48
  90. package/src/project-env-latest.ts +130 -0
  91. package/src/project-env-policy.ts +281 -0
  92. package/src/project-env-wording.ts +47 -0
  93. package/src/protocol.ts +2219 -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
+ }
@@ -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
- if (!Number.isSafeInteger(minimumCapabilityVersion) || minimumCapabilityVersion < 2) {
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 mkdir(path.dirname(paths.minimumRuntimeCapability), {
783
- recursive: true,
784
- mode: 0o700,
785
- });
786
- const existing = await readMinimumRuntimeCapability(paths);
787
- if (existing !== null && existing >= minimumCapabilityVersion) return;
788
- await writeAtomicJson(paths.minimumRuntimeCapability, {
789
- schemaVersion: 1,
790
- minimumRuntimeCapability: minimumCapabilityVersion,
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 ordinary locked acknowledgement.
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))) return true;
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
- const selectedDescriptor = await validateInstalledRuntime(
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
- return current === null || current === runningVersion;
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 exists = await assertRecoveryEntrypointReplaceable(paths);
1737
- if (exists) await removeDurably(paths.recoveryEntrypoint);
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 = await assertRecoveryEntrypointReplaceable(
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 exists = await assertRecoveryEntrypointReplaceable(paths);
2460
- if (!exists) {
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
- /** Return true when the exact managed entrypoint already exists. */
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<boolean> {
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(paths.recoveryEntrypoint);
2479
- if (!existing.equals(Buffer.from(recoveryEntrypointContents(paths)))) {
2480
- throw new Error(
2481
- `refusing to replace modified host recovery entrypoint at ${paths.recoveryEntrypoint}`,
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
- return true;
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 false;
2676
+ if (isMissing(error)) return "missing";
2487
2677
  throw error;
2488
2678
  }
2489
2679
  }
2490
2680
 
2491
- function recoveryEntrypointContents(paths: ManagedRuntimePaths): string {
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-v1
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
- capability=$generation/agent/bundled-runtime/capability.json
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.capabilitySchemaVersion < minimum) {
2951
+ if (minimum !== null && descriptor.runtimeCapabilityVersion < minimum) {
2733
2952
  throw new Error(
2734
- `refusing runtime capability ${descriptor.capabilitySchemaVersion}; this host requires capability ${minimum} after the bundled runtime owned task state`,
2953
+ `refusing runtime capability ${descriptor.runtimeCapabilityVersion}; this host requires capability ${minimum} after the bundled runtime owned task state`,
2735
2954
  );
2736
2955
  }
2737
2956
  }