@sellable/install 0.1.610 → 0.1.612-wip.phase189.20260812.2

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.609
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.612-wip.phase189.20260812.2
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.865
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-282pkYFADFrrIXyQcdUpQwakXiwe/AFUDpDZkTh29CFcGHnN7dvC0MCvaU4LffXA+ZuKCpuf02uxCLGiIEOjbw==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.609" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.612-wip.phase189.20260812.2" \
12
12
  && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.865" \
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.609" \
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.865') 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.612-wip.phase189.20260812.2-wip.hermes-desktop.20260811160530' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.865') 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.609', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.865', 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.612-wip.phase189.20260812.2', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.865', 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.609` is
6
+ `@sellable/install@0.1.612-wip.phase189.20260812.2` 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.612-wip.phase189.20260812.2-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";
@@ -67,11 +68,11 @@ import {
67
68
  HERMES_SNAPSHOT_HOOKS,
68
69
  HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES,
69
70
  applyHermesHostedBackgroundReviewPolicy,
70
- ensureHermesOpportunitySequencesCron,
71
71
  ensureHermesSnapshotCron,
72
72
  markHermesSnapshotDirty,
73
73
  mergeHermesState,
74
74
  migrateLegacyHermesUserState,
75
+ quarantineLegacyLightfieldVenv,
75
76
  reconcileHermesSnapshot,
76
77
  restoreLatestHermesSnapshot,
77
78
  } from "../hermes-memory-snapshot.mjs";
@@ -694,10 +695,6 @@ const ADMIN_LIVE_SKILLS_CONFIG_KEYS = Object.freeze([
694
695
  "disabled",
695
696
  "platform_disabled",
696
697
  ]);
697
- const ADMIN_SEALED_SKILL_DIRECTORIES = new Set([
698
- "sellable",
699
- "opportunity-sequences",
700
- ]);
701
698
 
