@sellable/install 0.1.611 → 0.1.617-wip.phase189.20260812.7

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.
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.611
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.617-wip.phase189.20260812.7
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.866
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-ry4l91gBit9s1z7aw+v2nq7ULkdugt5BGubJsZ9OPJHwNsCVe6x1iuEe/Alr5Fxr3tdSwUE1fti5PnusD2w86g==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.611" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.617-wip.phase189.20260812.7" \
12
12
  && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.866" \
13
13
  && test -n "${INSTALLER_INTEGRITY}" \
14
14
  && test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.611" \
23
23
  && mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
24
24
  && npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
25
25
  && npm install --prefix /opt/sellable-agent/mcp-root --omit=dev --ignore-scripts --no-audit --no-fund "${MCP_PACKAGE}" \
26
- && node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.609-wip.hermes-desktop.20260811160530' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.866') throw new Error('package identity rejected');" \
26
+ && node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.617-wip.phase189.20260812.7' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.866') throw new Error('package identity rejected');" \
27
27
  && node --input-type=module -e "import { buildExternalRuntimeClosure } from '/opt/sellable-agent/install-root/node_modules/@sellable/install/lib/sellable-agent/external-runtime-builder.mjs'; const built = buildExternalRuntimeClosure({ mcpPackageRoot: '/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp', mcpNodeModulesRoot: '/opt/sellable-agent/mcp-root/node_modules', hermesSourceRoot: '/opt/hermes', ownerUid: 0, ownerGid: 0 }); if (!built.closureDigest) throw new Error('runtime closure rejected');" \
28
- && node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.611', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.866', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
28
+ && node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.617-wip.phase189.20260812.7', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.866', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
29
29
  && npm cache clean --force
30
30
 
31
31
  COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
@@ -3,13 +3,13 @@
3
3
  This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
4
4
 
5
5
  Build from this directory after
6
- `@sellable/install@0.1.611` is
6
+ `@sellable/install@0.1.617-wip.phase189.20260812.7` is
7
7
  published:
8
8
 
9
9
  ```sh
10
10
  docker build --platform linux/amd64 \
11
11
  --build-arg INSTALLER_INTEGRITY='sha512-…' \
12
- --tag sellable-agent-host:0.1.609-wip.hermes-desktop.20260811160530 .
12
+ --tag sellable-agent-host:0.1.617-wip.phase189.20260812.7-wip.hermes-desktop.20260811160530 .
13
13
  ```
14
14
 
15
15
  Create a private `bootstrap/` directory owned by root with mode `0700`. It must contain `bootstrap.json`, `provider-auth`, and `slack-app`, each owned by root with mode `0600`. `bootstrap.json` contains only non-secret identities, hashes, package integrities, and the control-plane URL. The other two files contain the provider JSON and global Slack app token whose hashes are pinned by `bootstrap.json`.
@@ -40,6 +40,7 @@ import {
40
40
  import {
41
41
  buildAdminAgentServiceEnvironment,
42
42
  buildNativeHermesLaunch,
43
+ buildRuntimeServingObservation,
43
44
  } from "../admin-artifact/node_modules/@sellable/install/lib/sellable-agent/fly-customer-worker.mjs";
44
45
  import sodium from "../admin-artifact/node_modules/libsodium-wrappers/dist/modules-esm/libsodium-wrappers.mjs";
45
46
  import { SELLABLE_ALLOWED_TOOLSETS } from "../fly-capabilities-bridge.mjs";
@@ -72,6 +73,7 @@ import {
72
73
  markHermesSnapshotDirty,
73
74
  mergeHermesState,
74
75
  migrateLegacyHermesUserState,
76
+ quarantineLegacyLightfieldVenv,
75
77
  reconcileHermesSnapshot,
76
78
  restoreLatestHermesSnapshot,
77
79
  } from "../hermes-memory-snapshot.mjs";
@@ -1200,6 +1202,70 @@ function operationIdentity(claim, exchange) {
1200
1202
  };
1201
1203
  }
1202
1204
 
