@sellable/install 0.1.756 → 0.1.757
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 +1 -1
- package/lib/sellable-agent/fly-admin-image/Dockerfile +2 -0
- package/lib/sellable-agent/fly-customer-image/Dockerfile +2 -1
- package/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs +84 -0
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +189 -138
- package/lib/sellable-agent/fly-soul-bridge.mjs +19 -4
- package/lib/sellable-agent/hermes-memory-snapshot.mjs +47 -3
- 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 +87 -34
- package/lib/sellable-agent/provisioning-adapter.mjs +1 -1
- 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.757
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.938
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
9
|
ARG MCP_INTEGRITY=sha512-wcVCiCXVvKeE/SufP+2iXk8zgzwYWh78M/1vwa7oorF9UZbzut9szyZV47t4iMX9owYgBVaGwdlOC6eX8VQX4A==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.757" \
|
|
12
12
|
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.938" \
|
|
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.756" \
|
|
|
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.757' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.938') 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.757', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.938', 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.757` is
|
|
7
7
|
published:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
@@ -168,6 +168,7 @@ COPY hermes-first-welcome.mjs /usr/local/lib/sellable-agent/hermes-first-welcome
|
|
|
168
168
|
COPY fly-customer-image/hermes-runs-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs
|
|
169
169
|
COPY fly-customer-image/opener-stage-execution.mjs /usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs
|
|
170
170
|
COPY fly-customer-image/endpoint-uat-completion-gate.mjs /usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs
|
|
171
|
+
COPY fly-customer-image/customer-runtime-phase-telemetry.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs
|
|
171
172
|
COPY admin-native-gateway-supervisor.mjs /usr/local/lib/sellable-agent/admin-native-gateway-supervisor.mjs
|
|
172
173
|
COPY admin-endpoint-runtime.mjs /usr/local/lib/sellable-agent/admin-endpoint-runtime.mjs
|
|
173
174
|
COPY abandoned-runtime-scratch.mjs /usr/local/lib/sellable-agent/abandoned-runtime-scratch.mjs
|
|
@@ -265,6 +266,7 @@ RUN set -eux; \
|
|
|
265
266
|
/usr/local/lib/sellable-agent/admin-runtime/admin-state-generation.mjs \
|
|
266
267
|
/usr/local/lib/sellable-agent/admin-runtime/start-admin-runtime \
|
|
267
268
|
/usr/local/lib/sellable-agent/fly-customer-image/customer-runtime.mjs \
|
|
269
|
+
/usr/local/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs \
|
|
268
270
|
/usr/local/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs \
|
|
269
271
|
/usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs \
|
|
270
272
|
/usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs \
|
|
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
|
|
|
8
8
|
|
|
9
9
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.938
|
|
10
10
|
ARG MCP_INTEGRITY=sha512-wcVCiCXVvKeE/SufP+2iXk8zgzwYWh78M/1vwa7oorF9UZbzut9szyZV47t4iMX9owYgBVaGwdlOC6eX8VQX4A==
|
|
11
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
11
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.757
|
|
12
12
|
ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.202
|
|
13
13
|
ARG ADMIN_MCP_INTEGRITY=sha512-ym3aye/0vQ8tfULJlKz9JFOxvIQsl1G+n3np+TTgK/tecwNblBo/j3AkTnRqH9iomKzPbZzP+n4tEQsvsU8ByA==
|
|
14
14
|
ARG TUS_JS_CLIENT_VERSION=4.3.1
|
|
@@ -97,6 +97,7 @@ COPY fly-customer-image/model-auth-store.mjs /usr/local/lib/sellable-agent/fly-c
|
|
|
97
97
|
COPY fly-customer-image/hermes-runs-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs
|
|
98
98
|
COPY fly-customer-image/opener-stage-execution.mjs /usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs
|
|
99
99
|
COPY fly-customer-image/endpoint-uat-completion-gate.mjs /usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs
|
|
100
|
+
COPY fly-customer-image/customer-runtime-phase-telemetry.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs
|
|
100
101
|
COPY fly-customer-image/customer-runtime.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime.mjs
|
|
101
102
|
COPY fly-customer-image/rootfs/ /
|
|
102
103
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const CUSTOMER_RUNTIME_PHASES = new Set([
|
|
2
|
+
"runtime_secrets",
|
|
3
|
+
"snapshot_restore",
|
|
4
|
+
"legacy_migration",
|
|
5
|
+
"profile_materialization",
|
|
6
|
+
"default_profile_bundle",
|
|
7
|
+
"profile_configuration",
|
|
8
|
+
"memory_cron",
|
|
9
|
+
"pre_serving_snapshot",
|
|
10
|
+
"slack_serving_proof",
|
|
11
|
+
"customer_program_crons",
|
|
12
|
+
"post_cron_snapshot",
|
|
13
|
+
"native_skill_proof",
|
|
14
|
+
"serving_observation",
|
|
15
|
+
"operation_completion",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function phaseFailureCode(error) {
|
|
19
|
+
const code = error instanceof Error ? error.message : "";
|
|
20
|
+
return /^[A-Za-z0-9_:-]{1,160}$/.test(code)
|
|
21
|
+
? code
|
|
22
|
+
: "customer_runtime_phase_failed";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createCustomerRuntimePhaseTelemetry({
|
|
26
|
+
bootSessionId,
|
|
27
|
+
operationId,
|
|
28
|
+
now = () => Date.now(),
|
|
29
|
+
write = (entry) => {
|
|
30
|
+
process.stderr.write(`${JSON.stringify(entry)}\n`);
|
|
31
|
+
},
|
|
32
|
+
}) {
|
|
33
|
+
if (
|
|
34
|
+
typeof bootSessionId !== "string" ||
|
|
35
|
+
bootSessionId.length < 1 ||
|
|
36
|
+
typeof operationId !== "string" ||
|
|
37
|
+
operationId.length < 1
|
|
38
|
+
) {
|
|
39
|
+
throw new Error("customer_runtime_phase_identity_rejected");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const run = async (phase, work) => {
|
|
43
|
+
if (!CUSTOMER_RUNTIME_PHASES.has(phase) || typeof work !== "function") {
|
|
44
|
+
throw new Error("customer_runtime_phase_rejected");
|
|
45
|
+
}
|
|
46
|
+
const startedAt = now();
|
|
47
|
+
write({
|
|
48
|
+
ok: true,
|
|
49
|
+
code: "customer_runtime_phase",
|
|
50
|
+
phase,
|
|
51
|
+
status: "started",
|
|
52
|
+
elapsedMs: 0,
|
|
53
|
+
bootSessionId,
|
|
54
|
+
operationId,
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const result = await work();
|
|
58
|
+
write({
|
|
59
|
+
ok: true,
|
|
60
|
+
code: "customer_runtime_phase",
|
|
61
|
+
phase,
|
|
62
|
+
status: "completed",
|
|
63
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
64
|
+
bootSessionId,
|
|
65
|
+
operationId,
|
|
66
|
+
});
|
|
67
|
+
return result;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
write({
|
|
70
|
+
ok: false,
|
|
71
|
+
code: "customer_runtime_phase",
|
|
72
|
+
phase,
|
|
73
|
+
status: "failed",
|
|
74
|
+
elapsedMs: Math.max(0, now() - startedAt),
|
|
75
|
+
bootSessionId,
|
|
76
|
+
operationId,
|
|
77
|
+
errorCode: phaseFailureCode(error),
|
|
78
|
+
});
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return { run };
|
|
84
|
+
}
|
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
validateNativeSlackCustomerProfile,
|
|
87
87
|
} from "../profile-materializer.mjs";
|
|
88
88
|
import { reconcilePublicHermesSkills } from "../public-skills.mjs";
|
|
89
|
+
import { createCustomerRuntimePhaseTelemetry } from "./customer-runtime-phase-telemetry.mjs";
|
|
89
90
|
import {
|
|
90
91
|
holdEndpointUatNativeCompletion,
|
|
91
92
|
validateEndpointUatCompletionControl,
|
|
@@ -121,7 +122,7 @@ const RUNTIME_GENERATION = Number(
|
|
|
121
122
|
const BOOT_SESSION_ID = randomUUID();
|
|
122
123
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.3";
|
|
123
124
|
const INSTALLER_PACKAGE =
|
|
124
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
125
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.757";
|
|
125
126
|
const MCP_PACKAGE =
|
|
126
127
|
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.938";
|
|
127
128
|
export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
|
|
@@ -3673,121 +3674,157 @@ async function processOperation({
|
|
|
3673
3674
|
identity,
|
|
3674
3675
|
reattestAuthority,
|
|
3675
3676
|
}) {
|
|
3677
|
+
const phaseTelemetry = createCustomerRuntimePhaseTelemetry({
|
|
3678
|
+
bootSessionId: BOOT_SESSION_ID,
|
|
3679
|
+
operationId: claim.operation.id,
|
|
3680
|
+
});
|
|
3676
3681
|
const runtimeAuthorityScope = claim.runtimeAuthority?.scope ?? null;
|
|
3677
3682
|
const trustRootPem = readFileSync(TRUST_ROOT, "utf8");
|
|
3678
3683
|
const { desired, toolInclude } = compileDesired(claim, trustRootPem);
|
|
3679
3684
|
mkdirSync(PROFILES_ROOT, { recursive: true, mode: 0o700 });
|
|
3680
3685
|
chmodSync(PROFILES_ROOT, 0o700);
|
|
3681
|
-
const secrets = await
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3686
|
+
const secrets = await phaseTelemetry.run("runtime_secrets", async () => {
|
|
3687
|
+
const consumed = await consumeSecrets({
|
|
3688
|
+
claim,
|
|
3689
|
+
exchange,
|
|
3690
|
+
client,
|
|
3691
|
+
desired,
|
|
3692
|
+
});
|
|
3693
|
+
await publishManagedAuth({
|
|
3694
|
+
schemaVersion: "sellable-managed-auth/v1",
|
|
3695
|
+
profileId: desired.profileId,
|
|
3696
|
+
runtimeId: RUNTIME_ID,
|
|
3697
|
+
runtimeGeneration: RUNTIME_GENERATION,
|
|
3698
|
+
agentKind: "CUSTOMER",
|
|
3699
|
+
apiUrl: ORIGIN,
|
|
3700
|
+
token: consumed.mcpService,
|
|
3701
|
+
agent: {
|
|
3702
|
+
workspaceId: desired.workspaceId,
|
|
3703
|
+
agentId: desired.agentId,
|
|
3704
|
+
generation: desired.serviceCredentialGeneration,
|
|
3705
|
+
policyHash: desired.policyHash,
|
|
3706
|
+
selectedChannelIds: desired.selectedChannelIds,
|
|
3707
|
+
},
|
|
3708
|
+
});
|
|
3709
|
+
return consumed;
|
|
3697
3710
|
});
|
|
3698
3711
|
const profileRoot = join(PROFILES_ROOT, desired.profileId);
|
|
3699
|
-
const memoryRestore = await
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3712
|
+
const memoryRestore = await phaseTelemetry.run("snapshot_restore", () =>
|
|
3713
|
+
restoreLatestHermesSnapshot({
|
|
3714
|
+
profileId: desired.profileId,
|
|
3715
|
+
profileRoot,
|
|
3716
|
+
hermesRoot: DATA_ROOT,
|
|
3717
|
+
hermesVersion: HERMES_VERSION,
|
|
3718
|
+
fetchLatest: () => client.getMemorySnapshot(),
|
|
3719
|
+
})
|
|
3720
|
+
);
|
|
3706
3721
|
if (!memoryRestore.ok) throw new Error(memoryRestore.code);
|
|
3707
|
-
const legacyUserState =
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3722
|
+
const legacyUserState = await phaseTelemetry.run("legacy_migration", () =>
|
|
3723
|
+
migrateLegacyHermesUserState({
|
|
3724
|
+
profileId: desired.profileId,
|
|
3725
|
+
profileRoot,
|
|
3726
|
+
hermesRoot: DATA_ROOT,
|
|
3727
|
+
})
|
|
3728
|
+
);
|
|
3712
3729
|
if (!legacyUserState.ok) throw new Error(legacyUserState.code);
|
|
3713
|
-
const materialized =
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3730
|
+
const { materialized, pristineProfile } = await phaseTelemetry.run(
|
|
3731
|
+
"profile_materialization",
|
|
3732
|
+
() => {
|
|
3733
|
+
const materialized = materializeNativeSlackCustomerProfile({
|
|
3734
|
+
desired,
|
|
3735
|
+
profilesRoot: PROFILES_ROOT,
|
|
3736
|
+
});
|
|
3737
|
+
if (!materialized.ok) {
|
|
3738
|
+
const detail =
|
|
3739
|
+
materialized.code === "native_slack_profile_insufficient_disk"
|
|
3740
|
+
? `:available_bytes=${materialized.availableBytes}:required_bytes=${materialized.requiredBytes}`
|
|
3741
|
+
: materialized.causeCode
|
|
3742
|
+
? `:cause=${materialized.causeCode}`
|
|
3743
|
+
: "";
|
|
3744
|
+
throw new Error(`${materialized.code}${detail}`);
|
|
3745
|
+
}
|
|
3746
|
+
const pristineProfile = validateNativeSlackCustomerProfile({
|
|
3747
|
+
desired,
|
|
3748
|
+
profileRoot,
|
|
3749
|
+
});
|
|
3750
|
+
if (!pristineProfile.ok) throw new Error(pristineProfile.code);
|
|
3751
|
+
return { materialized, pristineProfile };
|
|
3752
|
+
}
|
|
3753
|
+
);
|
|
3731
3754
|
const activeProjection = buildDefaultProfileProjection({
|
|
3732
3755
|
kind: "CUSTOMER",
|
|
3733
3756
|
claim,
|
|
3734
3757
|
});
|
|
3735
|
-
const reconciledDefaultProfile = await
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3758
|
+
const reconciledDefaultProfile = await phaseTelemetry.run(
|
|
3759
|
+
"default_profile_bundle",
|
|
3760
|
+
() =>
|
|
3761
|
+
reconcileDefaultProfileBundle({
|
|
3762
|
+
activeProjection,
|
|
3763
|
+
materializerReceipt: pristineProfile.manifest,
|
|
3764
|
+
profileRoot,
|
|
3765
|
+
bundleRoot: DEFAULT_PROFILE_BUNDLE_ROOT,
|
|
3766
|
+
dataRoot: DATA_ROOT,
|
|
3767
|
+
reattestAuthority: async () => {
|
|
3768
|
+
await reattestAuthority();
|
|
3769
|
+
return activeProjection;
|
|
3770
|
+
},
|
|
3771
|
+
cronAdapter: customerDefaultProfileCronAdapter(
|
|
3772
|
+
desired.profileId,
|
|
3773
|
+
profileRoot
|
|
3774
|
+
),
|
|
3775
|
+
})
|
|
3776
|
+
);
|
|
3750
3777
|
if (!reconciledDefaultProfile.ok)
|
|
3751
3778
|
throw new Error(reconciledDefaultProfile.code);
|
|
3752
3779
|
const defaultProfile = reconciledDefaultProfile;
|
|
3753
|
-
const publicSkills =
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3780
|
+
const { publicSkills, runtimeConfig, soulProof } = await phaseTelemetry.run(
|
|
3781
|
+
"profile_configuration",
|
|
3782
|
+
async () => {
|
|
3783
|
+
const publicSkills = reconcilePublicHermesSkills({
|
|
3784
|
+
profileId: desired.profileId,
|
|
3785
|
+
profileRoot,
|
|
3786
|
+
dataRoot: DATA_ROOT,
|
|
3787
|
+
});
|
|
3788
|
+
installInternalAdminRuntimeSettings({ desired, profileRoot });
|
|
3789
|
+
const runtimeConfig = writeRuntimeHermesConfig({
|
|
3790
|
+
desired,
|
|
3791
|
+
toolInclude,
|
|
3792
|
+
profileRoot,
|
|
3793
|
+
secrets,
|
|
3794
|
+
claim,
|
|
3795
|
+
trustRootPem,
|
|
3796
|
+
});
|
|
3797
|
+
const soulProof = observeProfileSoul(desired.profileId, {
|
|
3798
|
+
dataRoot: DATA_ROOT,
|
|
3799
|
+
});
|
|
3800
|
+
if (soulProof.ok !== true) {
|
|
3801
|
+
throw new Error("customer_live_soul_loader_proof_rejected");
|
|
3802
|
+
}
|
|
3803
|
+
return { publicSkills, runtimeConfig, soulProof };
|
|
3804
|
+
}
|
|
3805
|
+
);
|
|
3773
3806
|
const observedSoulRawSha256 = soulProof.rawFileSha256;
|
|
3774
|
-
const memoryCron = await
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3807
|
+
const memoryCron = await phaseTelemetry.run("memory_cron", () =>
|
|
3808
|
+
ensureHermesSnapshotCron({
|
|
3809
|
+
profileId: desired.profileId,
|
|
3810
|
+
profileRoot,
|
|
3811
|
+
hermesRoot: DATA_ROOT,
|
|
3812
|
+
hermesCli: HERMES,
|
|
3813
|
+
})
|
|
3814
|
+
);
|
|
3780
3815
|
if (!memoryCron.ok) throw new Error(memoryCron.code);
|
|
3781
3816
|
const materializerChanged =
|
|
3782
3817
|
materialized.status !== "REUSED" || memoryCron.status !== "REUSED";
|
|
3783
3818
|
if (materializerChanged || defaultProfile.changed || publicSkills.changed)
|
|
3784
3819
|
markHermesSnapshotDirty(profileRoot);
|
|
3785
|
-
const memorySnapshot = await
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3820
|
+
const memorySnapshot = await phaseTelemetry.run("pre_serving_snapshot", () =>
|
|
3821
|
+
flushHermesSnapshotBeforeServing({
|
|
3822
|
+
client,
|
|
3823
|
+
profileId: desired.profileId,
|
|
3824
|
+
profileRoot,
|
|
3825
|
+
runtimeAuthorityScope,
|
|
3826
|
+
})
|
|
3827
|
+
);
|
|
3791
3828
|
if (!memorySnapshot.ok) markHermesSnapshotDirty(profileRoot);
|
|
3792
3829
|
const prove =
|
|
3793
3830
|
claim.operation.action === "INSTANTIATE"
|
|
@@ -3806,50 +3843,60 @@ async function processOperation({
|
|
|
3806
3843
|
newAgent: claim.operation.action === "INSTANTIATE",
|
|
3807
3844
|
};
|
|
3808
3845
|
await ensureHermesFirstWelcome(welcomeInput);
|
|
3809
|
-
const proof = await
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3846
|
+
const proof = await phaseTelemetry.run("slack_serving_proof", () =>
|
|
3847
|
+
prove({
|
|
3848
|
+
action: claim.operation.action,
|
|
3849
|
+
desired,
|
|
3850
|
+
runtimeConfig,
|
|
3851
|
+
secrets,
|
|
3852
|
+
profileDigest: pristineProfile.manifest.profileDigest,
|
|
3853
|
+
runtimeAuthorityScope,
|
|
3854
|
+
})
|
|
3855
|
+
);
|
|
3856
|
+
await phaseTelemetry.run("customer_program_crons", async () => {
|
|
3857
|
+
const customerProgramCrons = await ensureHermesCustomerProgramCrons({
|
|
3858
|
+
profileId: desired.profileId,
|
|
3859
|
+
profileRoot,
|
|
3860
|
+
hermesRoot: DATA_ROOT,
|
|
3861
|
+
workspaceId: desired.workspaceId,
|
|
3862
|
+
channelId: desired.defaultChannelId,
|
|
3863
|
+
timezone: desired.customerProgramTimezone,
|
|
3864
|
+
apiUrl: ORIGIN,
|
|
3865
|
+
authorityPath: managedAuthPath(desired.profileId),
|
|
3866
|
+
});
|
|
3867
|
+
if (!customerProgramCrons.ok) throw new Error(customerProgramCrons.code);
|
|
3868
|
+
const onboardingCron = await ensureHermesFirstCampaignOnboardingCron({
|
|
3869
|
+
profileId: desired.profileId,
|
|
3870
|
+
profileRoot,
|
|
3871
|
+
hermesRoot: DATA_ROOT,
|
|
3872
|
+
workspaceId: desired.workspaceId,
|
|
3873
|
+
channelId: desired.defaultChannelId,
|
|
3874
|
+
});
|
|
3875
|
+
if (!onboardingCron.ok) throw new Error(onboardingCron.code);
|
|
3834
3876
|
});
|
|
3835
|
-
if (!onboardingCron.ok) throw new Error(onboardingCron.code);
|
|
3836
3877
|
markHermesSnapshotDirty(profileRoot);
|
|
3837
|
-
const onboardingMemorySnapshot = await
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
runtimeAuthorityScope,
|
|
3842
|
-
});
|
|
3843
|
-
const nativeDefaultProfileProof = proof.gatewayReadback?.running
|
|
3844
|
-
? await proveCustomerRuntimeDefaultSkills({
|
|
3845
|
-
claim,
|
|
3846
|
-
exchange,
|
|
3878
|
+
const onboardingMemorySnapshot = await phaseTelemetry.run(
|
|
3879
|
+
"post_cron_snapshot",
|
|
3880
|
+
() =>
|
|
3881
|
+
flushHermesSnapshotBeforeServing({
|
|
3847
3882
|
client,
|
|
3848
|
-
gateway: proof.gateway,
|
|
3849
|
-
reattestAuthority,
|
|
3850
|
-
actions: defaultProfile.actions,
|
|
3851
3883
|
profileId: desired.profileId,
|
|
3884
|
+
profileRoot,
|
|
3885
|
+
runtimeAuthorityScope,
|
|
3852
3886
|
})
|
|
3887
|
+
);
|
|
3888
|
+
const nativeDefaultProfileProof = proof.gatewayReadback?.running
|
|
3889
|
+
? await phaseTelemetry.run("native_skill_proof", () =>
|
|
3890
|
+
proveCustomerRuntimeDefaultSkills({
|
|
3891
|
+
claim,
|
|
3892
|
+
exchange,
|
|
3893
|
+
client,
|
|
3894
|
+
gateway: proof.gateway,
|
|
3895
|
+
reattestAuthority,
|
|
3896
|
+
actions: defaultProfile.actions,
|
|
3897
|
+
profileId: desired.profileId,
|
|
3898
|
+
})
|
|
3899
|
+
)
|
|
3853
3900
|
: { status: "EMPTY", digest: sha256("[]") };
|
|
3854
3901
|
const receipt = completionReceipt({
|
|
3855
3902
|
claim,
|
|
@@ -3882,17 +3929,21 @@ async function processOperation({
|
|
|
3882
3929
|
);
|
|
3883
3930
|
if (!servingObservation)
|
|
3884
3931
|
throw new Error("customer_serving_observation_unavailable");
|
|
3885
|
-
const serving = await
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3932
|
+
const serving = await phaseTelemetry.run("serving_observation", () =>
|
|
3933
|
+
client.claimTick({
|
|
3934
|
+
leaseSeconds: 60,
|
|
3935
|
+
servingObservation,
|
|
3936
|
+
})
|
|
3937
|
+
);
|
|
3889
3938
|
if (serving?.servingObservationStatus !== "accepted") {
|
|
3890
3939
|
throw new Error(
|
|
3891
3940
|
`customer_serving_observation_rejected:${serving?.servingObservationCode ?? serving?.servingObservationStatus ?? "unknown"}`
|
|
3892
3941
|
);
|
|
3893
3942
|
}
|
|
3894
3943
|
}
|
|
3895
|
-
const completed = await
|
|
3944
|
+
const completed = await phaseTelemetry.run("operation_completion", () =>
|
|
3945
|
+
client.complete(receipt)
|
|
3946
|
+
);
|
|
3896
3947
|
if (completed?.accepted !== true)
|
|
3897
3948
|
throw new Error("customer_completion_rejected");
|
|
3898
3949
|
// Native delivery owns the welcome after readiness. A delivery failure must
|
|
@@ -43,6 +43,10 @@ export const SOUL_PROOF_STATE_UNPROVEN = "UNPROVEN";
|
|
|
43
43
|
export const SOUL_JOURNAL_SCHEMA_VERSION = "sellable-agent-soul-journal/v1";
|
|
44
44
|
|
|
45
45
|
const PROFILE_ID = /^[a-z0-9][a-z0-9_-]{0,127}$/;
|
|
46
|
+
// Runtime replacement must never disappear behind an unbounded synchronous
|
|
47
|
+
// flock wait. Ten seconds stays below the operation heartbeat interval and is
|
|
48
|
+
// long enough for the short profile metadata critical sections used here.
|
|
49
|
+
const SOUL_LOCK_ACQUIRE_TIMEOUT_SECONDS = 10;
|
|
46
50
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
47
51
|
const JOURNAL_LIMIT = 256 * 1024;
|
|
48
52
|
// JSON.stringify may encode one accepted content byte as a six-byte `\uXXXX`
|
|
@@ -490,10 +494,21 @@ function openAndLock(paths) {
|
|
|
490
494
|
) {
|
|
491
495
|
fail("soul_lock_file_rejected");
|
|
492
496
|
}
|
|
493
|
-
const locked = spawnSync(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
+
const locked = spawnSync(
|
|
498
|
+
FLOCK_EXECUTABLE,
|
|
499
|
+
[
|
|
500
|
+
"--exclusive",
|
|
501
|
+
"--timeout",
|
|
502
|
+
String(SOUL_LOCK_ACQUIRE_TIMEOUT_SECONDS),
|
|
503
|
+
"3",
|
|
504
|
+
],
|
|
505
|
+
{
|
|
506
|
+
stdio: ["ignore", "ignore", "pipe", fd],
|
|
507
|
+
encoding: "utf8",
|
|
508
|
+
timeout: (SOUL_LOCK_ACQUIRE_TIMEOUT_SECONDS + 2) * 1_000,
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
if (locked.status === 1) fail("soul_lock_busy");
|
|
497
512
|
if (locked.status !== 0) fail("soul_lock_failed");
|
|
498
513
|
return fd;
|
|
499
514
|
} catch (error) {
|
|
@@ -450,7 +450,7 @@ function assertHermesMergeRoot(path, { allowMissing = false } = {}) {
|
|
|
450
450
|
function mergeHermesStateEntries(
|
|
451
451
|
sourceRoot,
|
|
452
452
|
targetRoot,
|
|
453
|
-
{ skippedTopLevelNames = new Set() } = {}
|
|
453
|
+
{ skippedTopLevelNames = new Set(), linkFiles = false } = {}
|
|
454
454
|
) {
|
|
455
455
|
const merge = (source, target, relativeRoot = "") => {
|
|
456
456
|
for (const name of readdirSync(source)) {
|
|
@@ -489,8 +489,25 @@ function mergeHermesStateEntries(
|
|
|
489
489
|
mkdirSync(to, { mode: stat.mode & 0o777 });
|
|
490
490
|
merge(from, to, relativePath);
|
|
491
491
|
} else if (stat.isFile()) {
|
|
492
|
-
|
|
493
|
-
|
|
492
|
+
if (linkFiles) {
|
|
493
|
+
// Profile replacement happens on one encrypted Fly volume while the
|
|
494
|
+
// gateway is stopped and the profile lock is held. A hard link keeps
|
|
495
|
+
// durable Hermes state byte-identical without copying a potentially
|
|
496
|
+
// large state tree into the new managed profile. The old tree is
|
|
497
|
+
// unlinked asynchronously after promotion, leaving one live inode.
|
|
498
|
+
linkSync(from, to);
|
|
499
|
+
const linked = lstatSync(to);
|
|
500
|
+
if (
|
|
501
|
+
!linked.isFile() ||
|
|
502
|
+
linked.dev !== stat.dev ||
|
|
503
|
+
linked.ino !== stat.ino
|
|
504
|
+
) {
|
|
505
|
+
throw new Error("hermes_snapshot_merge_link_rejected");
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
copyFileSync(from, to);
|
|
509
|
+
chmodSync(to, stat.mode & 0o777);
|
|
510
|
+
}
|
|
494
511
|
} else if (stat.isFIFO() || stat.isSocket()) {
|
|
495
512
|
// Runtime control FIFOs and Unix sockets are process-local endpoints,
|
|
496
513
|
// not durable Hermes state. A normal Restart can leave one behind
|
|
@@ -537,6 +554,33 @@ export function mergeHermesState(
|
|
|
537
554
|
});
|
|
538
555
|
}
|
|
539
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Clone one stopped profile into a same-volume staging directory without
|
|
559
|
+
* duplicating durable file bytes. Managed files already rendered in the
|
|
560
|
+
* target win; every other durable file is linked and independently verified.
|
|
561
|
+
*/
|
|
562
|
+
export function linkHermesState(
|
|
563
|
+
sourceRoot,
|
|
564
|
+
targetRoot,
|
|
565
|
+
/** @type {{skipSourceTopLevelNames?:string[]}} */
|
|
566
|
+
{ skipSourceTopLevelNames = [] } = {}
|
|
567
|
+
) {
|
|
568
|
+
if (!assertHermesMergeRoot(sourceRoot, { allowMissing: true })) return;
|
|
569
|
+
assertHermesMergeRoot(targetRoot);
|
|
570
|
+
const source = lstatSync(sourceRoot);
|
|
571
|
+
const target = lstatSync(targetRoot);
|
|
572
|
+
if (source.dev !== target.dev) {
|
|
573
|
+
throw new Error("hermes_snapshot_merge_cross_device_rejected");
|
|
574
|
+
}
|
|
575
|
+
mergeHermesStateEntries(sourceRoot, targetRoot, {
|
|
576
|
+
skippedTopLevelNames: hermesTopLevelNameSet(
|
|
577
|
+
skipSourceTopLevelNames,
|
|
578
|
+
"hermes_snapshot_merge_policy_rejected"
|
|
579
|
+
),
|
|
580
|
+
linkFiles: true,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
540
584
|
/**
|
|
541
585
|
* Replace one mutable Hermes subtree while retaining explicitly sealed
|
|
542
586
|
* top-level namespaces in the staged target.
|
|
@@ -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.757";
|
|
42
42
|
const MCP_PACKAGE = "@sellable/mcp@0.1.938";
|
|
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.757" ||
|
|
1550
1550
|
pinned.mcpPackage !== "@sellable/mcp@0.1.938" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
readdirSync,
|
|
13
13
|
realpathSync,
|
|
14
14
|
renameSync,
|
|
15
|
+
rm,
|
|
15
16
|
rmSync,
|
|
16
17
|
statfsSync,
|
|
17
18
|
writeFileSync,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
HERMES_SNAPSHOT_HOOKS,
|
|
42
43
|
HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES,
|
|
43
44
|
applyHermesSellableHostedPolicy,
|
|
45
|
+
linkHermesState,
|
|
44
46
|
mergeHermesState,
|
|
45
47
|
} from "./hermes-memory-snapshot.mjs";
|
|
46
48
|
import { createMcpRuntimeBindingV2 } from "./mcp-runtime-binding.mjs";
|
|
@@ -70,8 +72,7 @@ const FINGERPRINT = /^[a-f0-9]{16}$/;
|
|
|
70
72
|
const PRIVATE_REFERENCE = /^(?:secret|oauth-private):\/\/[A-Za-z0-9/_-]+$/;
|
|
71
73
|
const RAW_SECRET =
|
|
72
74
|
/(?:^|[^A-Za-z0-9])(?:xapp|xoxb|xoxa|sat)_[A-Za-z0-9_-]+|(?:xapp|xoxb|xoxa)-[A-Za-z0-9_-]+|oauth-provider-secret-byte|asec\.v1\./i;
|
|
73
|
-
const
|
|
74
|
-
const NATIVE_SLACK_PEAK_COPY_MULTIPLIER = 2;
|
|
75
|
+
const NATIVE_SLACK_MATERIALIZATION_MARGIN_BYTES = 64 * 1024 * 1024;
|
|
75
76
|
export const SELLABLE_INTERNAL_AGENT_WORKSPACE_ID = "cmj0wv9pe02jx23xdv6wfhg9x";
|
|
76
77
|
const PRESSURE_CACHE_PATHS = Object.freeze([
|
|
77
78
|
[".npm", "_npx"],
|
|
@@ -95,20 +96,6 @@ function stableJson(value) {
|
|
|
95
96
|
return JSON.stringify(value);
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
function logicalTreeBytes(pathValue) {
|
|
99
|
-
if (!existsSync(pathValue)) return 0;
|
|
100
|
-
const entry = lstatSync(pathValue);
|
|
101
|
-
if (entry.isSymbolicLink()) {
|
|
102
|
-
throw new Error("native_slack_profile_symlink_rejected");
|
|
103
|
-
}
|
|
104
|
-
if (entry.isFile()) return entry.size;
|
|
105
|
-
if (!entry.isDirectory()) return 0;
|
|
106
|
-
return readdirSync(pathValue).reduce(
|
|
107
|
-
(total, name) => total + logicalTreeBytes(join(pathValue, name)),
|
|
108
|
-
0
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
99
|
function filesystemAvailableBytes(pathValue) {
|
|
113
100
|
const stats = statfsSync(pathValue);
|
|
114
101
|
return Number(stats.bavail) * Number(stats.bsize);
|
|
@@ -124,15 +111,57 @@ function removeWithoutFollowing(pathValue) {
|
|
|
124
111
|
return true;
|
|
125
112
|
}
|
|
126
113
|
|
|
127
|
-
function
|
|
114
|
+
function scheduleNativeSlackTreeRemoval(root, target) {
|
|
115
|
+
if (dirname(target) !== root || basename(target) === "") {
|
|
116
|
+
throw new Error("native_slack_profile_cleanup_path_rejected");
|
|
117
|
+
}
|
|
118
|
+
let entry;
|
|
119
|
+
try {
|
|
120
|
+
entry = lstatSync(target);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error?.code === "ENOENT") return false;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
126
|
+
throw new Error("native_slack_profile_cleanup_entry_rejected");
|
|
127
|
+
}
|
|
128
|
+
rm(target, { recursive: true, force: true }, (error) => {
|
|
129
|
+
if (error && error.code !== "ENOENT") {
|
|
130
|
+
process.stderr.write(
|
|
131
|
+
`${JSON.stringify({
|
|
132
|
+
ok: false,
|
|
133
|
+
code: "native_slack_profile_cleanup_failed",
|
|
134
|
+
entry: basename(target),
|
|
135
|
+
})}\n`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sweepStaleNativeSlackTrees(root, profileId) {
|
|
128
143
|
const prefix = `.${profileId}.`;
|
|
129
|
-
|
|
130
|
-
let removed = 0;
|
|
144
|
+
let scheduled = 0;
|
|
131
145
|
for (const name of readdirSync(root)) {
|
|
132
|
-
if (!name.startsWith(prefix)
|
|
133
|
-
|
|
146
|
+
if (!name.startsWith(prefix)) continue;
|
|
147
|
+
const staleRoot = join(root, name);
|
|
148
|
+
let cleanupRoot = staleRoot;
|
|
149
|
+
if (name.endsWith(".staging")) {
|
|
150
|
+
cleanupRoot = join(root, `.${profileId}.${randomUUID()}.cleanup`);
|
|
151
|
+
try {
|
|
152
|
+
renameSync(staleRoot, cleanupRoot);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// A cleanup scheduled by the prior operation may still be finishing
|
|
155
|
+
// after the profile lock is released. Treat that ENOENT as complete.
|
|
156
|
+
if (error?.code === "ENOENT") continue;
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
} else if (!name.endsWith(".cleanup")) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (scheduleNativeSlackTreeRemoval(root, cleanupRoot)) scheduled += 1;
|
|
134
163
|
}
|
|
135
|
-
return
|
|
164
|
+
return scheduled;
|
|
136
165
|
}
|
|
137
166
|
|
|
138
167
|
function prunePressureCaches(root) {
|
|
@@ -282,7 +311,7 @@ function validateDesired(desired) {
|
|
|
282
311
|
desired.serviceCredentialGeneration < 1 ||
|
|
283
312
|
desired.hermesCli !== "hermes" ||
|
|
284
313
|
desired.hermesVersion !== "0.18.0" ||
|
|
285
|
-
desired.installerPackage !== "@sellable/install@0.1.
|
|
314
|
+
desired.installerPackage !== "@sellable/install@0.1.757" ||
|
|
286
315
|
desired.mcpPackage !== "@sellable/mcp@0.1.938" ||
|
|
287
316
|
!Array.isArray(desired.toolInclude) ||
|
|
288
317
|
desired.toolInclude.length === 0 ||
|
|
@@ -2845,13 +2874,28 @@ export function materializeNativeSlackCustomerProfile({
|
|
|
2845
2874
|
root,
|
|
2846
2875
|
`.${desired.profileId}.${randomUUID()}.staging`
|
|
2847
2876
|
);
|
|
2848
|
-
const backupRoot = join(root, `.${desired.profileId}
|
|
2877
|
+
const backupRoot = join(root, `.${desired.profileId}.materializer-backup`);
|
|
2849
2878
|
const materialize = () => {
|
|
2850
2879
|
try {
|
|
2851
2880
|
if (dirname(activeRoot) !== root || dirname(stagingRoot) !== root) {
|
|
2852
2881
|
return { ok: false, code: "native_slack_profile_path_rejected" };
|
|
2853
2882
|
}
|
|
2854
|
-
|
|
2883
|
+
if (existsSync(backupRoot)) {
|
|
2884
|
+
const backup = lstatSync(backupRoot);
|
|
2885
|
+
if (backup.isSymbolicLink() || !backup.isDirectory()) {
|
|
2886
|
+
throw new Error("native_slack_profile_backup_rejected");
|
|
2887
|
+
}
|
|
2888
|
+
if (!existsSync(activeRoot)) renameSync(backupRoot, activeRoot);
|
|
2889
|
+
else {
|
|
2890
|
+
const staleBackupRoot = join(
|
|
2891
|
+
root,
|
|
2892
|
+
`.${desired.profileId}.${randomUUID()}.cleanup`
|
|
2893
|
+
);
|
|
2894
|
+
renameSync(backupRoot, staleBackupRoot);
|
|
2895
|
+
scheduleNativeSlackTreeRemoval(root, staleBackupRoot);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
sweepStaleNativeSlackTrees(root, desired.profileId);
|
|
2855
2899
|
const extraProfiles = readdirSync(root).filter((name) => {
|
|
2856
2900
|
if (name === desired.profileId || name.startsWith(".")) return false;
|
|
2857
2901
|
const pathValue = join(root, name);
|
|
@@ -2890,13 +2934,12 @@ export function materializeNativeSlackCustomerProfile({
|
|
|
2890
2934
|
(total, bytes) => total + Buffer.byteLength(bytes),
|
|
2891
2935
|
Buffer.byteLength(JSON.stringify(nativeSlackManifest(desired, {})))
|
|
2892
2936
|
);
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2937
|
+
// Replacement links durable same-volume state into staging, so it does
|
|
2938
|
+
// not need another full copy of the profile. Reserve only the managed
|
|
2939
|
+
// seed plus a bounded directory/metadata margin.
|
|
2940
|
+
const profileBytes = desiredSeedBytes;
|
|
2897
2941
|
const requiredBytes =
|
|
2898
|
-
profileBytes
|
|
2899
|
-
NATIVE_SLACK_COPY_MARGIN_BYTES;
|
|
2942
|
+
profileBytes + NATIVE_SLACK_MATERIALIZATION_MARGIN_BYTES;
|
|
2900
2943
|
let availableBytes = Number(diskFreeBytes(root));
|
|
2901
2944
|
if (!Number.isFinite(availableBytes) || availableBytes < 0) {
|
|
2902
2945
|
throw new Error("native_slack_profile_disk_readback_invalid");
|
|
@@ -2938,7 +2981,7 @@ export function materializeNativeSlackCustomerProfile({
|
|
|
2938
2981
|
desired,
|
|
2939
2982
|
soulBytes
|
|
2940
2983
|
);
|
|
2941
|
-
|
|
2984
|
+
linkHermesState(activeRoot, stagingRoot);
|
|
2942
2985
|
repointInternalCronProfileReferences(desired, stagingRoot);
|
|
2943
2986
|
mergeLiveMutableConfig(activeRoot, stagingRoot);
|
|
2944
2987
|
if (
|
|
@@ -2963,7 +3006,12 @@ export function materializeNativeSlackCustomerProfile({
|
|
|
2963
3006
|
renameSync(stagingRoot, activeRoot);
|
|
2964
3007
|
fsyncDirectory(root);
|
|
2965
3008
|
if (existsSync(backupRoot)) {
|
|
2966
|
-
|
|
3009
|
+
const cleanupRoot = join(
|
|
3010
|
+
root,
|
|
3011
|
+
`.${desired.profileId}.${randomUUID()}.cleanup`
|
|
3012
|
+
);
|
|
3013
|
+
renameSync(backupRoot, cleanupRoot);
|
|
3014
|
+
scheduleNativeSlackTreeRemoval(root, cleanupRoot);
|
|
2967
3015
|
}
|
|
2968
3016
|
return {
|
|
2969
3017
|
ok: true,
|
|
@@ -2974,7 +3022,12 @@ export function materializeNativeSlackCustomerProfile({
|
|
|
2974
3022
|
};
|
|
2975
3023
|
} catch (error) {
|
|
2976
3024
|
if (existsSync(stagingRoot) && !lstatSync(stagingRoot).isSymbolicLink()) {
|
|
2977
|
-
|
|
3025
|
+
const cleanupRoot = join(
|
|
3026
|
+
root,
|
|
3027
|
+
`.${desired.profileId}.${randomUUID()}.cleanup`
|
|
3028
|
+
);
|
|
3029
|
+
renameSync(stagingRoot, cleanupRoot);
|
|
3030
|
+
scheduleNativeSlackTreeRemoval(root, cleanupRoot);
|
|
2978
3031
|
}
|
|
2979
3032
|
if (!existsSync(activeRoot) && existsSync(backupRoot)) {
|
|
2980
3033
|
renameSync(backupRoot, activeRoot);
|
|
@@ -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.757";
|
|
29
29
|
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.938";
|
|
30
30
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
31
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|