702
699
  function adminPreservedSlackToolsets(config) {
703
700
  const platformToolsets = config?.platform_toolsets;
@@ -727,7 +724,7 @@ function adminPreservedSkillsIdentity(profileRoot) {
727
724
  const walk = (directory, relativePath = "") => {
728
725
  for (const name of readdirSync(directory).sort()) {
729
726
  const nextRelative = relativePath ? `${relativePath}/${name}` : name;
730
- if (ADMIN_SEALED_SKILL_DIRECTORIES.has(nextRelative)) continue;
727
+ if (nextRelative === "sellable") continue;
731
728
  const path = join(directory, name);
732
729
  const stat = lstatSync(path);
733
730
  if (stat.isSymbolicLink()) {
@@ -1034,22 +1031,12 @@ async function materializeAdminProfile({
1034
1031
  hermesCli: HERMES,
1035
1032
  });
1036
1033
  if (!memoryCron.ok) throw new Error(memoryCron.code);
1037
- const opportunitySequencesCron = await ensureHermesOpportunitySequencesCron({
1038
- profileId: profile,
1039
- profileRoot: profileHome,
1040
- hermesRoot: DATA_ROOT,
1041
- statePath: layout.statePath,
1042
- });
1043
- if (!opportunitySequencesCron.ok) {
1044
- throw new Error(opportunitySequencesCron.code);
1045
- }
1046
1034
  return {
1047
1035
  stage,
1048
1036
  promotion: promoted.promotion,
1049
1037
  profile,
1050
1038
  profileHome,
1051
1039
  adminHome,
1052
- opportunitySequencesCron,
1053
1040
  observedSoulRawSha256: promoted.observedSoulRawSha256,
1054
1041
  servers: [serverProofs.sellable, serverProofs["sellable-admin"]].map(
1055
1042
  ({ tools: _tools, ...proof }) => proof
@@ -1200,6 +1187,70 @@ function operationIdentity(claim, exchange) {
1200
1187
  };
1201
1188
  }
1202
1189
 
1190
+ function operationSnapshotIdentity(claim, exchange) {
1191
+ const authority = claim?.runtimeAuthority;
1192
+ if (!authority) return undefined;
1193
+ return {
1194
+ operationId: claim.operation.id,
1195
+ authorityGeneration: authority.authorityGeneration,
1196
+ runtimeArtifactId: authority.assignedRuntimeArtifactId,
1197
+ sourceMachineId: exchange.workerId,
1198
+ sourceMachineVersion: authority.machineVersion,
1199
+ sourceBootSessionId: BOOT_SESSION_ID,
1200
+ profileSchemaVersion: "admin-native-profile/v2",
1201
+ };
1202
+ }
1203
+
1204
+ function adminServingObservation(activeLaunch, activeGateway, exchange) {
1205
+ try {
1206
+ const authority = activeLaunch?.claim?.runtimeAuthority;
1207
+ const running =
1208
+ activeGateway?.child?.exitCode === null && !activeGateway.child.killed;
1209
+ const productMcp = activeLaunch?.profile?.servers?.find(
1210
+ (server) => server.alias === "sellable"
1211
+ );
1212
+ const adminMcp = activeLaunch?.profile?.servers?.find(
1213
+ (server) => server.alias === "sellable-admin"
1214
+ );
1215
+ if (!authority || !running || !activeLaunch.slackProof) return null;
1216
+ const observedAt = new Date();
1217
+ return buildRuntimeServingObservation({
1218
+ scope: authority.scope,
1219
+ kind: "ADMIN",
1220
+ operationId: activeLaunch.claim.operation.id,
1221
+ workspaceId: activeLaunch.claim.operation.workspaceId,
1222
+ agentId: activeLaunch.claim.operation.agentId,
1223
+ runtimeId: authority.runtimeId,
1224
+ authorityGeneration: authority.authorityGeneration,
1225
+ runtimeArtifactId: authority.runtimeArtifactId,
1226
+ machineId: exchange.workerId,
1227
+ machineVersion: authority.machineVersion,
1228
+ bootSessionId: BOOT_SESSION_ID,
1229
+ profileIdentity: activeLaunch.profile.profile,
1230
+ processIdentity: `gateway-${activeGateway.child.pid}-${activeLaunch.claim.operation.fence}`,
1231
+ observedAt: observedAt.toISOString(),
1232
+ expiresAt: new Date(observedAt.getTime() + 120_000).toISOString(),
1233
+ slack: {
1234
+ authenticated: true,
1235
+ socketModeConnected: true,
1236
+ apiReachable: true,
1237
+ },
1238
+ productMcp: {
1239
+ serverInfoName: productMcp?.serverInfoName,
1240
+ toolCount: productMcp?.toolCount,
1241
+ },
1242
+ adminMcp: {
1243
+ serverInfoName: adminMcp?.serverInfoName,
1244
+ toolCount: adminMcp?.toolCount,
1245
+ },
1246
+ nativeGateway: true,
1247
+ customerRouting: false,
1248
+ });
1249
+ } catch {
1250
+ return null;
1251
+ }
1252
+ }
1253
+
1203
1254
  function startOperationHeartbeat({ claim, exchange, client }) {
1204
1255
  const identity = operationIdentity(claim, exchange);
1205
1256
  let stopped = false;
@@ -1708,6 +1759,42 @@ function nativeSlackProfileInput(claim, installed) {
1708
1759
  };
1709
1760
  }
1710
1761
 
1762
+ async function proveAdminSlackServing(claim, secrets) {
1763
+ const slack = claim.runtimeContract?.slack;
1764
+ if (!slack) throw new Error("admin_runtime_slack_contract_rejected");
1765
+ const post = (method, token) =>
1766
+ fetch(`https://slack.com/api/${method}`, {
1767
+ method: "POST",
1768
+ headers: {
1769
+ authorization: `Bearer ${token}`,
1770
+ "content-type": "application/x-www-form-urlencoded",
1771
+ },
1772
+ body: "",
1773
+ signal: AbortSignal.timeout(15_000),
1774
+ });
1775
+ const [authResponse, socketResponse] = await Promise.all([
1776
+ post("auth.test", secrets.slackBot),
1777
+ post("apps.connections.open", secrets.slackApp),
1778
+ ]);
1779
+ const [auth, socket] = await Promise.all([
1780
+ authResponse.json(),
1781
+ socketResponse.json(),
1782
+ ]);
1783
+ if (
1784
+ !authResponse.ok ||
1785
+ auth?.ok !== true ||
1786
+ auth.team_id !== slack.teamId ||
1787
+ auth.user_id !== slack.botUserId ||
1788
+ !socketResponse.ok ||
1789
+ socket?.ok !== true ||
1790
+ typeof socket.url !== "string" ||
1791
+ !/^wss:\/\//.test(socket.url)
1792
+ ) {
1793
+ throw new Error("admin_runtime_slack_serving_rejected");
1794
+ }
1795
+ return true;
1796
+ }
1797
+
1711
1798
  async function fetchDeliveryCiphertext({
1712
1799
  operation,
1713
1800
  identity,
@@ -2264,7 +2351,12 @@ function completionReceipt({
2264
2351
  };
2265
2352
  }
2266
2353
 
2267
- async function flushHermesSnapshot({ client, profileId, profileRoot }) {
2354
+ async function flushHermesSnapshot({
2355
+ client,
2356
+ profileId,
2357
+ profileRoot,
2358
+ operationIdentity,
2359
+ }) {
2268
2360
  try {
2269
2361
  markHermesSnapshotDirty(profileRoot);
2270
2362
  return await reconcileHermesSnapshot({
@@ -2273,13 +2365,24 @@ async function flushHermesSnapshot({ client, profileId, profileRoot }) {
2273
2365
  hermesRoot: DATA_ROOT,
2274
2366
  hermesVersion: "0.20.0",
2275
2367
  debounceMs: 0,
2276
- upload: ({ archiveDigest, archiveBytes, sourceProfileId, bytes }) =>
2368
+ operationIdentity,
2369
+ upload: ({
2370
+ archiveDigest,
2371
+ archiveBytes,
2372
+ sourceProfileId,
2373
+ bytes,
2374
+ ok: _ok,
2375
+ status: _status,
2376
+ hermesVersion: _hermesVersion,
2377
+ ...v2
2378
+ }) =>
2277
2379
  client.putMemorySnapshot(
2278
2380
  {
2279
2381
  archiveDigest,
2280
2382
  archiveBytes,
2281
2383
  hermesVersion: "0.20.0",
2282
2384
  sourceProfileId,
2385
+ ...v2,
2283
2386
  },
2284
2387
  bytes
2285
2388
  ),
@@ -2992,6 +3095,11 @@ async function runAdminProduction() {
2992
3095
  callerProfileId: activeLaunch.profile.profile,
2993
3096
  }),
2994
3097
  leaseSeconds: 60,
3098
+ servingObservation: adminServingObservation(
3099
+ activeLaunch,
3100
+ activeGateway,
3101
+ exchange
3102
+ ),
2995
3103
  })
2996
3104
  : await client.claim(60);
2997
3105
  } catch (error) {
@@ -3061,16 +3169,6 @@ async function runAdminProduction() {
3061
3169
  ),
3062
3170
  });
3063
3171
  }
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
3172
  await sleep(1_000);
3075
3173
  continue;
3076
3174
  }
@@ -3182,6 +3280,14 @@ async function runAdminProduction() {
3182
3280
  fetchLatest: () => client.getMemorySnapshot(),
3183
3281
  });
3184
3282
  if (!memoryRestore.ok) throw new Error(memoryRestore.code);
3283
+ const legacyLightfieldVenv = quarantineLegacyLightfieldVenv({
3284
+ profileId: basename(profileContract.profileHome),
3285
+ profileRoot: profileContract.profileHome,
3286
+ hermesRoot: DATA_ROOT,
3287
+ });
3288
+ if (!legacyLightfieldVenv.ok) {
3289
+ throw new Error(legacyLightfieldVenv.code);
3290
+ }
3185
3291
  const legacyUserState = migrateLegacyHermesUserState({
3186
3292
  profileId: basename(profileContract.profileHome),
3187
3293
  profileRoot: profileContract.profileHome,
@@ -3214,6 +3320,7 @@ async function runAdminProduction() {
3214
3320
  client,
3215
3321
  profileId: profile.profile,
3216
3322
  profileRoot: profile.profileHome,
3323
+ operationIdentity: operationSnapshotIdentity(claim, exchange),
3217
3324
  });
3218
3325
  const settings = settingsProof(
3219
3326
  imported.receipt.generationRoot,
@@ -3248,8 +3355,14 @@ async function runAdminProduction() {
3248
3355
  modelAuth,
3249
3356
  });
3250
3357
  operationHeartbeat.assertHealthy();
3358
+ const slackProof =
3359
+ desiredLifecycle === "ON"
3360
+ ? await proveAdminSlackServing(claim, secrets)
3361
+ : false;
3251
3362
  const candidateLaunch =
3252
- desiredLifecycle === "ON" ? { profile, claim, installed } : null;
3363
+ desiredLifecycle === "ON"
3364
+ ? { profile, claim, installed, slackProof }
3365
+ : null;
3253
3366
  let candidateGateway = null;
3254
3367
  try {
3255
3368
  if (candidateLaunch) {
@@ -6,7 +6,7 @@ USER root
6
6
 
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.865
8
8
  ARG MCP_INTEGRITY=sha512-282pkYFADFrrIXyQcdUpQwakXiwe/AFUDpDZkTh29CFcGHnN7dvC0MCvaU4LffXA+ZuKCpuf02uxCLGiIEOjbw==
9
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.609
9
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.612-wip.phase189.20260812.2
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.609";
99
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.612-wip.phase189.20260812.2";
100
100
  const MCP_PACKAGE =
101
101
  process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.865";
102
102
 
@@ -32,6 +32,77 @@ 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, tools) =>
62
+ exactKeys(value, ["serverInfoName", "toolCount"]) &&
63
+ value.serverInfoName === name &&
64
+ value.toolCount === tools;
65
+ const observedAt = Date.parse(input.observedAt ?? "");
66
+ const expiresAt = Date.parse(input.expiresAt ?? "");
67
+ const kindExact =
68
+ input.kind === "CUSTOMER"
69
+ ? input.adminMcp === null && input.customerRouting === true
70
+ : input.kind === "ADMIN" &&
71
+ mcp(input.adminMcp, "sellable-admin-mcp", 46) &&
72
+ input.customerRouting === false;
73
+ if (
74
+ !exactKeys(input, keys) ||
75
+ !["ASSIGNED", "CANDIDATE"].includes(input.scope) ||
76
+ !keys
77
+ .slice(2, 13)
78
+ .filter((key) => key !== "authorityGeneration")
79
+ .every((key) => identity(input[key])) ||
80
+ !Number.isSafeInteger(input.authorityGeneration) ||
81
+ input.authorityGeneration < 1 ||
82
+ !Number.isFinite(observedAt) ||
83
+ !Number.isFinite(expiresAt) ||
84
+ expiresAt <= observedAt ||
85
+ expiresAt - observedAt > 5 * 60_000 ||
86
+ !exactKeys(input.slack, [
87
+ "authenticated",
88
+ "socketModeConnected",
89
+ "apiReachable",
90
+ ]) ||
91
+ input.slack.authenticated !== true ||
92
+ input.slack.socketModeConnected !== true ||
93
+ input.slack.apiReachable !== true ||
94
+ !mcp(input.productMcp, "sellable-mcp", 193) ||
95
+ input.nativeGateway !== true ||
96
+ !kindExact
97
+ ) {
98
+ throw new Error("runtime_serving_observation_rejected");
99
+ }
100
+ return Object.freeze({
101
+ schemaVersion: "sellable-agent-serving-observation/v2",
102
+ ...input,
103
+ });
104
+ }
105
+
35
106
  export function buildAgentServiceEnvironment({
36
107
  workspaceId,
37
108
  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
  }
@@ -55,22 +55,12 @@ export const HERMES_SNAPSHOT_CRON = Object.freeze({
55
55
  installedScript: "scripts/hermes-memory-reconcile.sh",
56
56
  noAgent: true,
57
57
  });
58
- export const HERMES_OPPORTUNITY_SEQUENCES_CRON = Object.freeze({
59
- name: "sellable-opportunity-sequences",
60
- schedule: "every 30m",
61
- deliver: "slack",
62
- skills: Object.freeze(["opportunity-sequences"]),
63
- enabledToolsets: Object.freeze(["safe", "sellable", "mcp"]),
64
- noAgent: false,
65
- });
66
- export const HERMES_OPPORTUNITY_SEQUENCES_JOURNAL_ROOT =
67
- "/opt/data/.sellable-agent/admin-runtime/opportunity-sequences";
68
58
  const LEGACY_HERMES_SNAPSHOT_SCRIPT = "scripts/hermes-memory-reconcile.sh";
69
59
 
70
60
  const HERMES = "/opt/hermes/.venv/bin/hermes";
71
- const HERMES_PYTHON = "/opt/hermes/.venv/bin/python";
72
61
  const VERSION = "0.20.0";
73
62
  const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
63
+ const BOUNDED_ID = /^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/;
74
64
  const SHA256 = /^[a-f0-9]{64}$/;
75
65
  const MAX_BYTES = 128 * 1024 * 1024;
76
66
  // Portable snapshots contain user-authored/customizable state plus the compact
@@ -277,77 +267,125 @@ export function mergeHermesState(sourceRoot, targetRoot) {
277
267
  merge(sourceRoot, targetRoot);
278
268
  }
279
269
 
280
- function cronJobs(profileRoot) {
281
- const path = join(profileRoot, "cron", "jobs.json");
282
- if (!existsSync(path)) return [];
283
- regularFile(path);
284
- const jobs = JSON.parse(readFileSync(path, "utf8")).jobs;
285
- if (!Array.isArray(jobs)) throw new Error("hermes_snapshot_cron_rejected");
286
- return jobs;
287
- }
270
+ const LEGACY_LIGHTFIELD_VENV = ".venv-amplify-audit";
271
+ const LEGACY_LIGHTFIELD_QUARANTINE_SCHEMA =
272
+ "sellable-agent-lightfield-venv-quarantine/v1";
288
273
 
289
- const sameJson = (left, right) =>
290
- JSON.stringify(left) === JSON.stringify(right);
274
+ function privateMigrationDirectory(path) {
275
+ if (existsSync(path)) {
276
+ const stat = lstatSync(path);
277
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
278
+ throw new Error("hermes_lightfield_quarantine_path_rejected");
279
+ }
280
+ return;
281
+ }
282
+ mkdirSync(path, { mode: 0o700 });
283
+ fsyncDirectory(dirname(path));
284
+ }
291
285
 
292
- export async function ensureHermesNamedCron({
286
+ /**
287
+ * Quarantine the one persisted pre-Phase-189 Lightfield virtualenv layout.
288
+ * Its ordinary Python symlinks must never weaken the general snapshot scan.
289
+ */
290
+ export function quarantineLegacyLightfieldVenv({
291
+ profileId,
293
292
  profileRoot,
294
- name,
295
- isExpected,
296
- isUpdatable = () => true,
297
- create,
298
- update,
299
- codePrefix = "hermes_named_cron",
300
- }) {
301
- if (
302
- !isAbsolute(profileRoot) ||
303
- !ID.test(name ?? "") ||
304
- typeof isExpected !== "function" ||
305
- typeof create !== "function" ||
306
- typeof update !== "function"
307
- ) {
308
- return failed(`${codePrefix}_identity_rejected`);
293
+ hermesRoot,
294
+ } = {}) {
295
+ if (!validProfilePath(profileId, profileRoot, hermesRoot)) {
296
+ return {
297
+ ...failed("hermes_lightfield_quarantine_identity_rejected"),
298
+ quarantinePath: null,
299
+ };
309
300
  }
301
+ const legacyPath = join(profileRoot, LEGACY_LIGHTFIELD_VENV);
302
+ const quarantineBase = join(
303
+ hermesRoot,
304
+ "backups",
305
+ "quarantine",
306
+ "legacy-lightfield-venv-v1"
307
+ );
308
+ const quarantinePath = join(quarantineBase, profileId);
309
+ const marker = join(
310
+ profileRoot,
311
+ ".sellable-agent",
312
+ "migrations",
313
+ "lightfield-venv-quarantine-v1.json"
314
+ );
310
315
  try {
311
- const before = cronJobs(profileRoot);
312
- const named = before.filter((job) => job?.name === name);
313
- if (named.length > 1 || (named[0] && !isUpdatable(named[0]))) {
314
- return failed(`${codePrefix}_inventory_rejected`);
316
+ if (existsSync(marker)) {
317
+ regularFile(marker);
318
+ const receipt = JSON.parse(readFileSync(marker, "utf8"));
319
+ if (
320
+ receipt?.schemaVersion !== LEGACY_LIGHTFIELD_QUARANTINE_SCHEMA ||
321
+ receipt.profileId !== profileId ||
322
+ receipt.source !== LEGACY_LIGHTFIELD_VENV ||
323
+ receipt.quarantinePath !== quarantinePath ||
324
+ !existsSync(quarantinePath) ||
325
+ lstatSync(quarantinePath).isSymbolicLink() ||
326
+ !lstatSync(quarantinePath).isDirectory()
327
+ ) {
328
+ throw new Error("hermes_lightfield_quarantine_receipt_rejected");
329
+ }
330
+ return { ok: true, status: "REUSED", profileId, quarantinePath };
315
331
  }
316
- if (named[0] && isExpected(named[0])) {
332
+ const sourcePresent = existsSync(legacyPath);
333
+ const targetPresent = existsSync(quarantinePath);
334
+ if (!sourcePresent && !targetPresent) {
317
335
  return {
318
336
  ok: true,
319
- status: "REUSED",
320
- jobId: named[0].id,
321
- job: named[0],
337
+ status: "NO_LEGACY_LAYOUT",
338
+ profileId,
339
+ quarantinePath: null,
322
340
  };
323
341
  }
324
- const siblings = before.filter((job) => job?.name !== name);
325
- const mutation = named[0] ? await update(named[0]) : await create();
326
- if (childFailed(mutation)) {
327
- return failed(`${codePrefix}_${named[0] ? "update" : "create"}_failed`);
342
+ if (sourcePresent && targetPresent) {
343
+ throw new Error("hermes_lightfield_quarantine_conflict");
328
344
  }
329
- const after = cronJobs(profileRoot);
330
- const readback = after.filter((job) => job?.name === name);
331
- const afterSiblings = after.filter((job) => job?.name !== name);
332
- if (!sameJson(afterSiblings, siblings)) {
333
- return failed(`${codePrefix}_sibling_readback_rejected`);
345
+ let status = "RESUMED";
346
+ if (sourcePresent) {
347
+ const source = lstatSync(legacyPath);
348
+ if (source.isSymbolicLink() || !source.isDirectory()) {
349
+ throw new Error("hermes_lightfield_quarantine_source_rejected");
350
+ }
351
+ privateMigrationDirectory(join(hermesRoot, "backups"));
352
+ privateMigrationDirectory(join(hermesRoot, "backups", "quarantine"));
353
+ privateMigrationDirectory(quarantineBase);
354
+ moveDurably(legacyPath, quarantinePath);
355
+ status = "QUARANTINED";
334
356
  }
335
- if (readback.length !== 1 || !isExpected(readback[0])) {
336
- return failed(
337
- `${codePrefix}_${named[0] ? "update_readback" : "readback"}_rejected`
338
- );
357
+ const quarantined = lstatSync(quarantinePath);
358
+ if (quarantined.isSymbolicLink() || !quarantined.isDirectory()) {
359
+ throw new Error("hermes_lightfield_quarantine_readback_rejected");
339
360
  }
361
+ atomicWrite(
362
+ marker,
363
+ `${JSON.stringify({
364
+ schemaVersion: LEGACY_LIGHTFIELD_QUARANTINE_SCHEMA,
365
+ profileId,
366
+ source: LEGACY_LIGHTFIELD_VENV,
367
+ quarantinePath,
368
+ })}\n`,
369
+ 0o600
370
+ );
371
+ return { ok: true, status, profileId, quarantinePath };
372
+ } catch {
340
373
  return {
341
- ok: true,
342
- status: named[0] ? "UPDATED" : "CREATED",
343
- jobId: readback[0].id,
344
- job: readback[0],
374
+ ...failed("hermes_lightfield_quarantine_failed"),
375
+ quarantinePath: null,
345
376
  };
346
- } catch {
347
- return failed(`${codePrefix}_install_failed`);
348
377
  }
349
378
  }
350
379
 
380
+ function cronJobs(profileRoot) {
381
+ const path = join(profileRoot, "cron", "jobs.json");
382
+ if (!existsSync(path)) return [];
383
+ regularFile(path);
384
+ const jobs = JSON.parse(readFileSync(path, "utf8")).jobs;
385
+ if (!Array.isArray(jobs)) throw new Error("hermes_snapshot_cron_rejected");
386
+ return jobs;
387
+ }
388
+
351
389
  function isCron(job) {
352
390
  return Boolean(
353
391
  job?.id &&
@@ -631,40 +669,25 @@ export async function ensureHermesSnapshotCron({
631
669
  readFileSync(reconcileTemplatePath),
632
670
  0o700
633
671
  );
634
- const result = await ensureHermesNamedCron({
635
- profileRoot,
636
- name: HERMES_SNAPSHOT_CRON.name,
637
- isExpected: isCron,
638
- isUpdatable: (job) => isCron(job) || isLegacyCron(job),
639
- codePrefix: "hermes_snapshot_cron",
640
- create: () =>
641
- childRunner(runChild)(
642
- invocation(
643
- [
644
- "--profile",
645
- profileId,
646
- "cron",
647
- "create",
648
- HERMES_SNAPSHOT_CRON.schedule,
649
- "--name",
650
- HERMES_SNAPSHOT_CRON.name,
651
- "--script",
652
- HERMES_SNAPSHOT_CRON.script,
653
- "--no-agent",
654
- ],
655
- hermesRoot,
656
- hermesCli
657
- )
658
- ),
659
- update: (job) =>
660
- childRunner(runChild)(
672
+ const before = cronJobs(profileRoot).filter(
673
+ (job) => job?.name === HERMES_SNAPSHOT_CRON.name
674
+ );
675
+ if (
676
+ before.length > 1 ||
677
+ (before[0] && !isCron(before[0]) && !isLegacyCron(before[0]))
678
+ ) {
679
+ return failed("hermes_snapshot_cron_inventory_rejected");
680
+ }
681
+ if (before[0]) {
682
+ if (isLegacyCron(before[0])) {
683
+ const migrated = await childRunner(runChild)(
661
684
  invocation(
662
685
  [
663
686
  "--profile",
664
687
  profileId,
665
688
  "cron",
666
689
  "edit",
667
- job.id,
690
+ before[0].id,
668
691
  "--script",
669
692
  HERMES_SNAPSHOT_CRON.script,
670
693
  "--no-agent",
@@ -672,194 +695,53 @@ export async function ensureHermesSnapshotCron({
672
695
  hermesRoot,
673
696
  hermesCli
674
697
  )
675
- ),
676
- });
677
- if (!result.ok) {
678
- if (result.code === "hermes_snapshot_cron_update_failed") {
679
- return failed("hermes_snapshot_cron_migrate_failed");
680
- }
681
- if (result.code === "hermes_snapshot_cron_update_readback_rejected") {
682
- return failed("hermes_snapshot_cron_migrate_readback_rejected");
698
+ );
699
+ if (childFailed(migrated)) {
700
+ return failed("hermes_snapshot_cron_migrate_failed");
701
+ }
702
+ const afterMigration = cronJobs(profileRoot).filter(
703
+ (job) => job?.name === HERMES_SNAPSHOT_CRON.name
704
+ );
705
+ return afterMigration.length === 1 && isCron(afterMigration[0])
706
+ ? {
707
+ ok: true,
708
+ status: "MIGRATED",
709
+ jobId: afterMigration[0].id,
710
+ profileId,
711
+ }
712
+ : failed("hermes_snapshot_cron_migrate_readback_rejected");
683
713
  }
684
- return result;
685
- }
686
- return {
687
- ok: true,
688
- status: result.status === "UPDATED" ? "MIGRATED" : result.status,
689
- jobId: result.jobId,
690
- profileId,
691
- };
692
- } catch {
693
- return failed("hermes_snapshot_cron_install_failed");
694
- }
695
- }
696
-
697
- const OPPORTUNITY_SEQUENCE_ASSETS = Object.freeze([
698
- "SKILL.md",
699
- "references/outbound-multichannel-v2.md",
700
- "references/positive-reply-to-agreed-meeting-v1.md",
701
- ]);
702
- const SLACK_USER_ID = /^U[A-Z0-9]{8,20}$/;
703
- const OPPORTUNITY_SEQUENCE_CRON_PYTHON = [
704
- "import json,sys",
705
- "from cron.jobs import create_job, update_job",
706
- "value=json.loads(sys.argv[1])",
707
- "job_id=value.pop('jobId', None)",
708
- "enabled=value.pop('enabled')",
709
- "result=update_job(job_id, {**value, 'enabled': enabled}) if job_id else create_job(**value)",
710
- "print(json.dumps({'id': result.get('id') if result else None}))",
711
- ].join("\n");
712
-
713
- function opportunitySequenceIdentity(profileRoot) {
714
- const root = join(profileRoot, "skills", "opportunity-sequences");
715
- return Object.fromEntries(
716
- OPPORTUNITY_SEQUENCE_ASSETS.map((relative) => {
717
- const path = join(root, relative);
718
- regularFile(path);
719
- return [relative, sha256(readFileSync(path))];
720
- })
721
- );
722
- }
723
-
724
- function opportunitySequencePrompt({
725
- notificationSlackUserId,
726
- journalRoot,
727
- hashes,
728
- }) {
729
- return [
730
- "Run the installed opportunity-sequences skill for one bounded page.",
731
- `Use the private journal at ${journalRoot}.`,
732
- `Require these installed asset hashes: ${JSON.stringify(hashes)}.`,
733
- `End with a compact native Slack summary addressed to <@${notificationSlackUserId}>.`,
734
- "Never send prospect outreach; never claim exactly-once delivery; never use a direct Slack or Lightfield transport outside the installed skill and Product MCP.",
735
- ].join(" ");
736
- }
737
-
738
- export async function ensureHermesOpportunitySequencesCron({
739
- profileId,
740
- profileRoot,
741
- hermesRoot,
742
- statePath,
743
- journalRoot = HERMES_OPPORTUNITY_SEQUENCES_JOURNAL_ROOT,
744
- hermesPython = HERMES_PYTHON,
745
- runChild,
746
- }) {
747
- if (
748
- !validProfilePath(profileId, profileRoot, hermesRoot) ||
749
- !existsSync(profileRoot) ||
750
- !isAbsolute(statePath) ||
751
- !isAbsolute(journalRoot)
752
- ) {
753
- return failed("hermes_opportunity_sequences_cron_identity_rejected");
754
- }
755
- try {
756
- const stateStat = regularFile(statePath);
757
- if ((stateStat.mode & 0o077) !== 0) {
758
- return failed("hermes_opportunity_sequences_cron_state_mode_rejected");
759
- }
760
- const state = JSON.parse(readFileSync(statePath, "utf8"));
761
- const notificationSlackUserId =
762
- state?.opportunitySequences?.notificationSlackUserId;
763
- if (!SLACK_USER_ID.test(notificationSlackUserId ?? "")) {
764
- return failed("hermes_opportunity_sequences_cron_notification_rejected");
714
+ return { ok: true, status: "REUSED", jobId: before[0].id, profileId };
765
715
  }
766
- mkdirSync(journalRoot, { recursive: true, mode: 0o700 });
767
- chmodSync(journalRoot, 0o700);
768
- const journalStat = lstatSync(journalRoot);
769
- if (
770
- journalStat.isSymbolicLink() ||
771
- !journalStat.isDirectory() ||
772
- (journalStat.mode & 0o077) !== 0
773
- ) {
774
- return failed("hermes_opportunity_sequences_cron_journal_rejected");
775
- }
776
- const hashes = opportunitySequenceIdentity(profileRoot);
777
- const prompt = opportunitySequencePrompt({
778
- notificationSlackUserId,
779
- journalRoot,
780
- hashes,
781
- });
782
- const expected = (job) =>
783
- Boolean(
784
- job?.id &&
785
- job.name === HERMES_OPPORTUNITY_SEQUENCES_CRON.name &&
786
- job.prompt === prompt &&
787
- job.no_agent === false &&
788
- job.enabled === true &&
789
- job.deliver === HERMES_OPPORTUNITY_SEQUENCES_CRON.deliver &&
790
- sameJson(job.skills, HERMES_OPPORTUNITY_SEQUENCES_CRON.skills) &&
791
- sameJson(
792
- job.enabled_toolsets,
793
- HERMES_OPPORTUNITY_SEQUENCES_CRON.enabledToolsets
794
- ) &&
795
- job.schedule?.kind === "interval" &&
796
- job.schedule?.minutes === 30
797
- );
798
- const mutate = (jobId = null) => {
799
- const spec = {
800
- prompt,
801
- schedule: HERMES_OPPORTUNITY_SEQUENCES_CRON.schedule,
802
- name: HERMES_OPPORTUNITY_SEQUENCES_CRON.name,
803
- deliver: HERMES_OPPORTUNITY_SEQUENCES_CRON.deliver,
804
- skills: [...HERMES_OPPORTUNITY_SEQUENCES_CRON.skills],
805
- enabled_toolsets: [
806
- ...HERMES_OPPORTUNITY_SEQUENCES_CRON.enabledToolsets,
716
+ const result = await childRunner(runChild)(
717
+ invocation(
718
+ [
719
+ "--profile",
720
+ profileId,
721
+ "cron",
722
+ "create",
723
+ HERMES_SNAPSHOT_CRON.schedule,
724
+ "--name",
725
+ HERMES_SNAPSHOT_CRON.name,
726
+ "--script",
727
+ HERMES_SNAPSHOT_CRON.script,
728
+ "--no-agent",
807
729
  ],
808
- no_agent: false,
809
- enabled: true,
810
- ...(jobId ? { jobId } : {}),
811
- };
812
- return childRunner(runChild)({
813
- command: hermesPython,
814
- args: ["-c", OPPORTUNITY_SEQUENCE_CRON_PYTHON, JSON.stringify(spec)],
815
- env: {
816
- ...process.env,
817
- HOME: hermesRoot,
818
- HERMES_HOME: profileRoot,
819
- HERMES_PROFILE: profileId,
820
- NO_COLOR: "1",
821
- },
822
- shell: false,
823
- timeoutMs: NATIVE_COMMAND_TIMEOUT_MS,
824
- maxOutputBytes: 64 * 1024,
825
- });
826
- };
827
- const result = await ensureHermesNamedCron({
828
- profileRoot,
829
- name: HERMES_OPPORTUNITY_SEQUENCES_CRON.name,
830
- isExpected: expected,
831
- create: () => mutate(),
832
- update: (job) => mutate(job.id),
833
- codePrefix: "hermes_opportunity_sequences_cron",
834
- });
835
- if (!result.ok) return result;
836
- const jobsMode =
837
- lstatSync(join(profileRoot, "cron", "jobs.json")).mode & 0o777;
838
- if ((jobsMode & 0o077) !== 0) {
839
- return failed("hermes_opportunity_sequences_cron_jobs_mode_rejected");
730
+ hermesRoot,
731
+ hermesCli
732
+ )
733
+ );
734
+ if (childFailed(result)) {
735
+ return failed("hermes_snapshot_cron_create_failed");
840
736
  }
841
- return {
842
- ok: true,
843
- status: result.status,
844
- jobId: result.jobId,
845
- name: result.job.name,
846
- prompt: result.job.prompt,
847
- schedule: result.job.schedule,
848
- profileId,
849
- enabledToolsets: result.job.enabled_toolsets,
850
- skills: result.job.skills,
851
- assetHashes: hashes,
852
- notificationSlackUserIdSource:
853
- "config-state.json:opportunitySequences.notificationSlackUserId",
854
- journalRoot,
855
- modes: {
856
- state: stateStat.mode & 0o777,
857
- journal: journalStat.mode & 0o777,
858
- jobs: jobsMode,
859
- },
860
- };
737
+ const after = cronJobs(profileRoot).filter(
738
+ (job) => job?.name === HERMES_SNAPSHOT_CRON.name
739
+ );
740
+ return after.length === 1 && isCron(after[0])
741
+ ? { ok: true, status: "CREATED", jobId: after[0].id, profileId }
742
+ : failed("hermes_snapshot_cron_readback_rejected");
861
743
  } catch {
862
- return failed("hermes_opportunity_sequences_cron_install_failed");
744
+ return failed("hermes_snapshot_cron_install_failed");
863
745
  }
864
746
  }
865
747
 
@@ -872,12 +754,31 @@ export async function captureHermesBackup({
872
754
  hermesCli = HERMES,
873
755
  maxArchiveBytes = MAX_BYTES,
874
756
  runChild,
757
+ operationIdentity = /** @type {any} */ (undefined),
875
758
  }) {
759
+ const operationIdentityAccepted =
760
+ operationIdentity === undefined ||
761
+ (operationIdentity &&
762
+ typeof operationIdentity === "object" &&
763
+ !Array.isArray(operationIdentity) &&
764
+ Object.keys(operationIdentity).sort().join(",") ===
765
+ "authorityGeneration,operationId,profileSchemaVersion,runtimeArtifactId,sourceBootSessionId,sourceMachineId,sourceMachineVersion" &&
766
+ BOUNDED_ID.test(operationIdentity.operationId ?? "") &&
767
+ Number.isSafeInteger(operationIdentity.authorityGeneration) &&
768
+ operationIdentity.authorityGeneration > 0 &&
769
+ BOUNDED_ID.test(operationIdentity.runtimeArtifactId ?? "") &&
770
+ BOUNDED_ID.test(operationIdentity.profileSchemaVersion ?? "") &&
771
+ [
772
+ operationIdentity.sourceMachineId,
773
+ operationIdentity.sourceMachineVersion,
774
+ operationIdentity.sourceBootSessionId,
775
+ ].every((value) => value === null || BOUNDED_ID.test(value ?? "")));
876
776
  if (
877
777
  !validProfilePath(profileId, profileRoot, hermesRoot) ||
878
778
  !existsSync(profileRoot) ||
879
779
  hermesVersion !== VERSION ||
880
- !isAbsolute(archiveDirectory)
780
+ !isAbsolute(archiveDirectory) ||
781
+ !operationIdentityAccepted
881
782
  ) {
882
783
  return failed("hermes_snapshot_capture_identity_rejected");
883
784
  }
@@ -917,6 +818,7 @@ export async function captureHermesBackup({
917
818
  archiveBytes: bytes.byteLength,
918
819
  hermesVersion,
919
820
  sourceProfileId: profileId,
821
+ ...(operationIdentity ?? {}),
920
822
  bytes,
921
823
  };
922
824
  } catch {
@@ -1022,6 +924,7 @@ async function reconcileHermesSnapshotOnce({
1022
924
  debounceMs = 2_000,
1023
925
  hermesCli = HERMES,
1024
926
  runChild,
927
+ operationIdentity = /** @type {any} */ (undefined),
1025
928
  }) {
1026
929
  if (!validProfilePath(profileId, profileRoot, hermesRoot)) {
1027
930
  return failed("hermes_snapshot_reconcile_identity_rejected");
@@ -1048,6 +951,7 @@ async function reconcileHermesSnapshotOnce({
1048
951
  hermesVersion,
1049
952
  hermesCli,
1050
953
  runChild,
954
+ operationIdentity,
1051
955
  });
1052
956
  if (!captured.ok) {
1053
957
  requeueClaim(paths.marker, claim);
@@ -1081,6 +985,7 @@ export async function reconcileHermesSnapshot({
1081
985
  debounceMs = 2_000,
1082
986
  hermesCli = HERMES,
1083
987
  runChild,
988
+ operationIdentity = /** @type {any} */ (undefined),
1084
989
  }) {
1085
990
  const key = `${resolve(hermesRoot)}\0${profileId}`;
1086
991
  const active = snapshotReconciles.get(key);
@@ -1095,6 +1000,7 @@ export async function reconcileHermesSnapshot({
1095
1000
  debounceMs,
1096
1001
  hermesCli,
1097
1002
  runChild,
1003
+ operationIdentity,
1098
1004
  }).finally(() => snapshotReconciles.delete(key));
1099
1005
  snapshotReconciles.set(key, operation);
1100
1006
  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.609";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.612-wip.phase189.20260812.2";
42
42
  const MCP_PACKAGE = "@sellable/mcp@0.1.865";
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.609" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.612-wip.phase189.20260812.2" ||
1550
1550
  pinned.mcpPackage !== "@sellable/mcp@0.1.865" ||
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.609" ||
281
+ desired.installerPackage !== "@sellable/install@0.1.612-wip.phase189.20260812.2" ||
282
282
  desired.mcpPackage !== "@sellable/mcp@0.1.865" ||
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.609";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.612-wip.phase189.20260812.2";
29
29
  export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.865";
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.610",
3
+ "version": "0.1.612-wip.phase189.20260812.2",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {