@sellable/install 0.1.593-wip.runtime-gates.20260811041542 → 0.1.593
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/container/Dockerfile +4 -4
- package/container/README.md +2 -2
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +34 -1
- package/lib/sellable-agent/fly-admin-image/start-admin-runtime +1 -1
- package/lib/sellable-agent/fly-customer-image/Dockerfile +1 -1
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +31 -5
- package/lib/sellable-agent/fly-customer-worker.mjs +1 -0
- package/lib/sellable-agent/fly-runtime-identity.mjs +2 -1
- package/lib/sellable-agent/hermes-bridge.mjs +22 -0
- package/lib/sellable-agent/host-bootstrap.mjs +1 -2
- package/lib/sellable-agent/host-worker.mjs +24 -29
- package/lib/sellable-agent/profile-materializer.mjs +1 -1
- package/lib/sellable-agent/provisioning-adapter.mjs +7 -4
- package/lib/sellable-agent/runtime-boundary.mjs +1 -0
- package/lib/sellable-agent/runtime-helper.mjs +84 -0
- package/lib/sellable-agent/runtime-reconciler.mjs +17 -0
- package/package.json +1 -1
package/container/Dockerfile
CHANGED
|
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.593
|
|
6
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.593
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.860
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
9
|
ARG MCP_INTEGRITY=sha512-jRA1KsCV2d4Zcuo5/UmJiP8P0NaoC3ZDvUFRQOc2AMSNDVmUR6gWvtkyAiqb1lukbFWKzfVlz2/4EWzxvb7hkQ==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.593
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.593" \
|
|
12
12
|
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.860" \
|
|
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.593-wip.runtime-gates.2
|
|
|
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.592' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.860') 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.593
|
|
28
|
+
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.593', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.860', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
|
|
29
29
|
&& npm cache clean --force
|
|
30
30
|
|
|
31
31
|
COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
|
package/container/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
|
|
4
4
|
|
|
5
5
|
Build from this directory after
|
|
6
|
-
`@sellable/install@0.1.593
|
|
6
|
+
`@sellable/install@0.1.593` is
|
|
7
7
|
published:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
docker build --platform linux/amd64 \
|
|
11
11
|
--build-arg INSTALLER_INTEGRITY='sha512-…' \
|
|
12
|
-
--tag sellable-agent-host:0.1.
|
|
12
|
+
--tag sellable-agent-host:0.1.592 .
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Create a private `bootstrap/` directory owned by root with mode `0700`. It must contain `bootstrap.json`, `provider-auth`, and `slack-app`, each owned by root with mode `0600`. `bootstrap.json` contains only non-secret identities, hashes, package integrities, and the control-plane URL. The other two files contain the provider JSON and global Slack app token whose hashes are pinned by `bootstrap.json`.
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
buildAdminAgentServiceEnvironment,
|
|
42
42
|
buildNativeHermesLaunch,
|
|
43
43
|
} from "../admin-artifact/node_modules/@sellable/install/lib/sellable-agent/fly-customer-worker.mjs";
|
|
44
|
+
import { expectedRuntimeGates } from "../admin-artifact/node_modules/@sellable/install/lib/sellable-agent/runtime-helper.mjs";
|
|
44
45
|
import sodium from "../admin-artifact/node_modules/libsodium-wrappers/dist/modules-esm/libsodium-wrappers.mjs";
|
|
45
46
|
import {
|
|
46
47
|
claimWithSoulObservation,
|
|
@@ -1181,6 +1182,28 @@ async function consumeRuntimeSecrets({ claim, exchange, client }) {
|
|
|
1181
1182
|
return { mcpService, slackApp, slackBot, modelCodex };
|
|
1182
1183
|
}
|
|
1183
1184
|
|
|
1185
|
+
function writeAdminRuntimeGates(desiredLifecycle) {
|
|
1186
|
+
const gates = expectedRuntimeGates(desiredLifecycle);
|
|
1187
|
+
const path = join(SECRET_ROOT, "gates.json");
|
|
1188
|
+
atomicPrivateFile(path, `${JSON.stringify(gates)}\n`);
|
|
1189
|
+
const stat = lstatSync(path);
|
|
1190
|
+
let observed;
|
|
1191
|
+
try {
|
|
1192
|
+
observed = JSON.parse(readFileSync(path, "utf8"));
|
|
1193
|
+
} catch {
|
|
1194
|
+
throw new Error("admin_runtime_gates_rejected");
|
|
1195
|
+
}
|
|
1196
|
+
if (
|
|
1197
|
+
stat.isSymbolicLink() ||
|
|
1198
|
+
!stat.isFile() ||
|
|
1199
|
+
(stat.mode & 0o777) !== 0o600 ||
|
|
1200
|
+
canonicalJson(observed) !== canonicalJson(gates)
|
|
1201
|
+
) {
|
|
1202
|
+
throw new Error("admin_runtime_gates_rejected");
|
|
1203
|
+
}
|
|
1204
|
+
return Object.freeze({ ...observed });
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1184
1207
|
function adminProfileFactoryAuthJson(model) {
|
|
1185
1208
|
if (model.value.schemaVersion !== "sellable-agent-model-auth-runtime/v3") {
|
|
1186
1209
|
return model.hermesRootAuthJson;
|
|
@@ -1326,6 +1349,7 @@ function installRuntimeSecrets({ claim, secrets, desiredLifecycle }) {
|
|
|
1326
1349
|
slackSecretFile,
|
|
1327
1350
|
`SLACK_BOT_TOKEN=${secrets.slackBot}\nSLACK_APP_TOKEN=${secrets.slackApp}\n`
|
|
1328
1351
|
);
|
|
1352
|
+
const runtimeGates = writeAdminRuntimeGates(desiredLifecycle);
|
|
1329
1353
|
const productConfigPath = join(SECRET_ROOT, "sellable-config.json");
|
|
1330
1354
|
atomicPrivateFile(
|
|
1331
1355
|
productConfigPath,
|
|
@@ -1344,6 +1368,7 @@ function installRuntimeSecrets({ claim, secrets, desiredLifecycle }) {
|
|
|
1344
1368
|
modelAuthState,
|
|
1345
1369
|
modelConfig: modelAuthConfigOverlay(model),
|
|
1346
1370
|
productConfigPath,
|
|
1371
|
+
runtimeGates,
|
|
1347
1372
|
slackSecretFile,
|
|
1348
1373
|
};
|
|
1349
1374
|
}
|
|
@@ -1528,6 +1553,7 @@ function clearRuntimeSecrets() {
|
|
|
1528
1553
|
"slack-app",
|
|
1529
1554
|
"slack-bot",
|
|
1530
1555
|
"profile-slack.env",
|
|
1556
|
+
"gates.json",
|
|
1531
1557
|
"sellable-config.json",
|
|
1532
1558
|
]) {
|
|
1533
1559
|
rmSync(join(SECRET_ROOT, name), { force: true });
|
|
@@ -2088,6 +2114,7 @@ function completionReceipt({
|
|
|
2088
2114
|
observedLifecycle,
|
|
2089
2115
|
memorySnapshot,
|
|
2090
2116
|
modelAuth,
|
|
2117
|
+
runtimeGates,
|
|
2091
2118
|
}) {
|
|
2092
2119
|
const identity = operationIdentity(claim, exchange);
|
|
2093
2120
|
const routeGenerationDigest = flyRuntimeDigest(
|
|
@@ -2135,7 +2162,9 @@ function completionReceipt({
|
|
|
2135
2162
|
package: sha256(JSON.stringify(packageSpecs())),
|
|
2136
2163
|
config: operation.profileContract.digest,
|
|
2137
2164
|
content: observedProfileDigest,
|
|
2138
|
-
process: sha256(
|
|
2165
|
+
process: sha256(
|
|
2166
|
+
JSON.stringify({ servers: profile.servers, modelAuth, runtimeGates })
|
|
2167
|
+
),
|
|
2139
2168
|
};
|
|
2140
2169
|
return {
|
|
2141
2170
|
...identity,
|
|
@@ -2163,6 +2192,7 @@ function completionReceipt({
|
|
|
2163
2192
|
: "MODEL_AUTH_DESIRED_PENDING_START",
|
|
2164
2193
|
status: "TRUE",
|
|
2165
2194
|
},
|
|
2195
|
+
{ code: "RUNTIME_GATES_PROVEN", status: "TRUE" },
|
|
2166
2196
|
{ code: "SLACK_TRAFFIC_DISABLED", status: "TRUE" },
|
|
2167
2197
|
{ code: "NO_BOOT_INSTALL", status: "TRUE" },
|
|
2168
2198
|
],
|
|
@@ -2533,6 +2563,7 @@ export async function runAdminCanary() {
|
|
|
2533
2563
|
},
|
|
2534
2564
|
};
|
|
2535
2565
|
const agentServiceEnvironment = adminAgentServiceEnvironment(canaryClaim);
|
|
2566
|
+
const runtimeGates = writeAdminRuntimeGates("ON");
|
|
2536
2567
|
|
|
2537
2568
|
const template = JSON.parse(readFileSync(ADMIN_PROFILE_TEMPLATE_PATH));
|
|
2538
2569
|
const { contentDigest, ...templateContent } = template;
|
|
@@ -2730,6 +2761,7 @@ export async function runAdminCanary() {
|
|
|
2730
2761
|
profileFactory: "stageAdminProfile",
|
|
2731
2762
|
profileDigest: profileContract.digest,
|
|
2732
2763
|
servers: profile.servers,
|
|
2764
|
+
runtimeGates,
|
|
2733
2765
|
workspaceAuthority: {
|
|
2734
2766
|
agentKind: canaryClaim.agentKind,
|
|
2735
2767
|
workspaceLockPresent:
|
|
@@ -3155,6 +3187,7 @@ async function runAdminProduction() {
|
|
|
3155
3187
|
observedLifecycle: desiredLifecycle,
|
|
3156
3188
|
memorySnapshot,
|
|
3157
3189
|
modelAuth,
|
|
3190
|
+
runtimeGates: installed.runtimeGates,
|
|
3158
3191
|
});
|
|
3159
3192
|
operationHeartbeat.assertHealthy();
|
|
3160
3193
|
const candidateLaunch =
|
|
@@ -7,7 +7,7 @@ install -d -o hermes -g hermes -m 0700 \
|
|
|
7
7
|
/opt/data/profiles \
|
|
8
8
|
/run/sellable-agent \
|
|
9
9
|
/run/sellable-agent/admin-runtime
|
|
10
|
-
chown hermes:hermes /opt/data
|
|
10
|
+
chown -R hermes:hermes /opt/data
|
|
11
11
|
chmod 0700 /opt/data
|
|
12
12
|
|
|
13
13
|
if [ -f /run/secrets/sellable-admin-recovery-private-key.pem ]; then
|
|
@@ -6,7 +6,7 @@ USER root
|
|
|
6
6
|
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.860
|
|
8
8
|
ARG MCP_INTEGRITY=sha512-jRA1KsCV2d4Zcuo5/UmJiP8P0NaoC3ZDvUFRQOc2AMSNDVmUR6gWvtkyAiqb1lukbFWKzfVlz2/4EWzxvb7hkQ==
|
|
9
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.593
|
|
9
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.593
|
|
10
10
|
ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.186-wip.phase158.20260802034727
|
|
11
11
|
ARG ADMIN_MCP_INTEGRITY=sha512-qYNew5wd2IfgVcp1UYOG9QvpF/8Mx19YJ5HQtMKzvSfUf3evePjtaKsoXLf2C/GUJ5QlLlHJo84VWOI3V01+9A==
|
|
12
12
|
ARG TUS_JS_CLIENT_VERSION=4.3.1
|
|
@@ -95,7 +95,7 @@ const RUNTIME_GENERATION = Number(
|
|
|
95
95
|
const BOOT_SESSION_ID = randomUUID();
|
|
96
96
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.0";
|
|
97
97
|
const INSTALLER_PACKAGE =
|
|
98
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.593
|
|
98
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.593";
|
|
99
99
|
const MCP_PACKAGE =
|
|
100
100
|
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.860";
|
|
101
101
|
|
|
@@ -217,6 +217,13 @@ export function customerRuntimeDoctorFailureCode(
|
|
|
217
217
|
return null;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
export function customerRuntimeDoctorRequired(action) {
|
|
221
|
+
// START already proves the binaries, Slack identities, gateway, and private
|
|
222
|
+
// API directly below. The broad interactive diagnostic belongs to REFRESH;
|
|
223
|
+
// running it on every boot caused five-minute timeout/retry storms.
|
|
224
|
+
return action !== "ENABLE_GATEWAY";
|
|
225
|
+
}
|
|
226
|
+
|
|
220
227
|
function exactArtifact(claim, type) {
|
|
221
228
|
const artifacts = claim?.revision?.artifacts?.filter(
|
|
222
229
|
(artifact) =>
|
|
@@ -1838,6 +1845,13 @@ function writeRuntimeHermesConfig({
|
|
|
1838
1845
|
},
|
|
1839
1846
|
},
|
|
1840
1847
|
});
|
|
1848
|
+
const gatesPath = join(secretRoot, "gates.json");
|
|
1849
|
+
atomicJson(gatesPath, {
|
|
1850
|
+
lifecycle: modelLifecycle,
|
|
1851
|
+
dispatch: modelLifecycle === "ON" ? "ENABLED" : "DISABLED",
|
|
1852
|
+
route: modelLifecycle === "ON" ? "OPEN" : "CLOSED",
|
|
1853
|
+
output: modelLifecycle === "ON" ? "OPEN" : "CLOSED",
|
|
1854
|
+
});
|
|
1841
1855
|
atomicJson(join(runtimeConfigRoot, "runtime-identity.json"), {
|
|
1842
1856
|
runtimeId: RUNTIME_ID,
|
|
1843
1857
|
profileId: desired.profileId,
|
|
@@ -1858,6 +1872,7 @@ function writeRuntimeHermesConfig({
|
|
|
1858
1872
|
runtimeHome: runtimeConfigRoot,
|
|
1859
1873
|
runtimeConfigPath,
|
|
1860
1874
|
runtimeMcpPath,
|
|
1875
|
+
gatesPath,
|
|
1861
1876
|
modelBundlePath,
|
|
1862
1877
|
hermesRootAuthStorePath,
|
|
1863
1878
|
hermesRootAuthMarkerPath,
|
|
@@ -1958,6 +1973,7 @@ export function applicationRuntimeEnv({ desired, runtimeConfig, secrets }) {
|
|
|
1958
1973
|
HERMES_CONFIG_PATH: runtimeConfig.runtimeConfigPath,
|
|
1959
1974
|
CODEX_HOME: dirname(runtimeConfig.modelBundlePath),
|
|
1960
1975
|
SELLABLE_AGENT_RUNTIME: "1",
|
|
1976
|
+
SELLABLE_AGENT_RUNTIME_GATE_FILE: runtimeConfig.gatesPath,
|
|
1961
1977
|
SELLABLE_AGENT_TURN_DESCRIPTOR_PATH: runtimeConfig.turnDescriptorPath,
|
|
1962
1978
|
SLACK_HOME_CHANNEL: desired.defaultChannelId,
|
|
1963
1979
|
SLACK_ALLOWED_CHANNELS: desired.selectedChannelIds.join(","),
|
|
@@ -2311,6 +2327,7 @@ async function proveProvisionedApplication({
|
|
|
2311
2327
|
}
|
|
2312
2328
|
|
|
2313
2329
|
async function proveApplication({
|
|
2330
|
+
action,
|
|
2314
2331
|
desired,
|
|
2315
2332
|
runtimeConfig,
|
|
2316
2333
|
secrets,
|
|
@@ -2323,10 +2340,18 @@ async function proveApplication({
|
|
|
2323
2340
|
profileDigest,
|
|
2324
2341
|
});
|
|
2325
2342
|
const runtimeEnv = applicationRuntimeEnv({ desired, runtimeConfig, secrets });
|
|
2326
|
-
const doctor =
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2343
|
+
const doctor = customerRuntimeDoctorRequired(action)
|
|
2344
|
+
? await run(HERMES, ["doctor"], {
|
|
2345
|
+
env: runtimeEnv,
|
|
2346
|
+
timeoutMs: 300_000,
|
|
2347
|
+
})
|
|
2348
|
+
: {
|
|
2349
|
+
status: 0,
|
|
2350
|
+
stdout: "Hermes START readiness uses direct runtime checks.",
|
|
2351
|
+
stderr: "",
|
|
2352
|
+
timedOut: false,
|
|
2353
|
+
elapsedMs: 0,
|
|
2354
|
+
};
|
|
2330
2355
|
const doctorFailure = customerRuntimeDoctorFailureCode(doctor);
|
|
2331
2356
|
if (doctorFailure) throw new Error(doctorFailure);
|
|
2332
2357
|
const slackResponse = await fetch(`${slackApiOrigin()}/api/auth.test`, {
|
|
@@ -2599,6 +2624,7 @@ async function processOperation({ claim, exchange, client, identity }) {
|
|
|
2599
2624
|
? proveProvisionedApplication
|
|
2600
2625
|
: proveApplication;
|
|
2601
2626
|
const proof = await prove({
|
|
2627
|
+
action: claim.operation.action,
|
|
2602
2628
|
desired,
|
|
2603
2629
|
runtimeConfig,
|
|
2604
2630
|
secrets,
|
|
@@ -388,6 +388,7 @@ export function buildNativeHermesLaunch({
|
|
|
388
388
|
HERMES_PROFILE: profileId,
|
|
389
389
|
HERMES_CONFIG_PATH: configPath,
|
|
390
390
|
SELLABLE_AGENT_RUNTIME: "1",
|
|
391
|
+
SELLABLE_AGENT_RUNTIME_GATE_FILE: join(resolvedSecretsRoot, "gates.json"),
|
|
391
392
|
SELLABLE_SLACK_APP_TOKEN_FD: "8",
|
|
392
393
|
SELLABLE_SLACK_BOT_TOKEN_FD: "9",
|
|
393
394
|
}),
|
|
@@ -1088,7 +1088,8 @@ export function createFlyCustomerControlClient({
|
|
|
1088
1088
|
return post(
|
|
1089
1089
|
"heartbeat",
|
|
1090
1090
|
"/api/v3/sellable-agent/worker/heartbeat",
|
|
1091
|
-
identityBody
|
|
1091
|
+
identityBody,
|
|
1092
|
+
{ replaySafe: true }
|
|
1092
1093
|
);
|
|
1093
1094
|
},
|
|
1094
1095
|
async claimEndpoint(leaseSeconds = 60) {
|
|
@@ -306,6 +306,17 @@ def _sellable_agent_call_meta():
|
|
|
306
306
|
# identity. Send no actor meta; the serving API enforces its own actor
|
|
307
307
|
# gates, as with installs outside the Sellable runtime.
|
|
308
308
|
return None
|
|
309
|
+
import json
|
|
310
|
+
gate_path = os.getenv("SELLABLE_AGENT_RUNTIME_GATE_FILE", "")
|
|
311
|
+
if not gate_path.startswith("/"):
|
|
312
|
+
raise RuntimeError("Sellable runtime gate path is not pinned")
|
|
313
|
+
try:
|
|
314
|
+
with open(gate_path, "r", encoding="utf-8") as gate_file:
|
|
315
|
+
gates = json.load(gate_file)
|
|
316
|
+
except (OSError, ValueError) as exc:
|
|
317
|
+
raise RuntimeError("Sellable runtime gates are unavailable") from exc
|
|
318
|
+
if gates != {"lifecycle": "ON", "dispatch": "ENABLED", "route": "OPEN", "output": "OPEN"}:
|
|
319
|
+
raise RuntimeError("Sellable runtime dispatch is fenced")
|
|
309
320
|
from gateway.session_context import get_session_env, session_context_engaged
|
|
310
321
|
if not session_context_engaged():
|
|
311
322
|
raise RuntimeError("Sellable MCP calls require an active Hermes session context")
|
|
@@ -594,6 +605,17 @@ def _sellable_agent_call_meta():
|
|
|
594
605
|
# identity. Send no actor meta; the serving API enforces its own actor
|
|
595
606
|
# gates, as with installs outside the Sellable runtime.
|
|
596
607
|
return None
|
|
608
|
+
import json
|
|
609
|
+
gate_path = os.getenv("SELLABLE_AGENT_RUNTIME_GATE_FILE", "")
|
|
610
|
+
if not gate_path.startswith("/"):
|
|
611
|
+
raise RuntimeError("Sellable runtime gate path is not pinned")
|
|
612
|
+
try:
|
|
613
|
+
with open(gate_path, "r", encoding="utf-8") as gate_file:
|
|
614
|
+
gates = json.load(gate_file)
|
|
615
|
+
except (OSError, ValueError) as exc:
|
|
616
|
+
raise RuntimeError("Sellable runtime gates are unavailable") from exc
|
|
617
|
+
if gates != {"lifecycle": "ON", "dispatch": "ENABLED", "route": "OPEN", "output": "OPEN"}:
|
|
618
|
+
raise RuntimeError("Sellable runtime dispatch is fenced")
|
|
597
619
|
from gateway.session_context import get_session_env, session_context_engaged
|
|
598
620
|
if not session_context_engaged():
|
|
599
621
|
raise RuntimeError("Sellable MCP calls require an active Hermes session context")
|
|
@@ -38,8 +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 =
|
|
42
|
-
"@sellable/install@0.1.593-wip.runtime-gates.20260811041542";
|
|
41
|
+
const INSTALLER_PACKAGE = "@sellable/install@0.1.593";
|
|
43
42
|
const MCP_PACKAGE = "@sellable/mcp@0.1.860";
|
|
44
43
|
const HERMES_VERSION = "0.18.0";
|
|
45
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.593
|
|
1549
|
+
pinned.installerPackage !== "@sellable/install@0.1.593" ||
|
|
1550
1550
|
pinned.mcpPackage !== "@sellable/mcp@0.1.860" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
@@ -2122,6 +2122,12 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
2122
2122
|
await helper({ action: "VERIFY_HERMES_BRIDGE", ...helperIdentity() });
|
|
2123
2123
|
return helper({ action: "CONTROL_RUNTIME", ...helperIdentity(), control });
|
|
2124
2124
|
};
|
|
2125
|
+
const setRuntimeLifecycle = (lifecycle) =>
|
|
2126
|
+
helper({
|
|
2127
|
+
action: "SET_RUNTIME_GATES",
|
|
2128
|
+
...helperIdentity(),
|
|
2129
|
+
lifecycle,
|
|
2130
|
+
});
|
|
2125
2131
|
const observeRuntimeLifecycle = (lifecycle) =>
|
|
2126
2132
|
helper({
|
|
2127
2133
|
action: "OBSERVE_RUNTIME_LIFECYCLE",
|
|
@@ -2562,13 +2568,10 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
2562
2568
|
});
|
|
2563
2569
|
Object.assign(ports.lifecycle, {
|
|
2564
2570
|
async fenceDispatch() {
|
|
2565
|
-
|
|
2566
|
-
// without introducing a second file-backed switch that can strand an
|
|
2567
|
-
// otherwise healthy runtime.
|
|
2568
|
-
await controlRuntime("DOWN");
|
|
2571
|
+
await setRuntimeLifecycle("OFF");
|
|
2569
2572
|
},
|
|
2570
2573
|
async fenceRouteOutput() {
|
|
2571
|
-
await
|
|
2574
|
+
await setRuntimeLifecycle("ARCHIVED");
|
|
2572
2575
|
},
|
|
2573
2576
|
async drain() {
|
|
2574
2577
|
await controlRuntime("DOWN");
|
|
@@ -2588,6 +2591,7 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
2588
2591
|
return observed;
|
|
2589
2592
|
},
|
|
2590
2593
|
async startGateway() {
|
|
2594
|
+
await setRuntimeLifecycle("ON");
|
|
2591
2595
|
await controlRuntime("UP");
|
|
2592
2596
|
await observeRuntimeLifecycle("ON");
|
|
2593
2597
|
},
|
|
@@ -2597,6 +2601,7 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
2597
2601
|
? "ARCHIVING"
|
|
2598
2602
|
: "OFF";
|
|
2599
2603
|
await controlRuntime("DOWN");
|
|
2604
|
+
await setRuntimeLifecycle(lifecycle);
|
|
2600
2605
|
await observeRuntimeLifecycle(lifecycle);
|
|
2601
2606
|
},
|
|
2602
2607
|
async proveNoDispatch() {
|
|
@@ -2604,7 +2609,9 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
2604
2609
|
context.activeClaim.operation.action === "ARCHIVE" ? "ARCHIVED" : "OFF";
|
|
2605
2610
|
const observed = await observeRuntimeLifecycle(lifecycle);
|
|
2606
2611
|
return {
|
|
2607
|
-
noDispatch:
|
|
2612
|
+
noDispatch:
|
|
2613
|
+
observed.gates.dispatch === "FENCED" &&
|
|
2614
|
+
observed.process.ready === false,
|
|
2608
2615
|
};
|
|
2609
2616
|
},
|
|
2610
2617
|
async observeGateway(input) {
|
|
@@ -2621,6 +2628,7 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
2621
2628
|
revisionId: observed.revisionId,
|
|
2622
2629
|
credentialGeneration: observed.credentialGeneration,
|
|
2623
2630
|
profileDigest: observed.profileDigest,
|
|
2631
|
+
gates: observed.gates,
|
|
2624
2632
|
secretsPresent: observed.secretsPresent,
|
|
2625
2633
|
process: observed.process,
|
|
2626
2634
|
runtimeObservation: observed.runtimeObservation,
|
|
@@ -2671,6 +2679,7 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
2671
2679
|
};
|
|
2672
2680
|
},
|
|
2673
2681
|
async rollback({ priorState }) {
|
|
2682
|
+
await setRuntimeLifecycle(priorState.observedLifecycle);
|
|
2674
2683
|
if (priorState.observedLifecycle === "ON") await controlRuntime("UP");
|
|
2675
2684
|
else await controlRuntime("DOWN");
|
|
2676
2685
|
await observeRuntimeLifecycle(priorState.observedLifecycle);
|
|
@@ -2679,35 +2688,21 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
2679
2688
|
try {
|
|
2680
2689
|
const evidence = {};
|
|
2681
2690
|
const confirmedSteps = [];
|
|
2682
|
-
|
|
2683
|
-
// concrete proof is now the stopped service, not a gates.json file.
|
|
2684
|
-
// Preserve the step identity so an in-flight archive can resume across
|
|
2685
|
-
// this runtime upgrade.
|
|
2686
|
-
let stopped;
|
|
2691
|
+
let gates;
|
|
2687
2692
|
try {
|
|
2688
|
-
|
|
2693
|
+
gates = await helper({
|
|
2694
|
+
action: "OBSERVE_RUNTIME_GATES",
|
|
2695
|
+
...helperIdentity(),
|
|
2696
|
+
lifecycle: "ARCHIVED",
|
|
2697
|
+
});
|
|
2689
2698
|
} catch {
|
|
2690
|
-
try {
|
|
2691
|
-
stopped = await observeRuntimeLifecycle("ARCHIVED");
|
|
2692
|
-
} catch {
|
|
2693
|
-
return {
|
|
2694
|
-
ok: true,
|
|
2695
|
-
observationId: `archive_${canonicalHostWorkerDigest({ service: "not_stopped" })}`,
|
|
2696
|
-
confirmedSteps,
|
|
2697
|
-
};
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
evidence.service = {
|
|
2701
|
-
lifecycle: stopped.lifecycle,
|
|
2702
|
-
processReady: stopped.process?.ready,
|
|
2703
|
-
};
|
|
2704
|
-
if (stopped.process?.ready !== false) {
|
|
2705
2699
|
return {
|
|
2706
2700
|
ok: true,
|
|
2707
|
-
observationId: `archive_${canonicalHostWorkerDigest(
|
|
2701
|
+
observationId: `archive_${canonicalHostWorkerDigest({ gates: "not_confirmed" })}`,
|
|
2708
2702
|
confirmedSteps,
|
|
2709
2703
|
};
|
|
2710
2704
|
}
|
|
2705
|
+
evidence.gates = gates.gates;
|
|
2711
2706
|
confirmedSteps.push("FENCE_ROUTE_OUTPUT");
|
|
2712
2707
|
let drain;
|
|
2713
2708
|
try {
|
|
@@ -277,7 +277,7 @@ function validateDesired(desired) {
|
|
|
277
277
|
desired.serviceCredentialGeneration < 1 ||
|
|
278
278
|
desired.hermesCli !== "hermes" ||
|
|
279
279
|
desired.hermesVersion !== "0.18.0" ||
|
|
280
|
-
desired.installerPackage !== "@sellable/install@0.1.593
|
|
280
|
+
desired.installerPackage !== "@sellable/install@0.1.593" ||
|
|
281
281
|
desired.mcpPackage !== "@sellable/mcp@0.1.860" ||
|
|
282
282
|
!Array.isArray(desired.toolInclude) ||
|
|
283
283
|
desired.toolInclude.length === 0 ||
|
|
@@ -25,8 +25,11 @@ 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.593
|
|
28
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.593";
|
|
29
29
|
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.860";
|
|
30
|
+
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
|
+
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|
|
32
|
+
);
|
|
30
33
|
|
|
31
34
|
export function deriveAgentProfileId(workspaceId, agentId, hostId) {
|
|
32
35
|
return deriveContainedProfileId(workspaceId, agentId, hostId);
|
|
@@ -611,7 +614,7 @@ function inspectProvisionedProfile({
|
|
|
611
614
|
!matchesReadback(manifest, request) ||
|
|
612
615
|
manifest.serviceCredentialReference !== serviceCredentialReference ||
|
|
613
616
|
manifest.installerPackage !== PINNED_INSTALL_PACKAGE ||
|
|
614
|
-
manifest.installerVersion !==
|
|
617
|
+
manifest.installerVersion !== PINNED_INSTALL_VERSION ||
|
|
615
618
|
manifest.mcpPackage !== PINNED_MCP_PACKAGE ||
|
|
616
619
|
manifest.credentialKeyVersion !== credentialKeyVersion ||
|
|
617
620
|
!/^[a-f0-9]{16}$/.test(manifest.credentialFingerprint) ||
|
|
@@ -825,7 +828,7 @@ function successReceipt({
|
|
|
825
828
|
subject: `agent-profile:${observed.profileId}`,
|
|
826
829
|
cli: {
|
|
827
830
|
installPackage: PINNED_INSTALL_PACKAGE,
|
|
828
|
-
installVersion:
|
|
831
|
+
installVersion: PINNED_INSTALL_VERSION,
|
|
829
832
|
mcpPackage: PINNED_MCP_PACKAGE,
|
|
830
833
|
command: "hermes profile bootstrap",
|
|
831
834
|
},
|
|
@@ -873,7 +876,7 @@ function failure(code, stages, extra = {}, request = null) {
|
|
|
873
876
|
: "agent-profile:unbound",
|
|
874
877
|
cli: {
|
|
875
878
|
installPackage: PINNED_INSTALL_PACKAGE,
|
|
876
|
-
installVersion:
|
|
879
|
+
installVersion: PINNED_INSTALL_VERSION,
|
|
877
880
|
mcpPackage: PINNED_MCP_PACKAGE,
|
|
878
881
|
command: "hermes profile bootstrap",
|
|
879
882
|
},
|
|
@@ -276,6 +276,7 @@ function runtimeEnvironment(contract) {
|
|
|
276
276
|
SLACK_ALLOWED_USERS: contract.memberAllowlist.join(","),
|
|
277
277
|
SLACK_REQUIRE_MENTION: "true",
|
|
278
278
|
SELLABLE_AGENT_RUNTIME: "1",
|
|
279
|
+
SELLABLE_AGENT_RUNTIME_GATE_FILE: "/runtime/gates.json",
|
|
279
280
|
SELLABLE_AGENT_RUNTIME_UID: String(contract.runtime.uid),
|
|
280
281
|
SELLABLE_AGENT_RUNTIME_GID: String(contract.runtime.gid),
|
|
281
282
|
SELLABLE_AGENT_HERMES_EXECUTABLE: contract.paths.hermesExecutablePath,
|
|
@@ -54,6 +54,8 @@ const ACTIONS = new Set([
|
|
|
54
54
|
"INSTALL_MCP_CREDENTIAL",
|
|
55
55
|
"INSTALL_PROVIDER_CREDENTIAL",
|
|
56
56
|
"INSTALL_RUNTIME_SERVICE_ENV",
|
|
57
|
+
"SET_RUNTIME_GATES",
|
|
58
|
+
"OBSERVE_RUNTIME_GATES",
|
|
57
59
|
"OBSERVE_RUNTIME_LIFECYCLE",
|
|
58
60
|
"CLEAN_PROFILE_SECRETS",
|
|
59
61
|
"CHOWN_PROFILE",
|
|
@@ -101,6 +103,20 @@ const ACTION_KEYS = Object.freeze({
|
|
|
101
103
|
"revisionId",
|
|
102
104
|
"profileDigest",
|
|
103
105
|
],
|
|
106
|
+
SET_RUNTIME_GATES: [
|
|
107
|
+
"action",
|
|
108
|
+
"profileId",
|
|
109
|
+
"operationId",
|
|
110
|
+
"fence",
|
|
111
|
+
"lifecycle",
|
|
112
|
+
],
|
|
113
|
+
OBSERVE_RUNTIME_GATES: [
|
|
114
|
+
"action",
|
|
115
|
+
"profileId",
|
|
116
|
+
"operationId",
|
|
117
|
+
"fence",
|
|
118
|
+
"lifecycle",
|
|
119
|
+
],
|
|
104
120
|
OBSERVE_RUNTIME_LIFECYCLE: [
|
|
105
121
|
"action",
|
|
106
122
|
"profileId",
|
|
@@ -765,6 +781,7 @@ function runtimeServiceEnvironmentValues({
|
|
|
765
781
|
SELLABLE_AGENT_POLICY_HASH: manifest.policyHash,
|
|
766
782
|
SELLABLE_AGENT_PROFILE_ID: profileId,
|
|
767
783
|
SELLABLE_AGENT_RUNTIME_ENV_LOADED: "1",
|
|
784
|
+
SELLABLE_AGENT_RUNTIME_GATE_FILE: "/runtime/gates.json",
|
|
768
785
|
SELLABLE_AGENT_RUNTIME_SECRET_ROOT: mcpRoot,
|
|
769
786
|
SELLABLE_AGENT_SELECTED_CHANNEL_IDS: JSON.stringify(
|
|
770
787
|
manifest.slackChannelIds
|
|
@@ -787,6 +804,56 @@ function runtimeServiceEnvironmentValues({
|
|
|
787
804
|
};
|
|
788
805
|
}
|
|
789
806
|
|
|
807
|
+
export function expectedRuntimeGates(lifecycle) {
|
|
808
|
+
if (lifecycle === "ON")
|
|
809
|
+
return { lifecycle, dispatch: "ENABLED", route: "OPEN", output: "OPEN" };
|
|
810
|
+
if (lifecycle === "OFF")
|
|
811
|
+
return { lifecycle, dispatch: "FENCED", route: "OPEN", output: "OPEN" };
|
|
812
|
+
if (["ARCHIVING", "ARCHIVED"].includes(lifecycle)) {
|
|
813
|
+
return {
|
|
814
|
+
lifecycle: "ARCHIVED",
|
|
815
|
+
dispatch: "FENCED",
|
|
816
|
+
route: "FENCED",
|
|
817
|
+
output: "FENCED",
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
throw new Error("runtime lifecycle rejected");
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function writeRuntimeGates(profile, lifecycle) {
|
|
824
|
+
const root = safeRuntimeStateRoot(profile);
|
|
825
|
+
const gates = expectedRuntimeGates(lifecycle);
|
|
826
|
+
const pathValue = join(root, "gates.json");
|
|
827
|
+
atomicCredential(
|
|
828
|
+
pathValue,
|
|
829
|
+
JSON.stringify(gates),
|
|
830
|
+
profile.runtimeUid,
|
|
831
|
+
profile.runtimeGid
|
|
832
|
+
);
|
|
833
|
+
const parsed = JSON.parse(readFileSync(pathValue, "utf8"));
|
|
834
|
+
if (stableJson(parsed) !== stableJson(gates))
|
|
835
|
+
throw new Error("runtime gates readback rejected");
|
|
836
|
+
return { path: pathValue, gates };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function observeRuntimeGates(profile, lifecycle) {
|
|
840
|
+
const root = safeRuntimeStateRoot(profile);
|
|
841
|
+
const pathValue = join(root, "gates.json");
|
|
842
|
+
const link = lstatSync(pathValue);
|
|
843
|
+
const parsed = JSON.parse(readFileSync(pathValue, "utf8"));
|
|
844
|
+
const expected = expectedRuntimeGates(lifecycle);
|
|
845
|
+
if (
|
|
846
|
+
link.isSymbolicLink() ||
|
|
847
|
+
!link.isFile() ||
|
|
848
|
+
link.uid !== profile.runtimeUid ||
|
|
849
|
+
link.gid !== profile.runtimeGid ||
|
|
850
|
+
(link.mode & 0o777) !== 0o600 ||
|
|
851
|
+
stableJson(parsed) !== stableJson(expected)
|
|
852
|
+
)
|
|
853
|
+
throw new Error("runtime gates observation rejected");
|
|
854
|
+
return { path: pathValue, gates: parsed };
|
|
855
|
+
}
|
|
856
|
+
|
|
790
857
|
function safePinnedFile(pathValue, label) {
|
|
791
858
|
const resolved = resolve(pathValue ?? "");
|
|
792
859
|
if (!isAbsolute(resolved) || !existsSync(resolved))
|
|
@@ -1358,6 +1425,7 @@ function installRuntimeServiceEnvironment(profile, request) {
|
|
|
1358
1425
|
if (!existsSync(target)) throw new Error("active profile absent");
|
|
1359
1426
|
const manifest = validateActiveManifest(target, request, profile);
|
|
1360
1427
|
const runtimeStateRoot = safeRuntimeStateRoot(profile);
|
|
1428
|
+
writeRuntimeGates(profile, "ON");
|
|
1361
1429
|
const { paths, values } = runtimeServiceEnvironmentValues({
|
|
1362
1430
|
profile,
|
|
1363
1431
|
profileId: request.profileId,
|
|
@@ -1912,6 +1980,15 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
|
|
|
1912
1980
|
if (request.action === "INSTALL_RUNTIME_SERVICE_ENV") {
|
|
1913
1981
|
return installRuntimeServiceEnvironment(profile, request);
|
|
1914
1982
|
}
|
|
1983
|
+
if (request.action === "SET_RUNTIME_GATES") {
|
|
1984
|
+
const written = writeRuntimeGates(profile, request.lifecycle);
|
|
1985
|
+
return {
|
|
1986
|
+
ok: true,
|
|
1987
|
+
profileId: request.profileId,
|
|
1988
|
+
lifecycle: request.lifecycle,
|
|
1989
|
+
...written,
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1915
1992
|
if (request.action === "CLEAN_PROFILE_SECRETS") {
|
|
1916
1993
|
for (const configured of [
|
|
1917
1994
|
profile.workerRuntimeSecretRoot,
|
|
@@ -2001,7 +2078,12 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
|
|
|
2001
2078
|
if (["OBSERVE_RUNTIME", "OBSERVE_RUNTIME_DRIFT"].includes(request.action)) {
|
|
2002
2079
|
return { ok: true, observation: await observeRuntime(profile, request) };
|
|
2003
2080
|
}
|
|
2081
|
+
if (request.action === "OBSERVE_RUNTIME_GATES") {
|
|
2082
|
+
const observed = observeRuntimeGates(profile, request.lifecycle);
|
|
2083
|
+
return { ok: true, profileId: request.profileId, ...observed };
|
|
2084
|
+
}
|
|
2004
2085
|
if (request.action === "OBSERVE_RUNTIME_LIFECYCLE") {
|
|
2086
|
+
const gateObservation = observeRuntimeGates(profile, request.lifecycle);
|
|
2005
2087
|
const { target } = profileTarget(
|
|
2006
2088
|
profile,
|
|
2007
2089
|
{ ...request, slot: "ACTIVE" },
|
|
@@ -2034,6 +2116,7 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
|
|
|
2034
2116
|
revisionId: observation.revisionId,
|
|
2035
2117
|
credentialGeneration: observation.serviceCredential.generation,
|
|
2036
2118
|
profileDigest: observation.profileDigest,
|
|
2119
|
+
gates: gateObservation.gates,
|
|
2037
2120
|
secretsPresent,
|
|
2038
2121
|
process: observation.process,
|
|
2039
2122
|
runtimeObservation: observation,
|
|
@@ -2066,6 +2149,7 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
|
|
|
2066
2149
|
revisionId: manifest.revisionId,
|
|
2067
2150
|
credentialGeneration: manifest.serviceCredentialGeneration,
|
|
2068
2151
|
profileDigest: manifest.profileDigest,
|
|
2152
|
+
gates: gateObservation.gates,
|
|
2069
2153
|
secretsPresent,
|
|
2070
2154
|
process: {
|
|
2071
2155
|
ready: false,
|
|
@@ -502,12 +502,29 @@ function finalizeSuccess(state, operation, status) {
|
|
|
502
502
|
}
|
|
503
503
|
|
|
504
504
|
function observationMatches(observation, operation, lifecycle) {
|
|
505
|
+
const expectedGates =
|
|
506
|
+
lifecycle === "ON"
|
|
507
|
+
? { lifecycle: "ON", dispatch: "ENABLED", route: "OPEN", output: "OPEN" }
|
|
508
|
+
: lifecycle === "OFF"
|
|
509
|
+
? {
|
|
510
|
+
lifecycle: "OFF",
|
|
511
|
+
dispatch: "FENCED",
|
|
512
|
+
route: "OPEN",
|
|
513
|
+
output: "OPEN",
|
|
514
|
+
}
|
|
515
|
+
: {
|
|
516
|
+
lifecycle: "ARCHIVED",
|
|
517
|
+
dispatch: "FENCED",
|
|
518
|
+
route: "FENCED",
|
|
519
|
+
output: "FENCED",
|
|
520
|
+
};
|
|
505
521
|
return Boolean(
|
|
506
522
|
observation &&
|
|
507
523
|
observation.lifecycle === lifecycle &&
|
|
508
524
|
observation.revisionId === operation.targetRevisionId &&
|
|
509
525
|
observation.credentialGeneration === operation.credentialGeneration &&
|
|
510
526
|
observation.profileDigest === operation.profileDigest &&
|
|
527
|
+
stableJson(observation.gates) === stableJson(expectedGates) &&
|
|
511
528
|
observation.secretsPresent === (lifecycle !== "ARCHIVED") &&
|
|
512
529
|
observation.process &&
|
|
513
530
|
observation.process.ready === (lifecycle === "ON") &&
|