aglio-worker 0.1.0 → 0.8.0
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/agent.js +156 -65
- package/package.json +2 -2
package/agent.js
CHANGED
|
@@ -7345,8 +7345,10 @@ var NOTIFICATION_TYPES = [
|
|
|
7345
7345
|
"run_finished",
|
|
7346
7346
|
"run_failed",
|
|
7347
7347
|
"review_feedback",
|
|
7348
|
+
"pr_ready",
|
|
7348
7349
|
"pr_merged",
|
|
7349
|
-
"mention"
|
|
7350
|
+
"mention",
|
|
7351
|
+
"comment"
|
|
7350
7352
|
];
|
|
7351
7353
|
var CREDENTIAL_SCOPES = ["workspace", "user"];
|
|
7352
7354
|
var CREDENTIAL_AUTH_TYPES = ["api_key", "oauth_token", "session_bundle"];
|
|
@@ -27267,45 +27269,58 @@ var repoConnectionReportSchema = external_exports.object({
|
|
|
27267
27269
|
repos: external_exports.array(repoConnectionCheckSchema)
|
|
27268
27270
|
});
|
|
27269
27271
|
|
|
27272
|
+
// ../../packages/shared/src/notifications.ts
|
|
27273
|
+
var DEFAULT_NOTIFICATION_PREFERENCES = {
|
|
27274
|
+
emailDigest: true,
|
|
27275
|
+
emailQuestions: true,
|
|
27276
|
+
emailFailures: true,
|
|
27277
|
+
emailPrReady: true
|
|
27278
|
+
};
|
|
27279
|
+
var notificationPreferencesSchema = external_exports.object({
|
|
27280
|
+
emailDigest: external_exports.boolean().catch(DEFAULT_NOTIFICATION_PREFERENCES.emailDigest),
|
|
27281
|
+
emailQuestions: external_exports.boolean().catch(DEFAULT_NOTIFICATION_PREFERENCES.emailQuestions),
|
|
27282
|
+
emailFailures: external_exports.boolean().catch(DEFAULT_NOTIFICATION_PREFERENCES.emailFailures),
|
|
27283
|
+
emailPrReady: external_exports.boolean().catch(DEFAULT_NOTIFICATION_PREFERENCES.emailPrReady)
|
|
27284
|
+
});
|
|
27285
|
+
function isSlackWebhookUrl(value) {
|
|
27286
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
27287
|
+
try {
|
|
27288
|
+
const url2 = new URL(value.trim());
|
|
27289
|
+
return url2.protocol === "https:";
|
|
27290
|
+
} catch {
|
|
27291
|
+
return false;
|
|
27292
|
+
}
|
|
27293
|
+
}
|
|
27294
|
+
|
|
27270
27295
|
// ../../packages/shared/src/routing.ts
|
|
27271
|
-
var ROUTING_LABEL_KEYS = ["os", "mode", "tools", "size"];
|
|
27272
27296
|
var ROUTING_OS_VALUES = ["linux", "macos"];
|
|
27273
27297
|
var ROUTING_MODE_VALUES = ["docker", "host"];
|
|
27274
|
-
var ROUTING_TOOL_VALUES = ["ios", "xcode", "android", "
|
|
27275
|
-
var ROUTING_SIZE_VALUES = ["small", "large"];
|
|
27276
|
-
var
|
|
27298
|
+
var ROUTING_TOOL_VALUES = ["ios", "xcode", "android", "gpu"];
|
|
27299
|
+
var ROUTING_SIZE_VALUES = ["small", "medium", "large"];
|
|
27300
|
+
var ROUTING_GPU_VALUES = ["t4", "a100", "h100"];
|
|
27301
|
+
var ROUTING_ARCH_VALUES = ["x64", "arm64"];
|
|
27302
|
+
var ROUTING_REGION_VALUES = ["us-east", "us-west", "eu-west", "ap-southeast"];
|
|
27303
|
+
var ROUTING_LABEL_SUGGESTIONS = {
|
|
27277
27304
|
os: ROUTING_OS_VALUES,
|
|
27278
27305
|
mode: ROUTING_MODE_VALUES,
|
|
27279
27306
|
tools: ROUTING_TOOL_VALUES,
|
|
27280
|
-
size: ROUTING_SIZE_VALUES
|
|
27281
|
-
|
|
27282
|
-
|
|
27283
|
-
|
|
27284
|
-
|
|
27285
|
-
|
|
27286
|
-
|
|
27287
|
-
|
|
27288
|
-
|
|
27289
|
-
|
|
27290
|
-
throw new Error(
|
|
27291
|
-
`Unknown routing label "${value}". Use os, mode, tools or size.`
|
|
27292
|
-
);
|
|
27293
|
-
}
|
|
27294
|
-
const allowed = ROUTING_LABEL_VALUES[key];
|
|
27295
|
-
if (!allowed.includes(match[2])) {
|
|
27296
|
-
throw new Error(
|
|
27297
|
-
`Unknown routing label "${value}". Use ${allowed.map((entry) => `${key}:${entry}`).join(", ")}.`
|
|
27298
|
-
);
|
|
27299
|
-
}
|
|
27300
|
-
return { key, value: match[2] };
|
|
27301
|
-
}
|
|
27307
|
+
size: ROUTING_SIZE_VALUES,
|
|
27308
|
+
gpu: ROUTING_GPU_VALUES,
|
|
27309
|
+
arch: ROUTING_ARCH_VALUES,
|
|
27310
|
+
region: ROUTING_REGION_VALUES
|
|
27311
|
+
};
|
|
27312
|
+
var ROUTING_LABEL_SUGGESTION_LIST = Object.entries(
|
|
27313
|
+
ROUTING_LABEL_SUGGESTIONS
|
|
27314
|
+
).flatMap(([key, values]) => values.map((value) => `${key}:${value}`));
|
|
27315
|
+
var ROUTING_LABEL_MAX_LENGTH = 64;
|
|
27316
|
+
var LABEL_PATTERN = /^([a-z][a-z0-9_-]*):([a-z0-9][a-z0-9._-]*)$/;
|
|
27302
27317
|
function parseRoutingLabels(input2) {
|
|
27303
27318
|
const values = Array.isArray(input2) ? input2 : typeof input2 === "string" ? input2.split(/[\s,]+/) : [];
|
|
27304
27319
|
const seen = /* @__PURE__ */ new Set();
|
|
27305
27320
|
const labels = [];
|
|
27306
27321
|
for (const value of values) {
|
|
27307
27322
|
if (typeof value !== "string") continue;
|
|
27308
|
-
const label = value.trim()
|
|
27323
|
+
const label = value.trim();
|
|
27309
27324
|
if (!label || seen.has(label)) continue;
|
|
27310
27325
|
seen.add(label);
|
|
27311
27326
|
labels.push(label);
|
|
@@ -27314,24 +27329,16 @@ function parseRoutingLabels(input2) {
|
|
|
27314
27329
|
}
|
|
27315
27330
|
function validateRoutingLabels(input2) {
|
|
27316
27331
|
const labels = parseRoutingLabels(input2);
|
|
27317
|
-
const seenValues = /* @__PURE__ */ new Map();
|
|
27318
|
-
const tools = /* @__PURE__ */ new Set();
|
|
27319
27332
|
for (const label of labels) {
|
|
27320
|
-
|
|
27321
|
-
|
|
27322
|
-
|
|
27323
|
-
|
|
27324
|
-
throw new Error(`Conflicting routing labels "${key}:${previous}" and "${label}".`);
|
|
27325
|
-
}
|
|
27333
|
+
if (label.length > ROUTING_LABEL_MAX_LENGTH) {
|
|
27334
|
+
throw new Error(
|
|
27335
|
+
`Invalid routing label "${label}". Labels are at most ${ROUTING_LABEL_MAX_LENGTH} characters.`
|
|
27336
|
+
);
|
|
27326
27337
|
}
|
|
27327
|
-
|
|
27328
|
-
|
|
27329
|
-
|
|
27330
|
-
|
|
27331
|
-
const mode = seenValues.get("mode");
|
|
27332
|
-
if (tools.has("ios")) {
|
|
27333
|
-
if (os && os !== "macos" || mode && mode !== "host") {
|
|
27334
|
-
throw new Error("tools:ios requires os:macos and mode:host.");
|
|
27338
|
+
if (!LABEL_PATTERN.test(label)) {
|
|
27339
|
+
throw new Error(
|
|
27340
|
+
`Invalid routing label "${label}". Use lowercase key:value, for example size:medium.`
|
|
27341
|
+
);
|
|
27335
27342
|
}
|
|
27336
27343
|
}
|
|
27337
27344
|
return labels;
|
|
@@ -27395,6 +27402,8 @@ var projectSettingsSchema = external_exports.object({
|
|
|
27395
27402
|
runRetryBackoffSeconds: external_exports.number().int().min(10).max(600).default(60),
|
|
27396
27403
|
runRetryMaxBackoffSeconds: external_exports.number().int().min(60).max(3600).default(600),
|
|
27397
27404
|
notificationSound: external_exports.boolean().default(true),
|
|
27405
|
+
/** Slack incoming webhook for question, failure and PR-ready alerts. Null disables Slack. */
|
|
27406
|
+
slackWebhookUrl: external_exports.string().trim().max(500).refine(isSlackWebhookUrl, "Slack webhook must be an https URL").nullable().default(null),
|
|
27398
27407
|
/** Rendered column strip: built-in columns and custom list ids. */
|
|
27399
27408
|
boardColumnOrder: external_exports.array(external_exports.string()).optional(),
|
|
27400
27409
|
/** Last repository connectivity check from Project settings or the worker gate. */
|
|
@@ -27594,11 +27603,12 @@ function isHostLocalSigningArtifact(name) {
|
|
|
27594
27603
|
var WORKER_HEARTBEAT_SECONDS = 15;
|
|
27595
27604
|
var WORKER_OFFLINE_AFTER_MISSES = 3;
|
|
27596
27605
|
var WORKER_OFFLINE_AFTER_SECONDS = WORKER_HEARTBEAT_SECONDS * WORKER_OFFLINE_AFTER_MISSES;
|
|
27606
|
+
var WORKER_CLAIM_LONG_POLL_SECONDS = 25;
|
|
27597
27607
|
var WORKER_MIN_VERSION = "0.1.0";
|
|
27598
27608
|
var WORKER_VERSION_HEADER = "x-aglio-worker-version";
|
|
27599
27609
|
var WORKER_MODES = ["docker", "host"];
|
|
27600
27610
|
var WORKER_OSES = ["linux", "macos"];
|
|
27601
|
-
var WORKER_LABEL_MAX_LENGTH =
|
|
27611
|
+
var WORKER_LABEL_MAX_LENGTH = ROUTING_LABEL_MAX_LENGTH;
|
|
27602
27612
|
var WORKER_IOS_LABEL = "tools:ios";
|
|
27603
27613
|
var WORKER_ENROLLMENT_TOKEN_PREFIX = "aglio_enr_";
|
|
27604
27614
|
var WORKER_TOKEN_PREFIX = "aglio_wkr_";
|
|
@@ -28138,6 +28148,10 @@ var cardLabelInputSchema = external_exports.object({
|
|
|
28138
28148
|
color: cardLabelColorSchema.default(DEFAULT_CARD_LABEL_COLOR)
|
|
28139
28149
|
});
|
|
28140
28150
|
|
|
28151
|
+
// ../../packages/shared/src/workspace.ts
|
|
28152
|
+
var INVITE_TTL_DAYS = 7;
|
|
28153
|
+
var INVITE_TTL_MS = INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3;
|
|
28154
|
+
|
|
28141
28155
|
// ../../packages/shared/src/workspace-theme.ts
|
|
28142
28156
|
var WORKSPACE_THEME_ACCENT_IDS = ["default", "violet", "emerald", "amber", "rose"];
|
|
28143
28157
|
var WORKSPACE_THEME_SCALES = ["compact", "default", "large"];
|
|
@@ -30748,15 +30762,38 @@ function parseClaimedJob(value) {
|
|
|
30748
30762
|
if (!lease || typeof lease.id !== "string" || typeof lease.token !== "string") return null;
|
|
30749
30763
|
return value;
|
|
30750
30764
|
}
|
|
30765
|
+
function claimHoldSeconds(config2) {
|
|
30766
|
+
const requested = config2.claimTimeoutSeconds;
|
|
30767
|
+
if (typeof requested !== "number" || !Number.isFinite(requested)) {
|
|
30768
|
+
return WORKER_CLAIM_LONG_POLL_SECONDS;
|
|
30769
|
+
}
|
|
30770
|
+
return Math.min(Math.max(Math.round(requested), 5), 120);
|
|
30771
|
+
}
|
|
30772
|
+
var CLAIM_REQUEST_GRACE_MS = 2e4;
|
|
30751
30773
|
async function claim2(config2, token, fetchImpl, signal) {
|
|
30752
|
-
const
|
|
30753
|
-
|
|
30754
|
-
|
|
30755
|
-
|
|
30756
|
-
|
|
30757
|
-
|
|
30758
|
-
|
|
30774
|
+
const holdSeconds = claimHoldSeconds(config2);
|
|
30775
|
+
const controller = new AbortController();
|
|
30776
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
30777
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
30778
|
+
const timer = setTimeout(
|
|
30779
|
+
() => controller.abort(new Error("Claim request did not answer in time.")),
|
|
30780
|
+
holdSeconds * 1e3 + CLAIM_REQUEST_GRACE_MS
|
|
30759
30781
|
);
|
|
30782
|
+
timer.unref?.();
|
|
30783
|
+
let response;
|
|
30784
|
+
try {
|
|
30785
|
+
response = await request(
|
|
30786
|
+
config2,
|
|
30787
|
+
fetchImpl,
|
|
30788
|
+
"/api/workers/claim",
|
|
30789
|
+
token,
|
|
30790
|
+
{ version: config2.version, capacity: config2.capacity, claimTimeoutSeconds: holdSeconds },
|
|
30791
|
+
controller.signal
|
|
30792
|
+
);
|
|
30793
|
+
} finally {
|
|
30794
|
+
clearTimeout(timer);
|
|
30795
|
+
signal?.removeEventListener("abort", onAbort);
|
|
30796
|
+
}
|
|
30760
30797
|
if (response.status === 204) return { status: 204, job: null, reason: null, message: null };
|
|
30761
30798
|
if (!response.ok) throw await errorFrom(response);
|
|
30762
30799
|
const body = await responseBody(response);
|
|
@@ -30767,6 +30804,15 @@ async function claim2(config2, token, fetchImpl, signal) {
|
|
|
30767
30804
|
message: typeof body.message === "string" ? body.message : null
|
|
30768
30805
|
};
|
|
30769
30806
|
}
|
|
30807
|
+
function isTransientControlPlaneError(error62) {
|
|
30808
|
+
if (error62 instanceof WorkerHttpError) return error62.status >= 500 || error62.status === 429;
|
|
30809
|
+
return true;
|
|
30810
|
+
}
|
|
30811
|
+
var CLAIM_BACKOFF_MS = [5e3, 1e4, 2e4, 3e4];
|
|
30812
|
+
function claimBackoffMs(attempt) {
|
|
30813
|
+
const index = Math.min(Math.max(Math.floor(attempt), 1), CLAIM_BACKOFF_MS.length) - 1;
|
|
30814
|
+
return CLAIM_BACKOFF_MS[index];
|
|
30815
|
+
}
|
|
30770
30816
|
async function jobTokenRequest(config2, fetchImpl, path, token, init) {
|
|
30771
30817
|
return fetchImpl(joinUrl(config2.apiUrl, path), {
|
|
30772
30818
|
method: init.method,
|
|
@@ -30805,18 +30851,23 @@ function workerCardApi(config2, fetchImpl, signal) {
|
|
|
30805
30851
|
return new Uint8Array(await response.arrayBuffer());
|
|
30806
30852
|
},
|
|
30807
30853
|
async updateRunStatus(input2) {
|
|
30808
|
-
|
|
30809
|
-
|
|
30810
|
-
|
|
30811
|
-
|
|
30812
|
-
|
|
30813
|
-
|
|
30814
|
-
|
|
30815
|
-
|
|
30816
|
-
|
|
30817
|
-
|
|
30854
|
+
await retryTransient(
|
|
30855
|
+
async () => {
|
|
30856
|
+
const response = await jobTokenRequest(
|
|
30857
|
+
config2,
|
|
30858
|
+
fetchImpl,
|
|
30859
|
+
`/api/workers/jobs/${input2.jobId}/run/status`,
|
|
30860
|
+
input2.token,
|
|
30861
|
+
{
|
|
30862
|
+
method: "POST",
|
|
30863
|
+
body: JSON.stringify({ leaseId: input2.leaseId, ...input2.patch }),
|
|
30864
|
+
signal
|
|
30865
|
+
}
|
|
30866
|
+
);
|
|
30867
|
+
if (!response.ok) throw await errorFrom(response);
|
|
30868
|
+
},
|
|
30869
|
+
{ signal }
|
|
30818
30870
|
);
|
|
30819
|
-
if (!response.ok) throw await errorFrom(response);
|
|
30820
30871
|
},
|
|
30821
30872
|
async appendRunEvents(input2) {
|
|
30822
30873
|
const response = await jobTokenRequest(
|
|
@@ -31004,6 +31055,24 @@ function jobApi(config2, fetchImpl, signal) {
|
|
|
31004
31055
|
}
|
|
31005
31056
|
};
|
|
31006
31057
|
}
|
|
31058
|
+
var TRANSIENT_RETRY_DELAYS_MS = [500, 1500];
|
|
31059
|
+
async function retryTransient(call, options = {}) {
|
|
31060
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
|
|
31061
|
+
let lastError;
|
|
31062
|
+
for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
31063
|
+
try {
|
|
31064
|
+
return await call();
|
|
31065
|
+
} catch (error62) {
|
|
31066
|
+
lastError = error62;
|
|
31067
|
+
if (!isTransientControlPlaneError(error62) || options.signal?.aborted) throw error62;
|
|
31068
|
+
const delay = TRANSIENT_RETRY_DELAYS_MS[attempt];
|
|
31069
|
+
if (delay === void 0) break;
|
|
31070
|
+
options.onRetry?.(error62, attempt + 1);
|
|
31071
|
+
await sleep2(delay);
|
|
31072
|
+
}
|
|
31073
|
+
}
|
|
31074
|
+
throw lastError;
|
|
31075
|
+
}
|
|
31007
31076
|
function fatalExitCode(error62) {
|
|
31008
31077
|
if (error62.status === 401 && error62.code === "worker_revoked") return 0;
|
|
31009
31078
|
if (error62.status === 426 || error62.code === "incompatible_worker_version") return 2;
|
|
@@ -31121,10 +31190,27 @@ async function runRemoteWorker(config2, deps = {}) {
|
|
|
31121
31190
|
1,
|
|
31122
31191
|
Math.min(workerCapacity(config2.capacity, resolved.capacity), workerCapacity(resolved.capacity))
|
|
31123
31192
|
);
|
|
31193
|
+
let claimAttempt = 0;
|
|
31194
|
+
const backOffClaim = async (error62) => {
|
|
31195
|
+
claimAttempt += 1;
|
|
31196
|
+
const delay = claimBackoffMs(claimAttempt);
|
|
31197
|
+
if (claimAttempt === 1) {
|
|
31198
|
+
log("claim unavailable, backing off", {
|
|
31199
|
+
...error62 instanceof WorkerHttpError ? { status: error62.status, code: error62.code } : {},
|
|
31200
|
+
error: error62 instanceof Error ? error62.message : String(error62),
|
|
31201
|
+
retryInSeconds: Math.round(delay / 1e3)
|
|
31202
|
+
});
|
|
31203
|
+
}
|
|
31204
|
+
await sleep2(delay);
|
|
31205
|
+
};
|
|
31124
31206
|
const runSlot = async () => {
|
|
31125
31207
|
while (!controller.signal.aborted) {
|
|
31126
31208
|
try {
|
|
31127
31209
|
const result = await claim2(config2, resolved.token, fetchImpl, controller.signal);
|
|
31210
|
+
if (claimAttempt > 0) {
|
|
31211
|
+
log("claim recovered", { attempts: claimAttempt });
|
|
31212
|
+
claimAttempt = 0;
|
|
31213
|
+
}
|
|
31128
31214
|
if (result.status === 204) {
|
|
31129
31215
|
lastReason = null;
|
|
31130
31216
|
continue;
|
|
@@ -31194,11 +31280,13 @@ async function runRemoteWorker(config2, deps = {}) {
|
|
|
31194
31280
|
}
|
|
31195
31281
|
} catch (error62) {
|
|
31196
31282
|
if (controller.signal.aborted) break;
|
|
31283
|
+
if (isTransientControlPlaneError(error62)) {
|
|
31284
|
+
await backOffClaim(error62);
|
|
31285
|
+
continue;
|
|
31286
|
+
}
|
|
31197
31287
|
if (error62 instanceof WorkerHttpError) {
|
|
31198
31288
|
fail2(error62, "claim");
|
|
31199
31289
|
if (controller.signal.aborted) break;
|
|
31200
|
-
} else {
|
|
31201
|
-
log("claim failed", { error: error62 instanceof Error ? error62.message : String(error62) });
|
|
31202
31290
|
}
|
|
31203
31291
|
await sleep2(5e3);
|
|
31204
31292
|
}
|
|
@@ -43330,6 +43418,8 @@ async function main() {
|
|
|
43330
43418
|
const runnerImage = defaultRunnerImage();
|
|
43331
43419
|
const dockerSocket = process.env.AGLIO_WORKER_DOCKER_SOCKET?.trim() || "/var/run/docker.sock";
|
|
43332
43420
|
const mcpPort = Number(process.env.AGLIO_WORKER_MCP_PORT ?? 4310);
|
|
43421
|
+
const claimTimeoutRaw = Number(process.env.AGLIO_WORKER_CLAIM_TIMEOUT_SECONDS);
|
|
43422
|
+
const claimTimeoutSeconds = Number.isFinite(claimTimeoutRaw) && claimTimeoutRaw > 0 ? claimTimeoutRaw : void 0;
|
|
43333
43423
|
const agentContainerId = process.env.HOSTNAME?.trim() || hostname3();
|
|
43334
43424
|
const runnerNetwork = mode === "docker" ? resolveRunnerNetworkMode({
|
|
43335
43425
|
override: process.env.AGLIO_WORKER_RUNNER_NETWORK,
|
|
@@ -43362,6 +43452,7 @@ async function main() {
|
|
|
43362
43452
|
stateFile,
|
|
43363
43453
|
// Simulators and Xcode share one signing context on a Mac host.
|
|
43364
43454
|
capacity: workerCapacityForMode(process.env.AGLIO_WORKER_CAPACITY ?? 1, mode),
|
|
43455
|
+
claimTimeoutSeconds,
|
|
43365
43456
|
workspaceRoot,
|
|
43366
43457
|
hostWorkspaceRoot,
|
|
43367
43458
|
runnerImage,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aglio-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Host machine agent for Aglio: claims jobs over the API and starts aglio-runner containers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"aglio-worker": "
|
|
7
|
+
"aglio-worker": "agent.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"agent.js"
|