@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.
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.756
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.756" \
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.756' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.938') throw new Error('package identity rejected');" \
26
+ && node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.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.756', 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}" \
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
@@ -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.756` is
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.756
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.756";
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 consumeSecrets({ claim, exchange, client, desired });
3682
- await publishManagedAuth({
3683
- schemaVersion: "sellable-managed-auth/v1",
3684
- profileId: desired.profileId,
3685
- runtimeId: RUNTIME_ID,
3686
- runtimeGeneration: RUNTIME_GENERATION,
3687
- agentKind: "CUSTOMER",
3688
- apiUrl: ORIGIN,
3689
- token: secrets.mcpService,
3690
- agent: {
3691
- workspaceId: desired.workspaceId,
3692
- agentId: desired.agentId,
3693
- generation: desired.serviceCredentialGeneration,
3694
- policyHash: desired.policyHash,
3695
- selectedChannelIds: desired.selectedChannelIds,
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 restoreLatestHermesSnapshot({
3700
- profileId: desired.profileId,
3701
- profileRoot,
3702
- hermesRoot: DATA_ROOT,
3703
- hermesVersion: HERMES_VERSION,
3704
- fetchLatest: () => client.getMemorySnapshot(),
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 = migrateLegacyHermesUserState({
3708
- profileId: desired.profileId,
3709
- profileRoot,
3710
- hermesRoot: DATA_ROOT,
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 = materializeNativeSlackCustomerProfile({
3714
- desired,
3715
- profilesRoot: PROFILES_ROOT,
3716
- });
3717
- if (!materialized.ok) {
3718
- const detail =
3719
- materialized.code === "native_slack_profile_insufficient_disk"
3720
- ? `:available_bytes=${materialized.availableBytes}:required_bytes=${materialized.requiredBytes}`
3721
- : materialized.causeCode
3722
- ? `:cause=${materialized.causeCode}`
3723
- : "";
3724
- throw new Error(`${materialized.code}${detail}`);
3725
- }
3726
- const pristineProfile = validateNativeSlackCustomerProfile({
3727
- desired,
3728
- profileRoot,
3729
- });
3730
- if (!pristineProfile.ok) throw new Error(pristineProfile.code);
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 reconcileDefaultProfileBundle({
3736
- activeProjection,
3737
- materializerReceipt: pristineProfile.manifest,
3738
- profileRoot,
3739
- bundleRoot: DEFAULT_PROFILE_BUNDLE_ROOT,
3740
- dataRoot: DATA_ROOT,
3741
- reattestAuthority: async () => {
3742
- await reattestAuthority();
3743
- return activeProjection;
3744
- },
3745
- cronAdapter: customerDefaultProfileCronAdapter(
3746
- desired.profileId,
3747
- profileRoot
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 = reconcilePublicHermesSkills({
3754
- profileId: desired.profileId,
3755
- profileRoot,
3756
- dataRoot: DATA_ROOT,
3757
- });
3758
- installInternalAdminRuntimeSettings({ desired, profileRoot });
3759
- const runtimeConfig = writeRuntimeHermesConfig({
3760
- desired,
3761
- toolInclude,
3762
- profileRoot,
3763
- secrets,
3764
- claim,
3765
- trustRootPem,
3766
- });
3767
- const soulProof = observeProfileSoul(desired.profileId, {
3768
- dataRoot: DATA_ROOT,
3769
- });
3770
- if (soulProof.ok !== true) {
3771
- throw new Error("customer_live_soul_loader_proof_rejected");
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 ensureHermesSnapshotCron({
3775
- profileId: desired.profileId,
3776
- profileRoot,
3777
- hermesRoot: DATA_ROOT,
3778
- hermesCli: HERMES,
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 flushHermesSnapshotBeforeServing({
3786
- client,
3787
- profileId: desired.profileId,
3788
- profileRoot,
3789
- runtimeAuthorityScope,
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 prove({
3810
- action: claim.operation.action,
3811
- desired,
3812
- runtimeConfig,
3813
- secrets,
3814
- profileDigest: pristineProfile.manifest.profileDigest,
3815
- runtimeAuthorityScope,
3816
- });
3817
- const customerProgramCrons = await ensureHermesCustomerProgramCrons({
3818
- profileId: desired.profileId,
3819
- profileRoot,
3820
- hermesRoot: DATA_ROOT,
3821
- workspaceId: desired.workspaceId,
3822
- channelId: desired.defaultChannelId,
3823
- timezone: desired.customerProgramTimezone,
3824
- apiUrl: ORIGIN,
3825
- authorityPath: managedAuthPath(desired.profileId),
3826
- });
3827
- if (!customerProgramCrons.ok) throw new Error(customerProgramCrons.code);
3828
- const onboardingCron = await ensureHermesFirstCampaignOnboardingCron({
3829
- profileId: desired.profileId,
3830
- profileRoot,
3831
- hermesRoot: DATA_ROOT,
3832
- workspaceId: desired.workspaceId,
3833
- channelId: desired.defaultChannelId,
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 flushHermesSnapshotBeforeServing({
3838
- client,
3839
- profileId: desired.profileId,
3840
- profileRoot,
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 client.claimTick({
3886
- leaseSeconds: 60,
3887
- servingObservation,
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 client.complete(receipt);
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(FLOCK_EXECUTABLE, ["--exclusive", "3"], {
494
- stdio: ["ignore", "ignore", "pipe", fd],
495
- encoding: "utf8",
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
- copyFileSync(from, to);
493
- chmodSync(to, stat.mode & 0o777);
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.756";
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.756" ||
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 NATIVE_SLACK_COPY_MARGIN_BYTES = 64 * 1024 * 1024;
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 sweepStaleNativeSlackStages(root, profileId) {
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
- const suffix = ".staging";
130
- let removed = 0;
144
+ let scheduled = 0;
131
145
  for (const name of readdirSync(root)) {
132
- if (!name.startsWith(prefix) || !name.endsWith(suffix)) continue;
133
- if (removeWithoutFollowing(join(root, name))) removed += 1;
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 removed;
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.756" ||
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}.${randomUUID()}.backup`);
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
- sweepStaleNativeSlackStages(root, desired.profileId);
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
- const profileBytes = Math.max(
2894
- desiredSeedBytes,
2895
- logicalTreeBytes(activeRoot)
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 * NATIVE_SLACK_PEAK_COPY_MULTIPLIER +
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
- mergeHermesState(activeRoot, stagingRoot);
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
- rmSync(backupRoot, { recursive: true, force: true });
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
- rmSync(stagingRoot, { recursive: true, force: true });
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.756";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.756",
3
+ "version": "0.1.757",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {