@sellable/install 0.1.359-phase111.20260720052618 → 0.1.359
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/bin/sellable-agent-egress-proxy.mjs +31 -0
- package/bin/sellable-agent-host-bootstrap.mjs +26 -0
- package/bin/sellable-agent-host-worker.mjs +9 -0
- package/bin/sellable-agent-runtime-helper.mjs +37 -0
- package/bin/sellable-agent-runtime-launcher.mjs +151 -0
- package/bin/sellable-agent-runtime-probe.mjs +163 -0
- package/bin/sellable-agent-sandbox-init.mjs +93 -0
- package/bin/sellable-install.mjs +2 -0
- package/container/Dockerfile +37 -0
- package/container/README.md +15 -0
- package/container/compose.yaml +22 -0
- package/container/entrypoint.sh +13 -0
- package/lib/sellable-agent/containment-contract.mjs +472 -0
- package/lib/sellable-agent/external-runtime-builder.mjs +272 -0
- package/lib/sellable-agent/hermes-bridge.mjs +497 -0
- package/lib/sellable-agent/host-bootstrap.mjs +458 -0
- package/lib/sellable-agent/host-worker.mjs +2067 -0
- package/lib/sellable-agent/profile-materializer.mjs +1410 -0
- package/lib/sellable-agent/provisioning-adapter.mjs +992 -0
- package/lib/sellable-agent/runtime-boundary.mjs +635 -0
- package/lib/sellable-agent/runtime-egress-proxy.mjs +241 -0
- package/lib/sellable-agent/runtime-helper.mjs +1428 -0
- package/lib/sellable-agent/runtime-reconciler.mjs +683 -0
- package/lib/sellable-agent/service-installer.mjs +441 -0
- package/package.json +11 -2
- package/services/s6/sellable-agent-egress-proxy/run +15 -0
- package/services/s6/sellable-agent-host-worker/run +4 -0
- package/services/s6/sellable-agent-runtime/run +19 -0
- package/skill-templates/refill-sends-evergreen.md +0 -5
- package/skill-templates/refill-sends-v2.md +1 -23
- package/skill-templates/refill-sends.md +1 -14
|
@@ -0,0 +1,2067 @@
|
|
|
1
|
+
import { randomUUID, createHash, createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
statSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
19
|
+
import {
|
|
20
|
+
commitRetainedProfilePromotion,
|
|
21
|
+
computeAgentProfileDigest,
|
|
22
|
+
deriveContainedProfileId,
|
|
23
|
+
finalizeRetainedProfilePromotion,
|
|
24
|
+
materializeAgentProfile,
|
|
25
|
+
recoverInterruptedProfilePromotion,
|
|
26
|
+
} from "./profile-materializer.mjs";
|
|
27
|
+
import {
|
|
28
|
+
applyRuntimeLifecycle,
|
|
29
|
+
projectRuntimeDriftTuple,
|
|
30
|
+
reconcileRuntimeRefresh,
|
|
31
|
+
} from "./runtime-reconciler.mjs";
|
|
32
|
+
import { runBoundedChildProcess } from "./provisioning-adapter.mjs";
|
|
33
|
+
|
|
34
|
+
export const HOST_WORKER_LIFECYCLE = Object.freeze([
|
|
35
|
+
"RECONCILE",
|
|
36
|
+
"CLAIM",
|
|
37
|
+
"HEARTBEAT",
|
|
38
|
+
"CONSUME_SECRET",
|
|
39
|
+
"WRITE_REQUEST",
|
|
40
|
+
"APPLY_ADAPTER",
|
|
41
|
+
"READ_RECEIPT",
|
|
42
|
+
"COMPLETE",
|
|
43
|
+
"CLEANUP",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
export const HOST_WORKER_BROKER_PORTS = Object.freeze([
|
|
47
|
+
"claim",
|
|
48
|
+
"heartbeat",
|
|
49
|
+
"consumeSecret",
|
|
50
|
+
"complete",
|
|
51
|
+
"getOperation",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
export const HOST_WORKER_ADAPTER_PORTS = Object.freeze([
|
|
55
|
+
"requiresSecret",
|
|
56
|
+
"reconcile",
|
|
57
|
+
"apply",
|
|
58
|
+
"readReceipt",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
62
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
63
|
+
const SECRET_PATTERN = /sat_[A-Za-z0-9_-]+|xox[bap]-[A-Za-z0-9-]+|asec\.v1\.|authorization[_-]?code|access[_-]?token|refresh[_-]?token/i;
|
|
64
|
+
const STATE_FILE = "worker-state.json";
|
|
65
|
+
|
|
66
|
+
function stableJson(value) {
|
|
67
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
68
|
+
if (value && typeof value === "object") {
|
|
69
|
+
return `{${Object.keys(value)
|
|
70
|
+
.sort()
|
|
71
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
72
|
+
.join(",")}}`;
|
|
73
|
+
}
|
|
74
|
+
return JSON.stringify(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function canonicalHostWorkerDigest(value) {
|
|
78
|
+
return createHash("sha256").update(stableJson(value)).digest("hex");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isInside(root, target) {
|
|
82
|
+
const path = relative(root, target);
|
|
83
|
+
return path === "" || (!path.startsWith("..") && !isAbsolute(path));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function realPrivateDirectory(pathValue) {
|
|
87
|
+
try {
|
|
88
|
+
if (!isAbsolute(pathValue) || !existsSync(pathValue)) return null;
|
|
89
|
+
const link = lstatSync(pathValue);
|
|
90
|
+
if (link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o077) !== 0) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return realpathSync(pathValue);
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function workerSigningAuthority(config) {
|
|
100
|
+
const configuredPath = resolve(config?.privateKeyPath ?? "");
|
|
101
|
+
const root = realPrivateDirectory(dirname(configuredPath));
|
|
102
|
+
const privateKeyPath = root ? join(root, basename(configuredPath)) : configuredPath;
|
|
103
|
+
if (!root || !existsSync(privateKeyPath)) throw new Error("worker signing key confinement failed");
|
|
104
|
+
const link = lstatSync(privateKeyPath);
|
|
105
|
+
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o077) !== 0) {
|
|
106
|
+
throw new Error("worker signing key rejected");
|
|
107
|
+
}
|
|
108
|
+
const privateKey = createPrivateKey(readFileSync(privateKeyPath, "utf8"));
|
|
109
|
+
if (privateKey.asymmetricKeyType !== "ed25519") throw new Error("worker signing key type rejected");
|
|
110
|
+
const publicKey = createPublicKey(privateKey);
|
|
111
|
+
if (publicKey.asymmetricKeyType !== "ed25519") throw new Error("worker public key type rejected");
|
|
112
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
113
|
+
const publicKeyHash = createHash("sha256").update(publicKeyPem).digest("hex");
|
|
114
|
+
if (config.publicKeyHash !== undefined && (
|
|
115
|
+
!SHA256.test(config.publicKeyHash) || config.publicKeyHash !== publicKeyHash
|
|
116
|
+
)) throw new Error("worker enrolled public key mismatch");
|
|
117
|
+
return { privateKey, publicKey, publicKeyPem, publicKeyHash };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function exactPorts(value, names) {
|
|
121
|
+
return value && names.every((name) => typeof value[name] === "function");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function validateHostWorkerConfig(config) {
|
|
125
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
126
|
+
return { ok: false, code: "invalid_worker_config" };
|
|
127
|
+
}
|
|
128
|
+
for (const key of ["enrollmentId", "hostId", "workerId"]) {
|
|
129
|
+
if (!SAFE_ID.test(config[key] ?? "")) {
|
|
130
|
+
return { ok: false, code: "invalid_worker_identity", field: key };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (
|
|
134
|
+
!Number.isSafeInteger(config.signerGeneration) ||
|
|
135
|
+
config.signerGeneration < 1 ||
|
|
136
|
+
!SHA256.test(config.enrollmentFingerprint ?? "") ||
|
|
137
|
+
!realPrivateDirectory(config.requestRoot) ||
|
|
138
|
+
!realPrivateDirectory(config.stateRoot) ||
|
|
139
|
+
!Number.isSafeInteger(config.leaseSeconds) ||
|
|
140
|
+
config.leaseSeconds < 15 ||
|
|
141
|
+
config.leaseSeconds > 120 ||
|
|
142
|
+
!Number.isSafeInteger(config.poll?.minDelayMs) ||
|
|
143
|
+
!Number.isSafeInteger(config.poll?.maxDelayMs) ||
|
|
144
|
+
typeof config.poll?.jitterRatio !== "number" ||
|
|
145
|
+
config.poll.minDelayMs < 1 ||
|
|
146
|
+
config.poll.maxDelayMs < config.poll.minDelayMs ||
|
|
147
|
+
config.poll.maxDelayMs > 60_000 ||
|
|
148
|
+
config.poll.jitterRatio < 0 ||
|
|
149
|
+
config.poll.jitterRatio > 0.5
|
|
150
|
+
) {
|
|
151
|
+
return { ok: false, code: "invalid_worker_config" };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
workerSigningAuthority(config);
|
|
155
|
+
} catch {
|
|
156
|
+
return { ok: false, code: "invalid_worker_signing_key" };
|
|
157
|
+
}
|
|
158
|
+
return { ok: true, config };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function atomicPrivateJson(pathValue, value) {
|
|
162
|
+
const parent = dirname(pathValue);
|
|
163
|
+
const temp = join(parent, `.${basename(pathValue)}.${process.pid}.${randomUUID()}.tmp`);
|
|
164
|
+
const bytes = `${JSON.stringify(value)}\n`;
|
|
165
|
+
if (SECRET_PATTERN.test(bytes)) throw new Error("secret material rejected from durable state");
|
|
166
|
+
writeFileSync(temp, bytes, { mode: 0o600, flag: "wx" });
|
|
167
|
+
chmodSync(temp, 0o600);
|
|
168
|
+
const descriptor = openSync(temp, "r");
|
|
169
|
+
try {
|
|
170
|
+
fsyncSync(descriptor);
|
|
171
|
+
} finally {
|
|
172
|
+
closeSync(descriptor);
|
|
173
|
+
}
|
|
174
|
+
renameSync(temp, pathValue);
|
|
175
|
+
const parentDescriptor = openSync(parent, "r");
|
|
176
|
+
try {
|
|
177
|
+
fsyncSync(parentDescriptor);
|
|
178
|
+
} finally {
|
|
179
|
+
closeSync(parentDescriptor);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readState(config) {
|
|
184
|
+
const pathValue = join(realpathSync(config.stateRoot), STATE_FILE);
|
|
185
|
+
try {
|
|
186
|
+
const link = lstatSync(pathValue);
|
|
187
|
+
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o077) !== 0) return null;
|
|
188
|
+
const parsed = JSON.parse(readFileSync(pathValue, "utf8"));
|
|
189
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function writeState(config, state) {
|
|
196
|
+
atomicPrivateJson(join(realpathSync(config.stateRoot), STATE_FILE), {
|
|
197
|
+
producer: "sellable-agent-host-worker",
|
|
198
|
+
subject: `worker:${config.workerId}`,
|
|
199
|
+
...state,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function startLeaseHeartbeat({ broker, identity, leaseSeconds, scheduler }) {
|
|
204
|
+
const clock = scheduler ?? {
|
|
205
|
+
schedule(callback, intervalMs) { return setInterval(callback, intervalMs); },
|
|
206
|
+
cancel(handle) { clearInterval(handle); },
|
|
207
|
+
};
|
|
208
|
+
if (typeof clock.schedule !== "function" || typeof clock.cancel !== "function") {
|
|
209
|
+
throw new Error("lease heartbeat scheduler rejected");
|
|
210
|
+
}
|
|
211
|
+
let stopped = false;
|
|
212
|
+
let lost = false;
|
|
213
|
+
let pending = Promise.resolve();
|
|
214
|
+
const tick = () => {
|
|
215
|
+
if (stopped || lost) return pending;
|
|
216
|
+
pending = pending.then(async () => {
|
|
217
|
+
if (stopped || lost) return;
|
|
218
|
+
try {
|
|
219
|
+
const renewed = await broker.heartbeat({ ...identity, leaseSeconds });
|
|
220
|
+
if (!renewed) lost = true;
|
|
221
|
+
} catch {
|
|
222
|
+
lost = true;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return pending;
|
|
226
|
+
};
|
|
227
|
+
const handle = clock.schedule(tick, Math.max(1_000, Math.floor(leaseSeconds * 1_000 / 3)));
|
|
228
|
+
return {
|
|
229
|
+
async current() { await pending; return !lost; },
|
|
230
|
+
async stop() {
|
|
231
|
+
if (!stopped) {
|
|
232
|
+
stopped = true;
|
|
233
|
+
clock.cancel(handle);
|
|
234
|
+
}
|
|
235
|
+
await pending;
|
|
236
|
+
return !lost;
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function removeRequest(pathValue, requestRoot) {
|
|
242
|
+
try {
|
|
243
|
+
const realRoot = realpathSync(requestRoot);
|
|
244
|
+
const resolved = resolve(pathValue);
|
|
245
|
+
if (dirname(resolved) !== realRoot || !isInside(realRoot, resolved)) return false;
|
|
246
|
+
if (!existsSync(resolved)) return true;
|
|
247
|
+
const link = lstatSync(resolved);
|
|
248
|
+
if (link.isSymbolicLink() || !link.isFile()) return false;
|
|
249
|
+
rmSync(resolved, { force: true });
|
|
250
|
+
return true;
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function cleanupRequests(config, preservePath = null) {
|
|
257
|
+
const root = realpathSync(config.requestRoot);
|
|
258
|
+
for (const name of readdirSync(root)) {
|
|
259
|
+
const pathValue = join(root, name);
|
|
260
|
+
if (preservePath && resolve(preservePath) === pathValue) continue;
|
|
261
|
+
removeRequest(pathValue, root);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function operationIdentity(activeClaim, config) {
|
|
266
|
+
return {
|
|
267
|
+
operationId: activeClaim.operation.id,
|
|
268
|
+
workspaceId: activeClaim.operation.workspaceId,
|
|
269
|
+
agentId: activeClaim.operation.agentId,
|
|
270
|
+
hostId: config.hostId,
|
|
271
|
+
workerEnrollmentId: config.enrollmentId,
|
|
272
|
+
workerId: config.workerId,
|
|
273
|
+
fence: activeClaim.operation.fence,
|
|
274
|
+
targetRevisionId: activeClaim.operation.targetRevisionId,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function effectId(activeClaim) {
|
|
279
|
+
return `sahe_${canonicalHostWorkerDigest({
|
|
280
|
+
operationId: activeClaim.operation.id,
|
|
281
|
+
workspaceId: activeClaim.operation.workspaceId,
|
|
282
|
+
agentId: activeClaim.operation.agentId,
|
|
283
|
+
targetRevisionId: activeClaim.operation.targetRevisionId,
|
|
284
|
+
})}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function validateClaim(activeClaim, config, now) {
|
|
288
|
+
const operation = activeClaim?.operation;
|
|
289
|
+
const revision = activeClaim?.revision;
|
|
290
|
+
if (
|
|
291
|
+
!operation ||
|
|
292
|
+
!revision ||
|
|
293
|
+
!SAFE_ID.test(operation.id ?? "") ||
|
|
294
|
+
!SAFE_ID.test(operation.workspaceId ?? "") ||
|
|
295
|
+
!SAFE_ID.test(operation.agentId ?? "") ||
|
|
296
|
+
!SAFE_ID.test(operation.profileId ?? "") ||
|
|
297
|
+
operation.profileId !== deriveContainedProfileId(
|
|
298
|
+
operation.workspaceId,
|
|
299
|
+
operation.agentId,
|
|
300
|
+
config.hostId,
|
|
301
|
+
) ||
|
|
302
|
+
!SAFE_ID.test(operation.targetRevisionId ?? "") ||
|
|
303
|
+
operation.targetRevisionId !== revision.id ||
|
|
304
|
+
!Number.isSafeInteger(operation.lifecycleGeneration) ||
|
|
305
|
+
operation.lifecycleGeneration < 1 ||
|
|
306
|
+
!/^[1-9][0-9]{0,19}$/.test(operation.fence ?? "") ||
|
|
307
|
+
!SHA256.test(revision.manifestHash ?? "") ||
|
|
308
|
+
new Date(operation.deadlineAt).getTime() <= now.getTime()
|
|
309
|
+
) {
|
|
310
|
+
return { ok: false, code: "invalid_claim" };
|
|
311
|
+
}
|
|
312
|
+
if (!exactObject(activeClaim.secretEnvelopes, ["mcpService", "slackBot"])) {
|
|
313
|
+
return { ok: false, code: "invalid_secret_envelopes" };
|
|
314
|
+
}
|
|
315
|
+
for (const [slot, envelope] of Object.entries(activeClaim.secretEnvelopes)) {
|
|
316
|
+
if (!envelope) continue;
|
|
317
|
+
const expectedPurpose = slot === "mcpService" ? "service-credential" : "slack-bot";
|
|
318
|
+
const expectedProvider = slot === "mcpService" ? "sellable" : "slack";
|
|
319
|
+
if (
|
|
320
|
+
!SAFE_ID.test(envelope.id ?? "") || envelope.purpose !== expectedPurpose ||
|
|
321
|
+
envelope.provider !== expectedProvider || envelope.workspaceId !== operation.workspaceId ||
|
|
322
|
+
envelope.agentId !== operation.agentId || envelope.operationId !== operation.id ||
|
|
323
|
+
envelope.hostId !== config.hostId || envelope.workerEnrollmentId !== config.enrollmentId ||
|
|
324
|
+
envelope.workerId !== config.workerId || envelope.fence !== operation.fence ||
|
|
325
|
+
!Number.isSafeInteger(envelope.generation) || envelope.generation < 1 ||
|
|
326
|
+
!/^[a-f0-9]{16,64}$/.test(envelope.fingerprint ?? "") ||
|
|
327
|
+
new Date(envelope.expiresAt).getTime() <= now.getTime()
|
|
328
|
+
) return { ok: false, code: "invalid_secret_envelope", slot };
|
|
329
|
+
}
|
|
330
|
+
const desired = compileClaimToProfileDesired(activeClaim, config);
|
|
331
|
+
if (!desired.ok) return { ok: false, code: "invalid_runtime_contract" };
|
|
332
|
+
return { ok: true };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function requiredSecretSlots(activeClaim) {
|
|
336
|
+
if (["INSTANTIATE", "RESTORE"].includes(activeClaim.operation.action)) return ["mcpService", "slackBot"];
|
|
337
|
+
if (activeClaim.operation.action === "ROTATE_CREDENTIAL") return ["mcpService"];
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function existingRequestForState(state, activeClaim, config, currentEffectId) {
|
|
342
|
+
try {
|
|
343
|
+
if (
|
|
344
|
+
state?.effectId !== currentEffectId ||
|
|
345
|
+
state?.operationId !== activeClaim.operation.id ||
|
|
346
|
+
state?.fence !== activeClaim.operation.fence ||
|
|
347
|
+
typeof state.requestFile !== "string"
|
|
348
|
+
) return null;
|
|
349
|
+
const realRoot = realpathSync(config.requestRoot);
|
|
350
|
+
const pathValue = resolve(state.requestFile);
|
|
351
|
+
if (dirname(pathValue) !== realRoot || !existsSync(pathValue)) return null;
|
|
352
|
+
const link = lstatSync(pathValue);
|
|
353
|
+
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600) return null;
|
|
354
|
+
const parsed = JSON.parse(readFileSync(pathValue, "utf8"));
|
|
355
|
+
if (parsed.operationId !== activeClaim.operation.id || parsed.fence !== activeClaim.operation.fence) return null;
|
|
356
|
+
let present = 0;
|
|
357
|
+
for (const slot of requiredSecretSlots(activeClaim)) {
|
|
358
|
+
const envelope = activeClaim.secretEnvelopes[slot];
|
|
359
|
+
const prefix = slot === "mcpService" ? "mcpService" : "slackBot";
|
|
360
|
+
const secretKey = slot === "mcpService" ? "serviceCredential" : "slackBotCredential";
|
|
361
|
+
if (parsed[secretKey] === undefined) continue;
|
|
362
|
+
present += 1;
|
|
363
|
+
if (
|
|
364
|
+
!envelope || parsed[`${prefix}EnvelopeId`] !== envelope.id ||
|
|
365
|
+
parsed[`${prefix}Generation`] !== envelope.generation ||
|
|
366
|
+
typeof parsed[secretKey] !== "string" || !parsed[secretKey]
|
|
367
|
+
) return null;
|
|
368
|
+
}
|
|
369
|
+
if (present === 0) return null;
|
|
370
|
+
return pathValue;
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function writeRequest(config, activeClaim, currentEffectId, secrets) {
|
|
377
|
+
const requestRoot = realpathSync(config.requestRoot);
|
|
378
|
+
const pathValue = join(requestRoot, `${currentEffectId}.request.json`);
|
|
379
|
+
const temp = join(requestRoot, `.${currentEffectId}.${randomUUID()}.tmp`);
|
|
380
|
+
const payload = {
|
|
381
|
+
producer: "sellable-agent-host-worker",
|
|
382
|
+
operationId: activeClaim.operation.id,
|
|
383
|
+
workspaceId: activeClaim.operation.workspaceId,
|
|
384
|
+
agentId: activeClaim.operation.agentId,
|
|
385
|
+
revisionId: activeClaim.operation.targetRevisionId,
|
|
386
|
+
fence: activeClaim.operation.fence,
|
|
387
|
+
...(secrets.mcpService ? {
|
|
388
|
+
mcpServiceEnvelopeId: activeClaim.secretEnvelopes.mcpService.id,
|
|
389
|
+
mcpServiceGeneration: secrets.mcpService.generation,
|
|
390
|
+
serviceCredential: secrets.mcpService.secret,
|
|
391
|
+
} : {}),
|
|
392
|
+
...(secrets.slackBot ? {
|
|
393
|
+
slackBotEnvelopeId: activeClaim.secretEnvelopes.slackBot.id,
|
|
394
|
+
slackBotGeneration: secrets.slackBot.generation,
|
|
395
|
+
slackBotCredential: secrets.slackBot.secret,
|
|
396
|
+
} : {}),
|
|
397
|
+
};
|
|
398
|
+
writeFileSync(temp, `${JSON.stringify(payload)}\n`, { mode: 0o600, flag: "wx" });
|
|
399
|
+
chmodSync(temp, 0o600);
|
|
400
|
+
const fileDescriptor = openSync(temp, "r");
|
|
401
|
+
try {
|
|
402
|
+
fsyncSync(fileDescriptor);
|
|
403
|
+
} finally {
|
|
404
|
+
closeSync(fileDescriptor);
|
|
405
|
+
}
|
|
406
|
+
renameSync(temp, pathValue);
|
|
407
|
+
const parentDescriptor = openSync(requestRoot, "r");
|
|
408
|
+
try {
|
|
409
|
+
fsyncSync(parentDescriptor);
|
|
410
|
+
} finally {
|
|
411
|
+
closeSync(parentDescriptor);
|
|
412
|
+
}
|
|
413
|
+
return pathValue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function readRequestSecrets(requestFile) {
|
|
417
|
+
if (!requestFile) return {};
|
|
418
|
+
const parsed = JSON.parse(readFileSync(requestFile, "utf8"));
|
|
419
|
+
return {
|
|
420
|
+
...(parsed.serviceCredential ? { mcpService: { generation: parsed.mcpServiceGeneration, secret: parsed.serviceCredential } } : {}),
|
|
421
|
+
...(parsed.slackBotCredential ? { slackBot: { generation: parsed.slackBotGeneration, secret: parsed.slackBotCredential } } : {}),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function claimHasSecrets(activeClaim) {
|
|
426
|
+
return requiredSecretSlots(activeClaim).length > 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function validReceipt(receipt, activeClaim, config) {
|
|
430
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) return false;
|
|
431
|
+
const serialized = JSON.stringify(receipt);
|
|
432
|
+
const identity = operationIdentity(activeClaim, config);
|
|
433
|
+
const receiptKeys = [
|
|
434
|
+
"operationId", "workspaceId", "agentId", "hostId", "workerEnrollmentId",
|
|
435
|
+
"workerId", "fence", "targetRevisionId", "status", "secretDisposition",
|
|
436
|
+
"observedLifecycle", "observedRevisionId", "signerGeneration", "signedDigest",
|
|
437
|
+
"runtimeHashes", "conditions", "errorClass", "auditIds",
|
|
438
|
+
];
|
|
439
|
+
const runtimeHashKeys = ["package", "config", "content", "process"];
|
|
440
|
+
return (
|
|
441
|
+
serialized.length <= 16 * 1024 &&
|
|
442
|
+
!SECRET_PATTERN.test(serialized) &&
|
|
443
|
+
Object.keys(receipt).sort().join("\0") === receiptKeys.sort().join("\0") &&
|
|
444
|
+
receipt.runtimeHashes &&
|
|
445
|
+
Object.keys(receipt.runtimeHashes).sort().join("\0") === runtimeHashKeys.sort().join("\0") &&
|
|
446
|
+
Object.values(receipt.runtimeHashes).every((value) => value === null || SHA256.test(value)) &&
|
|
447
|
+
Array.isArray(receipt.conditions) &&
|
|
448
|
+
receipt.conditions.every(
|
|
449
|
+
(condition) =>
|
|
450
|
+
condition &&
|
|
451
|
+
Object.keys(condition).sort().join("\0") === ["code", "status"].sort().join("\0") &&
|
|
452
|
+
/^[A-Z][A-Z0-9_]{0,63}$/.test(condition.code) &&
|
|
453
|
+
["TRUE", "FALSE", "UNKNOWN"].includes(condition.status)
|
|
454
|
+
) &&
|
|
455
|
+
Array.isArray(receipt.auditIds) &&
|
|
456
|
+
receipt.auditIds.every((value) => SAFE_ID.test(value)) &&
|
|
457
|
+
Object.entries(identity).every(([key, value]) => receipt[key] === value) &&
|
|
458
|
+
["SUCCEEDED", "FAILED", "UNCERTAIN", "BLOCKED"].includes(receipt.status) &&
|
|
459
|
+
["NOT_PRESENT", "NOT_CONSUMED", "INSTALLED", "UNCERTAIN"].includes(receipt.secretDisposition) &&
|
|
460
|
+
SHA256.test(receipt.signedDigest ?? "") &&
|
|
461
|
+
receipt.signerGeneration === config.signerGeneration &&
|
|
462
|
+
(receipt.status === "SUCCEEDED"
|
|
463
|
+
? receipt.secretDisposition === (claimHasSecrets(activeClaim) ? "INSTALLED" : "NOT_PRESENT") &&
|
|
464
|
+
receipt.observedRevisionId === activeClaim.operation.targetRevisionId
|
|
465
|
+
: receipt.secretDisposition !== "INSTALLED")
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function callCheckpoint(checkpoint, name) {
|
|
470
|
+
if (checkpoint) await checkpoint(name);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export async function runHostWorkerCycle(input = {}) {
|
|
474
|
+
const checked = validateHostWorkerConfig(input.config);
|
|
475
|
+
if (!checked.ok) return { ok: false, status: "INVALID_CONFIG", code: checked.code };
|
|
476
|
+
const { config, broker, adapter } = input;
|
|
477
|
+
if (!exactPorts(broker, HOST_WORKER_BROKER_PORTS) || !exactPorts(adapter, HOST_WORKER_ADAPTER_PORTS)) {
|
|
478
|
+
return { ok: false, status: "INVALID_PORT", code: "worker_port_missing" };
|
|
479
|
+
}
|
|
480
|
+
const now = input.now ?? new Date();
|
|
481
|
+
let state = readState(config);
|
|
482
|
+
let leaseHeartbeat = null;
|
|
483
|
+
const recoveryState = state;
|
|
484
|
+
const preservedRequest =
|
|
485
|
+
state?.status === "REQUEST_WRITTEN" || state?.status === "APPLYING"
|
|
486
|
+
? state.requestFile
|
|
487
|
+
: null;
|
|
488
|
+
cleanupRequests(config, preservedRequest);
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
if (state?.status === "COMPLETED" && state.operationId) {
|
|
492
|
+
const remote = await broker.getOperation({ operationId: state.operationId });
|
|
493
|
+
if (remote?.status === "SUCCEEDED" && remote.fence === state.fence) {
|
|
494
|
+
cleanupRequests(config);
|
|
495
|
+
return { ok: true, status: "ALREADY_COMPLETED", operationId: state.operationId, fence: state.fence };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let activeClaim;
|
|
500
|
+
try {
|
|
501
|
+
activeClaim = await broker.claim({ leaseSeconds: config.leaseSeconds });
|
|
502
|
+
} catch {
|
|
503
|
+
const observationId = `obs_${canonicalHostWorkerDigest({
|
|
504
|
+
class: "BROKER_UNAVAILABLE",
|
|
505
|
+
workerId: config.workerId,
|
|
506
|
+
previousObservationId: state?.observationId ?? null,
|
|
507
|
+
})}`;
|
|
508
|
+
const strategyFingerprint = canonicalHostWorkerDigest({
|
|
509
|
+
hypothesis: "broker unavailable; bounded backoff then re-observe claim surface",
|
|
510
|
+
primitive: "wait_then_claim",
|
|
511
|
+
target: config.hostId,
|
|
512
|
+
observationId,
|
|
513
|
+
});
|
|
514
|
+
writeState(config, { status: "BROKER_UNAVAILABLE", observationId, strategyFingerprint });
|
|
515
|
+
return { ok: false, status: "BROKER_UNAVAILABLE", observationId, strategyFingerprint };
|
|
516
|
+
}
|
|
517
|
+
if (!activeClaim) {
|
|
518
|
+
cleanupRequests(config);
|
|
519
|
+
return { ok: true, status: "IDLE" };
|
|
520
|
+
}
|
|
521
|
+
const valid = validateClaim(activeClaim, config, now);
|
|
522
|
+
if (!valid.ok) {
|
|
523
|
+
cleanupRequests(config);
|
|
524
|
+
return { ok: false, status: "CLAIM_REJECTED", code: valid.code };
|
|
525
|
+
}
|
|
526
|
+
const currentEffectId = effectId(activeClaim);
|
|
527
|
+
const previousObservationId = state?.observationId ?? null;
|
|
528
|
+
const strategyFingerprint = canonicalHostWorkerDigest({
|
|
529
|
+
hypothesis: "claimed operation requires exact-effect reconciliation before mutation",
|
|
530
|
+
primitive: "reconcile_then_apply",
|
|
531
|
+
target: currentEffectId,
|
|
532
|
+
observationId: previousObservationId,
|
|
533
|
+
});
|
|
534
|
+
state = {
|
|
535
|
+
status: "CLAIMED",
|
|
536
|
+
operationId: activeClaim.operation.id,
|
|
537
|
+
effectId: currentEffectId,
|
|
538
|
+
fence: activeClaim.operation.fence,
|
|
539
|
+
strategyFingerprint,
|
|
540
|
+
observationId: previousObservationId,
|
|
541
|
+
};
|
|
542
|
+
writeState(config, state);
|
|
543
|
+
await callCheckpoint(input.checkpoint, "after-claim");
|
|
544
|
+
|
|
545
|
+
const identity = operationIdentity(activeClaim, config);
|
|
546
|
+
const renewed = await broker.heartbeat({ ...identity, leaseSeconds: config.leaseSeconds });
|
|
547
|
+
if (!renewed) {
|
|
548
|
+
cleanupRequests(config);
|
|
549
|
+
writeState(config, { ...state, status: "LEASE_LOST", requestFile: undefined });
|
|
550
|
+
return { ok: false, status: "LEASE_LOST", operationId: identity.operationId, fence: identity.fence };
|
|
551
|
+
}
|
|
552
|
+
leaseHeartbeat = startLeaseHeartbeat({
|
|
553
|
+
broker, identity, leaseSeconds: config.leaseSeconds, scheduler: input.heartbeatScheduler,
|
|
554
|
+
});
|
|
555
|
+
const requireCurrentLease = async () => {
|
|
556
|
+
if (await leaseHeartbeat.current()) return;
|
|
557
|
+
const error = new Error("periodic lease heartbeat lost");
|
|
558
|
+
error.code = "LEASE_UNCERTAIN";
|
|
559
|
+
throw error;
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
const reconciled = await adapter.reconcile({ activeClaim, effectId: currentEffectId });
|
|
563
|
+
await requireCurrentLease();
|
|
564
|
+
const secretSlots = await adapter.requiresSecret({ activeClaim, effectId: currentEffectId, reconciled });
|
|
565
|
+
await requireCurrentLease();
|
|
566
|
+
if (
|
|
567
|
+
!Array.isArray(secretSlots) ||
|
|
568
|
+
JSON.stringify(secretSlots) !== JSON.stringify(requiredSecretSlots(activeClaim))
|
|
569
|
+
) throw new Error("adapter secret predicate invalid");
|
|
570
|
+
let receipt = await adapter.readReceipt({ activeClaim, effectId: currentEffectId });
|
|
571
|
+
let requestFile = existingRequestForState(recoveryState, activeClaim, config, currentEffectId);
|
|
572
|
+
if (!requestFile && preservedRequest) removeRequest(preservedRequest, config.requestRoot);
|
|
573
|
+
|
|
574
|
+
if (!validReceipt(receipt, activeClaim, config)) {
|
|
575
|
+
receipt = null;
|
|
576
|
+
if (secretSlots.length > 0) {
|
|
577
|
+
const missingSlot = secretSlots.find((slot) => !activeClaim.secretEnvelopes[slot]);
|
|
578
|
+
if (missingSlot) {
|
|
579
|
+
cleanupRequests(config);
|
|
580
|
+
return { ok: false, status: "SECRET_REQUIRED", code: `${missingSlot}_envelope_missing` };
|
|
581
|
+
}
|
|
582
|
+
const secrets = readRequestSecrets(requestFile);
|
|
583
|
+
for (const slot of secretSlots) {
|
|
584
|
+
if (secrets[slot]) continue;
|
|
585
|
+
const envelope = activeClaim.secretEnvelopes[slot];
|
|
586
|
+
if (!envelope) {
|
|
587
|
+
cleanupRequests(config);
|
|
588
|
+
return { ok: false, status: "SECRET_REQUIRED", code: `${slot}_envelope_missing` };
|
|
589
|
+
}
|
|
590
|
+
const secretResult = await broker.consumeSecret({
|
|
591
|
+
...identity,
|
|
592
|
+
envelopeId: envelope.id,
|
|
593
|
+
claimId: canonicalHostWorkerDigest({ currentEffectId, fence: identity.fence, slot }).slice(0, 32),
|
|
594
|
+
});
|
|
595
|
+
await requireCurrentLease();
|
|
596
|
+
const secretFingerprint = typeof secretResult?.secret === "string"
|
|
597
|
+
? createHash("sha256").update(secretResult.secret).digest("hex").slice(0, 16)
|
|
598
|
+
: null;
|
|
599
|
+
if (
|
|
600
|
+
!secretResult?.ok || typeof secretResult.secret !== "string" ||
|
|
601
|
+
secretResult.generation !== envelope.generation || secretFingerprint !== envelope.fingerprint
|
|
602
|
+
) {
|
|
603
|
+
cleanupRequests(config);
|
|
604
|
+
writeState(config, { ...state, status: "SECRET_UNCERTAIN", requestFile: undefined });
|
|
605
|
+
return { ok: false, status: "SECRET_UNCERTAIN", code: secretResult?.code ?? "secret_consume_failed" };
|
|
606
|
+
}
|
|
607
|
+
secrets[slot] = secretResult;
|
|
608
|
+
requestFile = writeRequest(config, activeClaim, currentEffectId, secrets);
|
|
609
|
+
writeState(config, { ...state, status: "REQUEST_WRITTEN", requestFile });
|
|
610
|
+
await callCheckpoint(input.checkpoint, `after-unwrap-${slot}`);
|
|
611
|
+
}
|
|
612
|
+
await callCheckpoint(input.checkpoint, "after-unwrap");
|
|
613
|
+
}
|
|
614
|
+
if (secretSlots.length === 0 && requestFile) {
|
|
615
|
+
removeRequest(requestFile, config.requestRoot);
|
|
616
|
+
requestFile = null;
|
|
617
|
+
}
|
|
618
|
+
await callCheckpoint(input.checkpoint, "before-adapter");
|
|
619
|
+
writeState(config, { ...state, status: "APPLYING", requestFile, reconciledStatus: reconciled?.status ?? "UNKNOWN" });
|
|
620
|
+
await adapter.apply({
|
|
621
|
+
activeClaim,
|
|
622
|
+
requestFile,
|
|
623
|
+
effectId: currentEffectId,
|
|
624
|
+
reconciled,
|
|
625
|
+
checkpoint: (name) => callCheckpoint(input.checkpoint, name),
|
|
626
|
+
});
|
|
627
|
+
await requireCurrentLease();
|
|
628
|
+
receipt = await adapter.readReceipt({ activeClaim, effectId: currentEffectId });
|
|
629
|
+
await requireCurrentLease();
|
|
630
|
+
}
|
|
631
|
+
if (!validReceipt(receipt, activeClaim, config)) {
|
|
632
|
+
cleanupRequests(config);
|
|
633
|
+
writeState(config, { ...state, status: "RECEIPT_INVALID", requestFile: undefined });
|
|
634
|
+
return { ok: false, status: "RECEIPT_INVALID" };
|
|
635
|
+
}
|
|
636
|
+
writeState(config, { ...state, status: "RECEIPT_DURABLE", requestFile });
|
|
637
|
+
await callCheckpoint(input.checkpoint, "after-receipt");
|
|
638
|
+
if (!(await leaseHeartbeat.stop())) {
|
|
639
|
+
const error = new Error("periodic lease heartbeat lost");
|
|
640
|
+
error.code = "LEASE_UNCERTAIN";
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
643
|
+
leaseHeartbeat = null;
|
|
644
|
+
const current = await broker.heartbeat({ ...identity, leaseSeconds: config.leaseSeconds });
|
|
645
|
+
if (!current) {
|
|
646
|
+
cleanupRequests(config);
|
|
647
|
+
writeState(config, { ...state, status: "LEASE_LOST", requestFile: undefined });
|
|
648
|
+
return { ok: false, status: "LEASE_LOST", operationId: identity.operationId, fence: identity.fence };
|
|
649
|
+
}
|
|
650
|
+
const completion = await broker.complete(receipt);
|
|
651
|
+
if (!completion?.accepted) {
|
|
652
|
+
cleanupRequests(config);
|
|
653
|
+
writeState(config, { ...state, status: "LEASE_LOST", requestFile: undefined });
|
|
654
|
+
return { ok: false, status: "LEASE_LOST", code: completion?.code ?? "completion_rejected" };
|
|
655
|
+
}
|
|
656
|
+
cleanupRequests(config);
|
|
657
|
+
writeState(config, { ...state, status: "COMPLETED", requestFile: undefined });
|
|
658
|
+
return { ok: true, status: "COMPLETED", operationId: identity.operationId, fence: identity.fence };
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (error?.code === "LEASE_UNCERTAIN") {
|
|
661
|
+
cleanupRequests(config);
|
|
662
|
+
writeState(config, { ...state, status: "LEASE_UNCERTAIN", requestFile: undefined });
|
|
663
|
+
return {
|
|
664
|
+
ok: false,
|
|
665
|
+
status: "LEASE_UNCERTAIN",
|
|
666
|
+
code: "periodic_lease_lost",
|
|
667
|
+
operationId: state?.operationId ?? null,
|
|
668
|
+
fence: state?.fence ?? null,
|
|
669
|
+
resumePredicate: "reconcile_exact_effect_before_retry",
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
if (error?.code === "SIMULATED_CRASH") {
|
|
673
|
+
const current = readState(config) ?? state ?? {};
|
|
674
|
+
writeState(config, { ...current, status: current.status ?? "CRASHED", crashSeam: String(error.message).replace(/^.* at /, "") });
|
|
675
|
+
return { ok: false, status: "CRASHED", code: "simulated_crash" };
|
|
676
|
+
}
|
|
677
|
+
cleanupRequests(config);
|
|
678
|
+
const observationId = `obs_${canonicalHostWorkerDigest({
|
|
679
|
+
class: "HOST_RUNTIME",
|
|
680
|
+
operationId: state?.operationId ?? null,
|
|
681
|
+
effectId: state?.effectId ?? null,
|
|
682
|
+
})}`;
|
|
683
|
+
writeState(config, {
|
|
684
|
+
...(state ?? {}),
|
|
685
|
+
status: "FAILED",
|
|
686
|
+
errorClass: "HOST_RUNTIME",
|
|
687
|
+
observationId,
|
|
688
|
+
requestFile: undefined,
|
|
689
|
+
});
|
|
690
|
+
return { ok: false, status: "FAILED", code: "host_runtime_failed", observationId };
|
|
691
|
+
} finally {
|
|
692
|
+
if (leaseHeartbeat) await leaseHeartbeat.stop();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function safeObservedFile(pathValue, { executable = false, ownerUid } = {}) {
|
|
697
|
+
const link = lstatSync(pathValue);
|
|
698
|
+
if (link.isSymbolicLink() || !link.isFile()) throw new Error("unsafe observed file");
|
|
699
|
+
const mode = link.mode & 0o777;
|
|
700
|
+
if (
|
|
701
|
+
!Number.isSafeInteger(ownerUid) || ownerUid < 0 || link.uid !== ownerUid ||
|
|
702
|
+
(mode & 0o022) !== 0 || (executable && (mode & 0o111) === 0)
|
|
703
|
+
) {
|
|
704
|
+
throw new Error("unsafe observed mode");
|
|
705
|
+
}
|
|
706
|
+
return { bytes: readFileSync(pathValue), stat: link };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function observeInstalledWorkerService(input) {
|
|
710
|
+
try {
|
|
711
|
+
const trustedRootUid = input.disposableFixture === true ? input.expectedRootUid : 0;
|
|
712
|
+
if (
|
|
713
|
+
!Number.isSafeInteger(trustedRootUid) || trustedRootUid < 0 ||
|
|
714
|
+
!Number.isSafeInteger(input.expectedWorkerUid) || input.expectedWorkerUid < 1 ||
|
|
715
|
+
typeof input.expectedWorkerUser !== "string" || !SAFE_ID.test(input.expectedWorkerUser) ||
|
|
716
|
+
typeof input.resolveUser !== "function" ||
|
|
717
|
+
input.resolveUser(input.expectedWorkerUid) !== input.expectedWorkerUser
|
|
718
|
+
) return { ok: false, code: "service_identity_invalid" };
|
|
719
|
+
const executable = safeObservedFile(input.executablePath, { executable: true, ownerUid: trustedRootUid });
|
|
720
|
+
const runScript = safeObservedFile(input.runScriptPath, { executable: true, ownerUid: trustedRootUid });
|
|
721
|
+
const packageFile = safeObservedFile(input.packagePath, { ownerUid: trustedRootUid });
|
|
722
|
+
const entrypoint = safeObservedFile(input.entrypointPath, { ownerUid: trustedRootUid });
|
|
723
|
+
const nodeExecutable = safeObservedFile(input.nodeExecutablePath, { executable: true, ownerUid: trustedRootUid });
|
|
724
|
+
const enrollment = safeObservedFile(input.enrollmentPath, { ownerUid: trustedRootUid });
|
|
725
|
+
const pidFile = safeObservedFile(input.pidPath, { ownerUid: trustedRootUid });
|
|
726
|
+
const enrollmentFingerprint = enrollment.bytes.toString("utf8").trim();
|
|
727
|
+
const pidText = pidFile.bytes.toString("utf8").trim();
|
|
728
|
+
if (
|
|
729
|
+
!SHA256.test(enrollmentFingerprint) || !/^[1-9][0-9]{0,9}$/.test(pidText) ||
|
|
730
|
+
!isAbsolute(input.configPath)
|
|
731
|
+
) {
|
|
732
|
+
return { ok: false, code: "service_identity_invalid" };
|
|
733
|
+
}
|
|
734
|
+
const pid = Number(pidText);
|
|
735
|
+
const statText = readFileSync(join(input.procRoot, pidText, "stat"), "utf8").trim();
|
|
736
|
+
const statusText = readFileSync(join(input.procRoot, pidText, "status"), "utf8");
|
|
737
|
+
const uidMatch = statusText.match(/^Uid:\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s*$/m);
|
|
738
|
+
const processUids = uidMatch ? uidMatch.slice(1).map(Number) : [];
|
|
739
|
+
const processUid = processUids[0] ?? null;
|
|
740
|
+
const observedExecutable = realpathSync(join(input.procRoot, pidText, "exe"));
|
|
741
|
+
const expectedNodeExecutable = realpathSync(input.nodeExecutablePath);
|
|
742
|
+
const cmdline = readFileSync(join(input.procRoot, pidText, "cmdline"))
|
|
743
|
+
.toString("utf8").split("\0").filter(Boolean);
|
|
744
|
+
const expectedCommand = [
|
|
745
|
+
input.nodeExecutablePath, input.entrypointPath, "--config", input.configPath,
|
|
746
|
+
];
|
|
747
|
+
const closeParen = statText.lastIndexOf(") ");
|
|
748
|
+
if (closeParen < 0 || !statText.startsWith(`${pid} (`)) {
|
|
749
|
+
return { ok: false, code: "process_identity_invalid" };
|
|
750
|
+
}
|
|
751
|
+
const processFields = statText.slice(closeParen + 2).split(/\s+/);
|
|
752
|
+
const processStartTicks = processFields[19];
|
|
753
|
+
const bootId = readFileSync(input.bootIdPath, "utf8").trim();
|
|
754
|
+
if (
|
|
755
|
+
!/^[0-9]+$/.test(processStartTicks ?? "") ||
|
|
756
|
+
!bootId ||
|
|
757
|
+
bootId.length > 128 ||
|
|
758
|
+
processUids.length !== 4 || processUids.some((uid) => uid !== input.expectedWorkerUid) ||
|
|
759
|
+
observedExecutable !== expectedNodeExecutable ||
|
|
760
|
+
JSON.stringify(cmdline) !== JSON.stringify(expectedCommand)
|
|
761
|
+
) {
|
|
762
|
+
return { ok: false, code: "process_identity_invalid" };
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
ok: true,
|
|
766
|
+
health: "READY",
|
|
767
|
+
serviceUser: input.expectedWorkerUser,
|
|
768
|
+
enrollmentFingerprint,
|
|
769
|
+
pid,
|
|
770
|
+
processUid,
|
|
771
|
+
processExecutable: observedExecutable,
|
|
772
|
+
processStartTicks,
|
|
773
|
+
bootId,
|
|
774
|
+
digests: {
|
|
775
|
+
executable: createHash("sha256").update(executable.bytes).digest("hex"),
|
|
776
|
+
entrypoint: createHash("sha256").update(entrypoint.bytes).digest("hex"),
|
|
777
|
+
nodeExecutable: createHash("sha256").update(nodeExecutable.bytes).digest("hex"),
|
|
778
|
+
runScript: createHash("sha256").update(runScript.bytes).digest("hex"),
|
|
779
|
+
package: createHash("sha256").update(packageFile.bytes).digest("hex"),
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
} catch {
|
|
783
|
+
return { ok: false, code: "service_observation_failed" };
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function parseMainArguments(argv) {
|
|
788
|
+
if (argv.length !== 2 || argv[0] !== "--config" || !isAbsolute(argv[1])) return null;
|
|
789
|
+
return argv[1];
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function createWorkerPromotionJournal(config) {
|
|
793
|
+
const { privateKey, publicKey } = workerSigningAuthority(config);
|
|
794
|
+
return {
|
|
795
|
+
signerGeneration: config.signerGeneration,
|
|
796
|
+
sign(body) {
|
|
797
|
+
return sign(null, Buffer.from(stableJson(body)), privateKey).toString("base64url");
|
|
798
|
+
},
|
|
799
|
+
verify(body, signature) {
|
|
800
|
+
try {
|
|
801
|
+
return verify(null, Buffer.from(stableJson(body)), publicKey, Buffer.from(signature, "base64url"));
|
|
802
|
+
} catch {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
export function buildSignedWorkerAuthorization({ config, verb, body, now = new Date() }) {
|
|
810
|
+
const { privateKey, publicKeyPem } = workerSigningAuthority(config);
|
|
811
|
+
const claims = {
|
|
812
|
+
enrollmentId: config.enrollmentId,
|
|
813
|
+
hostId: config.hostId,
|
|
814
|
+
workerId: config.workerId,
|
|
815
|
+
audience: "sellable-agent-worker/v1",
|
|
816
|
+
keyGeneration: config.signerGeneration,
|
|
817
|
+
verb,
|
|
818
|
+
operationId: body.operationId ?? null,
|
|
819
|
+
workspaceId: body.workspaceId ?? null,
|
|
820
|
+
agentId: body.agentId ?? null,
|
|
821
|
+
fence: body.fence ?? null,
|
|
822
|
+
targetRevisionId: body.targetRevisionId ?? null,
|
|
823
|
+
bodyDigest: canonicalHostWorkerDigest(body),
|
|
824
|
+
nonce: randomUUID(),
|
|
825
|
+
issuedAt: now.toISOString(),
|
|
826
|
+
expiresAt: new Date(now.getTime() + 60_000).toISOString(),
|
|
827
|
+
};
|
|
828
|
+
const signature = sign(null, Buffer.from(stableJson(claims)), privateKey).toString("base64url");
|
|
829
|
+
return `Worker ${Buffer.from(JSON.stringify({ claims, publicKeyPem, signature })).toString("base64url")}`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function createHttpWorkerBroker(config, fetchImpl = fetch) {
|
|
833
|
+
workerSigningAuthority(config);
|
|
834
|
+
const endpoint = String(config.controlPlaneUrl ?? "").replace(/\/$/, "");
|
|
835
|
+
if (!/^https:\/\//.test(endpoint)) throw new Error("https control plane required");
|
|
836
|
+
const post = async (verb, path, body) => {
|
|
837
|
+
const authorization = buildSignedWorkerAuthorization({ config, verb, body });
|
|
838
|
+
const response = await fetchImpl(`${endpoint}${path}`, {
|
|
839
|
+
method: "POST",
|
|
840
|
+
headers: { authorization, "content-type": "application/json" },
|
|
841
|
+
body: JSON.stringify(body),
|
|
842
|
+
signal: AbortSignal.timeout(15_000),
|
|
843
|
+
});
|
|
844
|
+
const value = await response.json();
|
|
845
|
+
if (!response.ok) return { ok: false, code: value.error ?? "broker_rejected" };
|
|
846
|
+
return value;
|
|
847
|
+
};
|
|
848
|
+
return {
|
|
849
|
+
async claim(body) { return (await post("claim", "/api/v3/sellable-agent/worker/claim", body)).claim ?? null; },
|
|
850
|
+
async heartbeat(body) { return (await post("heartbeat", "/api/v3/sellable-agent/worker/heartbeat", body)).renewed === true; },
|
|
851
|
+
async consumeSecret(body) {
|
|
852
|
+
const value = await post("secret-consume", "/api/v3/sellable-agent/worker/secret-consume", body);
|
|
853
|
+
return value.ok === false ? value : { ok: true, ...value };
|
|
854
|
+
},
|
|
855
|
+
async complete(body) { return post("complete", "/api/v3/sellable-agent/worker/complete", body); },
|
|
856
|
+
async getOperation() { return null; },
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function confinedReceiptPath(config, effectId) {
|
|
861
|
+
const stateRoot = realpathSync(config.stateRoot);
|
|
862
|
+
const receiptRoot = join(stateRoot, "receipts");
|
|
863
|
+
if (!existsSync(receiptRoot)) mkdirSync(receiptRoot, { recursive: true, mode: 0o700 });
|
|
864
|
+
const realReceiptRoot = realpathSync(receiptRoot);
|
|
865
|
+
const pathValue = join(realReceiptRoot, `${effectId}.json`);
|
|
866
|
+
if (dirname(pathValue) !== realReceiptRoot) throw new Error("receipt confinement failed");
|
|
867
|
+
return pathValue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function exactObject(value, keys) {
|
|
871
|
+
return Boolean(
|
|
872
|
+
value && typeof value === "object" && !Array.isArray(value) &&
|
|
873
|
+
Object.keys(value).sort().join("\0") === [...keys].sort().join("\0")
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function safeArtifactContent(artifacts, type) {
|
|
878
|
+
const selected = artifacts
|
|
879
|
+
.filter((artifact) => artifact.type === type)
|
|
880
|
+
.sort((left, right) => left.logicalPath.localeCompare(right.logicalPath));
|
|
881
|
+
if (selected.length === 0 || selected.some((artifact) => typeof artifact.safeContent !== "string")) {
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
for (const artifact of selected) {
|
|
885
|
+
if (
|
|
886
|
+
Buffer.byteLength(artifact.safeContent) !== artifact.sizeBytes ||
|
|
887
|
+
createHash("sha256").update(artifact.safeContent).digest("hex") !== artifact.contentHash ||
|
|
888
|
+
SECRET_PATTERN.test(artifact.safeContent)
|
|
889
|
+
) return null;
|
|
890
|
+
}
|
|
891
|
+
return selected.map((artifact) => artifact.safeContent).join("\n");
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Compile only the real Plan 02 AgentWorkerClaim DTO plus installer-owned pins.
|
|
896
|
+
* Plan 04 must populate the installation-scoped Slack identity contract; absence
|
|
897
|
+
* is an explicit fail-closed dependency, never an invented runtime identity.
|
|
898
|
+
*/
|
|
899
|
+
export function compileClaimToProfileDesired(activeClaim, config) {
|
|
900
|
+
try {
|
|
901
|
+
const operation = activeClaim?.operation;
|
|
902
|
+
const revision = activeClaim?.revision;
|
|
903
|
+
const manifest = revision?.manifest;
|
|
904
|
+
if (
|
|
905
|
+
!exactObject(manifest, ["version", "compilerVersion", "agent", "slack", "service", "artifacts"]) ||
|
|
906
|
+
manifest.version !== 1 || manifest.compilerVersion !== "sellable-agent/v1" ||
|
|
907
|
+
revision.compilerVersion !== manifest.compilerVersion || revision.id !== operation.targetRevisionId ||
|
|
908
|
+
!SAFE_ID.test(operation.profileId ?? "") ||
|
|
909
|
+
operation.profileId !== deriveContainedProfileId(
|
|
910
|
+
operation.workspaceId,
|
|
911
|
+
operation.agentId,
|
|
912
|
+
config.hostId,
|
|
913
|
+
) ||
|
|
914
|
+
!exactObject(manifest.agent, [
|
|
915
|
+
"workspaceId", "agentId", "slug", "name", "role", "desiredLifecycle",
|
|
916
|
+
"setupGeneration", "configGeneration",
|
|
917
|
+
]) ||
|
|
918
|
+
manifest.agent.workspaceId !== operation.workspaceId ||
|
|
919
|
+
manifest.agent.agentId !== operation.agentId ||
|
|
920
|
+
manifest.agent.setupGeneration !== operation.setupGeneration ||
|
|
921
|
+
manifest.agent.configGeneration !== operation.configGeneration ||
|
|
922
|
+
!Number.isSafeInteger(operation.lifecycleGeneration) || operation.lifecycleGeneration < 1 ||
|
|
923
|
+
!exactObject(manifest.slack, ["installationId", "selectedBindingIds", "defaultBindingId"]) ||
|
|
924
|
+
!Array.isArray(manifest.slack.selectedBindingIds) ||
|
|
925
|
+
!manifest.slack.selectedBindingIds.includes(manifest.slack.defaultBindingId) ||
|
|
926
|
+
!exactObject(manifest.service, ["principalId", "policyHash", "issuerReady"]) ||
|
|
927
|
+
manifest.service.issuerReady !== true || !SHA256.test(manifest.service.policyHash ?? "") ||
|
|
928
|
+
!Array.isArray(manifest.artifacts) || !Array.isArray(revision.artifacts)
|
|
929
|
+
) return { ok: false, code: "claim_runtime_manifest_invalid" };
|
|
930
|
+
|
|
931
|
+
const projectedArtifacts = revision.artifacts.map(({ safeContent: _safeContent, ...artifact }) => artifact);
|
|
932
|
+
if (stableJson(projectedArtifacts) !== stableJson(manifest.artifacts)) {
|
|
933
|
+
return { ok: false, code: "claim_artifact_identity_invalid" };
|
|
934
|
+
}
|
|
935
|
+
const soul = safeArtifactContent(revision.artifacts, "SOUL");
|
|
936
|
+
const skill = safeArtifactContent(revision.artifacts, "SKILL");
|
|
937
|
+
if (soul === null || skill === null) return { ok: false, code: "claim_safe_content_missing" };
|
|
938
|
+
|
|
939
|
+
const pinned = config.profileRuntimeContract;
|
|
940
|
+
const runtime = activeClaim.runtimeContract;
|
|
941
|
+
const slack = runtime?.slack;
|
|
942
|
+
const runtimeService = runtime?.service;
|
|
943
|
+
const policy = pinned?.policies?.[manifest.service.policyHash];
|
|
944
|
+
const credentialIdentity = activeClaim.secretEnvelopes?.mcpService ?? {
|
|
945
|
+
generation: runtimeService?.credentialGeneration,
|
|
946
|
+
fingerprint: runtimeService?.credentialFingerprint,
|
|
947
|
+
};
|
|
948
|
+
const selectedBindingIds = slack?.selectedBindings?.map((binding) => binding.id);
|
|
949
|
+
const channelIds = slack?.selectedBindings?.map((binding) => binding.channelId).sort();
|
|
950
|
+
const defaultBinding = slack?.selectedBindings?.find((binding) => binding.id === slack.defaultBindingId);
|
|
951
|
+
if (
|
|
952
|
+
!pinned || !runtime || !slack || !runtimeService || !policy ||
|
|
953
|
+
!exactObject(runtime, ["slack", "service"]) ||
|
|
954
|
+
!exactObject(slack, [
|
|
955
|
+
"installationId", "generation", "appId", "teamId", "botUserId", "scopes",
|
|
956
|
+
"credentialFingerprint", "selectedBindings", "defaultBindingId", "memberAllowlist", "hashes",
|
|
957
|
+
]) ||
|
|
958
|
+
!exactObject(runtimeService, ["principalId", "policyHash", "credentialGeneration", "credentialFingerprint"]) ||
|
|
959
|
+
pinned.hermesCli !== "hermes" || pinned.hermesVersion !== "0.18.0" ||
|
|
960
|
+
pinned.installerPackage !== "@sellable/install@0.1.359" ||
|
|
961
|
+
pinned.mcpPackage !== "@sellable/mcp@0.1.620" ||
|
|
962
|
+
!Array.isArray(policy.toolInclude) || policy.toolInclude.length === 0 ||
|
|
963
|
+
!Number.isSafeInteger(slack.generation) || slack.generation < 1 ||
|
|
964
|
+
!/^A[A-Z0-9]{8,20}$/.test(slack.appId ?? "") ||
|
|
965
|
+
!/^T[A-Z0-9]{8,20}$/.test(slack.teamId ?? "") ||
|
|
966
|
+
!/^[UW][A-Z0-9]{8,20}$/.test(slack.botUserId ?? "") ||
|
|
967
|
+
!Array.isArray(slack.scopes) || slack.scopes.length === 0 ||
|
|
968
|
+
stableJson(slack.scopes) !== stableJson([...new Set(slack.scopes)].sort()) ||
|
|
969
|
+
!Array.isArray(slack.selectedBindings) ||
|
|
970
|
+
slack.selectedBindings.some((binding) => !exactObject(binding, ["id", "channelId", "channelName", "isDefaultVerification"])) ||
|
|
971
|
+
stableJson(selectedBindingIds) !== stableJson([...manifest.slack.selectedBindingIds].sort()) ||
|
|
972
|
+
slack.installationId !== manifest.slack.installationId ||
|
|
973
|
+
slack.defaultBindingId !== manifest.slack.defaultBindingId ||
|
|
974
|
+
!Array.isArray(channelIds) || channelIds.length === 0 || !defaultBinding ||
|
|
975
|
+
!Array.isArray(slack.memberAllowlist) || slack.memberAllowlist.length === 0 ||
|
|
976
|
+
runtimeService.principalId !== manifest.service.principalId ||
|
|
977
|
+
runtimeService.policyHash !== manifest.service.policyHash ||
|
|
978
|
+
!Number.isSafeInteger(runtimeService.credentialGeneration) || runtimeService.credentialGeneration < 1 ||
|
|
979
|
+
!/^[a-f0-9]{16}$/.test(runtimeService.credentialFingerprint ?? "") ||
|
|
980
|
+
!Number.isSafeInteger(credentialIdentity?.generation) || credentialIdentity.generation < 1 ||
|
|
981
|
+
!/^[a-f0-9]{16}$/.test(credentialIdentity?.fingerprint ?? "") ||
|
|
982
|
+
credentialIdentity.generation !== runtimeService.credentialGeneration ||
|
|
983
|
+
credentialIdentity.fingerprint !== runtimeService.credentialFingerprint ||
|
|
984
|
+
(activeClaim.secretEnvelopes?.slackBot && (
|
|
985
|
+
activeClaim.secretEnvelopes.slackBot.installationId !== slack.installationId ||
|
|
986
|
+
activeClaim.secretEnvelopes.slackBot.generation !== slack.generation ||
|
|
987
|
+
activeClaim.secretEnvelopes.slackBot.fingerprint !== slack.credentialFingerprint
|
|
988
|
+
))
|
|
989
|
+
) return { ok: false, code: "plan04_runtime_contract_missing" };
|
|
990
|
+
const slackHashes = slack.hashes;
|
|
991
|
+
if (
|
|
992
|
+
!exactObject(slackHashes, ["installation", "app", "team", "bot", "channels", "defaultChannel", "memberAllowlist"]) ||
|
|
993
|
+
Object.values(slackHashes).some((value) => !SHA256.test(value)) ||
|
|
994
|
+
!SHA256.test(pinned.providerFingerprint ?? "")
|
|
995
|
+
) {
|
|
996
|
+
return { ok: false, code: "pinned_runtime_identity_invalid" };
|
|
997
|
+
}
|
|
998
|
+
if (
|
|
999
|
+
createHash("sha256").update(stableJson(channelIds)).digest("hex") !== slackHashes.channels ||
|
|
1000
|
+
createHash("sha256").update(defaultBinding.channelId).digest("hex") !== slackHashes.defaultChannel ||
|
|
1001
|
+
createHash("sha256").update(stableJson(slack.memberAllowlist)).digest("hex") !== slackHashes.memberAllowlist ||
|
|
1002
|
+
createHash("sha256").update(slack.appId).digest("hex") !== slackHashes.app ||
|
|
1003
|
+
createHash("sha256").update(slack.teamId).digest("hex") !== slackHashes.team ||
|
|
1004
|
+
createHash("sha256").update(slack.botUserId).digest("hex") !== slackHashes.bot ||
|
|
1005
|
+
canonicalHostWorkerDigest({
|
|
1006
|
+
appId: slack.appId,
|
|
1007
|
+
botUserId: slack.botUserId,
|
|
1008
|
+
generation: slack.generation,
|
|
1009
|
+
installationId: slack.installationId,
|
|
1010
|
+
scopes: slack.scopes,
|
|
1011
|
+
selectedBindings: slack.selectedBindings,
|
|
1012
|
+
teamId: slack.teamId,
|
|
1013
|
+
}) !== slackHashes.installation
|
|
1014
|
+
) return { ok: false, code: "pinned_runtime_channel_identity_invalid" };
|
|
1015
|
+
if (
|
|
1016
|
+
typeof pinned.providerReference !== "string" ||
|
|
1017
|
+
typeof pinned.xappReference !== "string" || !/^[a-f0-9]{16}$/.test(pinned.xappFingerprint ?? "") ||
|
|
1018
|
+
!/^[a-f0-9]{16}$/.test(slack.credentialFingerprint ?? "")
|
|
1019
|
+
) return { ok: false, code: "pinned_runtime_secret_reference_invalid" };
|
|
1020
|
+
|
|
1021
|
+
return {
|
|
1022
|
+
ok: true,
|
|
1023
|
+
desired: {
|
|
1024
|
+
workspaceId: operation.workspaceId,
|
|
1025
|
+
agentId: operation.agentId,
|
|
1026
|
+
hostId: config.hostId,
|
|
1027
|
+
workerId: config.workerId,
|
|
1028
|
+
installationId: manifest.slack.installationId,
|
|
1029
|
+
slackGeneration: slack.generation,
|
|
1030
|
+
fence: operation.fence,
|
|
1031
|
+
revisionId: operation.targetRevisionId,
|
|
1032
|
+
revisionSequence: operation.lifecycleGeneration,
|
|
1033
|
+
hermesCli: pinned.hermesCli,
|
|
1034
|
+
hermesVersion: pinned.hermesVersion,
|
|
1035
|
+
installerPackage: pinned.installerPackage,
|
|
1036
|
+
mcpPackage: pinned.mcpPackage,
|
|
1037
|
+
workspaceLockId: operation.workspaceId,
|
|
1038
|
+
toolInclude: [...policy.toolInclude].sort(),
|
|
1039
|
+
policyHash: manifest.service.policyHash,
|
|
1040
|
+
slackManifestHash: slackHashes.installation,
|
|
1041
|
+
slackAppHash: slackHashes.app,
|
|
1042
|
+
slackTeamHash: slackHashes.team,
|
|
1043
|
+
slackBotHash: slackHashes.bot,
|
|
1044
|
+
slackChannelHash: slackHashes.channels,
|
|
1045
|
+
slackDefaultChannelHash: slackHashes.defaultChannel,
|
|
1046
|
+
slackMemberAllowlistHash: slackHashes.memberAllowlist,
|
|
1047
|
+
slackChannelIds: [...channelIds],
|
|
1048
|
+
slackDefaultChannelId: defaultBinding.channelId,
|
|
1049
|
+
slackMemberAllowlist: [...slack.memberAllowlist],
|
|
1050
|
+
soul,
|
|
1051
|
+
soulHash: createHash("sha256").update(soul).digest("hex"),
|
|
1052
|
+
skill,
|
|
1053
|
+
skillsHash: createHash("sha256").update(skill).digest("hex"),
|
|
1054
|
+
providerReference: pinned.providerReference,
|
|
1055
|
+
providerFingerprint: pinned.providerFingerprint,
|
|
1056
|
+
serviceCredentialReference: `secret://agent/service/g${credentialIdentity.generation}`,
|
|
1057
|
+
serviceCredentialFingerprint: credentialIdentity.fingerprint,
|
|
1058
|
+
serviceCredentialGeneration: credentialIdentity.generation,
|
|
1059
|
+
xappReference: pinned.xappReference,
|
|
1060
|
+
xappFingerprint: pinned.xappFingerprint,
|
|
1061
|
+
xoxbReference: `secret://slack/install/${slack.installationId}/g${slack.generation}`,
|
|
1062
|
+
xoxbCiphertextFingerprint: slack.credentialFingerprint,
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
1065
|
+
} catch {
|
|
1066
|
+
return { ok: false, code: "claim_profile_compile_failed" };
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function confinedRuntimeStatePath(config, activeClaim) {
|
|
1071
|
+
const stateRoot = realpathSync(config.stateRoot);
|
|
1072
|
+
const runtimeRoot = join(stateRoot, "agent-runtime");
|
|
1073
|
+
if (!existsSync(runtimeRoot)) mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
|
|
1074
|
+
const realRuntimeRoot = realpathSync(runtimeRoot);
|
|
1075
|
+
const name = `${activeClaim.operation.workspaceId}-${activeClaim.operation.agentId}.json`;
|
|
1076
|
+
const pathValue = join(realRuntimeRoot, name);
|
|
1077
|
+
if (dirname(pathValue) !== realRuntimeRoot || !SAFE_ID.test(activeClaim.operation.workspaceId) || !SAFE_ID.test(activeClaim.operation.agentId)) {
|
|
1078
|
+
throw new Error("runtime state confinement failed");
|
|
1079
|
+
}
|
|
1080
|
+
return pathValue;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function readRuntimeState(config, activeClaim) {
|
|
1084
|
+
try {
|
|
1085
|
+
const pathValue = confinedRuntimeStatePath(config, activeClaim);
|
|
1086
|
+
if (!existsSync(pathValue)) return null;
|
|
1087
|
+
const link = lstatSync(pathValue);
|
|
1088
|
+
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600) return null;
|
|
1089
|
+
return JSON.parse(readFileSync(pathValue, "utf8"));
|
|
1090
|
+
} catch {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function persistRuntimeState(config, activeClaim, state) {
|
|
1096
|
+
atomicPrivateJson(confinedRuntimeStatePath(config, activeClaim), state);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function serviceCredentialFor(desired, secret) {
|
|
1100
|
+
return {
|
|
1101
|
+
secret,
|
|
1102
|
+
fingerprint: desired.serviceCredentialFingerprint,
|
|
1103
|
+
generation: desired.serviceCredentialGeneration,
|
|
1104
|
+
binding: {
|
|
1105
|
+
workspaceId: desired.workspaceId,
|
|
1106
|
+
agentId: desired.agentId,
|
|
1107
|
+
installationId: desired.installationId,
|
|
1108
|
+
generation: desired.serviceCredentialGeneration,
|
|
1109
|
+
hostId: desired.hostId,
|
|
1110
|
+
workerId: desired.workerId,
|
|
1111
|
+
fence: desired.fence,
|
|
1112
|
+
servicePurpose: "mcp-service",
|
|
1113
|
+
},
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function runtimeTuple(desired, observation, operation) {
|
|
1118
|
+
const tuple = independentlyObservedRuntimeTuple(observation, "ON");
|
|
1119
|
+
if (
|
|
1120
|
+
tuple.revisionId !== desired.revisionId ||
|
|
1121
|
+
tuple.revisionSequence !== desired.revisionSequence
|
|
1122
|
+
) throw new Error("initial runtime tuple revision rejected");
|
|
1123
|
+
return tuple;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function approvedRuntimeTuple(desired, operation, currentState) {
|
|
1127
|
+
const computed = computeAgentProfileDigest(desired);
|
|
1128
|
+
if (!computed.ok) throw new Error("approved profile digest rejected");
|
|
1129
|
+
return {
|
|
1130
|
+
revisionId: desired.revisionId,
|
|
1131
|
+
revisionSequence: desired.revisionSequence,
|
|
1132
|
+
lifecycle: currentState.observedLifecycle,
|
|
1133
|
+
packageHash: currentState.observedTuple.packageHash,
|
|
1134
|
+
configHash: computed.fileHashes["config.yaml"],
|
|
1135
|
+
mcpHash: computed.fileHashes["sellable/mcp.json"],
|
|
1136
|
+
policyHash: desired.policyHash,
|
|
1137
|
+
toolsHash: canonicalHostWorkerDigest(desired.toolInclude),
|
|
1138
|
+
skillsHash: desired.skillsHash,
|
|
1139
|
+
providerFingerprint: desired.providerFingerprint,
|
|
1140
|
+
serviceCredentialGeneration: desired.serviceCredentialGeneration,
|
|
1141
|
+
slackManifestHash: desired.slackManifestHash,
|
|
1142
|
+
slackAppHash: desired.slackAppHash,
|
|
1143
|
+
slackTeamHash: desired.slackTeamHash,
|
|
1144
|
+
slackBotHash: desired.slackBotHash,
|
|
1145
|
+
slackChannelHash: desired.slackChannelHash,
|
|
1146
|
+
slackDefaultChannelHash: desired.slackDefaultChannelHash,
|
|
1147
|
+
slackMemberAllowlistHash: desired.slackMemberAllowlistHash,
|
|
1148
|
+
slackChannelIds: [...desired.slackChannelIds],
|
|
1149
|
+
slackDefaultChannelId: desired.slackDefaultChannelId,
|
|
1150
|
+
slackMemberAllowlist: [...desired.slackMemberAllowlist],
|
|
1151
|
+
slackGeneration: desired.slackGeneration,
|
|
1152
|
+
contentHash: computed.profileDigest,
|
|
1153
|
+
soulHash: desired.soulHash,
|
|
1154
|
+
processHash: currentState.observedTuple.processHash,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function tupleRuntimeHashes(tuple) {
|
|
1159
|
+
return {
|
|
1160
|
+
package: tuple.packageHash,
|
|
1161
|
+
config: tuple.configHash,
|
|
1162
|
+
content: tuple.contentHash,
|
|
1163
|
+
process: tuple.processHash ?? null,
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function independentlyObservedRuntimeTuple(observed, lifecycle) {
|
|
1168
|
+
if (
|
|
1169
|
+
!observed || !Number.isSafeInteger(observed.revisionSequence) || observed.revisionSequence < 1 ||
|
|
1170
|
+
!["ON", "OFF", "ARCHIVED"].includes(lifecycle) || !Array.isArray(observed.tools) ||
|
|
1171
|
+
!Array.isArray(observed.slack?.channelIds) || observed.slack.channelIds.length < 1 ||
|
|
1172
|
+
observed.slack.channelIds.length > 100 ||
|
|
1173
|
+
observed.slack.channelIds.some((value) => !/^C[A-Z0-9]{8,20}$/.test(value)) ||
|
|
1174
|
+
new Set(observed.slack.channelIds).size !== observed.slack.channelIds.length ||
|
|
1175
|
+
!/^C[A-Z0-9]{8,20}$/.test(observed.slack?.defaultChannelId ?? "") ||
|
|
1176
|
+
!Array.isArray(observed.slack?.memberIds) || observed.slack.memberIds.length < 1 ||
|
|
1177
|
+
observed.slack.memberIds.length > 1_000 ||
|
|
1178
|
+
observed.slack.memberIds.some((value) => !/^[UW][A-Z0-9]{8,20}$/.test(value)) ||
|
|
1179
|
+
new Set(observed.slack.memberIds).size !== observed.slack.memberIds.length
|
|
1180
|
+
) throw new Error("independent runtime tuple rejected");
|
|
1181
|
+
return {
|
|
1182
|
+
revisionId: observed.revisionId,
|
|
1183
|
+
revisionSequence: observed.revisionSequence,
|
|
1184
|
+
lifecycle,
|
|
1185
|
+
packageHash: observed.hashes.package,
|
|
1186
|
+
configHash: observed.hashes.config,
|
|
1187
|
+
mcpHash: observed.hashes.mcp,
|
|
1188
|
+
policyHash: observed.hashes.policy,
|
|
1189
|
+
toolsHash: canonicalHostWorkerDigest([...observed.tools].sort()),
|
|
1190
|
+
skillsHash: observed.hashes.skills,
|
|
1191
|
+
providerFingerprint: observed.hashes.provider,
|
|
1192
|
+
serviceCredentialGeneration: observed.serviceCredential.generation,
|
|
1193
|
+
slackManifestHash: observed.hashes.slackManifest,
|
|
1194
|
+
slackAppHash: observed.slack.app,
|
|
1195
|
+
slackTeamHash: observed.slack.team,
|
|
1196
|
+
slackBotHash: observed.slack.bot,
|
|
1197
|
+
slackChannelHash: observed.slack.channels,
|
|
1198
|
+
slackDefaultChannelHash: observed.slack.defaultChannel,
|
|
1199
|
+
slackMemberAllowlistHash: observed.slack.memberAllowlist,
|
|
1200
|
+
slackChannelIds: [...observed.slack.channelIds],
|
|
1201
|
+
slackDefaultChannelId: observed.slack.defaultChannelId,
|
|
1202
|
+
slackMemberAllowlist: [...observed.slack.memberIds],
|
|
1203
|
+
slackGeneration: observed.slack.generation,
|
|
1204
|
+
contentHash: observed.hashes.content,
|
|
1205
|
+
soulHash: observed.hashes.soul,
|
|
1206
|
+
processHash: canonicalHostWorkerDigest(observed.process),
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function initialRuntimeState(activeClaim, desired, outcome) {
|
|
1211
|
+
const tuple = runtimeTuple(desired, outcome.observation, activeClaim.operation);
|
|
1212
|
+
return {
|
|
1213
|
+
desiredRevisionId: desired.revisionId,
|
|
1214
|
+
desiredRevisionSequence: activeClaim.operation.lifecycleGeneration,
|
|
1215
|
+
observedRevisionId: desired.revisionId,
|
|
1216
|
+
requestedLifecycle: "ON",
|
|
1217
|
+
observedLifecycle: "ON",
|
|
1218
|
+
acceptedFence: activeClaim.operation.fence,
|
|
1219
|
+
credentialGeneration: desired.serviceCredentialGeneration,
|
|
1220
|
+
routeFenced: false,
|
|
1221
|
+
outputFenced: false,
|
|
1222
|
+
dispatchEnabled: true,
|
|
1223
|
+
secretsPresent: true,
|
|
1224
|
+
profileDigest: outcome.receipt.profileDigest,
|
|
1225
|
+
archivedCredentialGeneration: null,
|
|
1226
|
+
archivedProfileDigest: null,
|
|
1227
|
+
receipts: {},
|
|
1228
|
+
desiredTuple: tuple,
|
|
1229
|
+
observedTuple: tuple,
|
|
1230
|
+
previousKnownGoodTuple: tuple,
|
|
1231
|
+
runtimeHashes: {
|
|
1232
|
+
package: outcome.observation.hashes.package,
|
|
1233
|
+
config: outcome.observation.hashes.config,
|
|
1234
|
+
content: outcome.observation.hashes.content,
|
|
1235
|
+
process: canonicalHostWorkerDigest(outcome.observation.process),
|
|
1236
|
+
},
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function lifecycleOperation(activeClaim, runtimeState, desired) {
|
|
1241
|
+
return {
|
|
1242
|
+
id: activeClaim.operation.id,
|
|
1243
|
+
eventId: activeClaim.operation.id,
|
|
1244
|
+
action: activeClaim.operation.action,
|
|
1245
|
+
fence: activeClaim.operation.fence,
|
|
1246
|
+
targetRevisionId: activeClaim.operation.targetRevisionId,
|
|
1247
|
+
targetRevisionSequence: activeClaim.operation.lifecycleGeneration,
|
|
1248
|
+
credentialGeneration: desired?.serviceCredentialGeneration ?? runtimeState.credentialGeneration,
|
|
1249
|
+
profileDigest: desired?.profileDigest ?? runtimeState.profileDigest,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function localCompletionReceipt({ activeClaim, config, outcome, effectId, secretConsumed }) {
|
|
1254
|
+
const identity = operationIdentity(activeClaim, config);
|
|
1255
|
+
const success = outcome?.ok === true;
|
|
1256
|
+
const uncertain = !success && (secretConsumed || outcome?.status === "UNCERTAIN");
|
|
1257
|
+
const observation = outcome?.observation ?? null;
|
|
1258
|
+
const body = {
|
|
1259
|
+
...identity,
|
|
1260
|
+
status: success ? "SUCCEEDED" : uncertain ? "UNCERTAIN" : "FAILED",
|
|
1261
|
+
secretDisposition: success
|
|
1262
|
+
? (secretConsumed ? "INSTALLED" : "NOT_PRESENT")
|
|
1263
|
+
: (secretConsumed ? "UNCERTAIN" : "NOT_PRESENT"),
|
|
1264
|
+
observedLifecycle: success ? (outcome.observedLifecycle ?? "ON") : "DEGRADED",
|
|
1265
|
+
observedRevisionId: success ? activeClaim.operation.targetRevisionId : null,
|
|
1266
|
+
signerGeneration: config.signerGeneration,
|
|
1267
|
+
runtimeHashes: {
|
|
1268
|
+
package: observation?.hashes?.package ?? outcome?.runtimeHashes?.package ?? null,
|
|
1269
|
+
config: observation?.hashes?.config ?? outcome?.runtimeHashes?.config ?? null,
|
|
1270
|
+
content: observation?.hashes?.content ?? outcome?.runtimeHashes?.content ?? null,
|
|
1271
|
+
process: observation?.process ? canonicalHostWorkerDigest(observation.process) : outcome?.runtimeHashes?.process ?? null,
|
|
1272
|
+
},
|
|
1273
|
+
conditions: [
|
|
1274
|
+
{ code: success ? "READY" : uncertain ? "RUNTIME_UNCERTAIN" : "PROVISIONING_FAILED", status: success ? "TRUE" : uncertain ? "UNKNOWN" : "FALSE" },
|
|
1275
|
+
],
|
|
1276
|
+
errorClass: success ? "NONE" : outcome?.code ?? outcome?.status ?? "HOST_RUNTIME",
|
|
1277
|
+
auditIds: [effectId],
|
|
1278
|
+
};
|
|
1279
|
+
return { ...body, signedDigest: canonicalHostWorkerDigest(body) };
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function readBoundSecret(pathValue) {
|
|
1283
|
+
const root = privateRootForAdapter(dirname(pathValue));
|
|
1284
|
+
if (!root || dirname(resolve(pathValue)) !== root) throw new Error("secret source confinement failed");
|
|
1285
|
+
const link = lstatSync(pathValue);
|
|
1286
|
+
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o077) !== 0) {
|
|
1287
|
+
throw new Error("secret source mode rejected");
|
|
1288
|
+
}
|
|
1289
|
+
return readFileSync(pathValue, "utf8").trim();
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async function invokePinnedRuntimeHelper(config, request) {
|
|
1293
|
+
const requestRoot = realPrivateDirectory(config.helperRequestRoot);
|
|
1294
|
+
if (!requestRoot) throw new Error("runtime helper request root rejected");
|
|
1295
|
+
const helper = config.runtimeHelperPath ?? "/usr/local/bin/sellable-agent-runtime-helper";
|
|
1296
|
+
if (helper !== "/usr/local/bin/sellable-agent-runtime-helper") throw new Error("runtime helper pin rejected");
|
|
1297
|
+
const pathValue = join(requestRoot, `helper-${randomUUID()}.json`);
|
|
1298
|
+
atomicPrivateJson(pathValue, request);
|
|
1299
|
+
try {
|
|
1300
|
+
const result = await runBoundedChildProcess({
|
|
1301
|
+
command: "/usr/bin/sudo",
|
|
1302
|
+
args: ["-n", helper, "--request", pathValue],
|
|
1303
|
+
env: { PATH: "/usr/local/bin:/usr/bin:/bin" },
|
|
1304
|
+
shell: false,
|
|
1305
|
+
timeoutMs: 30_000,
|
|
1306
|
+
maxOutputBytes: 32 * 1024,
|
|
1307
|
+
});
|
|
1308
|
+
if (
|
|
1309
|
+
result.exitCode !== 0 || result.timedOut || result.outputLimitExceeded ||
|
|
1310
|
+
SECRET_PATTERN.test(result.stdout) || SECRET_PATTERN.test(result.stderr)
|
|
1311
|
+
) throw new Error("runtime helper invocation failed");
|
|
1312
|
+
const lines = result.stdout.trim().split(/\r?\n/).filter(Boolean);
|
|
1313
|
+
if (lines.length !== 1) throw new Error("runtime helper receipt rejected");
|
|
1314
|
+
const receipt = JSON.parse(lines[0]);
|
|
1315
|
+
if (!receipt?.ok || SECRET_PATTERN.test(JSON.stringify(receipt))) throw new Error("runtime helper receipt rejected");
|
|
1316
|
+
return receipt;
|
|
1317
|
+
} finally {
|
|
1318
|
+
removeRequest(pathValue, requestRoot);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
1323
|
+
const ownershipReceipts = new Map();
|
|
1324
|
+
let containmentReceipt = null;
|
|
1325
|
+
let context = null;
|
|
1326
|
+
let refreshPrepared = null;
|
|
1327
|
+
let refreshRollbackExpected = null;
|
|
1328
|
+
let refreshClassificationObservation = null;
|
|
1329
|
+
if (
|
|
1330
|
+
dependencies &&
|
|
1331
|
+
(typeof dependencies !== "object" || Array.isArray(dependencies) ||
|
|
1332
|
+
Object.keys(dependencies).some((key) => !["invokeRuntimeHelper", "fetchImpl"].includes(key)) ||
|
|
1333
|
+
(dependencies.invokeRuntimeHelper && typeof dependencies.invokeRuntimeHelper !== "function") ||
|
|
1334
|
+
(dependencies.fetchImpl && typeof dependencies.fetchImpl !== "function"))
|
|
1335
|
+
) throw new Error("local action port dependencies rejected");
|
|
1336
|
+
const helper = (request) => dependencies.invokeRuntimeHelper
|
|
1337
|
+
? dependencies.invokeRuntimeHelper(request)
|
|
1338
|
+
: invokePinnedRuntimeHelper(config, request);
|
|
1339
|
+
const helperIdentity = () => ({
|
|
1340
|
+
profileId: context.profileId,
|
|
1341
|
+
operationId: context.activeClaim.operation.id,
|
|
1342
|
+
fence: context.activeClaim.operation.fence,
|
|
1343
|
+
});
|
|
1344
|
+
const controlRuntime = async (control) => {
|
|
1345
|
+
if (control !== "DOWN") await helper({ action: "VERIFY_HERMES_BRIDGE", ...helperIdentity() });
|
|
1346
|
+
return helper({ action: "CONTROL_RUNTIME", ...helperIdentity(), control });
|
|
1347
|
+
};
|
|
1348
|
+
const setRuntimeLifecycle = (lifecycle) => helper({
|
|
1349
|
+
action: "SET_RUNTIME_GATES", ...helperIdentity(), lifecycle,
|
|
1350
|
+
});
|
|
1351
|
+
const observeRuntimeLifecycle = (lifecycle) => helper({
|
|
1352
|
+
action: "OBSERVE_RUNTIME_LIFECYCLE", ...helperIdentity(), lifecycle,
|
|
1353
|
+
});
|
|
1354
|
+
const readDurableLifecycleEvidence = (name) => {
|
|
1355
|
+
const stateRoot = realpathSync(config.stateRoot);
|
|
1356
|
+
const pathValue = join(stateRoot, name);
|
|
1357
|
+
if (dirname(pathValue) !== stateRoot) throw new Error("lifecycle evidence path rejected");
|
|
1358
|
+
const link = lstatSync(pathValue);
|
|
1359
|
+
if (
|
|
1360
|
+
link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 ||
|
|
1361
|
+
link.uid !== config.workerUid || link.gid !== config.workerGid
|
|
1362
|
+
) throw new Error("lifecycle evidence file rejected");
|
|
1363
|
+
return JSON.parse(readFileSync(pathValue, "utf8"));
|
|
1364
|
+
};
|
|
1365
|
+
const ports = {
|
|
1366
|
+
bind(value) {
|
|
1367
|
+
context = value;
|
|
1368
|
+
refreshClassificationObservation = null;
|
|
1369
|
+
},
|
|
1370
|
+
materializer: {
|
|
1371
|
+
promotionJournal: createWorkerPromotionJournal(config),
|
|
1372
|
+
hostAppSecret: {
|
|
1373
|
+
async resolveHostSecret(request) {
|
|
1374
|
+
const contract = config.profileRuntimeContract;
|
|
1375
|
+
if (
|
|
1376
|
+
request.reference !== contract?.xappReference ||
|
|
1377
|
+
request.expectedFingerprint !== contract?.xappFingerprint
|
|
1378
|
+
) throw new Error("host app secret binding rejected");
|
|
1379
|
+
const secret = readBoundSecret(contract.xappSecretFile);
|
|
1380
|
+
return { secret, fingerprint: createHash("sha256").update(secret).digest("hex").slice(0, 16), binding: {
|
|
1381
|
+
workspaceId: request.workspaceId, agentId: request.agentId, installationId: request.installationId,
|
|
1382
|
+
generation: request.generation, hostId: request.hostId, workerId: request.workerId,
|
|
1383
|
+
fence: request.fence, servicePurpose: request.servicePurpose,
|
|
1384
|
+
} };
|
|
1385
|
+
},
|
|
1386
|
+
},
|
|
1387
|
+
workspaceBotSecret: {
|
|
1388
|
+
async decryptWorkspaceSecret(request) {
|
|
1389
|
+
const secret = context?.payload?.slackBotCredential;
|
|
1390
|
+
if (typeof secret !== "string" || !secret.startsWith("xoxb-")) throw new Error("slack bot request secret missing");
|
|
1391
|
+
return { secret, fingerprint: createHash("sha256").update(secret).digest("hex").slice(0, 16), generation: request.generation, binding: {
|
|
1392
|
+
workspaceId: request.workspaceId, agentId: request.agentId, installationId: request.installationId,
|
|
1393
|
+
generation: request.generation, hostId: request.hostId, workerId: request.workerId,
|
|
1394
|
+
fence: request.fence, servicePurpose: request.servicePurpose,
|
|
1395
|
+
} };
|
|
1396
|
+
},
|
|
1397
|
+
},
|
|
1398
|
+
credential: {
|
|
1399
|
+
async install(request) {
|
|
1400
|
+
if (!context?.requestFile) throw new Error("credential source request missing");
|
|
1401
|
+
const receipt = await helper({
|
|
1402
|
+
action: "INSTALL_MCP_CREDENTIAL",
|
|
1403
|
+
...helperIdentity(),
|
|
1404
|
+
sourceRequestFile: context.requestFile,
|
|
1405
|
+
generation: request.serviceCredential.generation,
|
|
1406
|
+
fingerprint: request.serviceCredential.fingerprint,
|
|
1407
|
+
});
|
|
1408
|
+
return receipt;
|
|
1409
|
+
},
|
|
1410
|
+
async cleanup(request) {
|
|
1411
|
+
return helper({ action: "CLEAN_PROFILE_SECRETS", ...helperIdentity(), profileId: request.profileId });
|
|
1412
|
+
},
|
|
1413
|
+
},
|
|
1414
|
+
gateway: {
|
|
1415
|
+
async start(request) {
|
|
1416
|
+
if (
|
|
1417
|
+
request?.profileId !== context.profileId ||
|
|
1418
|
+
!SHA256.test(request?.evidence?.profileDigest ?? "")
|
|
1419
|
+
) throw new Error("runtime service environment binding rejected");
|
|
1420
|
+
await helper({
|
|
1421
|
+
action: "INSTALL_RUNTIME_SERVICE_ENV",
|
|
1422
|
+
...helperIdentity(),
|
|
1423
|
+
revisionId: request.evidence.revisionId,
|
|
1424
|
+
profileDigest: request.evidence.profileDigest,
|
|
1425
|
+
});
|
|
1426
|
+
await helper({
|
|
1427
|
+
action: "INSTALL_PROVIDER_CREDENTIAL",
|
|
1428
|
+
...helperIdentity(),
|
|
1429
|
+
fingerprint: context.desired.providerFingerprint,
|
|
1430
|
+
});
|
|
1431
|
+
await helper({ action: "INSTALL_HERMES_BRIDGE", ...helperIdentity() });
|
|
1432
|
+
await controlRuntime("UP");
|
|
1433
|
+
},
|
|
1434
|
+
async stop() { await controlRuntime("DOWN"); },
|
|
1435
|
+
async observe() {
|
|
1436
|
+
const receipt = await helper({ action: "OBSERVE_RUNTIME", ...helperIdentity() });
|
|
1437
|
+
const {
|
|
1438
|
+
profileDigest: _profileDigest,
|
|
1439
|
+
tools: _tools,
|
|
1440
|
+
revisionSequence: _revisionSequence,
|
|
1441
|
+
...observation
|
|
1442
|
+
} = receipt.observation;
|
|
1443
|
+
return observation;
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
containment: {
|
|
1447
|
+
async probe(request) {
|
|
1448
|
+
containmentReceipt = await helper({
|
|
1449
|
+
action: "PROBE_CONTAINMENT",
|
|
1450
|
+
...helperIdentity(),
|
|
1451
|
+
profileRoot: request.profileRoot,
|
|
1452
|
+
revisionId: context.desired.revisionId,
|
|
1453
|
+
});
|
|
1454
|
+
return containmentReceipt;
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
1457
|
+
mcp: {
|
|
1458
|
+
async discoverTools() {
|
|
1459
|
+
if (!containmentReceipt?.ok) throw new Error("runtime MCP proof missing");
|
|
1460
|
+
return containmentReceipt.tools;
|
|
1461
|
+
},
|
|
1462
|
+
async safeRead({ workspaceId }) {
|
|
1463
|
+
return containmentReceipt?.safeRead === true && containmentReceipt.workspaceId === workspaceId
|
|
1464
|
+
? { ok: true, workspaceId }
|
|
1465
|
+
: { ok: false, code: "runtime_mcp_probe_missing" };
|
|
1466
|
+
},
|
|
1467
|
+
},
|
|
1468
|
+
ownership: {
|
|
1469
|
+
async apply(request) {
|
|
1470
|
+
const receipt = await helper({
|
|
1471
|
+
action: "CHOWN_PROFILE",
|
|
1472
|
+
...helperIdentity(),
|
|
1473
|
+
profileRoot: request.profileRoot,
|
|
1474
|
+
revisionId: request.revisionId,
|
|
1475
|
+
profileDigest: request.profileDigest,
|
|
1476
|
+
});
|
|
1477
|
+
ownershipReceipts.set(resolve(request.profileRoot), receipt);
|
|
1478
|
+
return receipt;
|
|
1479
|
+
},
|
|
1480
|
+
async observe(request) {
|
|
1481
|
+
const receipt = ownershipReceipts.get(resolve(request.profileRoot));
|
|
1482
|
+
return receipt ? { ok: true, entries: receipt.entries } : { ok: false, entries: [] };
|
|
1483
|
+
},
|
|
1484
|
+
async remove(request) {
|
|
1485
|
+
ownershipReceipts.delete(resolve(request.profileRoot));
|
|
1486
|
+
return helper({
|
|
1487
|
+
action: "REMOVE_PROFILE",
|
|
1488
|
+
...helperIdentity(),
|
|
1489
|
+
slot: request.slot,
|
|
1490
|
+
revisionId: request.revisionId,
|
|
1491
|
+
});
|
|
1492
|
+
},
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1495
|
+
refresh: {},
|
|
1496
|
+
lifecycle: {},
|
|
1497
|
+
};
|
|
1498
|
+
const restart = () => controlRuntime("RESTART");
|
|
1499
|
+
const rawRuntimeObservation = async ({ drift = false } = {}) => {
|
|
1500
|
+
const observed = (await helper({
|
|
1501
|
+
action: drift ? "OBSERVE_RUNTIME_DRIFT" : "OBSERVE_RUNTIME",
|
|
1502
|
+
...helperIdentity(),
|
|
1503
|
+
})).observation;
|
|
1504
|
+
if (
|
|
1505
|
+
observed?.profileId !== context.profileId ||
|
|
1506
|
+
observed.workspaceId !== context.desired.workspaceId || observed.agentId !== context.desired.agentId ||
|
|
1507
|
+
!SAFE_ID.test(observed.revisionId ?? "") || !Number.isSafeInteger(observed.revisionSequence) ||
|
|
1508
|
+
!Array.isArray(observed.tools) || observed.tools.length === 0 ||
|
|
1509
|
+
!observed.process?.ready || !Number.isSafeInteger(observed.process.pid) || observed.process.pid < 2 ||
|
|
1510
|
+
!/^[1-9][0-9]{0,31}$/.test(observed.process.startTicks ?? "") ||
|
|
1511
|
+
observed.process.serviceUser !== "sellable-agent-runtime" || observed.process.uid !== config.runtimeUid
|
|
1512
|
+
) throw new Error("runtime observation identity rejected");
|
|
1513
|
+
return observed;
|
|
1514
|
+
};
|
|
1515
|
+
const ensureRefreshTarget = async () => {
|
|
1516
|
+
if (refreshPrepared) return;
|
|
1517
|
+
const recovery = await recoverInterruptedProfilePromotion({
|
|
1518
|
+
desired: context.desired,
|
|
1519
|
+
operationId: context.activeClaim.operation.id,
|
|
1520
|
+
effectId: context.effectId,
|
|
1521
|
+
profilesRoot: config.profilesRoot,
|
|
1522
|
+
runtimeSecretsRoot: config.runtimeSecretsRoot,
|
|
1523
|
+
runtimeIdentity: { user: "sellable-agent-runtime", uid: config.runtimeUid, gid: config.runtimeGid },
|
|
1524
|
+
ports: ports.materializer,
|
|
1525
|
+
});
|
|
1526
|
+
if (!recovery.ok) throw new Error("interrupted refresh recovery unproven");
|
|
1527
|
+
const prior = refreshClassificationObservation ?? await rawRuntimeObservation();
|
|
1528
|
+
refreshClassificationObservation = null;
|
|
1529
|
+
const { profileDigest: _profileDigest, tools: _tools, ...priorMaterializerObservation } = prior;
|
|
1530
|
+
const result = await materializeAgentProfile({
|
|
1531
|
+
desired: context.desired,
|
|
1532
|
+
operationId: context.activeClaim.operation.id,
|
|
1533
|
+
effectId: context.effectId,
|
|
1534
|
+
profilesRoot: config.profilesRoot,
|
|
1535
|
+
runtimeSecretsRoot: config.runtimeSecretsRoot,
|
|
1536
|
+
mcpCredentialRoot: config.runtimeMcpSecretsRoot,
|
|
1537
|
+
runtimeIdentity: { user: "sellable-agent-runtime", uid: config.runtimeUid, gid: config.runtimeGid },
|
|
1538
|
+
reuseInstalledSecrets: prior,
|
|
1539
|
+
priorKnownGoodObservation: priorMaterializerObservation,
|
|
1540
|
+
retainPriorGood: true,
|
|
1541
|
+
prepareOnly: true,
|
|
1542
|
+
ports: ports.materializer,
|
|
1543
|
+
});
|
|
1544
|
+
if (
|
|
1545
|
+
!result.ok || result.status !== "PREPARED" ||
|
|
1546
|
+
result.receipt?.revisionId !== context.desired.revisionId ||
|
|
1547
|
+
!SHA256.test(result.receipt?.profileDigest ?? "")
|
|
1548
|
+
) throw new Error(`approved refresh profile rejected ${result.status ?? "unknown"}:${result.code ?? "none"}`);
|
|
1549
|
+
refreshPrepared = {
|
|
1550
|
+
prior,
|
|
1551
|
+
profileId: context.profileId,
|
|
1552
|
+
profileRoot: join(realpathSync(config.profilesRoot), context.profileId),
|
|
1553
|
+
backupRoot: join(realpathSync(config.profilesRoot), `${context.profileId}.rollback-prior-good`),
|
|
1554
|
+
};
|
|
1555
|
+
await helper({
|
|
1556
|
+
action: "INSTALL_RUNTIME_SERVICE_ENV",
|
|
1557
|
+
...helperIdentity(),
|
|
1558
|
+
revisionId: result.receipt.revisionId,
|
|
1559
|
+
profileDigest: result.receipt.profileDigest,
|
|
1560
|
+
});
|
|
1561
|
+
await helper({
|
|
1562
|
+
action: "INSTALL_PROVIDER_CREDENTIAL",
|
|
1563
|
+
...helperIdentity(),
|
|
1564
|
+
fingerprint: context.desired.providerFingerprint,
|
|
1565
|
+
});
|
|
1566
|
+
await helper({ action: "INSTALL_HERMES_BRIDGE", ...helperIdentity() });
|
|
1567
|
+
};
|
|
1568
|
+
const refreshThen = (effect) => async (input) => {
|
|
1569
|
+
await ensureRefreshTarget();
|
|
1570
|
+
return effect(input);
|
|
1571
|
+
};
|
|
1572
|
+
Object.assign(ports.refresh, {
|
|
1573
|
+
reloadMcp: refreshThen(restart),
|
|
1574
|
+
reloadSkills: refreshThen(restart),
|
|
1575
|
+
reloadEnvironmentOrRestart: refreshThen(restart),
|
|
1576
|
+
restartGateway: refreshThen(restart),
|
|
1577
|
+
restartProfileSession: refreshThen(restart),
|
|
1578
|
+
reprovisionProfile: refreshThen(restart),
|
|
1579
|
+
async observe({ desired, target, classification = false }) {
|
|
1580
|
+
const observed = await rawRuntimeObservation({ drift: classification });
|
|
1581
|
+
const expected = target ?? desired ?? refreshRollbackExpected;
|
|
1582
|
+
if (!expected || observed.revisionId !== expected.revisionId) {
|
|
1583
|
+
throw new Error("observed runtime revision rejected");
|
|
1584
|
+
}
|
|
1585
|
+
const tuple = independentlyObservedRuntimeTuple(
|
|
1586
|
+
observed, expected.lifecycle,
|
|
1587
|
+
);
|
|
1588
|
+
if (classification) refreshClassificationObservation = observed;
|
|
1589
|
+
if (refreshPrepared) {
|
|
1590
|
+
const containment = await helper({
|
|
1591
|
+
action: "PROBE_CONTAINMENT",
|
|
1592
|
+
...helperIdentity(),
|
|
1593
|
+
profileRoot: refreshPrepared.profileRoot,
|
|
1594
|
+
revisionId: observed.revisionId,
|
|
1595
|
+
});
|
|
1596
|
+
if (
|
|
1597
|
+
!containment?.ok || containment.safeRead !== true ||
|
|
1598
|
+
containment.workspaceId !== observed.workspaceId ||
|
|
1599
|
+
stableJson(containment.tools) !== stableJson(observed.tools) ||
|
|
1600
|
+
[
|
|
1601
|
+
"siblingReadDenied", "workerReadDenied", "adminReadDenied", "providerBytesDenied",
|
|
1602
|
+
"hostMountDenied", "dockerDenied", "unexpectedToolDenied", "wrongWorkspaceDispatchDenied",
|
|
1603
|
+
].some((key) => containment[key] !== true)
|
|
1604
|
+
) throw new Error("refreshed runtime containment proof rejected");
|
|
1605
|
+
}
|
|
1606
|
+
if (refreshPrepared && stableJson(projectRuntimeDriftTuple(tuple)) === stableJson(projectRuntimeDriftTuple(expected))) {
|
|
1607
|
+
const prepared = refreshPrepared;
|
|
1608
|
+
const committed = commitRetainedProfilePromotion({
|
|
1609
|
+
profilesRoot: config.profilesRoot,
|
|
1610
|
+
desired: context.desired,
|
|
1611
|
+
operationId: context.activeClaim.operation.id,
|
|
1612
|
+
effectId: context.effectId,
|
|
1613
|
+
ports: ports.materializer,
|
|
1614
|
+
targetObservation: observed,
|
|
1615
|
+
});
|
|
1616
|
+
if (!committed.ok) throw new Error("refresh promotion commit unproven");
|
|
1617
|
+
refreshPrepared = null;
|
|
1618
|
+
try {
|
|
1619
|
+
if (existsSync(prepared.backupRoot)) await ports.materializer.ownership.remove({
|
|
1620
|
+
profileId: prepared.profileId,
|
|
1621
|
+
profileRoot: prepared.backupRoot,
|
|
1622
|
+
slot: "BACKUP",
|
|
1623
|
+
revisionId: context.desired.revisionId,
|
|
1624
|
+
fence: context.desired.fence,
|
|
1625
|
+
});
|
|
1626
|
+
const finalized = finalizeRetainedProfilePromotion({
|
|
1627
|
+
profilesRoot: config.profilesRoot,
|
|
1628
|
+
desired: context.desired,
|
|
1629
|
+
operationId: context.activeClaim.operation.id,
|
|
1630
|
+
effectId: context.effectId,
|
|
1631
|
+
ports: ports.materializer,
|
|
1632
|
+
});
|
|
1633
|
+
if (!finalized.ok) throw new Error("refresh promotion finalize unproven");
|
|
1634
|
+
} catch {
|
|
1635
|
+
// The durable COMMITTED marker makes backup retirement restart-convergent.
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (
|
|
1639
|
+
refreshRollbackExpected &&
|
|
1640
|
+
stableJson(projectRuntimeDriftTuple(tuple)) === stableJson(projectRuntimeDriftTuple(refreshRollbackExpected))
|
|
1641
|
+
) refreshRollbackExpected = null;
|
|
1642
|
+
return tuple;
|
|
1643
|
+
},
|
|
1644
|
+
async rollback() {
|
|
1645
|
+
if (!refreshPrepared) throw new Error("prepared refresh rollback absent");
|
|
1646
|
+
await controlRuntime("DOWN");
|
|
1647
|
+
await ports.materializer.ownership.remove({
|
|
1648
|
+
profileId: refreshPrepared.profileId,
|
|
1649
|
+
profileRoot: refreshPrepared.profileRoot,
|
|
1650
|
+
slot: "ACTIVE",
|
|
1651
|
+
revisionId: context.desired.revisionId,
|
|
1652
|
+
fence: context.desired.fence,
|
|
1653
|
+
});
|
|
1654
|
+
renameSync(refreshPrepared.backupRoot, refreshPrepared.profileRoot);
|
|
1655
|
+
const manifest = JSON.parse(readFileSync(join(refreshPrepared.profileRoot, "agent-profile.json"), "utf8"));
|
|
1656
|
+
await ports.materializer.gateway.start({
|
|
1657
|
+
profileId: refreshPrepared.profileId,
|
|
1658
|
+
profileRoot: refreshPrepared.profileRoot,
|
|
1659
|
+
identity: { user: "sellable-agent-runtime", uid: config.runtimeUid, gid: config.runtimeGid },
|
|
1660
|
+
evidence: {
|
|
1661
|
+
revisionId: manifest.revisionId,
|
|
1662
|
+
profileDigest: manifest.profileDigest,
|
|
1663
|
+
xappFingerprint: refreshPrepared.prior.slack.xappFingerprint,
|
|
1664
|
+
xoxbFingerprint: refreshPrepared.prior.slack.xoxbFingerprint,
|
|
1665
|
+
serviceCredentialFingerprint: refreshPrepared.prior.serviceCredential.fingerprint,
|
|
1666
|
+
serviceCredentialGeneration: refreshPrepared.prior.serviceCredential.generation,
|
|
1667
|
+
},
|
|
1668
|
+
});
|
|
1669
|
+
refreshRollbackExpected = independentlyObservedRuntimeTuple(refreshPrepared.prior, "ON");
|
|
1670
|
+
refreshPrepared = null;
|
|
1671
|
+
},
|
|
1672
|
+
});
|
|
1673
|
+
Object.assign(ports.lifecycle, {
|
|
1674
|
+
async fenceDispatch() { await setRuntimeLifecycle("OFF"); },
|
|
1675
|
+
async fenceRouteOutput() { await setRuntimeLifecycle("ARCHIVED"); },
|
|
1676
|
+
async drain() {
|
|
1677
|
+
await controlRuntime("DOWN");
|
|
1678
|
+
const observed = { drained: true, remaining: 0 };
|
|
1679
|
+
atomicPrivateJson(join(realpathSync(config.stateRoot), `drain-${context.activeClaim.operation.id}.json`), {
|
|
1680
|
+
operationId: context.activeClaim.operation.id,
|
|
1681
|
+
fence: context.activeClaim.operation.fence,
|
|
1682
|
+
drained: true,
|
|
1683
|
+
remaining: 0,
|
|
1684
|
+
});
|
|
1685
|
+
return observed;
|
|
1686
|
+
},
|
|
1687
|
+
async startGateway() {
|
|
1688
|
+
await setRuntimeLifecycle("ON");
|
|
1689
|
+
await controlRuntime("UP");
|
|
1690
|
+
await observeRuntimeLifecycle("ON");
|
|
1691
|
+
},
|
|
1692
|
+
async stopGateway() {
|
|
1693
|
+
const lifecycle = context.activeClaim.operation.action === "ARCHIVE" ? "ARCHIVING" : "OFF";
|
|
1694
|
+
await controlRuntime("DOWN");
|
|
1695
|
+
await setRuntimeLifecycle(lifecycle);
|
|
1696
|
+
await observeRuntimeLifecycle(lifecycle);
|
|
1697
|
+
},
|
|
1698
|
+
async proveNoDispatch() {
|
|
1699
|
+
const lifecycle = context.activeClaim.operation.action === "ARCHIVE" ? "ARCHIVED" : "OFF";
|
|
1700
|
+
const observed = await observeRuntimeLifecycle(lifecycle);
|
|
1701
|
+
return { noDispatch: observed.gates.dispatch === "FENCED" && observed.process.ready === false };
|
|
1702
|
+
},
|
|
1703
|
+
async observeGateway(input) {
|
|
1704
|
+
const lifecycle = input.rollback
|
|
1705
|
+
? input.lifecycle ?? input.observedLifecycle
|
|
1706
|
+
: context.activeClaim.operation.action === "ARCHIVE" ? "ARCHIVED"
|
|
1707
|
+
: context.activeClaim.operation.action === "DISABLE_GATEWAY" ? "OFF" : "ON";
|
|
1708
|
+
const observed = await observeRuntimeLifecycle(lifecycle);
|
|
1709
|
+
const result = {
|
|
1710
|
+
lifecycle: observed.lifecycle,
|
|
1711
|
+
revisionId: observed.revisionId,
|
|
1712
|
+
credentialGeneration: observed.credentialGeneration,
|
|
1713
|
+
profileDigest: observed.profileDigest,
|
|
1714
|
+
gates: observed.gates,
|
|
1715
|
+
secretsPresent: observed.secretsPresent,
|
|
1716
|
+
process: observed.process,
|
|
1717
|
+
runtimeObservation: observed.runtimeObservation,
|
|
1718
|
+
};
|
|
1719
|
+
if (lifecycle === "ON" && observed.runtimeObservation) {
|
|
1720
|
+
result.runtimeTuple = independentlyObservedRuntimeTuple(
|
|
1721
|
+
observed.runtimeObservation, "ON",
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
return result;
|
|
1725
|
+
},
|
|
1726
|
+
async cleanSecrets() {
|
|
1727
|
+
await helper({ action: "CLEAN_PROFILE_SECRETS", ...helperIdentity() });
|
|
1728
|
+
},
|
|
1729
|
+
async acknowledgeRevocation({ operation }) {
|
|
1730
|
+
atomicPrivateJson(join(realpathSync(config.stateRoot), `revocation-${operation.id}.json`), { acknowledged: true, operationId: operation.id, fence: operation.fence });
|
|
1731
|
+
return { acknowledged: true };
|
|
1732
|
+
},
|
|
1733
|
+
async rehydrate({ desired, serviceCredential }) {
|
|
1734
|
+
const result = await materializeAgentProfile({
|
|
1735
|
+
desired,
|
|
1736
|
+
operationId: context.activeClaim.operation.id,
|
|
1737
|
+
effectId: context.effectId,
|
|
1738
|
+
profilesRoot: config.profilesRoot, runtimeSecretsRoot: config.runtimeSecretsRoot,
|
|
1739
|
+
runtimeIdentity: { user: "sellable-agent-runtime", uid: config.runtimeUid, gid: config.runtimeGid },
|
|
1740
|
+
serviceCredential, ports: ports.materializer,
|
|
1741
|
+
});
|
|
1742
|
+
if (!result.ok) throw new Error("rehydrate rejected");
|
|
1743
|
+
return { runtimeTuple: independentlyObservedRuntimeTuple(result.observation, "ON") };
|
|
1744
|
+
},
|
|
1745
|
+
async rollback({ priorState }) {
|
|
1746
|
+
await setRuntimeLifecycle(priorState.observedLifecycle);
|
|
1747
|
+
if (priorState.observedLifecycle === "ON") await controlRuntime("UP");
|
|
1748
|
+
else await controlRuntime("DOWN");
|
|
1749
|
+
await observeRuntimeLifecycle(priorState.observedLifecycle);
|
|
1750
|
+
},
|
|
1751
|
+
async reconcileArchive({ operation }) {
|
|
1752
|
+
try {
|
|
1753
|
+
const evidence = {};
|
|
1754
|
+
const confirmedSteps = [];
|
|
1755
|
+
let gates;
|
|
1756
|
+
try {
|
|
1757
|
+
gates = await helper({
|
|
1758
|
+
action: "OBSERVE_RUNTIME_GATES", ...helperIdentity(), lifecycle: "ARCHIVED",
|
|
1759
|
+
});
|
|
1760
|
+
} catch {
|
|
1761
|
+
return {
|
|
1762
|
+
ok: true,
|
|
1763
|
+
observationId: `archive_${canonicalHostWorkerDigest({ gates: "not_confirmed" })}`,
|
|
1764
|
+
confirmedSteps,
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
evidence.gates = gates.gates;
|
|
1768
|
+
confirmedSteps.push("FENCE_ROUTE_OUTPUT");
|
|
1769
|
+
let drain;
|
|
1770
|
+
try {
|
|
1771
|
+
drain = readDurableLifecycleEvidence(`drain-${operation.id}.json`);
|
|
1772
|
+
} catch {
|
|
1773
|
+
return {
|
|
1774
|
+
ok: true,
|
|
1775
|
+
observationId: `archive_${canonicalHostWorkerDigest(evidence)}`,
|
|
1776
|
+
confirmedSteps,
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
if (
|
|
1780
|
+
drain.operationId !== operation.id || drain.fence !== operation.fence ||
|
|
1781
|
+
!drain.drained || drain.remaining !== 0
|
|
1782
|
+
) throw new Error("archive drain evidence rejected");
|
|
1783
|
+
evidence.drain = drain;
|
|
1784
|
+
confirmedSteps.push("DRAIN");
|
|
1785
|
+
let observed;
|
|
1786
|
+
try {
|
|
1787
|
+
observed = await observeRuntimeLifecycle("ARCHIVING");
|
|
1788
|
+
} catch {
|
|
1789
|
+
try { observed = await observeRuntimeLifecycle("ARCHIVED"); } catch {
|
|
1790
|
+
return {
|
|
1791
|
+
ok: true,
|
|
1792
|
+
observationId: `archive_${canonicalHostWorkerDigest(evidence)}`,
|
|
1793
|
+
confirmedSteps,
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
evidence.stopped = observed;
|
|
1798
|
+
confirmedSteps.push("STOP_GATEWAY");
|
|
1799
|
+
if (observed.lifecycle !== "ARCHIVED" || observed.secretsPresent !== false) {
|
|
1800
|
+
return {
|
|
1801
|
+
ok: true,
|
|
1802
|
+
observationId: `archive_${canonicalHostWorkerDigest(evidence)}`,
|
|
1803
|
+
confirmedSteps,
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
confirmedSteps.push("CLEAN_SECRETS");
|
|
1807
|
+
let revoke;
|
|
1808
|
+
try {
|
|
1809
|
+
revoke = readDurableLifecycleEvidence(`revocation-${operation.id}.json`);
|
|
1810
|
+
} catch {
|
|
1811
|
+
return {
|
|
1812
|
+
ok: true,
|
|
1813
|
+
observationId: `archive_${canonicalHostWorkerDigest(evidence)}`,
|
|
1814
|
+
confirmedSteps,
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
if (
|
|
1818
|
+
revoke.operationId !== operation.id || revoke.fence !== operation.fence ||
|
|
1819
|
+
!revoke.acknowledged
|
|
1820
|
+
) throw new Error("archive revocation evidence rejected");
|
|
1821
|
+
evidence.revoke = revoke;
|
|
1822
|
+
confirmedSteps.push("ACK_REVOKE", "OBSERVE_ARCHIVED");
|
|
1823
|
+
return {
|
|
1824
|
+
ok: true,
|
|
1825
|
+
observationId: `archive_${canonicalHostWorkerDigest({ operation, evidence })}`,
|
|
1826
|
+
confirmedSteps,
|
|
1827
|
+
};
|
|
1828
|
+
} catch {
|
|
1829
|
+
return { ok: false };
|
|
1830
|
+
}
|
|
1831
|
+
},
|
|
1832
|
+
async persistProgress({ state }) { persistRuntimeState(config, context.activeClaim, state); return { persisted: true }; },
|
|
1833
|
+
});
|
|
1834
|
+
return ports;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
export function createPinnedHostWorkerAdapter(config, suppliedPorts) {
|
|
1838
|
+
const actionPorts = suppliedPorts ?? createLocalPinnedActionPorts(config);
|
|
1839
|
+
return {
|
|
1840
|
+
async requiresSecret({ activeClaim }) {
|
|
1841
|
+
return requiredSecretSlots(activeClaim);
|
|
1842
|
+
},
|
|
1843
|
+
async reconcile({ activeClaim, effectId }) {
|
|
1844
|
+
const receiptPath = confinedReceiptPath(config, effectId);
|
|
1845
|
+
if (existsSync(receiptPath)) return { status: "RECEIPT", effectId };
|
|
1846
|
+
const profileId = deriveContainedProfileId(
|
|
1847
|
+
activeClaim.operation.workspaceId, activeClaim.operation.agentId, config.hostId
|
|
1848
|
+
);
|
|
1849
|
+
const profilesRoot = privateRootForAdapter(config.profilesRoot);
|
|
1850
|
+
return profilesRoot && existsSync(join(profilesRoot, profileId, "agent-profile.json"))
|
|
1851
|
+
? { status: "PROMOTED", effectId }
|
|
1852
|
+
: { status: "ABSENT", effectId };
|
|
1853
|
+
},
|
|
1854
|
+
async apply({ activeClaim, requestFile, effectId }) {
|
|
1855
|
+
const secretConsumed = typeof requestFile === "string";
|
|
1856
|
+
let outcome;
|
|
1857
|
+
try {
|
|
1858
|
+
const action = activeClaim.operation.action;
|
|
1859
|
+
const compiled = compileClaimToProfileDesired(activeClaim, config);
|
|
1860
|
+
if (!compiled.ok) {
|
|
1861
|
+
outcome = { ok: false, status: "DESIRED_REJECTED", code: compiled.code };
|
|
1862
|
+
} else {
|
|
1863
|
+
const desired = compiled.desired;
|
|
1864
|
+
const payload = requestFile ? JSON.parse(readFileSync(requestFile, "utf8")) : null;
|
|
1865
|
+
if (secretConsumed && (payload.operationId !== activeClaim.operation.id || payload.fence !== activeClaim.operation.fence)) {
|
|
1866
|
+
throw new Error("request binding invalid");
|
|
1867
|
+
}
|
|
1868
|
+
for (const slot of requiredSecretSlots(activeClaim)) {
|
|
1869
|
+
const envelope = activeClaim.secretEnvelopes[slot];
|
|
1870
|
+
const secretKey = slot === "mcpService" ? "serviceCredential" : "slackBotCredential";
|
|
1871
|
+
const prefix = slot === "mcpService" ? "mcpService" : "slackBot";
|
|
1872
|
+
const secret = payload?.[secretKey];
|
|
1873
|
+
if (
|
|
1874
|
+
!envelope || payload?.[`${prefix}EnvelopeId`] !== envelope.id ||
|
|
1875
|
+
payload?.[`${prefix}Generation`] !== envelope.generation ||
|
|
1876
|
+
typeof secret !== "string" ||
|
|
1877
|
+
(slot === "mcpService" ? !secret.startsWith("sat_") : !secret.startsWith("xoxb-")) ||
|
|
1878
|
+
createHash("sha256").update(secret).digest("hex").slice(0, 16) !== envelope.fingerprint
|
|
1879
|
+
) throw new Error("request binding invalid");
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
const materialize = () => materializeAgentProfile({
|
|
1883
|
+
desired,
|
|
1884
|
+
operationId: activeClaim.operation.id,
|
|
1885
|
+
effectId,
|
|
1886
|
+
profilesRoot: config.profilesRoot,
|
|
1887
|
+
runtimeSecretsRoot: config.runtimeSecretsRoot,
|
|
1888
|
+
runtimeIdentity: {
|
|
1889
|
+
user: "sellable-agent-runtime",
|
|
1890
|
+
uid: config.runtimeUid,
|
|
1891
|
+
gid: config.runtimeGid,
|
|
1892
|
+
},
|
|
1893
|
+
serviceCredential: serviceCredentialFor(desired, payload?.serviceCredential),
|
|
1894
|
+
ports: actionPorts.materializer,
|
|
1895
|
+
});
|
|
1896
|
+
const currentState = readRuntimeState(config, activeClaim);
|
|
1897
|
+
actionPorts.bind?.({
|
|
1898
|
+
activeClaim,
|
|
1899
|
+
desired,
|
|
1900
|
+
currentState,
|
|
1901
|
+
profileId: deriveContainedProfileId(desired.workspaceId, desired.agentId, desired.hostId),
|
|
1902
|
+
requestFile,
|
|
1903
|
+
payload,
|
|
1904
|
+
effectId,
|
|
1905
|
+
});
|
|
1906
|
+
if (action === "INSTANTIATE") {
|
|
1907
|
+
outcome = await materialize();
|
|
1908
|
+
if (outcome.ok) persistRuntimeState(config, activeClaim, initialRuntimeState(activeClaim, desired, outcome));
|
|
1909
|
+
} else if (action === "ROTATE_CREDENTIAL") {
|
|
1910
|
+
if (!currentState || desired.serviceCredentialGeneration <= currentState.credentialGeneration) {
|
|
1911
|
+
outcome = { ok: false, status: "SUCCESSOR_REQUIRED", code: "SUCCESSOR_REQUIRED" };
|
|
1912
|
+
} else {
|
|
1913
|
+
outcome = await materialize();
|
|
1914
|
+
if (outcome.ok) persistRuntimeState(config, activeClaim, {
|
|
1915
|
+
...initialRuntimeState(activeClaim, desired, outcome),
|
|
1916
|
+
receipts: currentState.receipts,
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
} else if (action === "REFRESH") {
|
|
1920
|
+
if (!currentState) {
|
|
1921
|
+
outcome = { ok: false, status: "STATE_REJECTED", code: "STATE_REJECTED" };
|
|
1922
|
+
} else {
|
|
1923
|
+
const approvedTarget = approvedRuntimeTuple(desired, activeClaim.operation, currentState);
|
|
1924
|
+
let liveObserved;
|
|
1925
|
+
try {
|
|
1926
|
+
liveObserved = await actionPorts.refresh.observe({
|
|
1927
|
+
operation: { id: activeClaim.operation.id, fence: activeClaim.operation.fence },
|
|
1928
|
+
target: currentState.observedTuple,
|
|
1929
|
+
classification: true,
|
|
1930
|
+
});
|
|
1931
|
+
if (!projectRuntimeDriftTuple(liveObserved)) throw new Error("live refresh observation rejected");
|
|
1932
|
+
} catch {
|
|
1933
|
+
liveObserved = {
|
|
1934
|
+
...currentState.observedTuple,
|
|
1935
|
+
packageHash: canonicalHostWorkerDigest({
|
|
1936
|
+
invalidLiveObservation: true,
|
|
1937
|
+
operationId: activeClaim.operation.id,
|
|
1938
|
+
fence: activeClaim.operation.fence,
|
|
1939
|
+
}),
|
|
1940
|
+
processHash: null,
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
outcome = await reconcileRuntimeRefresh({
|
|
1944
|
+
desired: approvedTarget,
|
|
1945
|
+
approvedDesired: approvedTarget,
|
|
1946
|
+
observed: liveObserved,
|
|
1947
|
+
previousKnownGood: currentState.previousKnownGoodTuple,
|
|
1948
|
+
operation: { id: activeClaim.operation.id, fence: activeClaim.operation.fence },
|
|
1949
|
+
ports: actionPorts.refresh,
|
|
1950
|
+
});
|
|
1951
|
+
if (outcome.ok) persistRuntimeState(config, activeClaim, {
|
|
1952
|
+
...currentState,
|
|
1953
|
+
desiredRevisionId: desired.revisionId,
|
|
1954
|
+
desiredRevisionSequence: activeClaim.operation.lifecycleGeneration,
|
|
1955
|
+
observedRevisionId: desired.revisionId,
|
|
1956
|
+
credentialGeneration: desired.serviceCredentialGeneration,
|
|
1957
|
+
profileDigest: approvedTarget.contentHash,
|
|
1958
|
+
acceptedFence: activeClaim.operation.fence,
|
|
1959
|
+
desiredTuple: outcome.observed,
|
|
1960
|
+
observedTuple: outcome.observed,
|
|
1961
|
+
previousKnownGoodTuple: outcome.observed,
|
|
1962
|
+
runtimeHashes: tupleRuntimeHashes(outcome.observed),
|
|
1963
|
+
});
|
|
1964
|
+
outcome = {
|
|
1965
|
+
...outcome,
|
|
1966
|
+
observedLifecycle: outcome.observed?.lifecycle ?? currentState.observedLifecycle,
|
|
1967
|
+
runtimeHashes: outcome.ok ? tupleRuntimeHashes(outcome.observed) : currentState.runtimeHashes,
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
} else if (["ENABLE_GATEWAY", "DISABLE_GATEWAY", "ARCHIVE", "RESTORE"].includes(action)) {
|
|
1971
|
+
if (!currentState) {
|
|
1972
|
+
outcome = { ok: false, status: "STATE_REJECTED", code: "STATE_REJECTED" };
|
|
1973
|
+
} else {
|
|
1974
|
+
const restoring = action === "RESTORE";
|
|
1975
|
+
const computedProfile = computeAgentProfileDigest(desired);
|
|
1976
|
+
if (!computedProfile.ok) throw new Error("profile digest rejected");
|
|
1977
|
+
const operation = lifecycleOperation(activeClaim, currentState, {
|
|
1978
|
+
serviceCredentialGeneration: restoring
|
|
1979
|
+
? desired.serviceCredentialGeneration
|
|
1980
|
+
: currentState.credentialGeneration,
|
|
1981
|
+
profileDigest: restoring ? computedProfile.profileDigest : currentState.profileDigest,
|
|
1982
|
+
});
|
|
1983
|
+
if (restoring) {
|
|
1984
|
+
operation.targetRuntimeTuple = approvedRuntimeTuple(desired, activeClaim.operation, {
|
|
1985
|
+
...currentState,
|
|
1986
|
+
observedLifecycle: "ON",
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
const lifecyclePorts = restoring ? {
|
|
1990
|
+
...actionPorts.lifecycle,
|
|
1991
|
+
rehydrate: (input) => actionPorts.lifecycle.rehydrate({
|
|
1992
|
+
...input,
|
|
1993
|
+
desired,
|
|
1994
|
+
serviceCredential: serviceCredentialFor(desired, payload?.serviceCredential),
|
|
1995
|
+
}),
|
|
1996
|
+
} : actionPorts.lifecycle;
|
|
1997
|
+
outcome = await applyRuntimeLifecycle({ state: currentState, operation, ports: lifecyclePorts });
|
|
1998
|
+
if (outcome.state) persistRuntimeState(config, activeClaim, outcome.state);
|
|
1999
|
+
outcome = {
|
|
2000
|
+
...outcome,
|
|
2001
|
+
observedLifecycle: outcome.state?.observedLifecycle ?? currentState.observedLifecycle,
|
|
2002
|
+
runtimeHashes: restoring && outcome.ok
|
|
2003
|
+
? outcome.state?.runtimeHashes ?? null
|
|
2004
|
+
: currentState.runtimeHashes,
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
} else {
|
|
2008
|
+
outcome = { ok: false, status: "ACTION_REJECTED", code: "ACTION_REJECTED" };
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
} catch {
|
|
2012
|
+
outcome = { ok: false, status: "UNCERTAIN", code: "HOST_RUNTIME" };
|
|
2013
|
+
}
|
|
2014
|
+
const completion = localCompletionReceipt({ activeClaim, config, outcome, effectId, secretConsumed });
|
|
2015
|
+
atomicPrivateJson(confinedReceiptPath(config, effectId), completion);
|
|
2016
|
+
return { ok: outcome.ok === true, status: outcome.status };
|
|
2017
|
+
},
|
|
2018
|
+
async readReceipt({ effectId }) {
|
|
2019
|
+
const pathValue = confinedReceiptPath(config, effectId);
|
|
2020
|
+
if (!existsSync(pathValue)) return null;
|
|
2021
|
+
const link = lstatSync(pathValue);
|
|
2022
|
+
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600) return null;
|
|
2023
|
+
return JSON.parse(readFileSync(pathValue, "utf8"));
|
|
2024
|
+
},
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function privateRootForAdapter(pathValue) {
|
|
2029
|
+
try {
|
|
2030
|
+
if (!isAbsolute(pathValue) || !existsSync(pathValue)) return null;
|
|
2031
|
+
const link = lstatSync(pathValue);
|
|
2032
|
+
const mode = link.mode & 0o777;
|
|
2033
|
+
if (
|
|
2034
|
+
link.isSymbolicLink() || !link.isDirectory() ||
|
|
2035
|
+
(mode & 0o700) !== 0o700 || (mode & 0o066) !== 0
|
|
2036
|
+
) return null;
|
|
2037
|
+
return realpathSync(pathValue);
|
|
2038
|
+
} catch {
|
|
2039
|
+
return null;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
export async function runHostWorkerMain({ argv, adapter, fetchImpl, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) }) {
|
|
2044
|
+
try {
|
|
2045
|
+
const configPath = parseMainArguments(argv ?? []);
|
|
2046
|
+
if (!configPath) return { ok: false, code: "usage: --config /absolute/path" };
|
|
2047
|
+
const link = lstatSync(configPath);
|
|
2048
|
+
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o022) !== 0) {
|
|
2049
|
+
return { ok: false, code: "worker_config_confinement_failed" };
|
|
2050
|
+
}
|
|
2051
|
+
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
2052
|
+
const checked = validateHostWorkerConfig(config);
|
|
2053
|
+
if (!checked.ok) return { ok: false, code: checked.code };
|
|
2054
|
+
const selectedAdapter = adapter ?? createPinnedHostWorkerAdapter(config);
|
|
2055
|
+
const broker = createHttpWorkerBroker(config, fetchImpl);
|
|
2056
|
+
let failures = 0;
|
|
2057
|
+
while (true) {
|
|
2058
|
+
const result = await runHostWorkerCycle({ config, broker, adapter: selectedAdapter });
|
|
2059
|
+
failures = result.ok ? 0 : Math.min(failures + 1, 8);
|
|
2060
|
+
const base = Math.min(config.poll.maxDelayMs, config.poll.minDelayMs * 2 ** failures);
|
|
2061
|
+
const jitter = Math.floor(base * config.poll.jitterRatio * Math.random());
|
|
2062
|
+
await sleep(base + jitter);
|
|
2063
|
+
}
|
|
2064
|
+
} catch {
|
|
2065
|
+
return { ok: false, code: "host_worker_start_failed" };
|
|
2066
|
+
}
|
|
2067
|
+
}
|