@sellable/install 0.1.682 → 0.1.684
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 +7 -7
- package/container/README.md +1 -1
- package/lib/sellable-agent/external-runtime-builder.mjs +1 -1
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +13 -6
- package/lib/sellable-agent/fly-customer-image/Dockerfile +3 -3
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +6 -2
- package/lib/sellable-agent/fly-runtime-identity.mjs +2 -2
- package/lib/sellable-agent/hermes-memory-snapshot.mjs +317 -27
- package/lib/sellable-agent/host-bootstrap.mjs +2 -2
- package/lib/sellable-agent/host-worker.mjs +2 -2
- package/lib/sellable-agent/profile-materializer.mjs +2 -2
- package/lib/sellable-agent/provisioning-adapter.mjs +2 -2
- package/package.json +1 -1
package/container/Dockerfile
CHANGED
|
@@ -3,13 +3,13 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
7
|
-
ARG MCP_PACKAGE=@sellable/mcp@0.1.
|
|
6
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.684
|
|
7
|
+
ARG MCP_PACKAGE=@sellable/mcp@0.1.900
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
|
-
ARG MCP_INTEGRITY=sha512-
|
|
9
|
+
ARG MCP_INTEGRITY=sha512-xLAA8pzfKujvUYT+eg8FPTGgA2CIcgsPG3bM1NjMPLOSib7l32H4ca9SkbeBV7jOPCxcc3+XZBKEIhmtJufiEg==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.
|
|
12
|
-
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.684" \
|
|
12
|
+
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.900" \
|
|
13
13
|
&& test -n "${INSTALLER_INTEGRITY}" \
|
|
14
14
|
&& test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
|
|
15
15
|
&& test "$(npm view "${MCP_PACKAGE}" dist.integrity)" = "${MCP_INTEGRITY}" \
|
|
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.682" \
|
|
|
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.684' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.900') 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.684', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.900', 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,7 +3,7 @@
|
|
|
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.684` is
|
|
7
7
|
published:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
|
|
29
29
|
const MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
|
|
30
30
|
const MCP_PACKAGE = "@sellable/mcp";
|
|
31
|
-
const MCP_VERSION = "0.1.
|
|
31
|
+
const MCP_VERSION = "0.1.900";
|
|
32
32
|
const SAFE_COMPONENT = /^[A-Za-z0-9@._+-]+$/;
|
|
33
33
|
const HERMES_EXCLUDES = new Set([".git", ".playwright", "node_modules"]);
|
|
34
34
|
|
|
@@ -831,8 +831,10 @@ const ADMIN_LIVE_SKILLS_CONFIG_KEYS = Object.freeze([
|
|
|
831
831
|
"platform_disabled",
|
|
832
832
|
]);
|
|
833
833
|
const ADMIN_LIVE_PLUGIN_CONFIG_KEYS = Object.freeze(["enabled", "disabled"]);
|
|
834
|
-
const
|
|
835
|
-
|
|
834
|
+
const ADMIN_SEALED_SKILL_PATHS = new Set([
|
|
835
|
+
// The package owns only the root routing skill. Customer/operator skills
|
|
836
|
+
// live below this namespace and must survive profile materialization.
|
|
837
|
+
"sellable/SKILL.md",
|
|
836
838
|
// The Admin profile factory owns this whole namespace. Preserving an old
|
|
837
839
|
// package version as mutable operator state makes every package upgrade
|
|
838
840
|
// restage forever when any Admin skill changes.
|
|
@@ -865,7 +867,9 @@ function adminPreservedSkillsIdentity(profileRoot) {
|
|
|
865
867
|
const nextRelative = relativePath ? `${relativePath}/${name}` : name;
|
|
866
868
|
if (
|
|
867
869
|
rootName === "skills" &&
|
|
868
|
-
|
|
870
|
+
[...ADMIN_SEALED_SKILL_PATHS].some(
|
|
871
|
+
(path) => nextRelative === path || nextRelative.startsWith(`${path}/`)
|
|
872
|
+
)
|
|
869
873
|
)
|
|
870
874
|
continue;
|
|
871
875
|
const path = join(directory, name);
|
|
@@ -1188,9 +1192,7 @@ async function materializeAdminProfile({
|
|
|
1188
1192
|
join(profileHome, "skills"),
|
|
1189
1193
|
join(stageRoot, "skills"),
|
|
1190
1194
|
{
|
|
1191
|
-
|
|
1192
|
-
...ADMIN_SEALED_SKILL_DIRECTORIES,
|
|
1193
|
-
],
|
|
1195
|
+
preserveTargetPaths: [...ADMIN_SEALED_SKILL_PATHS],
|
|
1194
1196
|
}
|
|
1195
1197
|
);
|
|
1196
1198
|
}
|
|
@@ -3735,6 +3737,11 @@ async function runAdminProduction() {
|
|
|
3735
3737
|
backup.code ?? "admin_operation_backup_proof_unavailable"
|
|
3736
3738
|
);
|
|
3737
3739
|
}
|
|
3740
|
+
// BACKUP advances the Agent authority generation without replacing the
|
|
3741
|
+
// running gateway. Rebind its serving proof to the snapshot operation;
|
|
3742
|
+
// otherwise idle claim ticks keep presenting the prior generation and
|
|
3743
|
+
// Sellable correctly expires the otherwise-healthy Slack serving lease.
|
|
3744
|
+
if (activeLaunch) activeLaunch = { ...activeLaunch, claim };
|
|
3738
3745
|
operationHeartbeat.assertHealthy();
|
|
3739
3746
|
await operationHeartbeat.stop();
|
|
3740
3747
|
operationHeartbeat = null;
|
|
@@ -6,9 +6,9 @@ USER root
|
|
|
6
6
|
|
|
7
7
|
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
|
|
8
8
|
|
|
9
|
-
ARG MCP_PACKAGE=@sellable/mcp@0.1.
|
|
10
|
-
ARG MCP_INTEGRITY=sha512-
|
|
11
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
9
|
+
ARG MCP_PACKAGE=@sellable/mcp@0.1.900
|
|
10
|
+
ARG MCP_INTEGRITY=sha512-xLAA8pzfKujvUYT+eg8FPTGgA2CIcgsPG3bM1NjMPLOSib7l32H4ca9SkbeBV7jOPCxcc3+XZBKEIhmtJufiEg==
|
|
11
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.684
|
|
12
12
|
ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.186-wip.phase158.20260802034727
|
|
13
13
|
ARG ADMIN_MCP_INTEGRITY=sha512-qYNew5wd2IfgVcp1UYOG9QvpF/8Mx19YJ5HQtMKzvSfUf3evePjtaKsoXLf2C/GUJ5QlLlHJo84VWOI3V01+9A==
|
|
14
14
|
ARG TUS_JS_CLIENT_VERSION=4.3.1
|
|
@@ -106,9 +106,9 @@ const BOOT_SESSION_ID = randomUUID();
|
|
|
106
106
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.4";
|
|
107
107
|
const INSTALLER_PACKAGE =
|
|
108
108
|
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ??
|
|
109
|
-
"@sellable/install@0.1.
|
|
109
|
+
"@sellable/install@0.1.684";
|
|
110
110
|
const MCP_PACKAGE =
|
|
111
|
-
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.
|
|
111
|
+
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.900";
|
|
112
112
|
const DEFAULT_PROFILE_BUNDLE_ROOT =
|
|
113
113
|
"/usr/local/share/sellable-agent/default-profile-bundle";
|
|
114
114
|
const DEFAULT_PROFILE_CRON_PYTHON = [
|
|
@@ -3280,6 +3280,10 @@ async function main() {
|
|
|
3280
3280
|
backup.code ?? "customer_operation_backup_proof_unavailable"
|
|
3281
3281
|
);
|
|
3282
3282
|
}
|
|
3283
|
+
// BACKUP advances authority while deliberately keeping this gateway
|
|
3284
|
+
// alive. Future serving renewals must therefore use the snapshot
|
|
3285
|
+
// claim instead of the now-stale lifecycle operation generation.
|
|
3286
|
+
if (activeRuntime) activeRuntime = { ...activeRuntime, claim };
|
|
3283
3287
|
} catch (error) {
|
|
3284
3288
|
const failureReceipt = customerRuntimeBackupFailureCompletionReceipt({
|
|
3285
3289
|
claim,
|
|
@@ -1026,7 +1026,7 @@ export function createFlyCustomerControlClient({
|
|
|
1026
1026
|
/^[a-f0-9]{64}$/.test(value?.archiveDigest ?? "") &&
|
|
1027
1027
|
Number.isSafeInteger(value?.archiveBytes) &&
|
|
1028
1028
|
value.archiveBytes > 0 &&
|
|
1029
|
-
value.archiveBytes <=
|
|
1029
|
+
value.archiveBytes <= 512 * 1024 * 1024 &&
|
|
1030
1030
|
value.hermesVersion === "0.20.4" &&
|
|
1031
1031
|
/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value?.sourceProfileId ?? "");
|
|
1032
1032
|
const boundedSnapshotIdentity = (value) =>
|
|
@@ -1159,7 +1159,7 @@ export function createFlyCustomerControlClient({
|
|
|
1159
1159
|
/^[a-f0-9]{64}$/.test(result.archiveDigest ?? "") &&
|
|
1160
1160
|
Number.isSafeInteger(result.archiveBytes) &&
|
|
1161
1161
|
result.archiveBytes > 0 &&
|
|
1162
|
-
result.archiveBytes <=
|
|
1162
|
+
result.archiveBytes <= 512 * 1024 * 1024);
|
|
1163
1163
|
if (
|
|
1164
1164
|
!(isV2 ? identityReadbackAccepted : metadataAccepted(result)) ||
|
|
1165
1165
|
!signedUrlAccepted(result.signedUrl)
|
|
@@ -64,13 +64,7 @@ export const HERMES_OPPORTUNITY_SEQUENCES_CRON = Object.freeze({
|
|
|
64
64
|
schedule: "every 30m",
|
|
65
65
|
deliver: "slack",
|
|
66
66
|
skills: Object.freeze([HERMES_OPPORTUNITY_SEQUENCES_SKILL]),
|
|
67
|
-
enabledToolsets: Object.freeze([
|
|
68
|
-
"file",
|
|
69
|
-
"terminal",
|
|
70
|
-
"safe",
|
|
71
|
-
"sellable",
|
|
72
|
-
"mcp",
|
|
73
|
-
]),
|
|
67
|
+
enabledToolsets: Object.freeze(["file", "terminal", "safe", "sellable", "mcp"]),
|
|
74
68
|
noAgent: false,
|
|
75
69
|
});
|
|
76
70
|
export const HERMES_OPPORTUNITY_SEQUENCES_JOURNAL_ROOT =
|
|
@@ -88,7 +82,13 @@ export const HERMES_CLIENT_RELATIONSHIPS_CRONS = Object.freeze({
|
|
|
88
82
|
chaseSchedule: "30 13 * * 1-5",
|
|
89
83
|
deliver: "slack",
|
|
90
84
|
skills: Object.freeze([HERMES_CLIENT_RELATIONSHIPS_SKILL]),
|
|
91
|
-
enabledToolsets: Object.freeze([
|
|
85
|
+
enabledToolsets: Object.freeze([
|
|
86
|
+
"file",
|
|
87
|
+
"terminal",
|
|
88
|
+
"safe",
|
|
89
|
+
"sellable",
|
|
90
|
+
"mcp",
|
|
91
|
+
]),
|
|
92
92
|
noAgent: false,
|
|
93
93
|
});
|
|
94
94
|
export const CLIENT_RELATIONSHIPS_ASSETS = Object.freeze([
|
|
@@ -104,7 +104,12 @@ const VERSION = "0.20.4";
|
|
|
104
104
|
const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
105
105
|
const BOUNDED_ID = /^[A-Za-z0-9][A-Za-z0-9_./:-]{0,255}$/;
|
|
106
106
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
107
|
-
|
|
107
|
+
// Native state.db is durable conversation/thread state and grows over time.
|
|
108
|
+
// The previous 128 MiB ceiling left the Admin recovery ZIP only ~1.5 MiB of
|
|
109
|
+
// headroom in production and eventually made every protected Backup/Refresh
|
|
110
|
+
// fail after Hermes had already created the archive. Keep this bounded for
|
|
111
|
+
// memory/storage safety, but large enough for an established profile.
|
|
112
|
+
export const HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
|
|
108
113
|
const MAX_PLUGIN_DATA_BYTES = 10 * 1024 * 1024;
|
|
109
114
|
const MAX_PORTABLE_EXTERNAL_BYTES = 32 * 1024 * 1024;
|
|
110
115
|
const MAX_PORTABLE_CONFIG_BYTES = 64 * 1024;
|
|
@@ -115,6 +120,8 @@ const MAX_PORTABLE_CONFIG_BYTES = 64 * 1024;
|
|
|
115
120
|
// persistent retry scheduler back off instead of contending with the live
|
|
116
121
|
// Slack gateway for a full ten minutes.
|
|
117
122
|
const NATIVE_COMMAND_TIMEOUT_MS = 120_000;
|
|
123
|
+
const STATE_DB_COMPACTION_TIMEOUT_MS = 240_000;
|
|
124
|
+
export const HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS = 48;
|
|
118
125
|
const TRANSIENT_PROFILE_ROOTS = new Set([
|
|
119
126
|
".artifact-venv",
|
|
120
127
|
".cache",
|
|
@@ -415,27 +422,55 @@ export function mergeHermesState(
|
|
|
415
422
|
export function replaceHermesState(
|
|
416
423
|
sourceRoot,
|
|
417
424
|
targetRoot,
|
|
418
|
-
/** @type {{preserveTargetTopLevelNames?:string[]}} */
|
|
419
|
-
{ preserveTargetTopLevelNames = [] } = {}
|
|
425
|
+
/** @type {{preserveTargetTopLevelNames?:string[],preserveTargetPaths?:string[]}} */
|
|
426
|
+
{ preserveTargetTopLevelNames = [], preserveTargetPaths = [] } = {}
|
|
420
427
|
) {
|
|
421
428
|
assertHermesMergeRoot(targetRoot);
|
|
422
|
-
const
|
|
429
|
+
const preservedTopLevel = hermesTopLevelNameSet(
|
|
423
430
|
preserveTargetTopLevelNames,
|
|
424
431
|
"hermes_snapshot_replace_policy_rejected"
|
|
425
432
|
);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
433
|
+
const preservedPaths = new Set(
|
|
434
|
+
preserveTargetPaths.map((path) => {
|
|
435
|
+
if (
|
|
436
|
+
typeof path !== "string" ||
|
|
437
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/.test(
|
|
438
|
+
path
|
|
439
|
+
)
|
|
440
|
+
) {
|
|
441
|
+
throw new Error("hermes_snapshot_replace_policy_rejected");
|
|
442
|
+
}
|
|
443
|
+
return path;
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
for (const name of preservedTopLevel) preservedPaths.add(name);
|
|
447
|
+
const prune = (target, relativeRoot = "") => {
|
|
448
|
+
for (const name of readdirSync(target)) {
|
|
449
|
+
const relativePath = relativeRoot ? `${relativeRoot}/${name}` : name;
|
|
450
|
+
const targetPath = join(target, name);
|
|
451
|
+
const stat = lstatSync(targetPath);
|
|
452
|
+
if (stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile())) {
|
|
453
|
+
throw new Error("hermes_snapshot_merge_target_rejected");
|
|
454
|
+
}
|
|
455
|
+
if (preservedPaths.has(relativePath)) continue;
|
|
456
|
+
if (
|
|
457
|
+
[...preservedPaths].some((path) => path.startsWith(`${relativePath}/`))
|
|
458
|
+
) {
|
|
459
|
+
if (!stat.isDirectory()) {
|
|
460
|
+
throw new Error("hermes_snapshot_merge_target_rejected");
|
|
461
|
+
}
|
|
462
|
+
prune(targetPath, relativePath);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
rmSync(targetPath, { recursive: true, force: false });
|
|
432
466
|
}
|
|
433
|
-
|
|
434
|
-
|
|
467
|
+
};
|
|
468
|
+
prune(targetRoot);
|
|
435
469
|
if (!assertHermesMergeRoot(sourceRoot, { allowMissing: true })) return;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
470
|
+
// The merge is deliberately no-clobber. Exact sealed target paths survive,
|
|
471
|
+
// while mutable source siblings (for example sellable/<custom-skill>) are
|
|
472
|
+
// carried into the staged profile.
|
|
473
|
+
mergeHermesStateEntries(sourceRoot, targetRoot);
|
|
439
474
|
}
|
|
440
475
|
|
|
441
476
|
const LEGACY_LIGHTFIELD_VENV = ".venv-amplify-audit";
|
|
@@ -557,6 +592,77 @@ function cronJobs(profileRoot) {
|
|
|
557
592
|
return jobs;
|
|
558
593
|
}
|
|
559
594
|
|
|
595
|
+
function assertStagedCronDependencies(profileRoot) {
|
|
596
|
+
const skillNames = new Set();
|
|
597
|
+
const skillsRoot = join(profileRoot, "skills");
|
|
598
|
+
const walkSkills = (directory) => {
|
|
599
|
+
for (const name of readdirSync(directory)) {
|
|
600
|
+
const path = join(directory, name);
|
|
601
|
+
const stat = lstatSync(path);
|
|
602
|
+
if (stat.isSymbolicLink()) {
|
|
603
|
+
throw new Error("hermes_snapshot_dependency_rejected");
|
|
604
|
+
}
|
|
605
|
+
if (stat.isDirectory()) walkSkills(path);
|
|
606
|
+
else if (stat.isFile() && name === "SKILL.md") {
|
|
607
|
+
skillNames.add(basename(directory));
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
if (existsSync(skillsRoot)) {
|
|
612
|
+
const stat = lstatSync(skillsRoot);
|
|
613
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
614
|
+
throw new Error("hermes_snapshot_dependency_rejected");
|
|
615
|
+
}
|
|
616
|
+
walkSkills(skillsRoot);
|
|
617
|
+
}
|
|
618
|
+
const scriptsRoot = join(profileRoot, "scripts");
|
|
619
|
+
for (const job of cronJobs(profileRoot)) {
|
|
620
|
+
if (!job || typeof job !== "object" || Array.isArray(job)) {
|
|
621
|
+
throw new Error("hermes_snapshot_dependency_rejected");
|
|
622
|
+
}
|
|
623
|
+
const declaredSkills = [];
|
|
624
|
+
if (Object.hasOwn(job, "skill") && job.skill !== null) {
|
|
625
|
+
if (typeof job.skill !== "string" || !ID.test(job.skill)) {
|
|
626
|
+
throw new Error("hermes_snapshot_dependency_rejected");
|
|
627
|
+
}
|
|
628
|
+
declaredSkills.push(job.skill);
|
|
629
|
+
}
|
|
630
|
+
if (Object.hasOwn(job, "skills") && job.skills !== null) {
|
|
631
|
+
if (
|
|
632
|
+
!Array.isArray(job.skills) ||
|
|
633
|
+
job.skills.some((skill) => typeof skill !== "string" || !ID.test(skill))
|
|
634
|
+
) {
|
|
635
|
+
throw new Error("hermes_snapshot_dependency_rejected");
|
|
636
|
+
}
|
|
637
|
+
declaredSkills.push(...job.skills);
|
|
638
|
+
}
|
|
639
|
+
if (declaredSkills.some((skill) => !skillNames.has(skill))) {
|
|
640
|
+
throw new Error("hermes_snapshot_dependency_rejected");
|
|
641
|
+
}
|
|
642
|
+
if (Object.hasOwn(job, "script") && job.script !== null) {
|
|
643
|
+
if (
|
|
644
|
+
typeof job.script !== "string" ||
|
|
645
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/.test(
|
|
646
|
+
job.script
|
|
647
|
+
)
|
|
648
|
+
) {
|
|
649
|
+
throw new Error("hermes_snapshot_dependency_rejected");
|
|
650
|
+
}
|
|
651
|
+
const script = job.script.startsWith("scripts/")
|
|
652
|
+
? job.script.slice("scripts/".length)
|
|
653
|
+
: job.script;
|
|
654
|
+
const scriptPath = join(scriptsRoot, script);
|
|
655
|
+
if (!existsSync(scriptPath)) {
|
|
656
|
+
throw new Error("hermes_snapshot_dependency_rejected");
|
|
657
|
+
}
|
|
658
|
+
const stat = lstatSync(scriptPath);
|
|
659
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
660
|
+
throw new Error("hermes_snapshot_dependency_rejected");
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
560
666
|
const sameJson = (left, right) =>
|
|
561
667
|
JSON.stringify(left) === JSON.stringify(right);
|
|
562
668
|
|
|
@@ -695,6 +801,169 @@ const childRunner = (runChild) => runChild ?? runBoundedHermesChild;
|
|
|
695
801
|
const childFailed = (result) =>
|
|
696
802
|
result?.exitCode !== 0 || result?.timedOut || result?.outputLimitExceeded;
|
|
697
803
|
|
|
804
|
+
const STATE_DB_COMPACTION_SCRIPT = String.raw`
|
|
805
|
+
import json
|
|
806
|
+
import os
|
|
807
|
+
import sqlite3
|
|
808
|
+
import sys
|
|
809
|
+
import time
|
|
810
|
+
|
|
811
|
+
source_path, target_path = sys.argv[1], sys.argv[2]
|
|
812
|
+
retention_hours = int(sys.argv[3])
|
|
813
|
+
source = sqlite3.connect("file:" + source_path + "?mode=ro", uri=True)
|
|
814
|
+
destination = sqlite3.connect(target_path)
|
|
815
|
+
try:
|
|
816
|
+
source.backup(destination)
|
|
817
|
+
finally:
|
|
818
|
+
source.close()
|
|
819
|
+
|
|
820
|
+
try:
|
|
821
|
+
if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
|
|
822
|
+
raise RuntimeError("source backup failed quick_check")
|
|
823
|
+
destination.execute("PRAGMA foreign_keys=ON")
|
|
824
|
+
table_names = {
|
|
825
|
+
row[0]
|
|
826
|
+
for row in destination.execute(
|
|
827
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
828
|
+
)
|
|
829
|
+
}
|
|
830
|
+
pruned_sessions = 0
|
|
831
|
+
pruned_messages = 0
|
|
832
|
+
if {"sessions", "messages"}.issubset(table_names):
|
|
833
|
+
cutoff = time.time() - retention_hours * 60 * 60
|
|
834
|
+
session_columns = {
|
|
835
|
+
row[1] for row in destination.execute("PRAGMA table_info(sessions)")
|
|
836
|
+
}
|
|
837
|
+
pinned_clause = (
|
|
838
|
+
"AND COALESCE(pinned, 0) = 0" if "pinned" in session_columns else ""
|
|
839
|
+
)
|
|
840
|
+
destination.execute(
|
|
841
|
+
"CREATE TEMP TABLE snapshot_pruned_sessions(id TEXT PRIMARY KEY)"
|
|
842
|
+
)
|
|
843
|
+
destination.execute(
|
|
844
|
+
"WITH RECURSIVE doomed(id) AS ("
|
|
845
|
+
" SELECT id FROM sessions"
|
|
846
|
+
" WHERE source = 'cron' AND ended_at IS NOT NULL"
|
|
847
|
+
" AND COALESCE(last_activity_at, ended_at, started_at) < ? "
|
|
848
|
+
+ pinned_clause
|
|
849
|
+
+ " UNION"
|
|
850
|
+
" SELECT child.id FROM sessions child"
|
|
851
|
+
" JOIN doomed parent ON child.parent_session_id = parent.id"
|
|
852
|
+
") INSERT OR IGNORE INTO snapshot_pruned_sessions(id)"
|
|
853
|
+
" SELECT id FROM doomed",
|
|
854
|
+
(cutoff,),
|
|
855
|
+
)
|
|
856
|
+
pruned_sessions = destination.execute(
|
|
857
|
+
"SELECT COUNT(*) FROM snapshot_pruned_sessions"
|
|
858
|
+
).fetchone()[0]
|
|
859
|
+
pruned_messages = destination.execute(
|
|
860
|
+
"SELECT COUNT(*) FROM messages WHERE session_id IN "
|
|
861
|
+
"(SELECT id FROM snapshot_pruned_sessions)"
|
|
862
|
+
).fetchone()[0]
|
|
863
|
+
destination.execute(
|
|
864
|
+
"DELETE FROM messages WHERE session_id IN "
|
|
865
|
+
"(SELECT id FROM snapshot_pruned_sessions)"
|
|
866
|
+
)
|
|
867
|
+
if "session_model_usage" in table_names:
|
|
868
|
+
destination.execute(
|
|
869
|
+
"DELETE FROM session_model_usage WHERE session_id IN "
|
|
870
|
+
"(SELECT id FROM snapshot_pruned_sessions)"
|
|
871
|
+
)
|
|
872
|
+
destination.execute(
|
|
873
|
+
"DELETE FROM sessions WHERE id IN "
|
|
874
|
+
"(SELECT id FROM snapshot_pruned_sessions)"
|
|
875
|
+
)
|
|
876
|
+
if "system_prompts" in table_names:
|
|
877
|
+
destination.execute(
|
|
878
|
+
"DELETE FROM system_prompts WHERE NOT EXISTS ("
|
|
879
|
+
" SELECT 1 FROM sessions"
|
|
880
|
+
" WHERE sessions.system_prompt_hash = system_prompts.hash"
|
|
881
|
+
")"
|
|
882
|
+
)
|
|
883
|
+
destination.commit()
|
|
884
|
+
for table in ("messages_fts", "messages_fts_trigram", "messages_fts_cjk"):
|
|
885
|
+
if table in table_names:
|
|
886
|
+
destination.execute(
|
|
887
|
+
'INSERT INTO "' + table + '" ("' + table + '") VALUES (\'optimize\')'
|
|
888
|
+
)
|
|
889
|
+
destination.commit()
|
|
890
|
+
destination.execute("VACUUM")
|
|
891
|
+
if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
|
|
892
|
+
raise RuntimeError("compacted backup failed quick_check")
|
|
893
|
+
if destination.execute("PRAGMA foreign_key_check").fetchone() is not None:
|
|
894
|
+
raise RuntimeError("compacted backup failed foreign_key_check")
|
|
895
|
+
finally:
|
|
896
|
+
destination.close()
|
|
897
|
+
|
|
898
|
+
os.chmod(target_path, 0o600)
|
|
899
|
+
print(json.dumps({
|
|
900
|
+
"ok": True,
|
|
901
|
+
"prunedSessions": pruned_sessions,
|
|
902
|
+
"prunedMessages": pruned_messages,
|
|
903
|
+
"retentionHours": retention_hours,
|
|
904
|
+
"archiveStateBytes": os.path.getsize(target_path),
|
|
905
|
+
}, separators=(",", ":")))
|
|
906
|
+
`;
|
|
907
|
+
|
|
908
|
+
export async function compactHermesSnapshotStateDb({
|
|
909
|
+
sourcePath,
|
|
910
|
+
targetPath,
|
|
911
|
+
retentionHours = HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS,
|
|
912
|
+
hermesPython = HERMES_PYTHON,
|
|
913
|
+
runChild = undefined,
|
|
914
|
+
}) {
|
|
915
|
+
if (
|
|
916
|
+
!isAbsolute(sourcePath) ||
|
|
917
|
+
!isAbsolute(targetPath) ||
|
|
918
|
+
resolve(sourcePath) === resolve(targetPath) ||
|
|
919
|
+
!existsSync(sourcePath) ||
|
|
920
|
+
existsSync(targetPath) ||
|
|
921
|
+
!Number.isSafeInteger(retentionHours) ||
|
|
922
|
+
retentionHours < 1 ||
|
|
923
|
+
retentionHours > 30 * 24
|
|
924
|
+
) {
|
|
925
|
+
throw new Error("hermes_snapshot_state_db_compaction_rejected");
|
|
926
|
+
}
|
|
927
|
+
regularFile(sourcePath);
|
|
928
|
+
mkdirSync(dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
929
|
+
const result = await childRunner(runChild)({
|
|
930
|
+
command: hermesPython,
|
|
931
|
+
args: [
|
|
932
|
+
"-c",
|
|
933
|
+
STATE_DB_COMPACTION_SCRIPT,
|
|
934
|
+
sourcePath,
|
|
935
|
+
targetPath,
|
|
936
|
+
String(retentionHours),
|
|
937
|
+
],
|
|
938
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
939
|
+
shell: false,
|
|
940
|
+
timeoutMs: STATE_DB_COMPACTION_TIMEOUT_MS,
|
|
941
|
+
maxOutputBytes: 64 * 1024,
|
|
942
|
+
});
|
|
943
|
+
if (childFailed(result) || !existsSync(targetPath)) {
|
|
944
|
+
throw new Error("hermes_snapshot_state_db_compaction_failed");
|
|
945
|
+
}
|
|
946
|
+
const target = regularFile(targetPath);
|
|
947
|
+
let summary;
|
|
948
|
+
try {
|
|
949
|
+
summary = JSON.parse(String(result.stdout ?? ""));
|
|
950
|
+
} catch {
|
|
951
|
+
throw new Error("hermes_snapshot_state_db_compaction_failed");
|
|
952
|
+
}
|
|
953
|
+
if (
|
|
954
|
+
summary?.ok !== true ||
|
|
955
|
+
summary.retentionHours !== retentionHours ||
|
|
956
|
+
!Number.isSafeInteger(summary.prunedSessions) ||
|
|
957
|
+
summary.prunedSessions < 0 ||
|
|
958
|
+
!Number.isSafeInteger(summary.prunedMessages) ||
|
|
959
|
+
summary.prunedMessages < 0 ||
|
|
960
|
+
summary.archiveStateBytes !== target.size
|
|
961
|
+
) {
|
|
962
|
+
throw new Error("hermes_snapshot_state_db_compaction_failed");
|
|
963
|
+
}
|
|
964
|
+
return summary;
|
|
965
|
+
}
|
|
966
|
+
|
|
698
967
|
function nativeSummaryAccepted(result, complete, incomplete) {
|
|
699
968
|
const output = `${result?.stdout ?? ""}\n${result?.stderr ?? ""}`;
|
|
700
969
|
return (
|
|
@@ -807,6 +1076,10 @@ function stageHermesUserState({ profileId, profileRoot, hermesRoot }) {
|
|
|
807
1076
|
for (const name of HERMES_SNAPSHOT_USER_STATE_ENTRIES) {
|
|
808
1077
|
const source = join(profileRoot, name);
|
|
809
1078
|
if (existsSync(source)) {
|
|
1079
|
+
// state.db is copied online and compacted after the rest of the staged
|
|
1080
|
+
// tree is assembled. Never hard-link this one file: pruning the staged
|
|
1081
|
+
// recovery copy must be physically incapable of touching the live DB.
|
|
1082
|
+
if (name === "state.db") continue;
|
|
810
1083
|
stageUserStateEntry(
|
|
811
1084
|
source,
|
|
812
1085
|
join(stagedProfileRoot, name),
|
|
@@ -1426,7 +1699,9 @@ export async function ensureHermesClientRelationshipsCrons({
|
|
|
1426
1699
|
return failed("hermes_client_relationships_cron_prune_failed");
|
|
1427
1700
|
}
|
|
1428
1701
|
if (cronJobs(profileRoot).some((after) => after?.id === job.id)) {
|
|
1429
|
-
return failed(
|
|
1702
|
+
return failed(
|
|
1703
|
+
"hermes_client_relationships_cron_prune_readback_rejected"
|
|
1704
|
+
);
|
|
1430
1705
|
}
|
|
1431
1706
|
removed.push({ name: job.name, jobId: job.id, status: "REMOVED" });
|
|
1432
1707
|
}
|
|
@@ -1516,7 +1791,10 @@ export async function captureHermesBackup({
|
|
|
1516
1791
|
hermesVersion,
|
|
1517
1792
|
archiveDirectory = "/tmp",
|
|
1518
1793
|
hermesCli = HERMES,
|
|
1519
|
-
|
|
1794
|
+
hermesPython = HERMES_PYTHON,
|
|
1795
|
+
maxArchiveBytes = HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES,
|
|
1796
|
+
cronSessionRetentionHours = HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS,
|
|
1797
|
+
stateDbCompactor = compactHermesSnapshotStateDb,
|
|
1520
1798
|
runChild,
|
|
1521
1799
|
operationIdentity = /** @type {any} */ (undefined),
|
|
1522
1800
|
}) {
|
|
@@ -1554,6 +1832,18 @@ export async function captureHermesBackup({
|
|
|
1554
1832
|
let stagingRoot;
|
|
1555
1833
|
try {
|
|
1556
1834
|
stagingRoot = stageHermesUserState({ profileId, profileRoot, hermesRoot });
|
|
1835
|
+
const stagedProfileRoot = join(stagingRoot, "profiles", profileId);
|
|
1836
|
+
const sourceStateDb = join(profileRoot, "state.db");
|
|
1837
|
+
if (existsSync(sourceStateDb)) {
|
|
1838
|
+
await stateDbCompactor({
|
|
1839
|
+
sourcePath: sourceStateDb,
|
|
1840
|
+
targetPath: join(stagedProfileRoot, "state.db"),
|
|
1841
|
+
retentionHours: cronSessionRetentionHours,
|
|
1842
|
+
hermesPython,
|
|
1843
|
+
runChild,
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
assertStagedCronDependencies(stagedProfileRoot);
|
|
1557
1847
|
const result = await childRunner(runChild)(
|
|
1558
1848
|
invocation(["backup", "-o", archivePath], stagingRoot, hermesCli)
|
|
1559
1849
|
);
|
|
@@ -1588,7 +1878,7 @@ export async function captureHermesBackup({
|
|
|
1588
1878
|
} catch (error) {
|
|
1589
1879
|
const code = error instanceof Error ? error.message : "";
|
|
1590
1880
|
return failed(
|
|
1591
|
-
/^hermes_snapshot_(?:external_state|plugin_data|portable_config)/.test(
|
|
1881
|
+
/^hermes_snapshot_(?:external_state|plugin_data|portable_config|dependency|state_db)/.test(
|
|
1592
1882
|
code
|
|
1593
1883
|
)
|
|
1594
1884
|
? code
|
|
@@ -1783,7 +2073,7 @@ function validLatest(latest, hermesVersion) {
|
|
|
1783
2073
|
return (
|
|
1784
2074
|
Buffer.isBuffer(latest?.bytes) &&
|
|
1785
2075
|
latest.bytes.byteLength > 0 &&
|
|
1786
|
-
latest.bytes.byteLength <=
|
|
2076
|
+
latest.bytes.byteLength <= HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES &&
|
|
1787
2077
|
latest.archiveBytes === latest.bytes.byteLength &&
|
|
1788
2078
|
SHA256.test(latest.archiveDigest ?? "") &&
|
|
1789
2079
|
sha256(latest.bytes) === latest.archiveDigest &&
|
|
@@ -38,8 +38,8 @@ 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.
|
|
42
|
-
const MCP_PACKAGE = "@sellable/mcp@0.1.
|
|
41
|
+
const INSTALLER_PACKAGE = "@sellable/install@0.1.684";
|
|
42
|
+
const MCP_PACKAGE = "@sellable/mcp@0.1.900";
|
|
43
43
|
const HERMES_VERSION = "0.18.0";
|
|
44
44
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
45
45
|
const SHA256_16 = /^[a-f0-9]{16}$/;
|
|
@@ -1547,8 +1547,8 @@ export function compileClaimToProfileDesired(activeClaim, config) {
|
|
|
1547
1547
|
pinned.hermesCli !== "hermes" ||
|
|
1548
1548
|
pinned.hermesVersion !== "0.18.0" ||
|
|
1549
1549
|
pinned.installerPackage !==
|
|
1550
|
-
"@sellable/install@0.1.
|
|
1551
|
-
pinned.mcpPackage !== "@sellable/mcp@0.1.
|
|
1550
|
+
"@sellable/install@0.1.684" ||
|
|
1551
|
+
pinned.mcpPackage !== "@sellable/mcp@0.1.900" ||
|
|
1552
1552
|
!Array.isArray(policy.toolInclude) ||
|
|
1553
1553
|
policy.toolInclude.length === 0 ||
|
|
1554
1554
|
!Number.isSafeInteger(slack.generation) ||
|
|
@@ -283,8 +283,8 @@ function validateDesired(desired) {
|
|
|
283
283
|
desired.hermesCli !== "hermes" ||
|
|
284
284
|
desired.hermesVersion !== "0.18.0" ||
|
|
285
285
|
desired.installerPackage !==
|
|
286
|
-
"@sellable/install@0.1.
|
|
287
|
-
desired.mcpPackage !== "@sellable/mcp@0.1.
|
|
286
|
+
"@sellable/install@0.1.684" ||
|
|
287
|
+
desired.mcpPackage !== "@sellable/mcp@0.1.900" ||
|
|
288
288
|
!Array.isArray(desired.toolInclude) ||
|
|
289
289
|
desired.toolInclude.length === 0 ||
|
|
290
290
|
desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
|
|
@@ -26,8 +26,8 @@ import { deriveContainedProfileId } from "./profile-materializer.mjs";
|
|
|
26
26
|
|
|
27
27
|
export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
|
|
28
28
|
export const PINNED_INSTALL_PACKAGE =
|
|
29
|
-
"@sellable/install@0.1.
|
|
30
|
-
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.
|
|
29
|
+
"@sellable/install@0.1.684";
|
|
30
|
+
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.900";
|
|
31
31
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
32
32
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|
|
33
33
|
);
|