@lazyingart/agintiflow 0.20.331 → 0.20.336-integration.2
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/README.md +2 -0
- package/bin/aginti-document-worker-runtime-check.js +12 -0
- package/bin/aginti-document-worker.js +10 -0
- package/bin/aginti-execution-worker.js +26 -0
- package/bin/aginti-integration-analysis-state.js +10 -0
- package/bin/aginti-integration-analysis.js +7 -0
- package/bin/aginti-public-research.js +1 -0
- package/bin/aginti-worker-directory.js +178 -0
- package/docs/integration-analysis-state-migration.md +47 -0
- package/docs/integration-deep-research.md +9 -0
- package/docs/integration-worker-directory.md +99 -0
- package/docs/labcanvas-chatops-fallback.md +139 -0
- package/docs/supervision-campaign-ledger.md +1851 -0
- package/examples/document-worker/README.md +84 -0
- package/examples/document-worker/aginti-document-worker.service +67 -0
- package/examples/document-worker/document-worker.json +12 -0
- package/examples/document-worker/runtime-manifest.json +28 -0
- package/package.json +89 -4
- package/scripts/fixtures/integration-document-worker-smoke-fixture.js +190 -0
- package/scripts/fixtures/loopback-test-port.mjs +16 -0
- package/scripts/github-hosted-release-environment.js +22 -0
- package/scripts/live-production-planner-smoke.js +70 -0
- package/scripts/smoke-canvas-artifacts.js +203 -0
- package/scripts/smoke-coding-tools.js +676 -1
- package/scripts/smoke-docker-command.js +24 -0
- package/scripts/smoke-dynamic-step-budget.js +1032 -27
- package/scripts/smoke-execution-runtime-bundle.js +248 -0
- package/scripts/smoke-execution-worker-server.js +138 -0
- package/scripts/smoke-execution-worker-systemd-boundary.js +545 -0
- package/scripts/smoke-execution-worker.js +791 -0
- package/scripts/smoke-integration-analysis-api-server.js +1038 -0
- package/scripts/smoke-integration-analysis-coordinator.js +1035 -0
- package/scripts/smoke-integration-analysis-planner.js +5366 -0
- package/scripts/smoke-integration-analysis-session-authority-contract.js +223 -0
- package/scripts/smoke-integration-analysis-session-service.js +4628 -0
- package/scripts/smoke-integration-analysis-startup-recovery.js +846 -0
- package/scripts/smoke-integration-analysis-state-migrator.js +765 -0
- package/scripts/smoke-integration-analysis-state-prewrite-gate.js +1170 -0
- package/scripts/smoke-integration-api.js +2275 -0
- package/scripts/smoke-integration-authorities.js +3338 -0
- package/scripts/smoke-integration-document-artifacts.js +431 -0
- package/scripts/smoke-integration-document-content.js +244 -0
- package/scripts/smoke-integration-document-revision.js +909 -0
- package/scripts/smoke-integration-document-session-broker.js +1374 -0
- package/scripts/smoke-integration-document-worker-contract.js +197 -0
- package/scripts/smoke-integration-document-worker-cross-boundary.js +209 -0
- package/scripts/smoke-integration-document-worker-longevity.js +257 -0
- package/scripts/smoke-integration-document-worker-real-compile.js +138 -0
- package/scripts/smoke-integration-document-worker-server.js +411 -0
- package/scripts/smoke-integration-document-worker-service.js +214 -0
- package/scripts/smoke-integration-document-worker-store.js +419 -0
- package/scripts/smoke-integration-document-worker-systemd-boundary.js +353 -0
- package/scripts/smoke-integration-execution-worker-binding-config.js +102 -0
- package/scripts/smoke-integration-grounded-search.js +1355 -0
- package/scripts/smoke-integration-sandbox-profile.js +982 -0
- package/scripts/smoke-integration-worker-directory-cli.js +37 -0
- package/scripts/smoke-integration-worker-directory.js +399 -0
- package/scripts/smoke-local-failure-recovery.js +28 -0
- package/scripts/smoke-math-rendering.js +2 -1
- package/scripts/smoke-model-roles.js +46 -0
- package/scripts/smoke-public-research-wrapper.js +11 -3
- package/scripts/smoke-runtime-compat.js +8 -1
- package/scripts/smoke-scs-evidence-visibility.js +65 -0
- package/scripts/smoke-session-runtime.js +28 -0
- package/scripts/smoke-skills.js +174 -0
- package/scripts/smoke-tmux-tools.js +26 -0
- package/scripts/smoke-toolchain-docker.js +18 -0
- package/scripts/smoke-truthful-completion.js +3547 -117
- package/scripts/smoke-web-api.js +11 -2
- package/scripts/smoke-web-ui.js +11 -2
- package/scripts/smoke-webapp-command.js +67 -25
- package/scripts/test-document-worker-fixture.js +426 -0
- package/skills/deep-research/SKILL.md +1 -0
- package/src/agent-runner.js +3552 -174
- package/src/config.js +11 -5
- package/src/docker-sandbox.js +51 -9
- package/src/execution-runtime-bundle.js +639 -0
- package/src/execution-worker-api.js +245 -0
- package/src/execution-worker-client.js +1017 -0
- package/src/execution-worker-jobs.js +399 -0
- package/src/execution-worker-server.js +239 -0
- package/src/execution-worker-systemd-boundary.js +654 -0
- package/src/execution-worker.js +1453 -0
- package/src/guardrails.js +8 -1
- package/src/integration-analysis-cli.js +255 -0
- package/src/integration-analysis-config.js +614 -0
- package/src/integration-analysis-coordinator.js +529 -0
- package/src/integration-analysis-planner.js +4647 -0
- package/src/integration-analysis-server.js +778 -0
- package/src/integration-analysis-session-contract.js +18 -0
- package/src/integration-analysis-session-service.js +8269 -0
- package/src/integration-analysis-state-migration-cli.js +366 -0
- package/src/integration-analysis-state-migrator.js +591 -0
- package/src/integration-analysis-state-persistence.js +17 -0
- package/src/integration-analysis-vision.js +1086 -0
- package/src/integration-api.js +1505 -38
- package/src/integration-artifacts.js +235 -7
- package/src/integration-auth.js +1 -1
- package/src/integration-core-event-projector.js +10 -0
- package/src/integration-document-artifacts.js +492 -0
- package/src/integration-document-blob-store.js +226 -0
- package/src/integration-document-worker-cli.js +186 -0
- package/src/integration-document-worker-client.js +1719 -0
- package/src/integration-document-worker-config.js +299 -0
- package/src/integration-document-worker-contract.js +790 -0
- package/src/integration-document-worker-requirements.js +119 -0
- package/src/integration-document-worker-runtime.js +199 -0
- package/src/integration-document-worker-server.js +565 -0
- package/src/integration-document-worker-service.js +448 -0
- package/src/integration-document-worker-store.js +1837 -0
- package/src/integration-durable-common.js +586 -68
- package/src/integration-events.js +12 -2
- package/src/integration-execution-worker-binding-config.js +223 -0
- package/src/integration-execution-worker-router.js +428 -0
- package/src/integration-explicit-python.js +247 -0
- package/src/integration-expression-plot.js +361 -0
- package/src/integration-file-worker-client.js +725 -0
- package/src/integration-file-worker-contract.js +559 -0
- package/src/integration-file-worker-store.js +1100 -0
- package/src/integration-grounded-search.js +2612 -0
- package/src/integration-idempotency-store.js +429 -10
- package/src/integration-policy.js +242 -8
- package/src/integration-tex-compiler.js +654 -0
- package/src/integration-worker-directory.js +1015 -0
- package/src/json-extraction.js +66 -0
- package/src/model-client.js +293 -2
- package/src/perception-tools.js +3 -63
- package/src/permission-advice.js +152 -3
- package/src/public-research-server.js +1 -1
- package/src/response-only-source-quality.js +356 -0
- package/src/scs-controller.js +37 -1
- package/src/scs-evidence.js +516 -32
- package/src/session-runtime.js +8 -0
- package/src/skill-library.js +81 -5
- package/src/step-budget-controller.js +175 -18
- package/src/tmux-tools.js +10 -1
- package/src/web-query-privacy.js +44 -0
- package/src/web-search.js +11 -0
- package/src/workspace-tools.js +92 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { types as utilTypes } from "node:util";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ExecutionWorkerError,
|
|
5
|
+
executionJobRequestDigest,
|
|
6
|
+
validateExecutionJobRequest,
|
|
7
|
+
validateExecutionResult,
|
|
8
|
+
} from "./execution-worker.js";
|
|
9
|
+
import { contractDigest } from "./integration-policy.js";
|
|
10
|
+
|
|
11
|
+
export const EXECUTION_JOB_MANAGER_SCHEMA_VERSION = "aginti-execution-job-manager-v1";
|
|
12
|
+
export const EXECUTION_EVENT_SCHEMA_VERSION = "aginti-execution-event-v1";
|
|
13
|
+
export const EXECUTION_ZERO_EVENT_HASH = "0".repeat(64);
|
|
14
|
+
export const EXECUTION_MAX_RETAINED_JOBS = 128;
|
|
15
|
+
|
|
16
|
+
const JOB_ID = /^job_[A-Za-z0-9_-]{24,96}$/u;
|
|
17
|
+
const ARTIFACT_ID = /^art_[A-Za-z0-9_-]{32,86}$/u;
|
|
18
|
+
const DIGEST = /^[a-f0-9]{64}$/u;
|
|
19
|
+
const TERMINAL_STATES = new Set([
|
|
20
|
+
"succeeded",
|
|
21
|
+
"failed",
|
|
22
|
+
"timed_out",
|
|
23
|
+
"output_limited",
|
|
24
|
+
"cancelled",
|
|
25
|
+
"sandbox_error",
|
|
26
|
+
"artifact_invalid",
|
|
27
|
+
"termination_unproven",
|
|
28
|
+
"worker_error",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function fail(code, message, { status = 400, details } = {}) {
|
|
32
|
+
throw new ExecutionWorkerError(code, message, { status, details });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function exactObject(input, allowed, required, label) {
|
|
36
|
+
if (
|
|
37
|
+
!input || typeof input !== "object" || Array.isArray(input) || utilTypes.isProxy(input) ||
|
|
38
|
+
(Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null)
|
|
39
|
+
) {
|
|
40
|
+
fail("EXECUTION_REQUEST_INVALID", `${label} must be a plain data object.`);
|
|
41
|
+
}
|
|
42
|
+
const keys = Reflect.ownKeys(input);
|
|
43
|
+
if (keys.some((key) => typeof key !== "string" || !allowed.includes(key))) {
|
|
44
|
+
fail("EXECUTION_REQUEST_INVALID", `${label} contains an unsupported field.`);
|
|
45
|
+
}
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, key);
|
|
48
|
+
if (!descriptor?.enumerable || !Object.prototype.hasOwnProperty.call(descriptor, "value")) {
|
|
49
|
+
fail("EXECUTION_REQUEST_INVALID", `${label} must contain only data fields.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const key of required) {
|
|
53
|
+
if (!Object.prototype.hasOwnProperty.call(input, key)) {
|
|
54
|
+
fail("EXECUTION_REQUEST_INVALID", `${label}.${key} is required.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return input;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function reference(input, label = "execution job reference") {
|
|
61
|
+
const value = exactObject(input, ["jobId", "attempt"], ["jobId", "attempt"], label);
|
|
62
|
+
if (typeof value.jobId !== "string" || !JOB_ID.test(value.jobId)) {
|
|
63
|
+
fail("EXECUTION_REQUEST_INVALID", `${label}.jobId is invalid.`);
|
|
64
|
+
}
|
|
65
|
+
if (!Number.isSafeInteger(value.attempt) || value.attempt < 1 || value.attempt > 1_000_000) {
|
|
66
|
+
fail("EXECUTION_REQUEST_INVALID", `${label}.attempt is invalid.`);
|
|
67
|
+
}
|
|
68
|
+
return Object.freeze({ jobId: value.jobId, attempt: value.attempt });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function recordKey({ jobId, attempt }) {
|
|
72
|
+
return `${jobId}:${attempt}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function timestamp(value) {
|
|
76
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/u.test(value)) {
|
|
77
|
+
fail("EXECUTION_CLOCK_INVALID", "execution worker clock returned an invalid timestamp.", { status: 503 });
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function eventData(value) {
|
|
83
|
+
return Object.freeze(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function appendEvent(record, type, data, now) {
|
|
87
|
+
const previous = record.events.at(-1);
|
|
88
|
+
const unsigned = Object.freeze({
|
|
89
|
+
schemaVersion: EXECUTION_EVENT_SCHEMA_VERSION,
|
|
90
|
+
jobId: record.jobId,
|
|
91
|
+
attempt: record.attempt,
|
|
92
|
+
seq: record.events.length + 1,
|
|
93
|
+
previousHash: previous?.eventHash || EXECUTION_ZERO_EVENT_HASH,
|
|
94
|
+
type,
|
|
95
|
+
timestamp: timestamp(now()),
|
|
96
|
+
data: eventData(data),
|
|
97
|
+
});
|
|
98
|
+
const event = Object.freeze({ ...unsigned, eventHash: contractDigest(unsigned) });
|
|
99
|
+
record.events.push(event);
|
|
100
|
+
record.updatedAt = event.timestamp;
|
|
101
|
+
return event;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function publicRecord(record, { reused = false } = {}) {
|
|
105
|
+
return Object.freeze({
|
|
106
|
+
schemaVersion: EXECUTION_JOB_MANAGER_SCHEMA_VERSION,
|
|
107
|
+
jobId: record.jobId,
|
|
108
|
+
attempt: record.attempt,
|
|
109
|
+
sourceSha256: record.sourceSha256,
|
|
110
|
+
state: record.state,
|
|
111
|
+
createdAt: record.createdAt,
|
|
112
|
+
updatedAt: record.updatedAt,
|
|
113
|
+
reused,
|
|
114
|
+
terminal: TERMINAL_STATES.has(record.state),
|
|
115
|
+
result: record.result,
|
|
116
|
+
errorCode: record.errorCode,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function workerErrorCode(error) {
|
|
121
|
+
if (error instanceof ExecutionWorkerError && typeof error.code === "string") return error.code;
|
|
122
|
+
return "EXECUTION_WORKER_FAILURE";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createExecutionJobManager({
|
|
126
|
+
worker,
|
|
127
|
+
now = () => new Date().toISOString(),
|
|
128
|
+
maximumRetainedJobs = EXECUTION_MAX_RETAINED_JOBS,
|
|
129
|
+
} = {}) {
|
|
130
|
+
if (!worker || typeof worker.capabilities !== "function" || typeof worker.execute !== "function") {
|
|
131
|
+
throw new TypeError("worker must provide capabilities and execute");
|
|
132
|
+
}
|
|
133
|
+
if (typeof now !== "function") throw new TypeError("now must be a function");
|
|
134
|
+
if (!Number.isSafeInteger(maximumRetainedJobs) || maximumRetainedJobs < 8 || maximumRetainedJobs > 1_024) {
|
|
135
|
+
throw new TypeError("maximumRetainedJobs is invalid");
|
|
136
|
+
}
|
|
137
|
+
const records = new Map();
|
|
138
|
+
let activeJobs = 0;
|
|
139
|
+
|
|
140
|
+
function evictTerminalRecords() {
|
|
141
|
+
if (records.size < maximumRetainedJobs) return;
|
|
142
|
+
for (const [key, record] of records) {
|
|
143
|
+
if (TERMINAL_STATES.has(record.state)) {
|
|
144
|
+
records.delete(key);
|
|
145
|
+
if (records.size < maximumRetainedJobs) return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (records.size >= maximumRetainedJobs) {
|
|
149
|
+
fail("EXECUTION_BUSY", "execution worker retention is full of active jobs.", { status: 429 });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function find(input, label) {
|
|
154
|
+
const normalized = reference(input, label);
|
|
155
|
+
const record = records.get(recordKey(normalized));
|
|
156
|
+
if (!record) fail("EXECUTION_JOB_NOT_FOUND", "execution job was not found.", { status: 404 });
|
|
157
|
+
return record;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function capabilities() {
|
|
161
|
+
const workerCapabilities = await worker.capabilities();
|
|
162
|
+
const protocol = Object.freeze({
|
|
163
|
+
schemaVersion: EXECUTION_JOB_MANAGER_SCHEMA_VERSION,
|
|
164
|
+
durable: false,
|
|
165
|
+
tenantBlind: true,
|
|
166
|
+
retainedJobs: maximumRetainedJobs,
|
|
167
|
+
eventLedger: Object.freeze({ schemaVersion: EXECUTION_EVENT_SCHEMA_VERSION, hashChained: true, pruned: false }),
|
|
168
|
+
routes: Object.freeze([
|
|
169
|
+
"capabilities", "jobs.start", "jobs.status", "jobs.events", "jobs.cancel",
|
|
170
|
+
"artifacts.list", "artifacts.get",
|
|
171
|
+
]),
|
|
172
|
+
});
|
|
173
|
+
const workerMaximum = workerCapabilities.admission?.maximumConcurrentJobs;
|
|
174
|
+
const maximumConcurrentJobs = Number.isSafeInteger(workerMaximum) && workerMaximum > 0
|
|
175
|
+
? workerMaximum
|
|
176
|
+
: 0;
|
|
177
|
+
const state = workerCapabilities.ready !== true
|
|
178
|
+
? (workerCapabilities.admission?.state || "blocked")
|
|
179
|
+
: activeJobs < maximumConcurrentJobs ? "ready" : "busy";
|
|
180
|
+
const coordinatorAdmission = Object.freeze({
|
|
181
|
+
state,
|
|
182
|
+
activeJobs,
|
|
183
|
+
maximumConcurrentJobs,
|
|
184
|
+
workerAdmission: workerCapabilities.admission,
|
|
185
|
+
});
|
|
186
|
+
const ready = state === "ready";
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
...workerCapabilities,
|
|
189
|
+
ready,
|
|
190
|
+
admission: coordinatorAdmission,
|
|
191
|
+
coordinatorProtocol: protocol,
|
|
192
|
+
coordinatorProtocolDigest: contractDigest(protocol),
|
|
193
|
+
coordinatorHealthDigest: contractDigest({ ready, admission: coordinatorAdmission }),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function start(input) {
|
|
198
|
+
const request = validateExecutionJobRequest(input);
|
|
199
|
+
const key = recordKey(request);
|
|
200
|
+
const requestDigest = executionJobRequestDigest(request);
|
|
201
|
+
const existing = records.get(key);
|
|
202
|
+
if (existing) {
|
|
203
|
+
if (existing.requestDigest !== requestDigest) {
|
|
204
|
+
fail("EXECUTION_IDEMPOTENCY_CONFLICT", "execution job identity was reused with different input.", { status: 409 });
|
|
205
|
+
}
|
|
206
|
+
return publicRecord(existing, { reused: true });
|
|
207
|
+
}
|
|
208
|
+
const currentCapabilities = await worker.capabilities();
|
|
209
|
+
const raced = records.get(key);
|
|
210
|
+
if (raced) {
|
|
211
|
+
if (raced.requestDigest !== requestDigest) {
|
|
212
|
+
fail("EXECUTION_IDEMPOTENCY_CONFLICT", "execution job identity was reused with different input.", { status: 409 });
|
|
213
|
+
}
|
|
214
|
+
return publicRecord(raced, { reused: true });
|
|
215
|
+
}
|
|
216
|
+
if (currentCapabilities.ready !== true) {
|
|
217
|
+
const busy = currentCapabilities.admission?.state === "busy";
|
|
218
|
+
fail(
|
|
219
|
+
busy ? "EXECUTION_BUSY" : "EXECUTION_UNAVAILABLE",
|
|
220
|
+
busy ? "execution worker has no available slot." : "execution worker is not ready for admitted jobs.",
|
|
221
|
+
{ status: busy ? 429 : 503 }
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const maximumConcurrentJobs = currentCapabilities.admission?.maximumConcurrentJobs;
|
|
225
|
+
if (!Number.isSafeInteger(maximumConcurrentJobs) || maximumConcurrentJobs < 1 || maximumConcurrentJobs > 64) {
|
|
226
|
+
fail("EXECUTION_UNAVAILABLE", "execution worker admission contract is invalid.", { status: 503 });
|
|
227
|
+
}
|
|
228
|
+
if (activeJobs >= maximumConcurrentJobs) {
|
|
229
|
+
fail("EXECUTION_BUSY", "execution worker has no available slot.", { status: 429 });
|
|
230
|
+
}
|
|
231
|
+
evictTerminalRecords();
|
|
232
|
+
const createdAt = timestamp(now());
|
|
233
|
+
const record = {
|
|
234
|
+
jobId: request.jobId,
|
|
235
|
+
attempt: request.attempt,
|
|
236
|
+
sourceSha256: request.sourceSha256,
|
|
237
|
+
requestDigest,
|
|
238
|
+
request,
|
|
239
|
+
state: "running",
|
|
240
|
+
createdAt,
|
|
241
|
+
updatedAt: createdAt,
|
|
242
|
+
result: null,
|
|
243
|
+
errorCode: null,
|
|
244
|
+
controller: new AbortController(),
|
|
245
|
+
events: [],
|
|
246
|
+
completion: null,
|
|
247
|
+
};
|
|
248
|
+
activeJobs += 1;
|
|
249
|
+
try {
|
|
250
|
+
records.set(key, record);
|
|
251
|
+
appendEvent(record, "job.started", {
|
|
252
|
+
sourceSha256: request.sourceSha256,
|
|
253
|
+
timeoutMs: request.timeoutMs,
|
|
254
|
+
language: request.language,
|
|
255
|
+
}, now);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
records.delete(key);
|
|
258
|
+
activeJobs -= 1;
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
record.completion = Promise.resolve()
|
|
262
|
+
.then(() => worker.execute(request, { signal: record.controller.signal }))
|
|
263
|
+
.then((result) => {
|
|
264
|
+
const validatedResult = validateExecutionResult(result, {
|
|
265
|
+
jobId: record.jobId,
|
|
266
|
+
attempt: record.attempt,
|
|
267
|
+
sourceSha256: record.sourceSha256,
|
|
268
|
+
});
|
|
269
|
+
record.state = validatedResult.status;
|
|
270
|
+
record.result = validatedResult;
|
|
271
|
+
record.request = null;
|
|
272
|
+
appendEvent(record, "job.terminal", {
|
|
273
|
+
status: validatedResult.status,
|
|
274
|
+
resultDigest: validatedResult.resultDigest,
|
|
275
|
+
artifactIds: Object.freeze(validatedResult.artifacts.map(({ id }) => id)),
|
|
276
|
+
}, now);
|
|
277
|
+
return publicRecord(record);
|
|
278
|
+
})
|
|
279
|
+
.catch((error) => {
|
|
280
|
+
record.state = "worker_error";
|
|
281
|
+
record.errorCode = workerErrorCode(error);
|
|
282
|
+
record.request = null;
|
|
283
|
+
appendEvent(record, "job.terminal", {
|
|
284
|
+
status: "worker_error",
|
|
285
|
+
errorCode: record.errorCode,
|
|
286
|
+
artifactIds: Object.freeze([]),
|
|
287
|
+
}, now);
|
|
288
|
+
return publicRecord(record);
|
|
289
|
+
})
|
|
290
|
+
.finally(() => {
|
|
291
|
+
record.controller = null;
|
|
292
|
+
activeJobs -= 1;
|
|
293
|
+
});
|
|
294
|
+
return publicRecord(record);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function status(input) {
|
|
298
|
+
return publicRecord(find(input, "execution status request"));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function events(input) {
|
|
302
|
+
const request = exactObject(
|
|
303
|
+
input,
|
|
304
|
+
["jobId", "attempt", "afterSeq", "afterHash"],
|
|
305
|
+
["jobId", "attempt", "afterSeq", "afterHash"],
|
|
306
|
+
"execution events request"
|
|
307
|
+
);
|
|
308
|
+
const record = find({ jobId: request.jobId, attempt: request.attempt }, "execution events request");
|
|
309
|
+
if (!Number.isSafeInteger(request.afterSeq) || request.afterSeq < 0 || request.afterSeq > record.events.length) {
|
|
310
|
+
fail("EXECUTION_CURSOR_INVALID", "execution event cursor is invalid.", { status: 409 });
|
|
311
|
+
}
|
|
312
|
+
if (typeof request.afterHash !== "string" || !DIGEST.test(request.afterHash)) {
|
|
313
|
+
fail("EXECUTION_CURSOR_INVALID", "execution event cursor hash is invalid.", { status: 409 });
|
|
314
|
+
}
|
|
315
|
+
const expectedHash = request.afterSeq === 0
|
|
316
|
+
? EXECUTION_ZERO_EVENT_HASH
|
|
317
|
+
: record.events[request.afterSeq - 1].eventHash;
|
|
318
|
+
if (request.afterHash !== expectedHash) {
|
|
319
|
+
fail("EXECUTION_CURSOR_CONFLICT", "execution event cursor does not match the retained ledger.", { status: 409 });
|
|
320
|
+
}
|
|
321
|
+
const replay = Object.freeze(record.events.slice(request.afterSeq));
|
|
322
|
+
const cursor = record.events.at(-1);
|
|
323
|
+
return Object.freeze({
|
|
324
|
+
schemaVersion: EXECUTION_JOB_MANAGER_SCHEMA_VERSION,
|
|
325
|
+
jobId: record.jobId,
|
|
326
|
+
attempt: record.attempt,
|
|
327
|
+
events: replay,
|
|
328
|
+
cursor: Object.freeze({
|
|
329
|
+
seq: cursor?.seq || 0,
|
|
330
|
+
hash: cursor?.eventHash || EXECUTION_ZERO_EVENT_HASH,
|
|
331
|
+
}),
|
|
332
|
+
terminal: TERMINAL_STATES.has(record.state),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cancel(input) {
|
|
337
|
+
const record = find(input, "execution cancel request");
|
|
338
|
+
if (TERMINAL_STATES.has(record.state)) return publicRecord(record, { reused: true });
|
|
339
|
+
if (record.state !== "cancelling") {
|
|
340
|
+
record.state = "cancelling";
|
|
341
|
+
appendEvent(record, "job.cancel_requested", {}, now);
|
|
342
|
+
record.controller?.abort();
|
|
343
|
+
}
|
|
344
|
+
return publicRecord(record);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function listArtifacts(input) {
|
|
348
|
+
const record = find(input, "execution artifact list request");
|
|
349
|
+
const artifacts = Object.freeze(record.result?.status === "succeeded" ? [...record.result.artifacts] : []);
|
|
350
|
+
return Object.freeze({
|
|
351
|
+
schemaVersion: EXECUTION_JOB_MANAGER_SCHEMA_VERSION,
|
|
352
|
+
jobId: record.jobId,
|
|
353
|
+
attempt: record.attempt,
|
|
354
|
+
terminal: TERMINAL_STATES.has(record.state),
|
|
355
|
+
artifacts,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function getArtifact(input) {
|
|
360
|
+
const request = exactObject(
|
|
361
|
+
input,
|
|
362
|
+
["jobId", "attempt", "artifactId"],
|
|
363
|
+
["jobId", "attempt", "artifactId"],
|
|
364
|
+
"execution artifact get request"
|
|
365
|
+
);
|
|
366
|
+
if (typeof request.artifactId !== "string" || !ARTIFACT_ID.test(request.artifactId)) {
|
|
367
|
+
fail("EXECUTION_REQUEST_INVALID", "execution artifactId is invalid.");
|
|
368
|
+
}
|
|
369
|
+
const record = find({ jobId: request.jobId, attempt: request.attempt }, "execution artifact get request");
|
|
370
|
+
const artifact = record.result?.status === "succeeded"
|
|
371
|
+
? record.result.artifacts.find(({ id }) => id === request.artifactId)
|
|
372
|
+
: null;
|
|
373
|
+
if (!artifact) fail("EXECUTION_ARTIFACT_NOT_FOUND", "execution artifact was not found.", { status: 404 });
|
|
374
|
+
return Object.freeze({
|
|
375
|
+
schemaVersion: EXECUTION_JOB_MANAGER_SCHEMA_VERSION,
|
|
376
|
+
jobId: record.jobId,
|
|
377
|
+
attempt: record.attempt,
|
|
378
|
+
artifact,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function waitForTerminal(input) {
|
|
383
|
+
const record = find(input, "execution wait request");
|
|
384
|
+
await record.completion;
|
|
385
|
+
return publicRecord(record);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return Object.freeze({
|
|
389
|
+
kind: "aginti-execution-job-manager",
|
|
390
|
+
capabilities,
|
|
391
|
+
start,
|
|
392
|
+
status,
|
|
393
|
+
events,
|
|
394
|
+
cancel,
|
|
395
|
+
listArtifacts,
|
|
396
|
+
getArtifact,
|
|
397
|
+
waitForTerminal,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { EXECUTION_RUNTIME_BUNDLE_PRODUCTION_ROOT } from "./execution-runtime-bundle.js";
|
|
7
|
+
import {
|
|
8
|
+
createExecutionWorkerApiServer,
|
|
9
|
+
listenExecutionWorkerSystemdSocket,
|
|
10
|
+
validateExecutionWorkerBearerToken,
|
|
11
|
+
} from "./execution-worker-api.js";
|
|
12
|
+
import { createExecutionJobManager } from "./execution-worker-jobs.js";
|
|
13
|
+
import {
|
|
14
|
+
attestExecutionWorkerActivationEnvironment,
|
|
15
|
+
attestExecutionWorkerCgroupBoundary,
|
|
16
|
+
} from "./execution-worker-systemd-boundary.js";
|
|
17
|
+
import { createPythonExecutionWorker } from "./execution-worker.js";
|
|
18
|
+
|
|
19
|
+
export const EXECUTION_WORKER_SYSTEMD_UNIT = "aginti-execution-worker.service";
|
|
20
|
+
export const EXECUTION_WORKER_CREDENTIAL_NAME = "execution-worker-token";
|
|
21
|
+
export const EXECUTION_WORKER_CREDENTIALS_DIRECTORY = `/run/credentials/${EXECUTION_WORKER_SYSTEMD_UNIT}`;
|
|
22
|
+
export const EXECUTION_WORKER_SERVER_SCHEMA_VERSION = "aginti-execution-worker-server-v1";
|
|
23
|
+
|
|
24
|
+
const TOKEN_PATH = path.join(EXECUTION_WORKER_CREDENTIALS_DIRECTORY, EXECUTION_WORKER_CREDENTIAL_NAME);
|
|
25
|
+
const DIGEST = /^[a-f0-9]{64}$/u;
|
|
26
|
+
const MAX_CREDENTIAL_BYTES = 257;
|
|
27
|
+
|
|
28
|
+
export class ExecutionWorkerServerError extends Error {
|
|
29
|
+
constructor(code, message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "ExecutionWorkerServerError";
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fail(code, message) {
|
|
37
|
+
throw new ExecutionWorkerServerError(code, message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function insidePath(candidate, parent) {
|
|
41
|
+
const relative = path.relative(parent, candidate);
|
|
42
|
+
return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sameFile(left, right) {
|
|
46
|
+
return Boolean(
|
|
47
|
+
left && right && left.dev === right.dev && left.ino === right.ino && left.uid === right.uid
|
|
48
|
+
&& left.gid === right.gid && left.mode === right.mode && left.nlink === right.nlink
|
|
49
|
+
&& left.size === right.size && left.ctimeMs === right.ctimeMs
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateCredentialMetadata(directory, credential, { uid, gid }) {
|
|
54
|
+
const directoryMode = directory.mode & 0o7777;
|
|
55
|
+
const credentialMode = credential.mode & 0o7777;
|
|
56
|
+
const trustedUid = (value) => value === 0 || value === uid;
|
|
57
|
+
const trustedGid = (value) => value === 0 || value === gid;
|
|
58
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || !trustedUid(directory.uid) || !trustedGid(directory.gid)
|
|
59
|
+
|| (directoryMode !== 0o500 && directoryMode !== 0o550)) {
|
|
60
|
+
fail("EXECUTION_WORKER_CREDENTIAL_INVALID", "execution worker credential directory is not trusted.");
|
|
61
|
+
}
|
|
62
|
+
if (!credential.isFile() || credential.isSymbolicLink() || !trustedUid(credential.uid) || !trustedGid(credential.gid)
|
|
63
|
+
|| (credentialMode !== 0o400 && credentialMode !== 0o440) || credential.nlink !== 1
|
|
64
|
+
|| credential.size < 32 || credential.size > MAX_CREDENTIAL_BYTES) {
|
|
65
|
+
fail("EXECUTION_WORKER_CREDENTIAL_INVALID", "execution worker credential file is not trusted.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function loadCredential(filesystem, processObject) {
|
|
70
|
+
let directoryBefore;
|
|
71
|
+
let credentialBefore;
|
|
72
|
+
let handle;
|
|
73
|
+
try {
|
|
74
|
+
const [directoryReal, credentialReal] = await Promise.all([
|
|
75
|
+
filesystem.realpath(EXECUTION_WORKER_CREDENTIALS_DIRECTORY),
|
|
76
|
+
filesystem.realpath(TOKEN_PATH),
|
|
77
|
+
]);
|
|
78
|
+
if (directoryReal !== EXECUTION_WORKER_CREDENTIALS_DIRECTORY || credentialReal !== TOKEN_PATH) {
|
|
79
|
+
fail("EXECUTION_WORKER_CREDENTIAL_INVALID", "execution worker credential path traverses a symbolic link.");
|
|
80
|
+
}
|
|
81
|
+
[directoryBefore, credentialBefore] = await Promise.all([
|
|
82
|
+
filesystem.lstat(EXECUTION_WORKER_CREDENTIALS_DIRECTORY),
|
|
83
|
+
filesystem.lstat(TOKEN_PATH),
|
|
84
|
+
]);
|
|
85
|
+
const uid = processObject.geteuid?.() ?? processObject.getuid?.();
|
|
86
|
+
const gid = processObject.getegid?.() ?? processObject.getgid?.();
|
|
87
|
+
if (!Number.isSafeInteger(uid) || uid < 0 || !Number.isSafeInteger(gid) || gid < 0) {
|
|
88
|
+
fail("EXECUTION_WORKER_CREDENTIAL_INVALID", "execution worker credential process identity is unavailable.");
|
|
89
|
+
}
|
|
90
|
+
validateCredentialMetadata(directoryBefore, credentialBefore, { uid, gid });
|
|
91
|
+
try {
|
|
92
|
+
await filesystem.access(TOKEN_PATH, fsConstants.W_OK);
|
|
93
|
+
fail("EXECUTION_WORKER_CREDENTIAL_INVALID", "execution worker credential is writable by the service.");
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof ExecutionWorkerServerError) throw error;
|
|
96
|
+
if (!["EACCES", "EPERM", "EROFS"].includes(error?.code)) {
|
|
97
|
+
fail("EXECUTION_WORKER_CREDENTIAL_INVALID", "execution worker credential read-only state is unproven.");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
handle = await filesystem.open(
|
|
101
|
+
TOKEN_PATH,
|
|
102
|
+
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | (fsConstants.O_CLOEXEC || 0) | (fsConstants.O_NONBLOCK || 0)
|
|
103
|
+
);
|
|
104
|
+
const openedBefore = await handle.stat();
|
|
105
|
+
if (!sameFile(credentialBefore, openedBefore)) {
|
|
106
|
+
fail("EXECUTION_WORKER_CREDENTIAL_CHANGED", "execution worker credential changed before read.");
|
|
107
|
+
}
|
|
108
|
+
const raw = await handle.readFile("utf8");
|
|
109
|
+
const openedAfter = await handle.stat();
|
|
110
|
+
const [directoryAfter, credentialAfter, directoryRealAfter, credentialRealAfter] = await Promise.all([
|
|
111
|
+
filesystem.lstat(EXECUTION_WORKER_CREDENTIALS_DIRECTORY),
|
|
112
|
+
filesystem.lstat(TOKEN_PATH),
|
|
113
|
+
filesystem.realpath(EXECUTION_WORKER_CREDENTIALS_DIRECTORY),
|
|
114
|
+
filesystem.realpath(TOKEN_PATH),
|
|
115
|
+
]);
|
|
116
|
+
if (!sameFile(openedBefore, openedAfter) || !sameFile(openedAfter, credentialAfter)
|
|
117
|
+
|| !sameFile(directoryBefore, directoryAfter)
|
|
118
|
+
|| directoryRealAfter !== EXECUTION_WORKER_CREDENTIALS_DIRECTORY || credentialRealAfter !== TOKEN_PATH
|
|
119
|
+
|| Buffer.byteLength(raw, "utf8") !== openedAfter.size) {
|
|
120
|
+
fail("EXECUTION_WORKER_CREDENTIAL_CHANGED", "execution worker credential changed during read.");
|
|
121
|
+
}
|
|
122
|
+
if (raw.includes("\u0000") || raw.includes("\r")) {
|
|
123
|
+
fail("EXECUTION_WORKER_CREDENTIAL_INVALID", "execution worker credential framing is invalid.");
|
|
124
|
+
}
|
|
125
|
+
const token = raw.endsWith("\n") ? raw.slice(0, -1) : raw;
|
|
126
|
+
if (!token || token.includes("\n")) {
|
|
127
|
+
fail("EXECUTION_WORKER_CREDENTIAL_INVALID", "execution worker credential must contain exactly one line.");
|
|
128
|
+
}
|
|
129
|
+
return validateExecutionWorkerBearerToken(token);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error instanceof ExecutionWorkerServerError) throw error;
|
|
132
|
+
fail("EXECUTION_WORKER_CREDENTIAL_INVALID", "execution worker credential could not be read safely.");
|
|
133
|
+
} finally {
|
|
134
|
+
await handle?.close().catch(() => {});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateEnvironment(environment) {
|
|
139
|
+
if (!environment || typeof environment !== "object") {
|
|
140
|
+
throw new TypeError("environment must be an object");
|
|
141
|
+
}
|
|
142
|
+
if (environment.CREDENTIALS_DIRECTORY !== EXECUTION_WORKER_CREDENTIALS_DIRECTORY) {
|
|
143
|
+
fail("EXECUTION_WORKER_CONFIG_INVALID", "execution worker requires its exact systemd credential directory.");
|
|
144
|
+
}
|
|
145
|
+
const runtimeBundleDirectory = environment.AGINTI_EXECUTION_RUNTIME_BUNDLE_DIRECTORY;
|
|
146
|
+
if (typeof runtimeBundleDirectory !== "string" || !path.isAbsolute(runtimeBundleDirectory)
|
|
147
|
+
|| path.normalize(runtimeBundleDirectory) !== runtimeBundleDirectory
|
|
148
|
+
|| !insidePath(runtimeBundleDirectory, EXECUTION_RUNTIME_BUNDLE_PRODUCTION_ROOT)) {
|
|
149
|
+
fail("EXECUTION_WORKER_CONFIG_INVALID", "execution worker runtime bundle directory is invalid.");
|
|
150
|
+
}
|
|
151
|
+
const runtimeBundleRootDigest = environment.AGINTI_EXECUTION_RUNTIME_BUNDLE_DIGEST;
|
|
152
|
+
if (typeof runtimeBundleRootDigest !== "string" || !DIGEST.test(runtimeBundleRootDigest)) {
|
|
153
|
+
fail("EXECUTION_WORKER_CONFIG_INVALID", "execution worker runtime bundle digest is invalid.");
|
|
154
|
+
}
|
|
155
|
+
if (environment.AGINTI_EXECUTION_TEST_BYPASS !== undefined) {
|
|
156
|
+
fail("EXECUTION_WORKER_CONFIG_INVALID", "execution worker test bypass must not exist in production.");
|
|
157
|
+
}
|
|
158
|
+
const workerId = `worker_${crypto.createHash("sha256")
|
|
159
|
+
.update(`${EXECUTION_WORKER_SERVER_SCHEMA_VERSION}:${runtimeBundleRootDigest}`)
|
|
160
|
+
.digest("base64url")}`;
|
|
161
|
+
return Object.freeze({ runtimeBundleDirectory, runtimeBundleRootDigest, workerId });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function loadExecutionWorkerServerConfig({
|
|
165
|
+
filesystem = fs,
|
|
166
|
+
environment = process.env,
|
|
167
|
+
processObject = process,
|
|
168
|
+
} = {}) {
|
|
169
|
+
const config = validateEnvironment(environment);
|
|
170
|
+
if (!processObject || typeof processObject !== "object") throw new TypeError("processObject must be an object");
|
|
171
|
+
const bearerToken = await loadCredential(filesystem, processObject);
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
schemaVersion: EXECUTION_WORKER_SERVER_SCHEMA_VERSION,
|
|
174
|
+
...config,
|
|
175
|
+
bearerToken,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function createProductionExecutionWorkerServer({
|
|
180
|
+
config,
|
|
181
|
+
serverFactory,
|
|
182
|
+
listen = true,
|
|
183
|
+
} = {}) {
|
|
184
|
+
if (!config || config.schemaVersion !== EXECUTION_WORKER_SERVER_SCHEMA_VERSION) {
|
|
185
|
+
throw new TypeError("config must be a validated execution worker server config");
|
|
186
|
+
}
|
|
187
|
+
if (typeof listen !== "boolean") throw new TypeError("listen must be a boolean");
|
|
188
|
+
if (listen && ((process.geteuid?.() ?? process.getuid?.()) === 0)) {
|
|
189
|
+
fail("EXECUTION_WORKER_PROCESS_IDENTITY_INVALID", "execution worker must run as its dedicated non-root service user.");
|
|
190
|
+
}
|
|
191
|
+
const boundary = listen
|
|
192
|
+
? Object.freeze({
|
|
193
|
+
activation: attestExecutionWorkerActivationEnvironment(),
|
|
194
|
+
containment: await attestExecutionWorkerCgroupBoundary(),
|
|
195
|
+
})
|
|
196
|
+
: null;
|
|
197
|
+
const worker = createPythonExecutionWorker({
|
|
198
|
+
workerId: config.workerId,
|
|
199
|
+
runtimeBundleDirectory: config.runtimeBundleDirectory,
|
|
200
|
+
expectedRuntimeBundleRootDigest: config.runtimeBundleRootDigest,
|
|
201
|
+
...(boundary
|
|
202
|
+
? { containmentProbeImpl: async () => boundary.containment }
|
|
203
|
+
: {}),
|
|
204
|
+
});
|
|
205
|
+
const manager = createExecutionJobManager({ worker });
|
|
206
|
+
const server = createExecutionWorkerApiServer({
|
|
207
|
+
manager,
|
|
208
|
+
bearerToken: config.bearerToken,
|
|
209
|
+
...(serverFactory === undefined ? {} : { serverFactory }),
|
|
210
|
+
});
|
|
211
|
+
const address = listen ? await listenExecutionWorkerSystemdSocket(server) : null;
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
schemaVersion: EXECUTION_WORKER_SERVER_SCHEMA_VERSION,
|
|
214
|
+
worker,
|
|
215
|
+
manager,
|
|
216
|
+
server,
|
|
217
|
+
address,
|
|
218
|
+
boundary,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function installExecutionWorkerShutdownHandlers(server, { processObject = process } = {}) {
|
|
223
|
+
if (!server || typeof server.close !== "function") throw new TypeError("server must provide close");
|
|
224
|
+
if (!processObject || typeof processObject.once !== "function") throw new TypeError("processObject must provide once");
|
|
225
|
+
let closing = false;
|
|
226
|
+
const shutdown = () => {
|
|
227
|
+
if (closing) return;
|
|
228
|
+
closing = true;
|
|
229
|
+
const forced = setTimeout(() => processObject.exit?.(1), 5_000);
|
|
230
|
+
forced.unref?.();
|
|
231
|
+
server.close(() => {
|
|
232
|
+
clearTimeout(forced);
|
|
233
|
+
processObject.exitCode = 0;
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
processObject.once("SIGTERM", shutdown);
|
|
237
|
+
processObject.once("SIGINT", shutdown);
|
|
238
|
+
return Object.freeze({ shutdown });
|
|
239
|
+
}
|