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