1205
+ function operationSnapshotIdentity(claim, exchange) {
1206
+ const authority = claim?.runtimeAuthority;
1207
+ if (!authority) return undefined;
1208
+ return {
1209
+ operationId: claim.operation.id,
1210
+ authorityGeneration: authority.authorityGeneration,
1211
+ runtimeArtifactId: authority.assignedRuntimeArtifactId,
1212
+ sourceMachineId: exchange.workerId,
1213
+ sourceMachineVersion: authority.machineVersion,
1214
+ sourceBootSessionId: BOOT_SESSION_ID,
1215
+ profileSchemaVersion: "admin-native-profile/v2",
1216
+ };
1217
+ }
1218
+
1219
+ function adminServingObservation(activeLaunch, activeGateway, exchange) {
1220
+ try {
1221
+ const authority = activeLaunch?.claim?.runtimeAuthority;
1222
+ const running =
1223
+ activeGateway?.child?.exitCode === null && !activeGateway.child.killed;
1224
+ const productMcp = activeLaunch?.profile?.servers?.find(
1225
+ (server) => server.alias === "sellable"
1226
+ );
1227
+ const adminMcp = activeLaunch?.profile?.servers?.find(
1228
+ (server) => server.alias === "sellable-admin"
1229
+ );
1230
+ if (!authority || !running || !activeLaunch.slackProof) return null;
1231
+ const observedAt = new Date();
1232
+ return buildRuntimeServingObservation({
1233
+ scope: authority.scope,
1234
+ kind: "ADMIN",
1235
+ operationId: activeLaunch.claim.operation.id,
1236
+ workspaceId: activeLaunch.claim.operation.workspaceId,
1237
+ agentId: activeLaunch.claim.operation.agentId,
1238
+ runtimeId: authority.runtimeId,
1239
+ authorityGeneration: authority.authorityGeneration,
1240
+ runtimeArtifactId: authority.runtimeArtifactId,
1241
+ machineId: exchange.workerId,
1242
+ machineVersion: authority.machineVersion,
1243
+ bootSessionId: BOOT_SESSION_ID,
1244
+ profileIdentity: activeLaunch.profile.profile,
1245
+ processIdentity: `gateway-${activeGateway.child.pid}-${activeLaunch.claim.operation.fence}`,
1246
+ observedAt: observedAt.toISOString(),
1247
+ expiresAt: new Date(observedAt.getTime() + 120_000).toISOString(),
1248
+ slack: {
1249
+ authenticated: true,
1250
+ socketModeConnected: true,
1251
+ apiReachable: true,
1252
+ },
1253
+ productMcp: {
1254
+ serverInfoName: productMcp?.serverInfoName,
1255
+ toolCount: productMcp?.toolCount,
1256
+ },
1257
+ adminMcp: {
1258
+ serverInfoName: adminMcp?.serverInfoName,
1259
+ toolCount: adminMcp?.toolCount,
1260
+ },
1261
+ nativeGateway: true,
1262
+ customerRouting: false,
1263
+ });
1264
+ } catch {
1265
+ return null;
1266
+ }
1267
+ }
1268
+
1203
1269
  function startOperationHeartbeat({ claim, exchange, client }) {
1204
1270
  const identity = operationIdentity(claim, exchange);
1205
1271
  let stopped = false;
@@ -1708,6 +1774,42 @@ function nativeSlackProfileInput(claim, installed) {
1708
1774
  };
1709
1775
  }
1710
1776
 
1777
+ async function proveAdminSlackServing(claim, secrets) {
1778
+ const slack = claim.runtimeContract?.slack;
1779
+ if (!slack) throw new Error("admin_runtime_slack_contract_rejected");
1780
+ const post = (method, token) =>
1781
+ fetch(`https://slack.com/api/${method}`, {
1782
+ method: "POST",
1783
+ headers: {
1784
+ authorization: `Bearer ${token}`,
1785
+ "content-type": "application/x-www-form-urlencoded",
1786
+ },
1787
+ body: "",
1788
+ signal: AbortSignal.timeout(15_000),
1789
+ });
1790
+ const [authResponse, socketResponse] = await Promise.all([
1791
+ post("auth.test", secrets.slackBot),
1792
+ post("apps.connections.open", secrets.slackApp),
1793
+ ]);
1794
+ const [auth, socket] = await Promise.all([
1795
+ authResponse.json(),
1796
+ socketResponse.json(),
1797
+ ]);
1798
+ if (
1799
+ !authResponse.ok ||
1800
+ auth?.ok !== true ||
1801
+ auth.team_id !== slack.teamId ||
1802
+ auth.user_id !== slack.botUserId ||
1803
+ !socketResponse.ok ||
1804
+ socket?.ok !== true ||
1805
+ typeof socket.url !== "string" ||
1806
+ !/^wss:\/\//.test(socket.url)
1807
+ ) {
1808
+ throw new Error("admin_runtime_slack_serving_rejected");
1809
+ }
1810
+ return true;
1811
+ }
1812
+
1711
1813
  async function fetchDeliveryCiphertext({
1712
1814
  operation,
1713
1815
  identity,
@@ -2264,7 +2366,12 @@ function completionReceipt({
2264
2366
  };
2265
2367
  }
2266
2368
 
2267
- async function flushHermesSnapshot({ client, profileId, profileRoot }) {
2369
+ async function flushHermesSnapshot({
2370
+ client,
2371
+ profileId,
2372
+ profileRoot,
2373
+ operationIdentity,
2374
+ }) {
2268
2375
  try {
2269
2376
  markHermesSnapshotDirty(profileRoot);
2270
2377
  return await reconcileHermesSnapshot({
@@ -2273,13 +2380,24 @@ async function flushHermesSnapshot({ client, profileId, profileRoot }) {
2273
2380
  hermesRoot: DATA_ROOT,
2274
2381
  hermesVersion: "0.20.0",
2275
2382
  debounceMs: 0,
2276
- upload: ({ archiveDigest, archiveBytes, sourceProfileId, bytes }) =>
2383
+ operationIdentity,
2384
+ upload: ({
2385
+ archiveDigest,
2386
+ archiveBytes,
2387
+ sourceProfileId,
2388
+ bytes,
2389
+ ok: _ok,
2390
+ status: _status,
2391
+ hermesVersion: _hermesVersion,
2392
+ ...v2
2393
+ }) =>
2277
2394
  client.putMemorySnapshot(
2278
2395
  {
2279
2396
  archiveDigest,
2280
2397
  archiveBytes,
2281
2398
  hermesVersion: "0.20.0",
2282
2399
  sourceProfileId,
2400
+ ...v2,
2283
2401
  },
2284
2402
  bytes
2285
2403
  ),
@@ -2992,6 +3110,11 @@ async function runAdminProduction() {
2992
3110
  callerProfileId: activeLaunch.profile.profile,
2993
3111
  }),
2994
3112
  leaseSeconds: 60,
3113
+ servingObservation: adminServingObservation(
3114
+ activeLaunch,
3115
+ activeGateway,
3116
+ exchange
3117
+ ),
2995
3118
  })
2996
3119
  : await client.claim(60);
2997
3120
  } catch (error) {
@@ -3061,16 +3184,6 @@ async function runAdminProduction() {
3061
3184
  ),
3062
3185
  });
3063
3186
  }
3064
- if (
3065
- activeLaunch &&
3066
- (!activeGateway ||
3067
- activeGateway.child.exitCode !== null ||
3068
- activeGateway.child.killed)
3069
- ) {
3070
- await sleep(Math.min(30_000, 500 * 2 ** Math.min(failures, 6)));
3071
- activeGateway = await startNativeGateway(activeLaunch);
3072
- failures = 0;
3073
- }
3074
3187
  await sleep(1_000);
3075
3188
  continue;
3076
3189
  }
@@ -3182,6 +3295,14 @@ async function runAdminProduction() {
3182
3295
  fetchLatest: () => client.getMemorySnapshot(),
3183
3296
  });
