@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.
- package/container/Dockerfile +4 -4
- package/container/README.md +2 -2
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +142 -29
- package/lib/sellable-agent/fly-customer-image/Dockerfile +1 -1
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +1 -1
- package/lib/sellable-agent/fly-customer-worker.mjs +71 -0
- package/lib/sellable-agent/fly-runtime-identity.mjs +89 -14
- package/lib/sellable-agent/hermes-memory-snapshot.mjs +182 -276
- package/lib/sellable-agent/host-bootstrap.mjs +1 -1
- package/lib/sellable-agent/host-worker.mjs +1 -1
- package/lib/sellable-agent/profile-materializer.mjs +1 -1
- package/lib/sellable-agent/provisioning-adapter.mjs +1 -1
- package/lib/sellable-agent/runtime-reconciler.mjs +47 -0
- package/package.json +1 -1
package/container/Dockerfile
CHANGED
|
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
package/container/README.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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 (
|
|
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({
|
|
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
|
-
|
|
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"
|
|
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.
|
|
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.
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
-
|
|
1054
|
-
"memory-snapshot-get"
|
|
1108
|
+
body,
|
|
1109
|
+
"memory-snapshot-get",
|
|
1110
|
+
search
|
|
1055
1111
|
);
|
|
1056
1112
|
if (!result) return null;
|
|
1057
|
-
|
|
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 ({
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
332
|
+
const sourcePresent = existsSync(legacyPath);
|
|
333
|
+
const targetPresent = existsSync(quarantinePath);
|
|
334
|
+
if (!sourcePresent && !targetPresent) {
|
|
317
335
|
return {
|
|
318
336
|
ok: true,
|
|
319
|
-
status: "
|
|
320
|
-
|
|
321
|
-
|
|
337
|
+
status: "NO_LEGACY_LAYOUT",
|
|
338
|
+
profileId,
|
|
339
|
+
quarantinePath: null,
|
|
322
340
|
};
|
|
323
341
|
}
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
return
|
|
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
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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("
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|