3184
3297
  if (!memoryRestore.ok) throw new Error(memoryRestore.code);
3298
+ const legacyLightfieldVenv = quarantineLegacyLightfieldVenv({
3299
+ profileId: basename(profileContract.profileHome),
3300
+ profileRoot: profileContract.profileHome,
3301
+ hermesRoot: DATA_ROOT,
3302
+ });
3303
+ if (!legacyLightfieldVenv.ok) {
3304
+ throw new Error(legacyLightfieldVenv.code);
3305
+ }
3185
3306
  const legacyUserState = migrateLegacyHermesUserState({
3186
3307
  profileId: basename(profileContract.profileHome),
3187
3308
  profileRoot: profileContract.profileHome,
@@ -3214,6 +3335,7 @@ async function runAdminProduction() {
3214
3335
  client,
3215
3336
  profileId: profile.profile,
3216
3337
  profileRoot: profile.profileHome,
3338
+ operationIdentity: operationSnapshotIdentity(claim, exchange),
3217
3339
  });
3218
3340
  const settings = settingsProof(
3219
3341
  imported.receipt.generationRoot,
@@ -3248,8 +3370,14 @@ async function runAdminProduction() {
3248
3370
  modelAuth,
3249
3371
  });
3250
3372
  operationHeartbeat.assertHealthy();
3373
+ const slackProof =
3374
+ desiredLifecycle === "ON"
3375
+ ? await proveAdminSlackServing(claim, secrets)
3376
+ : false;
3251
3377
  const candidateLaunch =
3252
- desiredLifecycle === "ON" ? { profile, claim, installed } : null;
3378
+ desiredLifecycle === "ON"
3379
+ ? { profile, claim, installed, slackProof }
3380
+ : null;
3253
3381
  let candidateGateway = null;
3254
3382
  try {
3255
3383
  if (candidateLaunch) {
@@ -6,7 +6,7 @@ USER root
6
6
 
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.866
8
8
  ARG MCP_INTEGRITY=sha512-ry4l91gBit9s1z7aw+v2nq7ULkdugt5BGubJsZ9OPJHwNsCVe6x1iuEe/Alr5Fxr3tdSwUE1fti5PnusD2w86g==
9
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.611
9
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.617-wip.phase189.20260812.7
10
10
  ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.186-wip.phase158.20260802034727
11
11
  ARG ADMIN_MCP_INTEGRITY=sha512-qYNew5wd2IfgVcp1UYOG9QvpF/8Mx19YJ5HQtMKzvSfUf3evePjtaKsoXLf2C/GUJ5QlLlHJo84VWOI3V01+9A==
12
12
  ARG TUS_JS_CLIENT_VERSION=4.3.1
@@ -96,7 +96,7 @@ const RUNTIME_GENERATION = Number(
96
96
  const BOOT_SESSION_ID = randomUUID();
97
97
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.0";
98
98
  const INSTALLER_PACKAGE =
99
- process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.611";
99
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.617-wip.phase189.20260812.7";
100
100
  const MCP_PACKAGE =
101
101
  process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.866";
102
102
 
@@ -32,6 +32,79 @@ function stableJson(value) {
32
32
  return JSON.stringify(value);
33
33
  }
34
34
 
35
+ // CUSTOMER and ADMIN runtimes share this one serving-observation builder.
36
+ export function buildRuntimeServingObservation(input = {}) {
37
+ const keys = [
38
+ "scope",
39
+ "kind",
40
+ "operationId",
41
+ "workspaceId",
42
+ "agentId",
43
+ "runtimeId",
44
+ "authorityGeneration",
45
+ "runtimeArtifactId",
46
+ "machineId",
47
+ "machineVersion",
48
+ "bootSessionId",
49
+ "profileIdentity",
50
+ "processIdentity",
51
+ "observedAt",
52
+ "expiresAt",
53
+ "slack",
54
+ "productMcp",
55
+ "adminMcp",
56
+ "nativeGateway",
57
+ "customerRouting",
58
+ ];
59
+ const identity = (value) =>
60
+ typeof value === "string" && SAFE_ID.test(value);
61
+ const mcp = (value, name) =>
62
+ exactKeys(value, ["serverInfoName", "toolCount"]) &&
63
+ value.serverInfoName === name &&
64
+ Number.isSafeInteger(value.toolCount) &&
65
+ value.toolCount > 0 &&
66
+ value.toolCount <= 10_000;
67
+ const observedAt = Date.parse(input.observedAt ?? "");
68
+ const expiresAt = Date.parse(input.expiresAt ?? "");
69
+ const kindExact =
70
+ input.kind === "CUSTOMER"
71
+ ? input.adminMcp === null && input.customerRouting === true
72
+ : input.kind === "ADMIN" &&
73
+ mcp(input.adminMcp, "sellable-admin-mcp") &&
74
+ input.customerRouting === false;
75
+ if (
76
+ !exactKeys(input, keys) ||
77
+ !["ASSIGNED", "CANDIDATE"].includes(input.scope) ||
78
+ !keys
79
+ .slice(2, 13)
80
+ .filter((key) => key !== "authorityGeneration")
81
+ .every((key) => identity(input[key])) ||
82
+ !Number.isSafeInteger(input.authorityGeneration) ||
83
+ input.authorityGeneration < 1 ||
84
+ !Number.isFinite(observedAt) ||
85
+ !Number.isFinite(expiresAt) ||
86
+ expiresAt <= observedAt ||
87
+ expiresAt - observedAt > 5 * 60_000 ||
88
+ !exactKeys(input.slack, [
89
+ "authenticated",
90
+ "socketModeConnected",
91
+ "apiReachable",
92
+ ]) ||
93
+ input.slack.authenticated !== true ||
94
+ input.slack.socketModeConnected !== true ||
95
+ input.slack.apiReachable !== true ||
96
+ !mcp(input.productMcp, "sellable-mcp") ||
97
+ input.nativeGateway !== true ||
98
+ !kindExact
99
+ ) {
100
+ throw new Error("runtime_serving_observation_rejected");
101
+ }
102
+ return Object.freeze({
103
+ schemaVersion: "sellable-agent-serving-observation/v2",
104
+ ...input,
105
+ });
106
+ }
107
+
35
108
  export function buildAgentServiceEnvironment({
36
109
  workspaceId,
37
110
  agentId,
@@ -541,7 +541,7 @@ function tusUploadAccepted(value) {
541
541
  value.signedUploadToken.length <= 16_384 &&
542
542
  !/\s/.test(value.signedUploadToken) &&
543
543
  value.bucketName === "agent-hermes-backups-v1" &&
544
- /^workspaces\/[A-Za-z0-9_-]{1,128}\/(?:customer|admin)\/archives\/[a-f0-9]{64}\.zip$/.test(
544
+ /^workspaces\/[A-Za-z0-9_-]{1,128}\/(?:(?:customer|admin)\/archives|agents\/[A-Za-z0-9_-]{1,128}\/operations\/[A-Za-z0-9_-]{1,128})\/[a-f0-9]{64}\.zip$/.test(
545
545
  value?.objectName ?? ""
546
546
  )
547
547
  );
@@ -784,6 +784,7 @@ export async function claimWithSoulObservation({
784
784
  tracker,
785
785
  readSoul,
786
786
  leaseSeconds = 60,
787
+ servingObservation = /** @type {any} */ (null),
787
788
  }) {
788
789
  // Deliver lifecycle work before any potentially slow local loader proof.
789
790
  // Reconciliation uses the same cadence only after the server confirms that
@@ -791,6 +792,7 @@ export async function claimWithSoulObservation({
791
792
  const tick = await client.claimTick({
792
793
  leaseSeconds,
793
794
  soulObservation: tracker.pending(),
795
+ ...(servingObservation ? { servingObservation } : {}),
794
796
  });
795
797
  if (tick?.claim) return tick.claim;
796
798
 
@@ -958,9 +960,9 @@ export function createFlyCustomerControlClient({
958
960
  }
959
961
  }
960
962
  };
961
- const memorySnapshotControl = async (method, body, verb) => {
963
+ const memorySnapshotControl = async (method, body, verb, search = "") => {
962
964
  const response = await fetchImpl(
963
- `${endpoint}/api/v3/sellable-agent/worker/memory-snapshot`,
965
+ `${endpoint}/api/v3/sellable-agent/worker/memory-snapshot${search}`,
964
966
  {
965
967
  method,
966
968
  headers: {
@@ -988,6 +990,37 @@ export function createFlyCustomerControlClient({
988
990
  value.archiveBytes <= 128 * 1024 * 1024 &&
989
991
  value.hermesVersion === "0.20.0" &&
990
992
  /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value?.sourceProfileId ?? "");
993
+ const boundedSnapshotIdentity = (value) =>
994
+ /^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/.test(value ?? "");
995
+ const metadataV2Accepted = (value) =>
996
+ metadataAccepted(value) &&
997
+ Object.keys(value).sort().join(",") ===
998
+ "archiveBytes,archiveDigest,authorityGeneration,hermesVersion,operationId,profileSchemaVersion,runtimeArtifactId,sourceBootSessionId,sourceMachineId,sourceMachineVersion,sourceProfileId" &&
999
+ boundedSnapshotIdentity(value.operationId) &&
1000
+ Number.isSafeInteger(value.authorityGeneration) &&
1001
+ value.authorityGeneration > 0 &&
1002
+ boundedSnapshotIdentity(value.runtimeArtifactId) &&
1003
+ boundedSnapshotIdentity(value.profileSchemaVersion) &&
1004
+ [
1005
+ value.sourceMachineId,
1006
+ value.sourceMachineVersion,
1007
+ value.sourceBootSessionId,
1008
+ ].every(
1009
+ (identityValue) =>
1010
+ identityValue === null || boundedSnapshotIdentity(identityValue)
1011
+ );
1012
+ const snapshotReadbackExpected = (value) =>
1013
+ value &&
1014
+ typeof value === "object" &&
1015
+ !Array.isArray(value) &&
1016
+ Object.keys(value).sort().join(",") ===
1017
+ "authorityGeneration,operationId,profileSchemaVersion,runtimeArtifactId,sourceProfileId" &&
1018
+ boundedSnapshotIdentity(value.operationId) &&
1019
+ Number.isSafeInteger(value.authorityGeneration) &&
1020
+ value.authorityGeneration > 0 &&
1021
+ boundedSnapshotIdentity(value.runtimeArtifactId) &&
1022
+ /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value.sourceProfileId ?? "") &&
1023
+ boundedSnapshotIdentity(value.profileSchemaVersion);
991
1024
  const signedUrlAccepted = (value) => {
992
1025
  try {
993
1026
  return new URL(value).protocol === "https:";
@@ -996,8 +1029,9 @@ export function createFlyCustomerControlClient({
996
1029
  }
997
1030
  };
998
1031
  const putMemorySnapshot = async (metadata, archive) => {
1032
+ const isV2 = Object.hasOwn(metadata ?? {}, "operationId");
999
1033
  if (
1000
- !metadataAccepted(metadata) ||
1034
+ !(isV2 ? metadataV2Accepted(metadata) : metadataAccepted(metadata)) ||
1001
1035
  !Buffer.isBuffer(archive) ||
1002
1036
  metadata.archiveBytes !== archive.byteLength ||
1003
1037
  createHash("sha256").update(archive).digest("hex") !==
@@ -1009,14 +1043,20 @@ export function createFlyCustomerControlClient({
1009
1043
  const value = await memorySnapshotControl(
1010
1044
  "PUT",
1011
1045
  {
1012
- schemaVersion: "sellable-hermes-snapshot-upload/v1",
1046
+ schemaVersion: isV2
1047
+ ? "sellable-hermes-snapshot-upload/v2"
1048
+ : "sellable-hermes-snapshot-upload/v1",
1013
1049
  ...metadata,
1014
1050
  },
1015
1051
  "memory-snapshot-put"
1016
1052
  );
1017
1053
  if (
1018
1054
  !tusUploadAccepted(value) ||
1019
- !value.objectName.endsWith(`/${metadata.archiveDigest}.zip`)
1055
+ !value.objectName.endsWith(
1056
+ isV2
1057
+ ? `/operations/${metadata.operationId}/${metadata.archiveDigest}.zip`
1058
+ : `/${metadata.archiveDigest}.zip`
1059
+ )
1020
1060
  ) {
1021
1061
  throw new WorkerControlResponseError(
1022
1062
  "memory_snapshot_upload_url_rejected"
@@ -1034,7 +1074,9 @@ export function createFlyCustomerControlClient({
1034
1074
  const committed = await memorySnapshotControl(
1035
1075
  "POST",
1036
1076
  {
1037
- schemaVersion: "sellable-hermes-snapshot-commit/v1",
1077
+ schemaVersion: isV2
1078
+ ? "sellable-hermes-snapshot-commit/v2"
1079
+ : "sellable-hermes-snapshot-commit/v1",
1038
1080
  ...metadata,
1039
1081
  },
1040
1082
  "memory-snapshot-put"
@@ -1047,14 +1089,42 @@ export function createFlyCustomerControlClient({
1047
1089
  }
1048
1090
  return committed;
1049
1091
  };
1050
- const getMemorySnapshot = async () => {
1092
+ const getMemorySnapshot = async (expected) => {
1093
+ const isV2 = expected !== undefined;
1094
+ if (isV2 && !snapshotReadbackExpected(expected)) {
1095
+ throw new WorkerControlResponseError("memory_snapshot_download_rejected");
1096
+ }
1097
+ const body = isV2
1098
+ ? {
1099
+ schemaVersion: "sellable-hermes-snapshot-get/v2",
1100
+ operationId: expected.operationId,
1101
+ }
1102
+ : { schemaVersion: "sellable-hermes-snapshot-get/v1" };
1103
+ const search = isV2
1104
+ ? `?${new URLSearchParams(body).toString()}`
1105
+ : "";
1051
1106
  const result = await memorySnapshotControl(
1052
1107
  "GET",
1053
- { schemaVersion: "sellable-hermes-snapshot-get/v1" },
1054
- "memory-snapshot-get"
1108
+ body,
1109
+ "memory-snapshot-get",
1110
+ search
1055
1111
  );
1056
1112
  if (!result) return null;
1057
- if (!metadataAccepted(result) || !signedUrlAccepted(result.signedUrl)) {
1113
+ const identityReadbackAccepted =
1114
+ !isV2 ||
1115
+ (result.operationId === expected.operationId &&
1116
+ result.authorityGeneration === expected.authorityGeneration &&
1117
+ result.runtimeArtifactId === expected.runtimeArtifactId &&
1118
+ result.sourceProfileId === expected.sourceProfileId &&
1119
+ result.profileSchemaVersion === expected.profileSchemaVersion &&
1120
+ /^[a-f0-9]{64}$/.test(result.archiveDigest ?? "") &&
1121
+ Number.isSafeInteger(result.archiveBytes) &&
1122
+ result.archiveBytes > 0 &&
1123
+ result.archiveBytes <= 128 * 1024 * 1024);
1124
+ if (
1125
+ !(isV2 ? identityReadbackAccepted : metadataAccepted(result)) ||
1126
+ !signedUrlAccepted(result.signedUrl)
1127
+ ) {
1058
1128
  throw new WorkerControlResponseError(
1059
1129
  "memory_snapshot_download_url_rejected"
1060
1130
  );
@@ -1074,10 +1144,15 @@ export function createFlyCustomerControlClient({
1074
1144
  }
1075
1145
  return { ...result, signedUrl: undefined, bytes };
1076
1146
  };
1077
- const claimTick = async ({ leaseSeconds = 60, soulObservation } = {}) =>
1147
+ const claimTick = async ({
1148
+ leaseSeconds = 60,
1149
+ soulObservation,
1150
+ servingObservation,
1151
+ } = {}) =>
1078
1152
  post("claim", "/api/v3/sellable-agent/worker/claim", {
1079
1153
  leaseSeconds,
1080
1154
  ...(soulObservation ? { soulObservation } : {}),
1155
+ ...(servingObservation ? { servingObservation } : {}),
1081
1156
  });
1082
1157
  return Object.freeze({
1083
1158
  claimTick,
@@ -1153,8 +1228,8 @@ export function createFlyCustomerControlClient({
1153
1228
  async putMemorySnapshot(metadata, archive) {
1154
1229
  return putMemorySnapshot(metadata, archive);
1155
1230
  },
1156
- async getMemorySnapshot() {
1157
- return getMemorySnapshot();
1231
+ async getMemorySnapshot(expected) {
1232
+ return getMemorySnapshot(expected);
1158
1233
  },
1159
1234
  });
1160
1235
  }
@@ -71,6 +71,7 @@ const HERMES = "/opt/hermes/.venv/bin/hermes";
71
71
  const HERMES_PYTHON = "/opt/hermes/.venv/bin/python";
72
72
  const VERSION = "0.20.0";
73
73
  const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
74
+ const BOUNDED_ID = /^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/;
74
75
  const SHA256 = /^[a-f0-9]{64}$/;
75
76
  const MAX_BYTES = 128 * 1024 * 1024;
76
77
  // Portable snapshots contain user-authored/customizable state plus the compact
@@ -277,6 +278,116 @@ export function mergeHermesState(sourceRoot, targetRoot) {
277
278
  merge(sourceRoot, targetRoot);
278
279
  }
279
280
 
281
+ const LEGACY_LIGHTFIELD_VENV = ".venv-amplify-audit";
282
+ const LEGACY_LIGHTFIELD_QUARANTINE_SCHEMA =
283
+ "sellable-agent-lightfield-venv-quarantine/v1";
284
+
285
+ function privateMigrationDirectory(path) {
286
+ if (existsSync(path)) {
287
+ const stat = lstatSync(path);
288
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
289
+ throw new Error("hermes_lightfield_quarantine_path_rejected");
290
+ }
291
+ return;
292
+ }
293
+ mkdirSync(path, { mode: 0o700 });
294
+ fsyncDirectory(dirname(path));
295
+ }
296
+
297
+ /**
298
+ * Quarantine the one persisted pre-Phase-189 Lightfield virtualenv layout.
299
+ * Its ordinary Python symlinks must never weaken the general snapshot scan.
300
+ */
301
+ export function quarantineLegacyLightfieldVenv({
302
+ profileId,
303
+ profileRoot,
304
+ hermesRoot,
305
+ } = {}) {
306
+ if (!validProfilePath(profileId, profileRoot, hermesRoot)) {
307
+ return {
308
+ ...failed("hermes_lightfield_quarantine_identity_rejected"),
309
+ quarantinePath: null,
310
+ };
311
+ }
312
+ const legacyPath = join(profileRoot, LEGACY_LIGHTFIELD_VENV);
313
+ const quarantineBase = join(
314
+ hermesRoot,
315
+ "backups",
316
+ "quarantine",
317
+ "legacy-lightfield-venv-v1"
318
+ );
319
+ const quarantinePath = join(quarantineBase, profileId);
320
+ const marker = join(
321
+ profileRoot,
322
+ ".sellable-agent",
323
+ "migrations",
324
+ "lightfield-venv-quarantine-v1.json"
325
+ );
326
+ try {
327
+ if (existsSync(marker)) {
328
+ regularFile(marker);
329
+ const receipt = JSON.parse(readFileSync(marker, "utf8"));
330
+ if (
331
+ receipt?.schemaVersion !== LEGACY_LIGHTFIELD_QUARANTINE_SCHEMA ||
332
+ receipt.profileId !== profileId ||
333
+ receipt.source !== LEGACY_LIGHTFIELD_VENV ||
334
+ receipt.quarantinePath !== quarantinePath ||
335
+ !existsSync(quarantinePath) ||
336
+ lstatSync(quarantinePath).isSymbolicLink() ||
337
+ !lstatSync(quarantinePath).isDirectory()
338
+ ) {
339
+ throw new Error("hermes_lightfield_quarantine_receipt_rejected");
340
+ }
341
+ return { ok: true, status: "REUSED", profileId, quarantinePath };
342
+ }
343
+ const sourcePresent = existsSync(legacyPath);
344
+ const targetPresent = existsSync(quarantinePath);
345
+ if (!sourcePresent && !targetPresent) {
346
+ return {
347
+ ok: true,
348
+ status: "NO_LEGACY_LAYOUT",
349
+ profileId,
350
+ quarantinePath: null,
351
+ };
352
+ }
353
+ if (sourcePresent && targetPresent) {
354
+ throw new Error("hermes_lightfield_quarantine_conflict");
355
+ }
356
+ let status = "RESUMED";
357
+ if (sourcePresent) {
358
+ const source = lstatSync(legacyPath);
359
+ if (source.isSymbolicLink() || !source.isDirectory()) {
360
+ throw new Error("hermes_lightfield_quarantine_source_rejected");
361
+ }
362
+ privateMigrationDirectory(join(hermesRoot, "backups"));
363
+ privateMigrationDirectory(join(hermesRoot, "backups", "quarantine"));
364
+ privateMigrationDirectory(quarantineBase);
365
+ moveDurably(legacyPath, quarantinePath);
366
+ status = "QUARANTINED";
367
+ }
368
+ const quarantined = lstatSync(quarantinePath);
369
+ if (quarantined.isSymbolicLink() || !quarantined.isDirectory()) {
370
+ throw new Error("hermes_lightfield_quarantine_readback_rejected");
371
+ }
372
+ atomicWrite(
373
+ marker,
374
+ `${JSON.stringify({
375
+ schemaVersion: LEGACY_LIGHTFIELD_QUARANTINE_SCHEMA,
376
+ profileId,
377
+ source: LEGACY_LIGHTFIELD_VENV,
378
+ quarantinePath,
379
+ })}\n`,
380
+ 0o600
381
+ );
382
+ return { ok: true, status, profileId, quarantinePath };
383
+ } catch {
384
+ return {
385
+ ...failed("hermes_lightfield_quarantine_failed"),
386
+ quarantinePath: null,
387
+ };
388
+ }
389
+ }
390
+
280
391
  function cronJobs(profileRoot) {
281
392
  const path = join(profileRoot, "cron", "jobs.json");
282
393
  if (!existsSync(path)) return [];
@@ -872,12 +983,31 @@ export async function captureHermesBackup({
872
983
  hermesCli = HERMES,
873
984
  maxArchiveBytes = MAX_BYTES,
874
985
  runChild,
986
+ operationIdentity = /** @type {any} */ (undefined),
875
987
  }) {
988
+ const operationIdentityAccepted =
989
+ operationIdentity === undefined ||
990
+ (operationIdentity &&
991
+ typeof operationIdentity === "object" &&
992
+ !Array.isArray(operationIdentity) &&
993
+ Object.keys(operationIdentity).sort().join(",") ===
994
+ "authorityGeneration,operationId,profileSchemaVersion,runtimeArtifactId,sourceBootSessionId,sourceMachineId,sourceMachineVersion" &&
995
+ BOUNDED_ID.test(operationIdentity.operationId ?? "") &&
996
+ Number.isSafeInteger(operationIdentity.authorityGeneration) &&
997
+ operationIdentity.authorityGeneration > 0 &&
998
+ BOUNDED_ID.test(operationIdentity.runtimeArtifactId ?? "") &&
999
+ BOUNDED_ID.test(operationIdentity.profileSchemaVersion ?? "") &&
1000
+ [
1001
+ operationIdentity.sourceMachineId,
1002
+ operationIdentity.sourceMachineVersion,
1003
+ operationIdentity.sourceBootSessionId,
1004
+ ].every((value) => value === null || BOUNDED_ID.test(value ?? "")));
876
1005
  if (
877
1006
  !validProfilePath(profileId, profileRoot, hermesRoot) ||
878
1007
  !existsSync(profileRoot) ||
879
1008
  hermesVersion !== VERSION ||
880
- !isAbsolute(archiveDirectory)
1009
+ !isAbsolute(archiveDirectory) ||
1010
+ !operationIdentityAccepted
881
1011
  ) {
882
1012
  return failed("hermes_snapshot_capture_identity_rejected");
883
1013
  }
@@ -917,6 +1047,7 @@ export async function captureHermesBackup({
917
1047
  archiveBytes: bytes.byteLength,
918
1048
  hermesVersion,
919
1049
  sourceProfileId: profileId,
1050
+ ...(operationIdentity ?? {}),
920
1051
  bytes,
921
1052
  };
922
1053
  } catch {
@@ -1022,6 +1153,7 @@ async function reconcileHermesSnapshotOnce({
1022
1153
  debounceMs = 2_000,
1023
1154
  hermesCli = HERMES,
1024
1155
  runChild,
1156
+ operationIdentity = /** @type {any} */ (undefined),
1025
1157
  }) {
1026
1158
  if (!validProfilePath(profileId, profileRoot, hermesRoot)) {
1027
1159
  return failed("hermes_snapshot_reconcile_identity_rejected");
@@ -1048,6 +1180,7 @@ async function reconcileHermesSnapshotOnce({
1048
1180
  hermesVersion,
1049
1181
  hermesCli,
1050
1182
  runChild,
1183
+ operationIdentity,
1051
1184
  });
1052
1185
  if (!captured.ok) {
1053
1186
  requeueClaim(paths.marker, claim);
@@ -1081,6 +1214,7 @@ export async function reconcileHermesSnapshot({
1081
1214
  debounceMs = 2_000,
1082
1215
  hermesCli = HERMES,
1083
1216
  runChild,
1217
+ operationIdentity = /** @type {any} */ (undefined),
1084
1218
  }) {
1085
1219
  const key = `${resolve(hermesRoot)}\0${profileId}`;
1086
1220
  const active = snapshotReconciles.get(key);
@@ -1095,6 +1229,7 @@ export async function reconcileHermesSnapshot({
1095
1229
  debounceMs,
1096
1230
  hermesCli,
1097
1231
  runChild,
1232
+ operationIdentity,
1098
1233
  }).finally(() => snapshotReconciles.delete(key));
1099
1234
  snapshotReconciles.set(key, operation);
1100
1235
  return operation;
@@ -38,7 +38,7 @@ import { installAgentHostServices } from "./service-installer.mjs";
38
38
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
39
39
  const VERSION = "sellable-agent-host-bootstrap/v1";
40
40
  const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
41
- const INSTALLER_PACKAGE = "@sellable/install@0.1.611";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.617-wip.phase189.20260812.7";
42
42
  const MCP_PACKAGE = "@sellable/mcp@0.1.866";
43
43
  const HERMES_VERSION = "0.18.0";
44
44
  const SHA256 = /^[a-f0-9]{64}$/;
@@ -1546,7 +1546,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1546
1546
  ]) ||
1547
1547
  pinned.hermesCli !== "hermes" ||
1548
1548
  pinned.hermesVersion !== "0.18.0" ||
1549
- pinned.installerPackage !== "@sellable/install@0.1.611" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.617-wip.phase189.20260812.7" ||
1550
1550
  pinned.mcpPackage !== "@sellable/mcp@0.1.866" ||
1551
1551
  !Array.isArray(policy.toolInclude) ||
1552
1552
  policy.toolInclude.length === 0 ||
@@ -278,7 +278,7 @@ function validateDesired(desired) {
278
278
  desired.serviceCredentialGeneration < 1 ||
279
279
  desired.hermesCli !== "hermes" ||
280
280
  desired.hermesVersion !== "0.18.0" ||
281
- desired.installerPackage !== "@sellable/install@0.1.611" ||
281
+ desired.installerPackage !== "@sellable/install@0.1.617-wip.phase189.20260812.7" ||
282
282
  desired.mcpPackage !== "@sellable/mcp@0.1.866" ||
283
283
  !Array.isArray(desired.toolInclude) ||
284
284
  desired.toolInclude.length === 0 ||
@@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url";
25
25
  import { deriveContainedProfileId } from "./profile-materializer.mjs";
26
26
 
27
27
  export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
28
- export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.611";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.617-wip.phase189.20260812.7";
29
29
  export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.866";
30
30
  const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
31
31
  PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
@@ -39,6 +39,53 @@ export const LIFECYCLE_TRANSITION_ORDER = Object.freeze({
39
39
  ],
40
40
  });
41
41
 
42
+ const REPAIR_ID = /^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/;
43
+
44
+ // The control plane owns the durable repair budget. This runtime seam only
45
+ // executes the single action returned by that authority.
46
+ export async function applyBoundedRuntimeRepair(input = {}) {
47
+ const {
48
+ workspaceId,
49
+ agentId,
50
+ agentKind,
51
+ assignedRuntimeArtifactId,
52
+ readinessIdentity,
53
+ ports,
54
+ } = input;
55
+ if (
56
+ ![workspaceId, agentId, assignedRuntimeArtifactId, readinessIdentity].every(
57
+ (value) => REPAIR_ID.test(value ?? "")
58
+ ) ||
59
+ !["CUSTOMER", "ADMIN"].includes(agentKind) ||
60
+ !ports ||
61
+ typeof ports.recordFailure !== "function" ||
62
+ typeof ports.restartLocal !== "function" ||
63
+ typeof ports.requestMachineRestart !== "function"
64
+ ) {
65
+ return { ok: false, status: "REPAIR_INPUT_REJECTED" };
66
+ }
67
+ const authority = Object.freeze({
68
+ workspaceId,
69
+ agentId,
70
+ agentKind,
71
+ runtimeArtifactId: assignedRuntimeArtifactId,
72
+ readinessIdentity,
73
+ });
74
+ const decision = await ports.recordFailure(authority);
75
+ if (decision?.action === "LOCAL_RESTART") {
76
+ await ports.restartLocal(authority);
77
+ return { ok: true, status: "LOCAL_RESTART", exhausted: false };
78
+ }
79
+ if (decision?.action === "MACHINE_RESTART") {
80
+ await ports.requestMachineRestart(authority);
81
+ return { ok: true, status: "MACHINE_RESTART", exhausted: false };
82
+ }
83
+ if (decision?.action === "EXHAUSTED") {
84
+ return { ok: true, status: "EXHAUSTED", exhausted: true };
85
+ }
86
+ return { ok: false, status: "REPAIR_DECISION_REJECTED" };
87
+ }
88
+
42
89
  const DRIFT_GROUPS = [
43
90
  {
44
91
  action: RUNTIME_DRIFT_ACTIONS.PACKAGE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.611",
3
+ "version": "0.1.617-wip.phase189.20260812.7",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {